diff --git a/Cargo.lock b/Cargo.lock index 5895889b..d5276021 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstyle" version = "1.0.7" @@ -430,6 +445,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-link", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -513,7 +540,10 @@ dependencies = [ "opentelemetry-jaeger-propagator", "opentelemetry-otlp", "opentelemetry_sdk", + "oxide-auth", + "oxide-auth-axum", "parking_lot", + "percent-encoding", "rand", "regex", "reqwest", @@ -1281,6 +1311,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.4.0" @@ -1816,6 +1870,37 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "oxide-auth" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c136ac668d12ba0b5b8ce159b95c7600fda826dc599a1e1916f8461c0d16a84" +dependencies = [ + "base64 0.21.7", + "chrono", + "hmac", + "once_cell", + "rand", + "rmp-serde", + "rust-argon2", + "serde", + "serde_derive", + "serde_json", + "sha2", + "subtle", + "url", +] + +[[package]] +name = "oxide-auth-axum" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f0303e212596cb24ba33f4a7ae8f69e84596bea5fc65d089de65b09bfbcb8" +dependencies = [ + "axum 0.7.5", + "oxide-auth", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -1839,6 +1924,12 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pear" version = "0.2.9" @@ -2187,6 +2278,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "ruma" version = "0.12.1" @@ -3543,6 +3656,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 0da6eef1..73ea8719 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ axum = { version = "0.7", default-features = false, features = [ "http2", "json", "matched-path", + "query", ], optional = true } axum-extra = { version = "0.9", features = ["typed-header"] } axum-server = { version = "0.6", features = ["tls-rustls"] } @@ -142,6 +143,11 @@ tikv-jemallocator = { version = "0.5.0", features = [ sd-notify = { version = "0.4.1", optional = true } +# Oidc authentication utilities. +oxide-auth = { version = "0.6.1" } +oxide-auth-axum = { version = "0.5.0" } +percent-encoding = { version = "2.3.1" } + # Used for matrix spec type definitions and helpers [dependencies.ruma] features = [ diff --git a/src/api/client_server/oidc/authorize.rs b/src/api/client_server/oidc/authorize.rs new file mode 100644 index 00000000..91a7caf5 --- /dev/null +++ b/src/api/client_server/oidc/authorize.rs @@ -0,0 +1,227 @@ +use crate::{Result, Error, services}; +use oxide_auth_axum::{OAuthResponse, OAuthRequest}; +use oxide_auth::{ + endpoint::{OwnerConsent, Solicitation}, + frontends::simple::endpoint::FnSolicitor, + primitives::registrar::PreGrant, +}; +use ruma::api::client::error::ErrorKind as ClientErrorKind; + +use axum::extract::Query; +use serde_html_form; +use reqwest::Url; +use percent_encoding::percent_decode_str; + + +/// The set of query parameters a client needs to get authorization. +#[derive(serde::Deserialize, Debug)] +pub struct OAuthQuery { + client_id: String, + redirect_uri: Url, + scope: String, + state: String, + code_challenge: String, + code_challenge_method: String, + response_type: String, + response_mode: String, +} + +/// # `GET /_matrix/client/unstable/org.matrix.msc2964/authorize` +/// +/// Authenticate a user and device, and solicit the user's consent. +/// +/// Redirects to the login page if no token or token not belonging to any user. +/// [super::login::oidc_login] takes it up at the same point, so it's either +/// the client has a token, or the user does user password. Then the user gets +/// access to stage two, [authorize_consent]. +pub async fn authorize( + Query(query): Query, + oauth: OAuthRequest, +) -> Result { + tracing::trace!("processing OAuth request: {query:?}"); + // Enforce MSC2964's restrictions on OAuth2 flow. + let Ok(scope) = percent_decode_str(&query.scope).decode_utf8() else { + return Err(Error::BadRequest( + ClientErrorKind::Unknown, + "the scope could not be percent-decoded" + )); + }; + //if ! scope.contains("urn:matrix:api:*") { + if ! scope.contains("urn:matrix:org.matrix.msc2967.client:api:*") { + return Err(Error::BadRequest( + ClientErrorKind::Unknown, + "the scope does not include the client API" + )); + } + if ! scope.contains("urn:matrix:org.matrix.msc2967.client:device:") { + return Err(Error::BadRequest( + ClientErrorKind::Unknown, + "the scope does not include a device ID" + )); + } + if query.code_challenge_method != "S256" { + return Err(Error::BadRequest( + ClientErrorKind::Unknown, + "unsupported code challenge method" + )); + } + + // Redirect to the login page if no token or token not known. + let hostname = services() + .globals + .config + .well_known + .client + .as_ref() + .map(|s| s.domain().expect("well-known client should be a domain")); + let login_redirect = OAuthResponse::default() + .body(&login_form(hostname, &query)) + .content_type("text/html") + .expect("should set Content-Type on OAuth response"); + match oauth.authorization_header() { + | None => { + return Ok(login_redirect); + }, + | Some(token) => if services().users.find_from_token(token).is_err() { + return Ok(login_redirect); + } + } + // TODO register the device ID ? + + services() + .oidc + .endpoint() + .with_solicitor(FnSolicitor(move |_: &mut _, solicitation: Solicitation<'_>| + OwnerConsent::InProgress( + OAuthResponse::default() + .body(&consent_page_html( + "/_matrix/client/unstable/org.matrix.msc2964/authorize", + solicitation, + hostname.unwrap_or("conduwuit"), + )) + .content_type("text/html") + .expect("set content type on consent form") + ) + )) + .authorization_flow() + .execute(oauth) + .map_err(|_| Error::BadRequest(ClientErrorKind::Unknown, "authorization failed")) +} + +/// Wether a user allows their device to access this homeserver's resources. +#[derive(serde::Deserialize)] +pub struct Allowance { + allow: Option, +} + +/// # `POST /_matrix/client/unstable/org.matrix.msc2964/authorize?allow=[Option]` +/// +/// Authorize the device based on the user's consent. If the user allows +/// it to access their data, the client may request a token at the +/// [super::token::token] endpoint. +pub async fn authorize_consent( + Query(Allowance { allow }): Query, + oauth: OAuthRequest, +) -> Result { + let allowed = allow.unwrap_or(false); + tracing::debug!("processing user's consent: {:?} - {:?}", allowed, oauth); + + services() + .oidc + .endpoint() + .with_solicitor(FnSolicitor(move |_: &mut _, solicitation: Solicitation<'_>| + match allowed { + | false => OwnerConsent::Denied, + | true => OwnerConsent::Authorized(solicitation.pre_grant().client_id.clone()) + } + )) + .authorization_flow() + .execute(oauth) + .map_err(|_| Error::BadRequest(ClientErrorKind::Unknown, "consent request failed")) +} + +fn login_form( + hostname: Option<&str>, + OAuthQuery { + client_id, + redirect_uri, + scope, + state, + code_challenge, + code_challenge_method, + response_type, + response_mode, + }: &OAuthQuery, +) -> String { + let hostname = hostname.unwrap_or(""); + + format!( + r#" + + + + + + + +
+

{hostname} login

+
+ + + + + + + + + + + +
+
+ + + "# + ) +} + +pub(crate) fn consent_page_html( + route: &str, + solicitation: Solicitation<'_>, + hostname: &str, +) -> String { + let state = solicitation.state(); + let grant = solicitation.pre_grant(); + let PreGrant { client_id, redirect_uri, scope } = grant; + let mut args = vec![ + ("response_type", "code"), + ("client_id", client_id.as_str()), + ("redirect_uri", redirect_uri.as_str()), + ]; + if let Some(state) = state { + args.push(("state", state)); + } + let args = serde_html_form::to_string(args).unwrap(); + + format!( + r#" + + + + + + +
+

{hostname} login

+ '{client_id}' (at {redirect_uri}) is requesting permission for '{scope}' +
+ + +
+
+ + + "#, + ) +} diff --git a/src/api/client_server/oidc.rs b/src/api/client_server/oidc/discovery.rs similarity index 100% rename from src/api/client_server/oidc.rs rename to src/api/client_server/oidc/discovery.rs diff --git a/src/api/client_server/oidc/login.rs b/src/api/client_server/oidc/login.rs new file mode 100644 index 00000000..80fe230f --- /dev/null +++ b/src/api/client_server/oidc/login.rs @@ -0,0 +1,148 @@ +use super::authorize::consent_page_html; +use oxide_auth_axum::{OAuthRequest, OAuthResponse}; +use oxide_auth::{ + endpoint::{OwnerConsent, Solicitation}, + frontends::simple::endpoint::FnSolicitor, +}; +use axum::extract::{Form, FromRequest}; +use crate::{ + services, + Error, + Result, +}; +use ruma::{ + user_id::UserId, + api::client::error::ErrorKind as ClientErrorKind, +}; +use reqwest::Url; +use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; + +/// The set of query parameters a client needs to get authorization. +#[derive(serde::Deserialize, Debug)] +pub struct LoginForm { + username: String, + password: String, + client_id: String, + redirect_uri: Url, + scope: String, + state: String, + code_challenge: String, + code_challenge_method: String, + response_type: String, + response_mode: String, +} + +impl From for String { + /// Turn the OAuth parameters from a Form into a GET query, suitable for + /// then turning it into oxide-auth's OAuthRequest. Strips the unneeded + /// username and password. + fn from(value: LoginForm) -> Self { + let encode = |text: &str| -> String { + utf8_percent_encode(text, NON_ALPHANUMERIC).to_string() + }; + + format!( + "?client_id={}&redirect_uri={}&scope={}&state={}&code_challenge={}&code_challenge_method={}&response_type={}&response_mode={}", + encode(&value.client_id), + encode(&value.redirect_uri.to_string()), + encode(&value.scope), + encode(&value.state), + encode(&value.code_challenge), + encode(&value.code_challenge_method), + encode(&value.response_type), + encode(&value.response_mode), + ) + } +} + +/// # `POST /_matrix/client/unstable/org.matrix.msc2964/login` +/// +/// Display a login UI to the user and return an authorization code on success. +/// We presume that the OAuth2 query parameters are provided in the form. +/// With the code, the client may then access stage two, +/// [super::authorize::authorize_consent]. +pub async fn oidc_login( + Form(login_query): Form, +) -> Result { + // Only accept local usernames. Mostly to simplify things at first. + let Ok(user_id) = UserId::parse_with_server_name( + login_query.username.clone(), + &services().globals.config.server_name, + ) else { + return Err(Error::BadRequest(ClientErrorKind::InvalidUsername, "username is invalid")); + }; + + if ! services().users.exists(&user_id).expect("searchable users db") { + return Err(Error::BadRequest(ClientErrorKind::Unknown, "unknown username")); + } + tracing::info!("logging in: {user_id:?}"); + let Some(valid_hash) = services() + .users + .password_hash(&user_id) + .expect("searchable users hashes") else { + return Err(Error::BadRequest(ClientErrorKind::forbidden(), "Wrong username or password.")); + }; + + if valid_hash.is_empty() { + return Err(Error::BadRequest( + ClientErrorKind::UserDeactivated, + "The user has been deactivated.", + )); + } + let hash_matches = argon2::verify_encoded( + &valid_hash, + login_query.password.as_bytes() + ).unwrap_or(false); + if ! hash_matches { + return Err(Error::BadRequest( + ClientErrorKind::forbidden(), + "Wrong username or password.", + )); + } + + // Build up a GET query and parse it as an OAuthRequest. + let login_query: String = login_query.into(); + let login_url = http::Uri::builder() + .scheme("https") + .authority(services().globals.config.server_name.as_str()) + .path_and_query(login_query) + .build() + .expect("should be parseable"); + let req: http::Request = http::Request::builder() + .method("GET") + .uri(&login_url) + .body(axum::body::Body::empty()) + .expect("login form OAuth parameters parseable as a query"); + let oauth = OAuthRequest::from_request(req, &"") + .await + .expect("request parseable as an OAuth query"); + let hostname = services() + .globals + .config + .well_known + .client + .as_ref() + .map(|s| s.domain().expect("well-known client should be a domain")); + + services() + .oidc + .endpoint() + .with_solicitor(FnSolicitor(move |_: &mut _, solicitation: Solicitation<'_>| + OwnerConsent::InProgress( + OAuthResponse::default() + .body(&consent_page_html( + "/_matrix/client/unstable/org.matrix.msc2964/authorize", + solicitation, + hostname.unwrap_or("conduwuit"), + )) + .content_type("text/html") + .expect("set content type on consent form") + ) + )) + .authorization_flow() + .execute(oauth) + .map_err(|_| + Error::BadRequest(ClientErrorKind::InvalidParam, "authorization failed") + ) +} + diff --git a/src/api/client_server/oidc/mod.rs b/src/api/client_server/oidc/mod.rs new file mode 100644 index 00000000..5be3dba8 --- /dev/null +++ b/src/api/client_server/oidc/mod.rs @@ -0,0 +1,9 @@ +mod discovery; +mod authorize; +mod login; +mod token; + +pub use discovery::get_auth_metadata; +pub use authorize::{authorize, authorize_consent}; +pub use login::oidc_login; +pub use token::token; diff --git a/src/api/client_server/oidc/token.rs b/src/api/client_server/oidc/token.rs new file mode 100644 index 00000000..ec541c65 --- /dev/null +++ b/src/api/client_server/oidc/token.rs @@ -0,0 +1,83 @@ +/// Implementation of [MSC2964]'s OAuth2 restricted flow using the [oxide-auth] +/// crate. See the MSC for restrictions that apply to this flow. +/// +/// [MSC2965]: https://github.com/matrix-org/matrix-spec-proposals/pull/2965 +/// [oxide-auth]: https://docs.rs/oxide-auth + +use crate::{Result, services, Error}; +use oxide_auth_axum::{OAuthResponse, OAuthRequest}; +use oxide_auth::endpoint::QueryParameter; +use axum::response::IntoResponse; +use ruma::api::client::error::ErrorKind as ClientErrorKind; + +/// # `POST /_matrix/client/unstable/org.matrix.msc2964/token` +/// +/// Depending on `grant_type`, either deliver a new token to a device, and store +/// it in the server's ring, or refresh the token. +pub async fn token( + oauth: OAuthRequest, +) -> Result { + let Some(body) = oauth.body() else { + return Err( + Error::BadRequest(ClientErrorKind::Unknown, "OAuth request had an empty body") + ); + }; + let grant_type = body + .unique_value("grant_type") + .map(|value| value.to_string()); + let endpoint = services().oidc.endpoint(); + + match grant_type.as_deref() { + | Some("authorization_code") => + endpoint + .access_token_flow() + .execute(oauth) + .map_err(|_| + Error::BadRequest(ClientErrorKind::Unknown, "token grant failed")), + | Some("refresh_token") => + endpoint + .refresh_flow() + .execute(oauth) + .map_err(|_| + Error::BadRequest(ClientErrorKind::Unknown, "token refresh failed")), + | _ => Err(Error::BadRequest( + ClientErrorKind::Unknown, + "The request's grant type is unsupported" + )), + } +} + +/// Sample protected content. TODO check that resources are available with the returned token. +pub(crate) async fn _protected_resource( + oauth: OAuthRequest, +) -> impl IntoResponse { + const DENY_TEXT: &str = " +This page should be accessed via an oauth token from the client in the example. Click + +here to begin the authorization process. + +"; + let protect = services() + .oidc + .endpoint() + .with_scopes(vec!["default-scope".parse().unwrap()]) + .resource_flow() + .execute(oauth); + + match protect { + Ok(_grant) => Ok("Hello, world"), + Err(Ok(response)) => { + let error: OAuthResponse = response + //.header(ContentType::HTML) + .body(DENY_TEXT) + //.finalize() + .into(); + Err(Ok(error)) + } + Err(Err(_)) => Err(Err( + Error::BadRequest(ClientErrorKind::Unknown, "authentication failed") + )), + } +} + + diff --git a/src/main.rs b/src/main.rs index 3c583b06..be0cb7b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use axum::{ extract::{DefaultBodyLimit, FromRequestParts, MatchedPath}, middleware::map_response, response::{IntoResponse, Response}, - routing::{any, get, on, MethodFilter}, + routing::{any, get, on, post, MethodFilter}, Router, }; use axum_server::{bind, bind_rustls, tls_rustls::RustlsConfig, Handle as ServerHandle}; @@ -433,6 +433,22 @@ fn routes(config: &Config) -> Router { .route( "/_matrix/client/auth_metadata", get(client_server::get_auth_metadata), + ) + .route( + "/_matrix/client/unstable/org.matrix.msc2964/authorize", + get(client_server::authorize) + ) + .route( + "/_matrix/client/unstable/org.matrix.msc2964/authorize", + post(client_server::authorize_consent) + ) + .route( + "/_matrix/client/unstable/org.matrix.msc2964/login", + post(client_server::oidc_login) + ) + .route( + "/_matrix/client/unstable/org.matrix.msc2964/token", + post(client_server::token) ) .route( "/_matrix/client/r0/rooms/:room_id/initialSync", diff --git a/src/service/mod.rs b/src/service/mod.rs index c328bf7e..4075bd7d 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -15,6 +15,7 @@ pub mod appservice; pub mod globals; pub mod key_backups; pub mod media; +pub mod oidc; pub mod pdu; pub mod pusher; pub mod rooms; @@ -36,6 +37,7 @@ pub struct Services { pub key_backups: key_backups::Service, pub media: media::Service, pub sending: Arc, + pub oidc: Arc, } impl Services { @@ -123,6 +125,7 @@ impl Services { sending: sending::Service::build(db, &config), globals: globals::Service::load(db, config)?, + oidc: Arc::new(oidc::Service::preconfigured()), }) } async fn memory_usage(&self) -> String { diff --git a/src/service/oidc/mod.rs b/src/service/oidc/mod.rs new file mode 100644 index 00000000..b9a1579c --- /dev/null +++ b/src/service/oidc/mod.rs @@ -0,0 +1,77 @@ +/// OIDC service. +/// +/// Provides the registrar, authorizer and issuer needed by [api::client::oidc]. +/// The whole OAuth2 flow is taken care of by [oxide-auth]. +/// +/// TODO At the moment this service provides no method to dynamically add a +/// client. That would need a dedicated space in the database. +/// +/// [oxide-auth]: https://docs.rs/oxide-auth + +use crate::Result; +use oxide_auth::{ + frontends::simple::endpoint::{Generic, Vacant}, + primitives::{ + prelude::{ + AuthMap, + Authorizer, + Client, + ClientMap, + Issuer, + RandomGenerator, + Registrar, + TokenMap, + }, + registrar::RegisteredUrl, + }, +}; +use std::sync::Mutex; + +pub struct Service { + registrar: Mutex, + authorizer: Mutex>, + issuer: Mutex>, +} + +impl Service { + pub fn preconfigured() -> Self { + Service { + registrar: Mutex::new( + vec![Client::public( + "LocalClient", + RegisteredUrl::Semantic( + "http://localhost/clientside/endpoint".parse().unwrap(), + ), + "default-scope".parse().unwrap(), + )] + .into_iter() + .collect(), + ), + // Authorization tokens are 16 byte random keys to a memory hash map. + authorizer: Mutex::new(AuthMap::new(RandomGenerator::new(16))), + // Bearer tokens are also random generated but 256-bit tokens, since they live longer + // and this example is somewhat paranoid. + // + // We could also use a `TokenSigner::ephemeral` here to create signed tokens which can + // be read and parsed by anyone, but not maliciously created. However, they can not be + // revoked and thus don't offer even longer lived refresh tokens. + issuer: Mutex::new(TokenMap::new(RandomGenerator::new(16))), + } + } + + /// The oxide-auth carry-all endpoint. + pub fn endpoint(&self) -> Generic { + Generic { + registrar: self.registrar.lock().unwrap(), + authorizer: self.authorizer.lock().unwrap(), + issuer: self.issuer.lock().unwrap(), + // Solicitor configured later. + solicitor: Vacant, + // Scope configured later. + scopes: Vacant, + // `rocket::Response` is `Default`, so we don't need more configuration. + response: Vacant, + } + } +} +