mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2025-07-08 13:40:01 +02:00
impl MSC2964: OIDC token flow
# Conflicts: # Cargo.lock
This commit is contained in:
parent
db3a2dc468
commit
5ab7b61129
11 changed files with 553 additions and 1 deletions
10
Cargo.toml
10
Cargo.toml
|
@ -103,6 +103,7 @@ features = [
|
||||||
"matched-path",
|
"matched-path",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"query",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies.axum-extra]
|
[workspace.dependencies.axum-extra]
|
||||||
|
@ -537,6 +538,15 @@ features = ["std"]
|
||||||
[workspace.dependencies.maplit]
|
[workspace.dependencies.maplit]
|
||||||
version = "1.0.2"
|
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
|
# Patches
|
||||||
#
|
#
|
||||||
|
|
|
@ -90,6 +90,9 @@ serde.workspace = true
|
||||||
sha1.workspace = true
|
sha1.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
oxide-auth.workspace = true
|
||||||
|
oxide-auth-axum.workspace = true
|
||||||
|
percent-encoding.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
214
src/api/client/oidc/authorize.rs
Normal file
214
src/api/client/oidc/authorize.rs
Normal file
|
@ -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<crate::State>,
|
||||||
|
Query(query): Query<OAuthQuery>,
|
||||||
|
oauth: OAuthRequest,
|
||||||
|
) -> Result<OAuthResponse> {
|
||||||
|
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<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `POST /_matrix/client/unstable/org.matrix.msc2964/authorize?allow=[Option<bool>]`
|
||||||
|
///
|
||||||
|
/// 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<Allowance>,
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
oauth: OAuthRequest,
|
||||||
|
) -> Result<OAuthResponse> {
|
||||||
|
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#"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<center>
|
||||||
|
<h1>{hostname} login</h1>
|
||||||
|
<form action="/_matrix/client/unstable/org.matrix.msc2964/login" method="post">
|
||||||
|
<input type="text" name="username" placeholder="Username" required>
|
||||||
|
<input type="password" name="password" placeholder="Password" required>
|
||||||
|
<input type="hidden" name="client_id" value="{client_id}">
|
||||||
|
<input type="hidden" name="redirect_uri" value="{redirect_uri}">
|
||||||
|
<input type="hidden" name="scope" value="{scope}">
|
||||||
|
<input type="hidden" name="state" value="{state}">
|
||||||
|
<input type="hidden" name="code_challenge" value="{code_challenge}">
|
||||||
|
<input type="hidden" name="code_challenge_method" value="{code_challenge_method}">
|
||||||
|
<input type="hidden" name="response_type" value="{response_type}">
|
||||||
|
<input type="hidden" name="response_mode" value="{response_mode}">
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</center>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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#"
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'>
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<center>
|
||||||
|
<h1>{hostname} login</h1>
|
||||||
|
'{client_id}' (at {redirect_uri}) is requesting permission for '{scope}'
|
||||||
|
<form method="post">
|
||||||
|
<input type="submit" value="Accept" formaction="{route}?{args}&allow=true">
|
||||||
|
<input type="submit" value="Deny" formaction="{route}?{args}&deny=true">
|
||||||
|
</form>
|
||||||
|
</center>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
}
|
130
src/api/client/oidc/login.rs
Normal file
130
src/api/client/oidc/login.rs
Normal file
|
@ -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<LoginForm> 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<crate::State>,
|
||||||
|
Form(login_query): Form<LoginForm>,
|
||||||
|
) -> Result<OAuthResponse> {
|
||||||
|
// 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<axum::body::Body> = 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:?}"))))
|
||||||
|
}
|
|
@ -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 discovery;
|
||||||
|
mod login;
|
||||||
|
mod authorize;
|
||||||
|
mod token;
|
||||||
|
|
||||||
pub(crate) use self::discovery::get_auth_metadata;
|
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;
|
||||||
|
|
78
src/api/client/oidc/token.rs
Normal file
78
src/api/client/oidc/token.rs
Normal file
|
@ -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<crate::State>,
|
||||||
|
oauth: OAuthRequest,
|
||||||
|
) -> Result<OAuthResponse> {
|
||||||
|
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<crate::State>,
|
||||||
|
oauth: OAuthRequest,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
const DENY_TEXT: &str = "<html>
|
||||||
|
This page should be accessed via an oauth token from the client in the example. Click
|
||||||
|
<a href=\"/authorize?response_type=code&client_id=LocalClient\">
|
||||||
|
here</a> to begin the authorization process.
|
||||||
|
</html>
|
||||||
|
";
|
||||||
|
|
||||||
|
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:?}"))))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -118,9 +118,18 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||||
.ruma_route(&client::get_protocols_route)
|
.ruma_route(&client::get_protocols_route)
|
||||||
.route("/_matrix/client/unstable/thirdparty/protocols",
|
.route("/_matrix/client/unstable/thirdparty/protocols",
|
||||||
get(client::get_protocols_route_unstable))
|
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",
|
.route("/_matrix/client/unstable/org.matrix.msc2965/auth_metadata",
|
||||||
get(client::get_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_message_event_route)
|
||||||
.ruma_route(&client::send_state_event_for_key_route)
|
.ruma_route(&client::send_state_event_for_key_route)
|
||||||
.ruma_route(&client::get_state_events_route)
|
.ruma_route(&client::get_state_events_route)
|
||||||
|
|
|
@ -111,6 +111,7 @@ webpage.workspace = true
|
||||||
webpage.optional = true
|
webpage.optional = true
|
||||||
blurhash.workspace = true
|
blurhash.workspace = true
|
||||||
blurhash.optional = true
|
blurhash.optional = true
|
||||||
|
oxide-auth.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
@ -19,6 +19,7 @@ pub mod globals;
|
||||||
pub mod key_backups;
|
pub mod key_backups;
|
||||||
pub mod media;
|
pub mod media;
|
||||||
pub mod moderation;
|
pub mod moderation;
|
||||||
|
pub mod oidc;
|
||||||
pub mod presence;
|
pub mod presence;
|
||||||
pub mod pusher;
|
pub mod pusher;
|
||||||
pub mod resolver;
|
pub mod resolver;
|
||||||
|
|
87
src/service/oidc/mod.rs
Normal file
87
src/service/oidc/mod.rs
Normal file
|
@ -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<ClientMap>,
|
||||||
|
authorizer: Mutex<AuthMap<RandomGenerator>>,
|
||||||
|
issuer: Mutex<TokenMap<RandomGenerator>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl crate::Service for Service {
|
||||||
|
fn build(_args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||||
|
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<impl Registrar + '_, impl Authorizer + '_, impl Issuer + '_> {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -109,6 +109,7 @@ impl Services {
|
||||||
users: build!(users::Service),
|
users: build!(users::Service),
|
||||||
moderation: build!(moderation::Service),
|
moderation: build!(moderation::Service),
|
||||||
announcements: build!(announcements::Service),
|
announcements: build!(announcements::Service),
|
||||||
|
oidc: build!(oidc::Service),
|
||||||
|
|
||||||
manager: Mutex::new(None),
|
manager: Mutex::new(None),
|
||||||
service,
|
service,
|
||||||
|
|
Loading…
Add table
Reference in a new issue