You've already forked authentication-service
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:
@ -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,
|
||||||
|
@ -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())
|
||||||
|
@ -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,
|
||||||
|
@ -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(),
|
||||||
|
@ -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?;
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
@ -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;
|
@ -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,
|
||||||
|
Reference in New Issue
Block a user