initial commit

This commit is contained in:
bain 2024-02-25 18:59:24 +01:00
commit 140b583071
Signed by: bain
GPG key ID: 31F0F25E3BED0B9B
6 changed files with 3653 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
config.toml

2773
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

22
Cargo.toml Normal file
View file

@ -0,0 +1,22 @@
[package]
name = "anon"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tide = { version = "0.16.0", features = [] }
async-std = { version = "1.8.0", features = ["attributes"] }
serde = { version = "1.0", features = ["derive"] }
ring = { version = "0.17.8", features = ["std"] }
anyhow = "1.0.70"
base64 = "0.21.0"
uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] }
toml = "0.7.3"
validator = { version = "0.15", features = ["derive"] }
lettre = { version = "0.10.4", default-features = false, features = ["async-std1-rustls-tls", "builder", "hostname", "smtp-transport"] }
chrono = "0.4.24"
serde_json = "1.0"
hex = "0.4.3"
handlebars = "5.1.0"

306
src/authorization.html Normal file
View file

@ -0,0 +1,306 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<title></title>
<link href="/static/style.css" rel="stylesheet" />
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
:root {
--background: rgb(24, 24, 27);
--text: #fefefe;
--primary: #606060;
--secondary: #303035;
--success: #76B041;
--caution: #9E2A2B;
--caution-darker: #c83033;
}
.issuer-name {
color: var(--primary);
font-weight: bolder;
}
.issuer-name a {
color: var(--primary);
text-decoration: underline;
text-decoration-thickness: 4px;
&:hover {
color: var(--text);
}
}
.account-number-input {
font-family: monospace;
font-size: 24pt;
width: 19ch;
border: none;
padding: 0.4rem 1rem;
padding-right: 0;
background: none;
color: inherit;
}
@media only screen and (max-width: 40em) {
.account-number-input {
font-size: xx-large;
}
}
.account-number-input[type="password"] {
letter-spacing: 0.1875ch;
}
html {
height: 100vmin;
}
body {
background: var(--background);
color: var(--text);
font-family: Poppins, sans-serif;
margin: 0;
box-sizing: border-box;
}
main {
margin: 20vmin auto 0;
width: min-content;
}
.client-name {
margin: 0.2rem 1ch 2.5rem;
font-size: xx-large;
}
textarea:focus,
input:focus {
outline: none;
}
.container {
display: flex;
border: 2px solid var(--primary);
border-radius: 5px;
width: min-content;
}
.container[data-valid="true"] {
border-color: var(--success);
}
.container[data-valid="false"] {
border-color: var(--caution);
}
.container button {
display: inline-flex;
align-items: center;
margin: 3px 1rem 0;
}
button {
background: none;
border: none;
color: var(--text);
cursor: pointer;
}
.login-button {
background: var(--secondary);
padding: 0.8rem;
font-weight: bold;
font-size: larger;
width: 100%;
margin: 0.5rem 0 0;
border-radius: 5px;
}
.login-button:hover {
background: var(--primary);
}
.generate-text {
text-align: center;
margin: 1.5rem 0;
}
a {
color: inherit;
}
.info-accounts {
background: var(--caution-darker);
width: 100%;
padding: 1rem;
border-radius: 5px;
box-sizing: border-box;
transition: ease 0.4s;
overflow: hidden;
}
.info-accounts.hidden {
height: 0;
opacity: 0;
}
button[data-closed="false"] .closed {
display: none;
}
button[data-closed="true"] .open {
display: none;
}
@media (prefers-color-scheme: light) {
:root {
--background: #fefefe;
--text: #2f2f2f;
--primary: #b0b0b0;
--secondary: #d9d9d9;
--success: #76B041;
--caution: #FF6B6C;
--caution-darker: #c83033;
}
.info-accounts {
color: #fefefe;
}
}
</style>
</head>
<body>
<main>
<h1 class="issuer-name"><a href="/">^NON</a> : {{issuer_name}}</h1>
<h1 lang="en">You are logging into</h1>
<h1 lang="cs" hidden>Přihlašujete se do</h1>
<p class="client-name">{{name}}</p>
<div class="container" id="accICont">
<input id="accInput" inputmode="numeric" autocomplete="current-password" autofocus type="password"
class="account-number-input" placeholder="0000000000000000" maxlength="16" />
<button id="showHideButton" data-closed="false" type="button" onclick="toggleInputType()">
<svg class="open" xmlns="http://www.w3.org/2000/svg" height="24" fill="currentColor"
viewBox="0 -960 960 960" width="24">
<path
d="M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54 137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z" />
</svg>
<svg class="closed" xmlns="http://www.w3.org/2000/svg" height="24" fill="currentColor" viewBox="0 -960 960 960" width="24">
<path
d="m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z" />
</svg>
</button>
</div>
<div>
<button class="login-button" onclick="login()" type="button">
<span lang="en">Log in</span><span lang="cs" hidden>Přihlásit se</span></button>
<p class="generate-text"><a href="javascript:void(0)" onclick="generateAccount()">
<span lang="en">or generate a new account number</span>
<span lang="cs" hidden>nebo vytvořit nové číslo</span>
</a></p>
<p id="infoBox" hidden class="info-accounts hidden">
<span lang="en"><b>Careful!</b> Your account number is the only way to access your accounts. Keep it
somewhere safe and out of sight!</span>
<span lang="cs" hidden><b>Pozor!</b> Toto číslo je jediný způsob, jak se přihlásit k vašim účtům.
Poznamenejte si ho a s nikým ho nesdílejte.</span>
</p>
</div>
</main>
<script>
accInput.addEventListener("beforeinput", ev => {
if (!/[0-9]*/.test(ev.data)) {
ev.preventDefault();
return;
}
});
accInput.addEventListener("input", ev => {
if (accInput.value.length == accInput.maxLength) {
const p = [...accInput.value.replace(/ +/g, "")];
const payload = p.slice(0, p.length - 1);
accICont.dataset.valid = calculateLuhnPrime(payload) == p[p.length - 1];
} else {
accICont.dataset.valid = "pending";
}
if (accInput.type == "password") return;
formatInput();
});
function formatInput() {
const caret = accInput.selectionStart;
const digitsBeforeCaret = accInput.value.slice(0, caret).replace(/ +/g, "").length;
const offset = Math.floor(digitsBeforeCaret / 4);
// const offset = Math.floor(caret/4) - (caret % 4 == 0);
const formatted = [...accInput.value.replace(/ +/g, "")].map((c, i) => i != 0 && i % 4 == 0 ? " " + c : c).join("");
if (formatted != accInput.value) {
accInput.value = formatted;
accInput.selectionStart = digitsBeforeCaret + offset;
accInput.selectionEnd = digitsBeforeCaret + offset;
}
}
function toggleInputType() {
showHideButton.dataset.closed = showHideButton.dataset.closed == "true" ? "false" : "true"
if (accInput.type == "password") {
accInput.maxLength = 19;
accInput.placeholder = "0000 0000 0000 0000";
formatInput();
accInput.type = "text";
} else {
accInput.value = accInput.value.replace(/ +/g, "");
accInput.maxLength = 16;
accInput.type = "password";
accInput.placeholder = "0000000000000000";
}
}
function login() {
if (accICont.dataset.valid != "true") return;
const digits = accInput.value.replace(/ +/g, "");
window.localStorage.setItem("code", digits);
let params = new URLSearchParams(window.location.search);
params.set("uid", digits);
window.location.search = params;
}
/**
* Luhn algorithm with some changes so people don't put in their credit cards :)
*/
function calculateLuhnPrime(payload) {
const sum = payload.map(Number).reduce((a, b, i) => a + (i % 2 == 0 ? [...String(b * 2)].map(Number).reduce((i, j) => i + j) : b), 0);
return sum % 10;
}
function generateAccount() {
accInput.type != "text" && toggleInputType();
let array = new Uint8Array(15);
window.crypto.getRandomValues(array);
let accNumber = [...array].map(n => String(Math.floor(n / 255 * 10))).join("");
accNumber += calculateLuhnPrime([...accNumber]);
accInput.value = accNumber;
accICont.dataset.valid = true;
formatInput();
infoBox.hidden = false;
setTimeout(() => infoBox.classList.toggle("hidden", false), 0);
}
const savedCode = window.localStorage.getItem("code");
if (savedCode) {
accInput.value = savedCode;
accICont.dataset.valid = true;
}
// i18n :)
if (document.body.querySelectorAll(`[lang="${navigator.language.slice(0, 2)}"]`).length)
document.body.querySelectorAll('[lang]').forEach(el => navigator.language.startsWith(el.lang) ? el.removeAttribute('hidden') : el.remove());
</script>
</body>
</html>

526
src/main.rs Normal file
View file

@ -0,0 +1,526 @@
mod names;
use async_std::fs;
use async_std::fs::File;
use async_std::sync::Mutex;
use ring::rand::SecureRandom;
use ring::rand::SystemRandom;
use std::collections::HashMap;
use std::collections::LinkedList;
use std::env;
use std::sync::Arc;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use tide::http::Url;
use tide::log;
use tide::prelude::json;
use tide::Request;
use tide::Response;
use toml::Table;
use validator::ValidationErrors;
use anyhow::{anyhow, Result};
use async_std::io::ReadExt;
use serde::{Deserialize, Serialize};
use handlebars::Handlebars;
use std::error::Error;
use std::fmt;
use base64::{engine::general_purpose as base64_coder, Engine as _};
#[derive(Debug)]
pub struct ClientError {
pub status_code: u16,
pub code: String,
pub message: String,
}
impl ClientError {
pub fn new(status_code: u16, code: &str, message: &str) -> Self {
Self {
status_code,
code: code.to_owned(),
message: message.to_owned(),
}
}
}
impl fmt::Display for ClientError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {} ({})", self.status_code, self.code, self.message)
}
}
impl Error for ClientError {}
#[derive(Deserialize)]
pub struct Config {
pub host: String,
pub port: u16,
pub clients: Table,
pub issuer_uri: String,
pub issuer_name: String,
pub rsa_key_file: String,
pub salt: String,
}
#[derive(Deserialize, Clone)]
pub struct Client {
pub name: String,
pub client_secret: String,
pub redirect_uris: Vec<String>,
}
impl Config {
pub async fn from_file(file: &mut File) -> Result<Config> {
let mut buf = String::new();
file.read_to_string(&mut buf).await?;
match toml::from_str::<Config>(&buf) {
Ok(v) => Ok(v),
Err(e) => Err(anyhow!("failed to parse config file: {}", e)),
}
}
}
async fn error_handler(mut res: tide::Response) -> tide::Result {
if let Some(err) = res.downcast_error::<ClientError>() {
let status = err.status_code;
res.set_body(json!({
"code": err.code,
"message": err.message,
}));
res.set_status(status);
}
if let Some(err) = res.downcast_error::<ValidationErrors>() {
res.set_body(json!({
"code": "validation_error",
"message": "Invalid input",
"errors": err
}));
res.set_status(422);
}
if let Some(err) = res.downcast_error::<OAuthError>() {
res.set_body(tide::Body::from_json(err)?);
res.set_status(400);
}
Ok(res)
}
async fn authorize(req: Request<AppState>) -> tide::Result {
#[derive(Deserialize)]
struct Query {
// response_type: String,
client_id: String,
// scope: String, // dont care rn
state: String,
redirect_uri: String,
uid: Option<String>,
code_challenge: Option<String>,
code_challenge_method: Option<String>,
nonce: Option<String>,
}
let q: Query = req.query()?;
let i = req
.state()
.clients
.get(&q.client_id)
.ok_or(OAuthError::new("invalid_client", "Unknown client"))?;
if q.uid.is_none() {
#[derive(Serialize)]
struct Fill<'a> {
name: &'a str,
issuer_name: &'a str
}
return Ok(Response::builder(200)
.body(
req.state()
.handlebars
.render("login-page", &Fill { name: &i.name, issuer_name: &req.state().issuer_name })?,
)
.header("Content-Type", "text/html")
.into());
}
let uid = q.uid.unwrap();
println!("Hello {},", uid);
// check redirect uri validity
if i.redirect_uris.iter().all(|r| r.as_str() != q.redirect_uri) {
return Err(ClientError::new(400, "bad_redirect", "invalid redirect uri").into());
}
println!("You logged into \"{}\"", i.name);
let random = SystemRandom::new();
let mut salt = [0u8; 32];
random.fill(&mut salt)?;
let code = base64_coder::URL_SAFE_NO_PAD.encode(&salt);
let redirect =
Url::parse_with_params(&q.redirect_uri, &[("state", &q.state), ("code", &code)])?;
let pkce = if q.code_challenge.is_some() && q.code_challenge_method.is_some() {
Some(PKCE {
challenge: q.code_challenge.unwrap(),
method: q.code_challenge_method.unwrap(),
})
} else {
None
};
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let mut auths = req.state().authorizations.lock().await;
// clean up old authorizations
while let Some(s) = auths.expirations.front() {
if s.1 > timestamp {
break;
}
let s = auths.expirations.pop_front().unwrap();
auths.auths.remove(&s.0);
}
auths.auths.insert(
code.clone(),
Authorization {
client_id: q.client_id,
uid,
redirect_uri: q.redirect_uri,
pkce,
nonce: q.nonce,
exp: timestamp + 60,
},
);
auths.expirations.push_back((code.clone(), timestamp + 60));
let mut resp = Response::new(302);
resp.insert_header("Location", redirect.as_str());
Ok(resp)
}
fn parse_basic_auth(header: &str) -> Option<(String, String)> {
let v = header.strip_prefix("Basic ")?;
let parsed = String::from_utf8(base64_coder::STANDARD.decode(v).ok()?).ok()?;
let parts: Vec<&str> = parsed.split(':').take(2).collect();
if parts.len() != 2 {
return None;
}
Some((parts[0].to_owned(), parts[1].to_owned()))
}
#[derive(Debug, Serialize)]
pub struct OAuthError {
pub error: String,
pub error_description: String,
}
impl OAuthError {
fn new(error: &'static str, description: &'static str) -> Self {
Self {
error: error.into(),
error_description: description.into(),
}
}
}
impl fmt::Display for OAuthError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: ({})", self.error, self.error_description)
}
}
impl Error for OAuthError {}
fn create_id_token(
app_state: &AppState,
client_id: &str,
uid: &str,
nonce: Option<String>,
) -> anyhow::Result<String> {
let header = base64_coder::URL_SAFE_NO_PAD.encode(
json!({
"alg": "RS256",
"typ": "JWT"
})
.to_string(),
);
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
use ring::hmac;
let mangler = hmac::Key::new(hmac::HMAC_SHA256, (app_state.salt.clone() + uid).as_bytes());
let mangle = |what: &str| {
let tag = hmac::sign(&mangler, (what.to_owned() + client_id).as_bytes());
hex::encode(tag.as_ref())
};
let choose = |what: &str, from: &[&'static str]| {
let tag = hmac::sign(&mangler, (what.to_owned() + client_id).as_bytes());
let bytes = tag.as_ref();
let mut i: usize = 0;
i = (i << 8) + bytes[0] as usize;
i = (i << 8) + bytes[1] as usize;
i = (i << 8) + bytes[2] as usize;
i = (i << 8) + bytes[3] as usize;
from[i % from.len()]
};
let adjective = choose("family_name", names::ADJECTIVES);
let animal = choose("given_name", names::ANIMALS);
// Generate an identity unique to the specified client_id
// This way nobody can take data from different clients and correlate
// user information. Since we don't even keep logs here, there is no
// way to know who is who.
let mut body = json!({
"kid": "master",
"iss": app_state.issuer_uri,
"sub": mangle("sub")[0..32],
"aud": client_id,
"exp": now+600,
"iat": now,
"given_name": animal,
"family_name": adjective,
"email": mangle("email")[0..15].to_owned()+"@email.invalid",
"email_verified": false,
"preferred_username": adjective.to_owned()+animal,
});
if let Some(nonce) = nonce {
body["nonce"] = nonce.into();
}
let claims = base64_coder::URL_SAFE_NO_PAD.encode(body.to_string());
let message = format!("{}.{}", &header, &claims);
let mut signature = vec![0; app_state.signing_key.public().modulus_len()];
app_state.signing_key.sign(
&ring::signature::RSA_PKCS1_SHA256,
&ring::rand::SystemRandom::new(),
message.as_bytes(),
&mut signature,
)?;
let signature = base64_coder::URL_SAFE_NO_PAD.encode(&signature);
Ok(format!("{}.{}", message, signature))
}
async fn authenticate(mut req: Request<AppState>) -> tide::Result {
#[derive(Deserialize)]
struct Body {
grant_type: String,
code: String,
client_id: Option<String>,
client_secret: Option<String>,
redirect_uri: String,
code_verifier: Option<String>,
}
let body: Body = req.body_form().await?;
if body.grant_type != "authorization_code" {
return Err(OAuthError::new("unsupported_grant_type", "").into());
}
// parse credentials from basic auth or from request body
let credentials = req
.header("Authorization")
.map(|v| v.get(0).unwrap().as_str())
.and_then(parse_basic_auth)
.or_else(|| {
if body.client_id.is_none() || body.client_secret.is_none() {
return None;
}
Some((body.client_id.unwrap(), body.client_secret.unwrap()))
});
let credentials = credentials.ok_or(OAuthError::new(
"invalid_client",
"Credentials not found in Basic auth or in req body",
))?;
// authenticate client
let client_info = req
.state()
.clients
.get(&credentials.0)
.ok_or(OAuthError::new("invalid_client", "Unknown client"))?;
if ring::constant_time::verify_slices_are_equal(
credentials.1.as_bytes(),
&client_info.client_secret.as_bytes(),
)
.is_err()
{
return Err(OAuthError::new("invalid_client", "Wrong secret").into());
}
// check authorization code
// this actuall consumes the code. It is not possible to retry auth with the same
// authorization code. It's always the client's fault, so we don't care really.
let code_info = {
let mut auths = req.state().authorizations.lock().await;
auths.auths
.remove(&body.code)
.ok_or(OAuthError::new("invalid_grant", "code not valid"))
}?;
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
if code_info.exp < timestamp {
return Err(OAuthError::new("invalid_grant", "code expired").into());
}
if code_info.redirect_uri != body.redirect_uri {
return Err(OAuthError::new("invalid_request", "redirect uri incorrect").into());
}
// verify PKCE if the client used it
if code_info.pkce.is_some() {
let pkce = code_info.pkce.unwrap();
let code_verifier = body
.code_verifier
.ok_or(OAuthError::new("invalid_request", "PKCE not present"))?;
let computed_challenge = match pkce.method.as_str() {
"S256" => {
let sha_digest =
ring::digest::digest(&ring::digest::SHA256, code_verifier.as_bytes());
let a = base64_coder::URL_SAFE_NO_PAD.encode(sha_digest.as_ref());
a
}
_ => pkce.challenge.clone(), // not the best, but since the only other option is
// "plain", then I guess its fine
};
// Needs constant time equality checking since we might be comparing plain text.
// This is a non-issue when using the S265 method
if ring::constant_time::verify_slices_are_equal(
computed_challenge.as_bytes(),
pkce.challenge.as_bytes(),
)
.is_err()
{
return Err(OAuthError::new("invalid_request", "PKCE challenge failed").into());
}
}
// give access code
Ok(Response::builder(200).body(json!({
"access_token": "SOME-TOKEN",
"token_type": "Bearer",
"expires_in": 600,
"id_token": create_id_token(req.state(), &credentials.0, &code_info.uid, code_info.nonce)?,
})).into())
}
async fn jwks(req: Request<AppState>) -> tide::Result {
let pk: ring::rsa::PublicKeyComponents<Vec<u8>> = req.state().signing_key.public().into();
Ok(Response::builder(200).body(json!({
"keys": [{
"kty": "RSA",
"kid": "master",
"use": "sig",
"n": base64_coder::URL_SAFE_NO_PAD.encode(pk.n),
"e": base64_coder::URL_SAFE_NO_PAD.encode(pk.e),
}],
})).into())
}
async fn configuration_endpoint(req: Request<AppState>) -> tide::Result {
let uri = Url::parse(&req.state().issuer_uri)?;
Ok(Response::builder(200).body(json!({
"issuer": uri,
"authorization_endpoint": uri.join("/authorize")?,
"token_endpoint": uri.join("/token")?,
"jwks_uri": uri.join("/jwks")?,
"response_types_supported": ["code", "id_token"],
"subject_types_supported": ["pairwise"],
"id_token_signing_alg_values_supported": ["RS256"],
})).into())
}
pub struct AuthStore {
pub auths:HashMap<String, Authorization>,
pub expirations: LinkedList<(String, u64)>,
}
#[derive(Clone)]
pub struct AppState {
pub clients: Arc<HashMap<String, Client>>,
pub authorizations: Arc<Mutex<AuthStore>>,
pub issuer_uri: String,
pub issuer_name: String,
pub signing_key: Arc<ring::rsa::KeyPair>,
pub salt: String,
pub handlebars: Arc<Handlebars<'static>>,
}
pub struct PKCE {
pub challenge: String,
pub method: String,
}
pub struct Authorization {
pub client_id: String,
pub uid: String,
pub redirect_uri: String,
pub pkce: Option<PKCE>,
pub nonce: Option<String>,
pub exp: u64,
}
#[async_std::main]
async fn main() -> tide::Result<()> {
log::start();
let mut conf_file =
File::open(env::var("CONFIG_FILE").unwrap_or("config.toml".to_owned())).await?;
let config = Config::from_file(&mut conf_file).await?;
let mut clients: HashMap<String, Client> = HashMap::new();
for c in config.clients {
let client_id = c.0;
let info: Client = c.1.try_into()?;
clients.insert(client_id, info);
}
let key_data = fs::read(config.rsa_key_file).await?;
let mut handlebars = Handlebars::new();
handlebars.register_template_string("login-page", include_str!("authorization.html"))?;
let mut app = tide::with_state(AppState {
clients: Arc::new(clients),
authorizations: Arc::new(Mutex::new(AuthStore {
auths: HashMap::new(),
expirations: LinkedList::new(),
})),
issuer_uri: config.issuer_uri,
issuer_name: config.issuer_name,
salt: config.salt,
signing_key: Arc::new(ring::rsa::KeyPair::from_pkcs8(&key_data)?),
handlebars: Arc::new(handlebars),
});
app.with(tide::utils::After(error_handler));
app.at("/authorize").get(authorize);
app.at("/token").post(authenticate);
app.at("/jwks").get(jwks);
app.at("/.well-known/openid-configuration").get(configuration_endpoint);
app.listen(format!("{}:{}", config.host, config.port))
.await?;
Ok(())
}

24
src/names.rs Normal file
View file

@ -0,0 +1,24 @@
pub const ADJECTIVES: &[&'static str] = &[
"Affinity", "Artisan", "Benevolent", "Breezy", "Cheerful", "Compass", "Curious",
"Eager", "Ebullient", "Effervescent", "Esteem", "Evergreen", "Flourish", "Genuine",
"Grounded", "Harmony", "Inventive", "Journey", "Jubilant", "Kaleidoscope", "Kaleidoscopic",
"Kindred", "Marvel", "Melodious", "Optimistic", "Resilient", "Riverstone", "Serendipitous",
"Serendipity", "Serene", "Sincere", "Skybound", "Sparkling", "Sprightly",
"Stardust", "Stargazer", "Starry", "Steadfast", "Sunbeam", "Tapestry",
"Tenacious", "Upbeat", "Valiant", "Verdant", "Vigorous", "Wanderlust",
"Whimsical", "Witty", "Wonderstruck", "Zephyr"
];
pub const ANIMALS: &[&'static str] = &[
"Axolotl", "Badger", "Baku", "Butterfly", "Capybara", "Carbuncle",
"Chameleon", "Cheetah", "Deer", "Dolphin", "Dryad", "Echidna",
"Elephant", "Fennec Fox", "Flamingo", "Fox", "Fu", "Gnome",
"Hippocampus", "Hippogriff", "Hippopotamus", "Hummingbird", "Iguana", "Kangaroo",
"Kelpie", "Kirin", "Koala", "Dragon", "Leviathan", "Lion",
"Lynx", "Manatee", "Manok", "Meerkat", "Mooncalf", "Naiad",
"Narwhal", "Orangutan", "Owl", "Panda", "Parrot", "Peacock",
"Penguin", "Phoenix", "Pixie", "Qilin", "Quokka", "Raccoon",
"Otter", "Seahorse", "Serpent", "Simurgh", "Sphynx", "Sprite",
"Squirrel", "Sylph", "Tapir", "Toucan", "Turtle", "Unicorn",
"Whale", "Ziz"
];