From a96a5b32194ac21aa28bc747a2629eacd065e7bd Mon Sep 17 00:00:00 2001 From: lafleur Date: Wed, 16 Apr 2025 15:12:00 +0200 Subject: [PATCH] impl MSC2964: OIDC token flow --- Cargo.lock | 129 +++++++++++++++++++ Cargo.toml | 10 ++ src/api/Cargo.toml | 3 + src/api/client/oidc/authorize.rs | 214 +++++++++++++++++++++++++++++++ src/api/client/oidc/login.rs | 130 +++++++++++++++++++ src/api/client/oidc/mod.rs | 18 +++ src/api/client/oidc/token.rs | 78 +++++++++++ src/api/router.rs | 11 +- src/service/Cargo.toml | 1 + src/service/mod.rs | 1 + src/service/oidc/mod.rs | 87 +++++++++++++ src/service/services.rs | 1 + 12 files changed, 682 insertions(+), 1 deletion(-) create mode 100644 src/api/client/oidc/authorize.rs create mode 100644 src/api/client/oidc/login.rs create mode 100644 src/api/client/oidc/token.rs create mode 100644 src/service/oidc/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 04e4f36e..66ef3859 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,21 @@ dependencies = [ "alloc-no-stdlib", ] +[[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.10" @@ -94,6 +109,12 @@ dependencies = [ "password-hash", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" @@ -540,6 +561,17 @@ dependencies = [ "digest", ] +[[package]] +name = "blake2b_simd" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -704,7 +736,10 @@ 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]] @@ -1098,6 +1133,12 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2459fc9262a1aa204eb4b5764ad4f189caec88aea9634389c0a25f8be7f6265e" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "coolor" version = "1.0.0" @@ -2125,6 +2166,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 0.61.0", +] + +[[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 = "icu_collections" version = "1.5.0" @@ -3073,6 +3138,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", + "oxide-auth", +] + [[package]] name = "parking" version = "2.2.1" @@ -3729,6 +3825,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.10.1" @@ -3925,6 +4043,17 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "rust-argon2" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d9848531d60c9cbbcf9d166c885316c24bc0e2a9d3eba0956bb6cbbd79bc6e8" +dependencies = [ + "base64 0.21.7", + "blake2b_simd", + "constant_time_eq", +] + [[package]] name = "rust-librocksdb-sys" version = "0.33.0+9.11.1" diff --git a/Cargo.toml b/Cargo.toml index 318c89ac..76150764 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,6 +103,7 @@ features = [ "matched-path", "tokio", "tracing", + "query", ] [workspace.dependencies.axum-extra] @@ -537,6 +538,15 @@ features = ["std"] [workspace.dependencies.maplit] version = "1.0.2" +[workspace.dependencies.oxide-auth] +version = "0.6.1" + +[workspace.dependencies.oxide-auth-axum] +version = "0.5.0" + +[workspace.dependencies.percent-encoding] +version = "2.3.1" + # # Patches # diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index 15ada812..b9a61901 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -90,6 +90,9 @@ serde.workspace = true sha1.workspace = true tokio.workspace = true tracing.workspace = true +oxide-auth.workspace = true +oxide-auth-axum.workspace = true +percent-encoding.workspace = true [lints] workspace = true diff --git a/src/api/client/oidc/authorize.rs b/src/api/client/oidc/authorize.rs new file mode 100644 index 00000000..2d5adef6 --- /dev/null +++ b/src/api/client/oidc/authorize.rs @@ -0,0 +1,214 @@ +use oxide_auth_axum::{OAuthResponse, OAuthRequest}; +use oxide_auth::{ + endpoint::{OwnerConsent, Solicitation}, + frontends::simple::endpoint::FnSolicitor, + primitives::registrar::PreGrant, +}; +use axum::extract::{Query, State}; +use serde_html_form; +use conduwuit::{Result, err}; +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(crate) 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(crate) async fn authorize( + State(services): State, + 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(err!(Request(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(err!(Request(Unknown("the scope does not include the client API")))); + } + if ! scope.contains("urn:matrix:org.matrix.msc2967.client:device:") { + return Err(err!(Request(Unknown("the scope does not include a device ID")))); + } + if query.code_challenge_method != "S256" { + return Err(err!(Request(Unknown("unsupported code challenge method")))); + } + + // Redirect to the login page if no token or token not known. + let hostname = services + .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).await.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(|err| err!("authorization failed: {err:?}")) +} + +/// Wether a user allows their device to access this homeserver's resources. +#[derive(serde::Deserialize)] +pub(crate) 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(crate) async fn authorize_consent( + Query(Allowance { allow }): Query, + State(services): State, + 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(|err| err!(Request(Unknown("consent request failed: {err:?}")))) +} + +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/oidc/login.rs b/src/api/client/oidc/login.rs new file mode 100644 index 00000000..5fa7dd81 --- /dev/null +++ b/src/api/client/oidc/login.rs @@ -0,0 +1,130 @@ +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, State}; +use conduwuit::{ + Result, + err, + utils::hash::verify_password, +}; +use ruma::user_id::UserId; +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(crate) 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(crate) async fn oidc_login( + State(services): State, + Form(login_query): Form, +) -> Result { + // Only accept local usernames. Mostly to simplify things at first. + let user_id = UserId::parse_with_server_name( + login_query.username.clone(), + &services.config.server_name + ) + .map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?; + + if ! services.users.exists(&user_id).await { + return Err(err!(Request(Unknown("unknown username")))); + } + tracing::info!("logging in: {user_id:?}"); + let valid_hash = services + .users + .password_hash(&user_id) + .await + .inspect_err(|e| tracing::info!("could not get user's hash: {e:?}"))?; + + if valid_hash.is_empty() { + return Err(err!(Request(UserDeactivated("the user's hash was not found")))); + } + if let Err(_) = verify_password(&login_query.password, &valid_hash) { + return Err(err!(Request(InvalidParam("password does not match")))); + } + + // 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.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 + .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(|err| err!(Request(Unknown("authorization failed: {err:?}")))) +} diff --git a/src/api/client/oidc/mod.rs b/src/api/client/oidc/mod.rs index c0cab565..3b9604a9 100644 --- a/src/api/client/oidc/mod.rs +++ b/src/api/client/oidc/mod.rs @@ -1,3 +1,21 @@ +/// OIDC +/// +/// Stands for OpenID Connect, and is an authentication scheme relying on OAuth2. +/// The MSC2964 Matrix Spec Proposal describes an authentication process based +/// on OIDC with restrictions. See the [sample flow] for details on what's expected. +/// +/// This module implements the needed endpoints. It relies on [`service::oidc`] and +/// the [oxide-auth] crate. +/// +/// [oxide-auth]: https://docs.rs/oxide-auth +/// [sample flow]: https://github.com/sandhose/matrix-spec-proposals/blob/msc/sandhose/oauth2-profile/proposals/2964-oauth2-profile.md#sample-flow + mod discovery; +mod login; +mod authorize; +mod token; pub(crate) use self::discovery::get_auth_metadata; +pub(crate) use self::login::oidc_login; +pub(crate) use self::authorize::{authorize, authorize_consent}; +pub(crate) use self::token::token; diff --git a/src/api/client/oidc/token.rs b/src/api/client/oidc/token.rs new file mode 100644 index 00000000..3d3f19db --- /dev/null +++ b/src/api/client/oidc/token.rs @@ -0,0 +1,78 @@ +/// 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 oxide_auth_axum::{OAuthResponse, OAuthRequest}; +use oxide_auth::endpoint::QueryParameter; +use axum::{ + extract::State, + response::IntoResponse, +}; +use conduwuit::{Result, err}; + +/// # `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(crate) async fn token( + State(services): State, + oauth: OAuthRequest, +) -> Result { + let Some(body) = oauth.body() else { + return Err(err!(Request(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(|err| err!(Request(Unknown("token grant failed: {err:?}")))), + | Some("refresh_token") => + endpoint + .refresh_flow() + .execute(oauth) + .map_err(|err| err!(Request(Unknown("token refresh failed: {err:?}")))), + | other => + Err(err!(Request(Unknown("unsupported grant type: {other:?}")))), + } +} + +/// Sample protected content. TODO check that resources are available with the returned token. +pub(crate) async fn _protected_resource( + State(services): State, + 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(Err(err!(Request(Unknown("auth failed: {err:?}"))))), + } +} + diff --git a/src/api/router.rs b/src/api/router.rs index e7393837..e60c186f 100644 --- a/src/api/router.rs +++ b/src/api/router.rs @@ -117,9 +117,18 @@ pub fn build(router: Router, server: &Server) -> Router { .ruma_route(&client::get_protocols_route) .route("/_matrix/client/unstable/thirdparty/protocols", get(client::get_protocols_route_unstable)) - // MSC2965 is still not stabilized. See https://github.com/sandhose/matrix-spec-proposals/blob/msc/sandhose/oidc-discovery/proposals/2965-auth-metadata.md#unstable-prefix + // MSC2965 route. .route("/_matrix/client/unstable/org.matrix.msc2965/auth_metadata", get(client::get_auth_metadata)) + // MSC2964 routes. + .route("/_matrix/client/unstable/org.matrix.msc2964/authorize", + get(client::authorize)) + .route("/_matrix/client/unstable/org.matrix.msc2964/authorize", + post(client::authorize_consent)) + .route("/_matrix/client/unstable/org.matrix.msc2964/login", + post(client::oidc_login)) + .route("/_matrix/client/unstable/org.matrix.msc2964/token", + post(client::token)) .ruma_route(&client::send_message_event_route) .ruma_route(&client::send_state_event_for_key_route) .ruma_route(&client::get_state_events_route) diff --git a/src/service/Cargo.toml b/src/service/Cargo.toml index 8b0d1405..b62bf1e8 100644 --- a/src/service/Cargo.toml +++ b/src/service/Cargo.toml @@ -111,6 +111,7 @@ webpage.workspace = true webpage.optional = true blurhash.workspace = true blurhash.optional = true +oxide-auth.workspace = true [lints] workspace = true diff --git a/src/service/mod.rs b/src/service/mod.rs index 3d7a3aa9..a8047ea0 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -19,6 +19,7 @@ pub mod globals; pub mod key_backups; pub mod media; pub mod moderation; +pub mod oidc; pub mod presence; pub mod pusher; pub mod resolver; diff --git a/src/service/oidc/mod.rs b/src/service/oidc/mod.rs new file mode 100644 index 00000000..57802b54 --- /dev/null +++ b/src/service/oidc/mod.rs @@ -0,0 +1,87 @@ +/// 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 conduwuit::Result; +use oxide_auth::{ + frontends::simple::endpoint::{Generic, Vacant}, + primitives::{ + prelude::{ + AuthMap, + Authorizer, + Client, + ClientMap, + Issuer, + RandomGenerator, + Registrar, + TokenMap, + }, + registrar::RegisteredUrl, + }, +}; + +use async_trait::async_trait; +use std::sync::{Arc, Mutex}; + +pub struct Service { + registrar: Mutex, + authorizer: Mutex>, + issuer: Mutex>, +} + +#[async_trait] +impl crate::Service for Service { + fn build(_args: crate::Args<'_>) -> Result> { + Ok(Arc::new(Self::preconfigured())) + } + + fn name(&self) -> &str { crate::service::make_name(std::module_path!()) } +} + +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, + } + } +} diff --git a/src/service/services.rs b/src/service/services.rs index daece245..aaa7d941 100644 --- a/src/service/services.rs +++ b/src/service/services.rs @@ -109,6 +109,7 @@ impl Services { users: build!(users::Service), moderation: build!(moderation::Service), announcements: build!(announcements::Service), + oidc: build!(oidc::Service), manager: Mutex::new(None), service,