1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-29 22:01:14 +03:00

Support signed userinfo responses

This commit is contained in:
Quentin Gliech
2022-04-21 11:49:49 +02:00
parent 0c8656f464
commit 25193ebaa5
9 changed files with 127 additions and 12 deletions

View File

@ -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"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -82,6 +82,9 @@ pub struct Client<T: StorageBackend> {
/// Client /// Client
pub id_token_signed_response_alg: Option<JsonWebSignatureAlg>, pub id_token_signed_response_alg: Option<JsonWebSignatureAlg>,
/// JWS alg algorithm REQUIRED for signing UserInfo Responses.
pub userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
/// Requested authentication method for the token endpoint /// Requested authentication method for the token endpoint
pub token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>, pub token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>,
@ -112,6 +115,7 @@ impl<S: StorageBackendMarker> From<Client<S>> for Client<()> {
tos_uri: c.tos_uri, tos_uri: c.tos_uri,
jwks: c.jwks, jwks: c.jwks,
id_token_signed_response_alg: c.id_token_signed_response_alg, 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_method: c.token_endpoint_auth_method,
token_endpoint_auth_signing_alg: c.token_endpoint_auth_signing_alg, token_endpoint_auth_signing_alg: c.token_endpoint_auth_signing_alg,
initiate_login_uri: c.initiate_login_uri, initiate_login_uri: c.initiate_login_uri,

View File

@ -59,14 +59,20 @@ use oauth2_types::{
use rand::{distributions::Alphanumeric, thread_rng, Rng}; use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{PgConnection, PgPool, Postgres, Transaction}; use sqlx::{PgConnection, PgPool, Postgres, Transaction};
use thiserror::Error;
use url::Url; use url::Url;
use crate::views::{LoginRequest, PostAuthAction, ReauthRequest, RegisterRequest}; use crate::views::{LoginRequest, PostAuthAction, ReauthRequest, RegisterRequest};
#[derive(Debug, Error)]
pub enum RouteError { pub enum RouteError {
#[error(transparent)]
Internal(Box<dyn std::error::Error + Send + Sync + 'static>), Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
#[error(transparent)]
Anyhow(anyhow::Error), Anyhow(anyhow::Error),
#[error("could not find client")]
ClientNotFound, ClientNotFound,
#[error("invalid redirect uri")]
InvalidRedirectUri, InvalidRedirectUri,
} }
@ -218,6 +224,7 @@ fn resolve_response_mode(
} }
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
#[tracing::instrument(skip_all, err)]
pub(crate) async fn get( pub(crate) async fn get(
Extension(templates): Extension<Templates>, Extension(templates): Extension<Templates>,
Extension(pool): Extension<PgPool>, Extension(pool): Extension<PgPool>,
@ -413,7 +420,10 @@ pub(crate) async fn get(
let response = match res { let response = match res {
Ok(r) => r, 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()) Ok((cookie_jar, response).into_response())

View File

@ -105,7 +105,8 @@ pub(crate) async fn get(
let subject_types_supported = Some(vec![SubjectType::Public]); 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]); let display_values_supported = Some(vec![Display::Page]);
@ -148,6 +149,7 @@ pub(crate) async fn get(
userinfo_endpoint, userinfo_endpoint,
subject_types_supported, subject_types_supported,
id_token_signing_alg_values_supported, id_token_signing_alg_values_supported,
userinfo_signing_alg_values_supported,
display_values_supported, display_values_supported,
claim_types_supported, claim_types_supported,
claims_supported, claims_supported,

View File

@ -75,6 +75,7 @@ pub(crate) async fn post(
body.jwks_uri.as_ref(), body.jwks_uri.as_ref(),
body.jwks.as_ref(), body.jwks.as_ref(),
body.id_token_signed_response_alg, body.id_token_signed_response_alg,
body.userinfo_signed_response_alg,
body.token_endpoint_auth_method, body.token_endpoint_auth_method,
body.token_endpoint_auth_signing_alg, body.token_endpoint_auth_signing_alg,
body.initiate_login_uri.as_ref(), body.initiate_login_uri.as_ref(),

View File

@ -317,7 +317,13 @@ async fn authorization_code_grant(
claims::AT_HASH.insert(&mut claims, hash(Sha256::new(), &access_token_str)?)?; claims::AT_HASH.insert(&mut claims, hash(Sha256::new(), &access_token_str)?)?;
claims::C_HASH.insert(&mut claims, hash(Sha256::new(), &grant.code)?)?; 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 = DecodedJsonWebToken::new(header, claims);
let id_token = id_token.sign(key_store).await?; let id_token = id_token.sign(key_store).await?;

View File

@ -12,12 +12,18 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::sync::Arc;
use axum::{ use axum::{
extract::Extension, extract::Extension,
response::{IntoResponse, Response}, 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 oauth2_types::scope;
use serde::Serialize; use serde::Serialize;
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
@ -32,10 +38,21 @@ struct UserInfo {
email_verified: Option<bool>, email_verified: Option<bool>,
} }
#[derive(Serialize)]
struct SignedUserInfo {
iss: String,
aud: String,
#[serde(flatten)]
user_info: UserInfo,
}
pub async fn get( pub async fn get(
Extension(url_builder): Extension<UrlBuilder>,
Extension(pool): Extension<PgPool>, Extension(pool): Extension<PgPool>,
Extension(key_store): Extension<Arc<StaticKeystore>>,
user_authorization: UserAuthorization, user_authorization: UserAuthorization,
) -> Result<impl IntoResponse, Response> { ) -> Result<Response, Response> {
// TODO: error handling
let mut conn = pool let mut conn = pool
.acquire() .acquire()
.await .await
@ -48,7 +65,7 @@ pub async fn get(
.map_err(IntoResponse::into_response)?; .map_err(IntoResponse::into_response)?;
let user = session.browser_session.user; let user = session.browser_session.user;
let mut res = UserInfo { let mut user_info = UserInfo {
sub: user.sub, sub: user.sub,
username: user.username, username: user.username,
email: None, email: None,
@ -57,10 +74,36 @@ pub async fn get(
if session.scope.contains(&scope::EMAIL) { if session.scope.contains(&scope::EMAIL) {
if let Some(email) = user.primary_email { if let Some(email) = user.primary_email {
res.email_verified = Some(email.confirmed_at.is_some()); user_info.email_verified = Some(email.confirmed_at.is_some());
res.email = Some(email.email); 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())
}
} }

View File

@ -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;

View File

@ -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;

View File

@ -45,6 +45,7 @@ pub struct OAuth2ClientLookup {
jwks_uri: Option<String>, jwks_uri: Option<String>,
jwks: Option<serde_json::Value>, jwks: Option<serde_json::Value>,
id_token_signed_response_alg: Option<String>, id_token_signed_response_alg: Option<String>,
userinfo_signed_response_alg: Option<String>,
token_endpoint_auth_method: Option<String>, token_endpoint_auth_method: Option<String>,
token_endpoint_auth_signing_alg: Option<String>, token_endpoint_auth_signing_alg: Option<String>,
initiate_login_uri: Option<String>, initiate_login_uri: Option<String>,
@ -153,6 +154,15 @@ impl TryInto<Client<PostgresqlBackend>> for OAuth2ClientLookup {
source, 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 let token_endpoint_auth_method = self
.token_endpoint_auth_method .token_endpoint_auth_method
.map(|s| s.parse()) .map(|s| s.parse())
@ -214,6 +224,7 @@ impl TryInto<Client<PostgresqlBackend>> for OAuth2ClientLookup {
tos_uri, tos_uri,
jwks, jwks,
id_token_signed_response_alg, id_token_signed_response_alg,
userinfo_signed_response_alg,
token_endpoint_auth_method, token_endpoint_auth_method,
token_endpoint_auth_signing_alg, token_endpoint_auth_signing_alg,
initiate_login_uri, initiate_login_uri,
@ -245,6 +256,7 @@ pub async fn lookup_client(
c.jwks_uri, c.jwks_uri,
c.jwks, c.jwks,
c.id_token_signed_response_alg, c.id_token_signed_response_alg,
c.userinfo_signed_response_alg,
c.token_endpoint_auth_method, c.token_endpoint_auth_method,
c.token_endpoint_auth_signing_alg, c.token_endpoint_auth_signing_alg,
c.initiate_login_uri c.initiate_login_uri
@ -286,6 +298,7 @@ pub async fn lookup_client_by_client_id(
c.jwks_uri, c.jwks_uri,
c.jwks, c.jwks,
c.id_token_signed_response_alg, c.id_token_signed_response_alg,
c.userinfo_signed_response_alg,
c.token_endpoint_auth_method, c.token_endpoint_auth_method,
c.token_endpoint_auth_signing_alg, c.token_endpoint_auth_signing_alg,
c.initiate_login_uri c.initiate_login_uri
@ -320,6 +333,7 @@ pub async fn insert_client(
jwks_uri: Option<&Url>, jwks_uri: Option<&Url>,
jwks: Option<&JsonWebKeySet>, jwks: Option<&JsonWebKeySet>,
id_token_signed_response_alg: Option<JsonWebSignatureAlg>, id_token_signed_response_alg: Option<JsonWebSignatureAlg>,
userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>, token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>,
token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>, token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
initiate_login_uri: Option<&Url>, 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 = jwks.map(serde_json::to_value).transpose().unwrap(); // TODO
let jwks_uri = jwks_uri.map(Url::as_str); 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 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_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 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); let initiate_login_uri = initiate_login_uri.map(Url::as_str);
@ -355,11 +370,12 @@ pub async fn insert_client(
jwks_uri, jwks_uri,
jwks, jwks,
id_token_signed_response_alg, id_token_signed_response_alg,
userinfo_signed_response_alg,
token_endpoint_auth_method, token_endpoint_auth_method,
token_endpoint_auth_signing_alg, token_endpoint_auth_signing_alg,
initiate_login_uri) initiate_login_uri)
VALUES 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 RETURNING id
"#, "#,
client_id, client_id,
@ -376,6 +392,7 @@ pub async fn insert_client(
jwks_uri, jwks_uri,
jwks, jwks,
id_token_signed_response_alg, id_token_signed_response_alg,
userinfo_signed_response_alg,
token_endpoint_auth_method, token_endpoint_auth_method,
token_endpoint_auth_signing_alg, token_endpoint_auth_signing_alg,
initiate_login_uri, initiate_login_uri,