diff --git a/crates/data-model/src/oauth2/client.rs b/crates/data-model/src/oauth2/client.rs index b34dc271..27fd7037 100644 --- a/crates/data-model/src/oauth2/client.rs +++ b/crates/data-model/src/oauth2/client.rs @@ -1,4 +1,4 @@ -// Copyright 2021 The Matrix.org Foundation C.I.C. +// Copyright 2021, 2022 The Matrix.org Foundation C.I.C. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -82,6 +82,9 @@ pub struct Client { /// Client pub id_token_signed_response_alg: Option, + /// JWS alg algorithm REQUIRED for signing UserInfo Responses. + pub userinfo_signed_response_alg: Option, + /// Requested authentication method for the token endpoint pub token_endpoint_auth_method: Option, @@ -112,6 +115,7 @@ impl From> for Client<()> { tos_uri: c.tos_uri, jwks: c.jwks, id_token_signed_response_alg: c.id_token_signed_response_alg, + userinfo_signed_response_alg: c.userinfo_signed_response_alg, token_endpoint_auth_method: c.token_endpoint_auth_method, token_endpoint_auth_signing_alg: c.token_endpoint_auth_signing_alg, initiate_login_uri: c.initiate_login_uri, diff --git a/crates/handlers/src/oauth2/authorization.rs b/crates/handlers/src/oauth2/authorization.rs index a837f7fd..f4c8aa30 100644 --- a/crates/handlers/src/oauth2/authorization.rs +++ b/crates/handlers/src/oauth2/authorization.rs @@ -59,14 +59,20 @@ use oauth2_types::{ use rand::{distributions::Alphanumeric, thread_rng, Rng}; use serde::{Deserialize, Serialize}; use sqlx::{PgConnection, PgPool, Postgres, Transaction}; +use thiserror::Error; use url::Url; use crate::views::{LoginRequest, PostAuthAction, ReauthRequest, RegisterRequest}; +#[derive(Debug, Error)] pub enum RouteError { + #[error(transparent)] Internal(Box), + #[error(transparent)] Anyhow(anyhow::Error), + #[error("could not find client")] ClientNotFound, + #[error("invalid redirect uri")] InvalidRedirectUri, } @@ -218,6 +224,7 @@ fn resolve_response_mode( } #[allow(clippy::too_many_lines)] +#[tracing::instrument(skip_all, err)] pub(crate) async fn get( Extension(templates): Extension, Extension(pool): Extension, @@ -413,7 +420,10 @@ pub(crate) async fn get( let response = match res { Ok(r) => r, - Err(_e) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + Err(err) => { + tracing::error!(%err); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + } }; Ok((cookie_jar, response).into_response()) diff --git a/crates/handlers/src/oauth2/discovery.rs b/crates/handlers/src/oauth2/discovery.rs index ef836253..95cca56c 100644 --- a/crates/handlers/src/oauth2/discovery.rs +++ b/crates/handlers/src/oauth2/discovery.rs @@ -105,7 +105,8 @@ pub(crate) async fn get( let subject_types_supported = Some(vec![SubjectType::Public]); - let id_token_signing_alg_values_supported = jwt_signing_alg_values_supported; + let id_token_signing_alg_values_supported = jwt_signing_alg_values_supported.clone(); + let userinfo_signing_alg_values_supported = jwt_signing_alg_values_supported; let display_values_supported = Some(vec![Display::Page]); @@ -148,6 +149,7 @@ pub(crate) async fn get( userinfo_endpoint, subject_types_supported, id_token_signing_alg_values_supported, + userinfo_signing_alg_values_supported, display_values_supported, claim_types_supported, claims_supported, diff --git a/crates/handlers/src/oauth2/registration.rs b/crates/handlers/src/oauth2/registration.rs index 9dd8e64b..03742289 100644 --- a/crates/handlers/src/oauth2/registration.rs +++ b/crates/handlers/src/oauth2/registration.rs @@ -75,6 +75,7 @@ pub(crate) async fn post( body.jwks_uri.as_ref(), body.jwks.as_ref(), body.id_token_signed_response_alg, + body.userinfo_signed_response_alg, body.token_endpoint_auth_method, body.token_endpoint_auth_signing_alg, body.initiate_login_uri.as_ref(), diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index 3ce6e76c..1ae2f188 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -317,7 +317,13 @@ async fn authorization_code_grant( claims::AT_HASH.insert(&mut claims, hash(Sha256::new(), &access_token_str)?)?; claims::C_HASH.insert(&mut claims, hash(Sha256::new(), &grant.code)?)?; - let header = key_store.prepare_header(JsonWebSignatureAlg::Rs256).await?; + let header = key_store + .prepare_header( + client + .id_token_signed_response_alg + .unwrap_or(JsonWebSignatureAlg::Rs256), + ) + .await?; let id_token = DecodedJsonWebToken::new(header, claims); let id_token = id_token.sign(key_store).await?; diff --git a/crates/handlers/src/oauth2/userinfo.rs b/crates/handlers/src/oauth2/userinfo.rs index d36d9af2..358fa4ef 100644 --- a/crates/handlers/src/oauth2/userinfo.rs +++ b/crates/handlers/src/oauth2/userinfo.rs @@ -12,12 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::sync::Arc; + use axum::{ extract::Extension, response::{IntoResponse, Response}, - Json, + Json, TypedHeader, }; -use mas_axum_utils::{internal_error, user_authorization::UserAuthorization}; +use headers::ContentType; +use hyper::StatusCode; +use mas_axum_utils::{internal_error, user_authorization::UserAuthorization, UrlBuilder}; +use mas_jose::{DecodedJsonWebToken, SigningKeystore, StaticKeystore}; +use mime::Mime; use oauth2_types::scope; use serde::Serialize; use serde_with::skip_serializing_none; @@ -32,10 +38,21 @@ struct UserInfo { email_verified: Option, } +#[derive(Serialize)] +struct SignedUserInfo { + iss: String, + aud: String, + #[serde(flatten)] + user_info: UserInfo, +} + pub async fn get( + Extension(url_builder): Extension, Extension(pool): Extension, + Extension(key_store): Extension>, user_authorization: UserAuthorization, -) -> Result { +) -> Result { + // TODO: error handling let mut conn = pool .acquire() .await @@ -48,7 +65,7 @@ pub async fn get( .map_err(IntoResponse::into_response)?; let user = session.browser_session.user; - let mut res = UserInfo { + let mut user_info = UserInfo { sub: user.sub, username: user.username, email: None, @@ -57,10 +74,36 @@ pub async fn get( if session.scope.contains(&scope::EMAIL) { if let Some(email) = user.primary_email { - res.email_verified = Some(email.confirmed_at.is_some()); - res.email = Some(email.email); + user_info.email_verified = Some(email.confirmed_at.is_some()); + user_info.email = Some(email.email); } } - Ok(Json(res)) + if let Some(alg) = session.client.userinfo_signed_response_alg { + let header = key_store + .prepare_header(alg) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) + .map_err(IntoResponse::into_response)?; + + let user_info = SignedUserInfo { + iss: url_builder.oidc_issuer().to_string(), + aud: session.client.client_id, + user_info, + }; + + let user_info = DecodedJsonWebToken::new(header, user_info); + let user_info = user_info + .sign(key_store.as_ref()) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) + .map_err(IntoResponse::into_response)?; + + let token = user_info.serialize(); + let application_jwt: Mime = "application/jwt".parse().unwrap(); + let content_type = ContentType::from(application_jwt); + Ok((TypedHeader(content_type), token).into_response()) + } else { + Ok(Json(user_info).into_response()) + } } diff --git a/crates/storage/migrations/20220421091049_oauth2_client_more_metadata.down.sql b/crates/storage/migrations/20220421091049_oauth2_client_more_metadata.down.sql new file mode 100644 index 00000000..95917ea3 --- /dev/null +++ b/crates/storage/migrations/20220421091049_oauth2_client_more_metadata.down.sql @@ -0,0 +1,16 @@ +-- Copyright 2022 The Matrix.org Foundation C.I.C. +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +ALTER TABLE oauth2_clients + DROP COLUMN "userinfo_signed_response_alg" TEXT; diff --git a/crates/storage/migrations/20220421091049_oauth2_client_more_metadata.up.sql b/crates/storage/migrations/20220421091049_oauth2_client_more_metadata.up.sql new file mode 100644 index 00000000..1b30a733 --- /dev/null +++ b/crates/storage/migrations/20220421091049_oauth2_client_more_metadata.up.sql @@ -0,0 +1,16 @@ +-- Copyright 2022 The Matrix.org Foundation C.I.C. +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +ALTER TABLE oauth2_clients + ADD COLUMN "userinfo_signed_response_alg" TEXT; diff --git a/crates/storage/src/oauth2/client.rs b/crates/storage/src/oauth2/client.rs index 2468e4d7..ede88845 100644 --- a/crates/storage/src/oauth2/client.rs +++ b/crates/storage/src/oauth2/client.rs @@ -45,6 +45,7 @@ pub struct OAuth2ClientLookup { jwks_uri: Option, jwks: Option, id_token_signed_response_alg: Option, + userinfo_signed_response_alg: Option, token_endpoint_auth_method: Option, token_endpoint_auth_signing_alg: Option, initiate_login_uri: Option, @@ -153,6 +154,15 @@ impl TryInto> for OAuth2ClientLookup { source, })?; + let userinfo_signed_response_alg = self + .userinfo_signed_response_alg + .map(|s| s.parse()) + .transpose() + .map_err(|source| ClientFetchError::ParseField { + field: "userinfo_signed_response_alg", + source, + })?; + let token_endpoint_auth_method = self .token_endpoint_auth_method .map(|s| s.parse()) @@ -214,6 +224,7 @@ impl TryInto> for OAuth2ClientLookup { tos_uri, jwks, id_token_signed_response_alg, + userinfo_signed_response_alg, token_endpoint_auth_method, token_endpoint_auth_signing_alg, initiate_login_uri, @@ -245,6 +256,7 @@ pub async fn lookup_client( c.jwks_uri, c.jwks, c.id_token_signed_response_alg, + c.userinfo_signed_response_alg, c.token_endpoint_auth_method, c.token_endpoint_auth_signing_alg, c.initiate_login_uri @@ -286,6 +298,7 @@ pub async fn lookup_client_by_client_id( c.jwks_uri, c.jwks, c.id_token_signed_response_alg, + c.userinfo_signed_response_alg, c.token_endpoint_auth_method, c.token_endpoint_auth_signing_alg, c.initiate_login_uri @@ -320,6 +333,7 @@ pub async fn insert_client( jwks_uri: Option<&Url>, jwks: Option<&JsonWebKeySet>, id_token_signed_response_alg: Option, + userinfo_signed_response_alg: Option, token_endpoint_auth_method: Option, token_endpoint_auth_signing_alg: Option, initiate_login_uri: Option<&Url>, @@ -334,6 +348,7 @@ pub async fn insert_client( let jwks = jwks.map(serde_json::to_value).transpose().unwrap(); // TODO let jwks_uri = jwks_uri.map(Url::as_str); let id_token_signed_response_alg = id_token_signed_response_alg.map(|v| v.to_string()); + let userinfo_signed_response_alg = userinfo_signed_response_alg.map(|v| v.to_string()); let token_endpoint_auth_method = token_endpoint_auth_method.map(|v| v.to_string()); let token_endpoint_auth_signing_alg = token_endpoint_auth_signing_alg.map(|v| v.to_string()); let initiate_login_uri = initiate_login_uri.map(Url::as_str); @@ -355,11 +370,12 @@ pub async fn insert_client( jwks_uri, jwks, id_token_signed_response_alg, + userinfo_signed_response_alg, token_endpoint_auth_method, token_endpoint_auth_signing_alg, initiate_login_uri) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) RETURNING id "#, client_id, @@ -376,6 +392,7 @@ pub async fn insert_client( jwks_uri, jwks, id_token_signed_response_alg, + userinfo_signed_response_alg, token_endpoint_auth_method, token_endpoint_auth_signing_alg, initiate_login_uri,