diff --git a/crates/core/src/filters/client.rs b/crates/core/src/filters/client.rs index acb3619c..5a77707d 100644 --- a/crates/core/src/filters/client.rs +++ b/crates/core/src/filters/client.rs @@ -14,44 +14,32 @@ //! Handle client authentication +use std::borrow::Cow; + +use chrono::{Duration, Utc}; use headers::{authorization::Basic, Authorization}; -use serde::{de::DeserializeOwned, Deserialize}; +use jwt_compact::{ + alg::{Hs256, Hs256Key, Hs384, Hs384Key, Hs512, Hs512Key}, + Algorithm, AlgorithmExt, AlgorithmSignature, TimeOptions, Token, UntrustedToken, +}; +use oauth2_types::requests::ClientAuthenticationMethod; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_with::skip_serializing_none; use thiserror::Error; use warp::{reject::Reject, Filter, Rejection}; use super::headers::typed_header; -use crate::config::{OAuth2ClientConfig, OAuth2Config}; - -/// Type of client authentication that succeeded -#[derive(Debug, PartialEq, Eq)] -pub enum ClientAuthentication { - /// `client_secret_basic` authentication, where the `client_id` and - /// `client_secret` are sent through the `Authorization` header with - /// `Basic` authentication - ClientSecretBasic, - - /// `client_secret_post` authentication, where the `client_id` and - /// `client_secret` are sent in the request body - ClientSecretPost, - - /// `none` authentication for public clients, where only the `client_id` is - /// sent in the request body - None, -} - -impl ClientAuthentication { - #[must_use] - /// Check if the authenticated client is public or not - pub fn public(&self) -> bool { - matches!(self, &Self::None) - } -} +use crate::{ + config::{OAuth2ClientConfig, OAuth2Config}, + errors::WrapError, +}; /// Protect an enpoint with client authentication #[must_use] pub fn client_authentication( oauth2_config: &OAuth2Config, -) -> impl Filter + audience: String, +) -> impl Filter + Clone + Send + Sync @@ -64,25 +52,19 @@ pub fn client_authentication( let client_id = auth.0.username().to_string(); let client_secret = Some(auth.0.password().to_string()); ( - ClientAuthentication::ClientSecretBasic, - client_id, - client_secret, + ClientCredentials::Pair { + via: CredentialsVia::AuthorizationHeader, + client_id, + client_secret, + }, body, ) }) // Or from the form body .or(warp::body::form().map(|form: ClientAuthForm| { - let ClientAuthForm { - client_id, - client_secret, - body, - } = form; - let auth_type = if client_secret.is_some() { - ClientAuthentication::ClientSecretPost - } else { - ClientAuthentication::None - }; - (auth_type, client_id, client_secret, body) + let ClientAuthForm { credentials, body } = form; + + (credentials, body) })) .unify() .untuple_one(); @@ -90,6 +72,7 @@ pub fn client_authentication( let clients = oauth2_config.clients.clone(); warp::any() .map(move || clients.clone()) + .and(warp::any().map(move || audience.clone())) .and(credentials) .and_then(authenticate_client) .untuple_one() @@ -108,39 +91,257 @@ enum ClientAuthenticationError { #[error("client secret required for client {client_id:?}")] ClientSecretRequired { client_id: String }, + + #[error("wrong audience in client assertion: expected {expected:?}, got {got:?}")] + AudienceMismatch { expected: String, got: String }, + + #[error("invalid client assertion")] + InvalidAssertion, } impl Reject for ClientAuthenticationError {} +#[skip_serializing_none] +#[derive(Serialize, Deserialize)] +struct ClientAssertionClaims { + #[serde(rename = "iss")] + issuer: String, + #[serde(rename = "sub")] + subject: String, + #[serde(rename = "aud")] + audience: String, + // TODO: use the JTI and ensure it is only used once + #[serde(default, rename = "jti")] + jwt_id: Option, +} + +struct UnsignedSignature(Vec); +impl AlgorithmSignature for UnsignedSignature { + fn try_from_slice(slice: &[u8]) -> anyhow::Result { + Ok(Self(slice.to_vec())) + } + + fn as_bytes(&self) -> std::borrow::Cow<'_, [u8]> { + Cow::Borrowed(&self.0) + } +} + +struct Unsigned<'a>(&'a str); +impl<'a> Algorithm for Unsigned<'a> { + type SigningKey = (); + + type VerifyingKey = (); + + type Signature = UnsignedSignature; + + fn name(&self) -> std::borrow::Cow<'static, str> { + Cow::Owned(self.0.to_string()) + } + + fn sign(&self, _signing_key: &Self::SigningKey, _message: &[u8]) -> Self::Signature { + UnsignedSignature(Vec::new()) + } + + fn verify_signature( + &self, + _signature: &Self::Signature, + _verifying_key: &Self::VerifyingKey, + _message: &[u8], + ) -> bool { + true + } +} + +fn verify_token( + untrusted_token: &UntrustedToken, + key: &str, +) -> anyhow::Result> { + match untrusted_token.algorithm() { + "HS256" => { + let key = Hs256Key::new(key); + let token = Hs256.validate_integrity(untrusted_token, &key)?; + Ok(token) + } + "HS384" => { + let key = Hs384Key::new(key); + let token = Hs384.validate_integrity(untrusted_token, &key)?; + Ok(token) + } + "HS512" => { + let key = Hs512Key::new(key); + let token = Hs512.validate_integrity(untrusted_token, &key)?; + Ok(token) + } + alg => anyhow::bail!("unsupported signing algorithm {}", alg), + } +} + async fn authenticate_client( clients: Vec, - auth_type: ClientAuthentication, - client_id: String, - client_secret: Option, + audience: String, + credentials: ClientCredentials, body: T, -) -> Result<(ClientAuthentication, OAuth2ClientConfig, T), Rejection> { - let client = clients - .iter() - .find(|client| client.client_id == client_id) - .ok_or_else(|| ClientAuthenticationError::ClientNotFound { - client_id: client_id.to_string(), - })?; +) -> Result<(ClientAuthenticationMethod, OAuth2ClientConfig, T), Rejection> { + let auth_type = credentials.authentication_type(); + let client = match credentials { + ClientCredentials::Pair { + client_id, + client_secret, + .. + } => { + let client = clients + .iter() + .find(|client| client.client_id == client_id) + .ok_or_else(|| ClientAuthenticationError::ClientNotFound { + client_id: client_id.to_string(), + })?; - let client = match (client_secret, client.client_secret.as_ref()) { - (None, None) => Ok(client), - (Some(ref given), Some(expected)) if given == expected => Ok(client), - (Some(_), Some(_)) => Err(ClientAuthenticationError::ClientSecretMismatch { client_id }), - (Some(_), None) => Err(ClientAuthenticationError::NoClientSecret { client_id }), - (None, Some(_)) => Err(ClientAuthenticationError::ClientSecretRequired { client_id }), + match (client_secret, client.client_secret.as_ref()) { + (None, None) => Ok(client), + (Some(ref given), Some(expected)) if given == expected => Ok(client), + (Some(_), Some(_)) => { + Err(ClientAuthenticationError::ClientSecretMismatch { client_id }) + } + (Some(_), None) => Err(ClientAuthenticationError::NoClientSecret { client_id }), + (None, Some(_)) => { + Err(ClientAuthenticationError::ClientSecretRequired { client_id }) + } + } + } + ClientCredentials::Assertion { + client_id, + client_assertion_type: ClientAssertionType::JwtBearer, + client_assertion, + } => { + let untrusted_token = UntrustedToken::new(&client_assertion).wrap_error()?; + + // client_id might have been passed as parameter. If not, it should be inferred + // from the token, as per rfc7521 sec. 4.2 + // TODO: this is not a pretty way to do it + let client_id = client_id + .ok_or(()) // Dumb error type + .or_else(|()| { + let alg = Unsigned(untrusted_token.algorithm()); + // We need to deserialize the token once without verifying the signature to get + // the client_id + let token: Token = + alg.validate_integrity(&untrusted_token, &())?; + + Ok::<_, anyhow::Error>(token.claims().custom.subject.clone()) + }) + .wrap_error()?; + + let client = clients + .iter() + .find(|client| client.client_id == client_id) + .ok_or_else(|| ClientAuthenticationError::ClientNotFound { + client_id: client_id.to_string(), + })?; + + if let Some(client_secret) = &client.client_secret { + let token = verify_token(&untrusted_token, client_secret).wrap_error()?; + + let time_options = TimeOptions::new(Duration::minutes(1), Utc::now); + + // rfc7523 sec. 3.4: expiration must be set and validated + let claims = token + .claims() + .validate_expiration(&time_options) + .wrap_error()?; + + // rfc7523 sec. 3.5: "not before" can be set and must be validated if present + if claims.not_before.is_some() { + claims.validate_maturity(&time_options).wrap_error()?; + } + + // rfc7523 sec. 3.3: the audience is the URL being called + if claims.custom.audience != audience { + Err(ClientAuthenticationError::AudienceMismatch { + expected: audience, + got: claims.custom.audience.clone(), + }) + // rfc7523 sec. 3.1 & 3.2: both the issuer and the subject must + // match the client_id + } else if claims.custom.issuer != claims.custom.subject + || claims.custom.issuer != client_id + { + Err(ClientAuthenticationError::InvalidAssertion) + } else { + Ok(client) + } + } else { + Err(ClientAuthenticationError::ClientSecretRequired { + client_id: client_id.to_string(), + }) + } + } }?; Ok((auth_type, client.clone(), body)) } +#[derive(Deserialize)] +enum ClientAssertionType { + #[serde(rename = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")] + JwtBearer, +} + +enum CredentialsVia { + FormBody, + AuthorizationHeader, +} + +impl Default for CredentialsVia { + fn default() -> Self { + Self::FormBody + } +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum ClientCredentials { + // Order here is important: serde tries to deserialize enum variants in order, so if "Pair" + // was before "Assertion", a client_assertion with a client_id would match the "Pair" + // variant first + Assertion { + client_id: Option, + client_assertion_type: ClientAssertionType, + client_assertion: String, + }, + Pair { + #[serde(skip)] + via: CredentialsVia, + client_id: String, + client_secret: Option, + }, +} + +impl ClientCredentials { + fn authentication_type(&self) -> ClientAuthenticationMethod { + match self { + ClientCredentials::Pair { + via: CredentialsVia::FormBody, + client_secret: None, + .. + } => ClientAuthenticationMethod::None, + ClientCredentials::Pair { + via: CredentialsVia::FormBody, + client_secret: Some(_), + .. + } => ClientAuthenticationMethod::ClientSecretPost, + ClientCredentials::Pair { + via: CredentialsVia::AuthorizationHeader, + .. + } => ClientAuthenticationMethod::ClientSecretBasic, + ClientCredentials::Assertion { .. } => ClientAuthenticationMethod::ClientSecretJwt, + } + } +} + #[derive(Deserialize)] struct ClientAuthForm { - client_id: String, - client_secret: Option, + #[serde(flatten)] + credentials: ClientCredentials, #[serde(flatten)] body: T, @@ -148,10 +349,16 @@ struct ClientAuthForm { #[cfg(test)] mod tests { + use headers::authorization::Credentials; + use jwt_compact::{Claims, Header}; use mas_config::ConfigurationSection; + use serde_json::json; use super::*; + // Long client_secret to support it as a HS512 key + const CLIENT_SECRET: &str = "leek2zaeyeb8thai7piehea3vah6ool9oanin9aeraThuci9EeghaekaiD1upe4Quoh7xeMae2meitohj0Waaveiwaorah1yazohr6Vae7iebeiRaWene5IeWeeciezu"; + fn oauth2_config() -> OAuth2Config { let mut config = OAuth2Config::test(); config.clients.push(OAuth2ClientConfig { @@ -161,7 +368,12 @@ mod tests { }); config.clients.push(OAuth2ClientConfig { client_id: "confidential".to_string(), - client_secret: Some("secret".to_string()), + client_secret: Some(CLIENT_SECRET.to_string()), + redirect_uris: Vec::new(), + }); + config.clients.push(OAuth2ClientConfig { + client_id: "confidential-2".to_string(), + client_secret: Some(CLIENT_SECRET.to_string()), redirect_uris: Vec::new(), }); config @@ -174,17 +386,126 @@ mod tests { } #[tokio::test] - async fn client_secret_post() { - let filter = client_authentication::
(&oauth2_config()); + async fn client_secret_jwt_hs256() { + client_secret_jwt::<'_, Hs256>().await; + } + + #[tokio::test] + async fn client_secret_jwt_hs384() { + client_secret_jwt::<'_, Hs384>().await; + } + + #[tokio::test] + async fn client_secret_jwt_hs512() { + client_secret_jwt::<'_, Hs512>().await; + } + + async fn client_secret_jwt<'k, A>() + where + A: Algorithm + Default, + A::SigningKey: From<&'k [u8]>, + { + let audience = "https://example.com/token".to_string(); + let filter = client_authentication::(&oauth2_config(), audience.clone()); + let time_options = TimeOptions::default(); + + let key = A::SigningKey::from(CLIENT_SECRET.as_bytes()); + let alg = A::default(); + let header = Header::default(); + let claims = Claims::new(ClientAssertionClaims { + issuer: "confidential".to_string(), + subject: "confidential".to_string(), + audience, + jwt_id: None, + }) + .set_duration_and_issuance(&time_options, Duration::seconds(15)); + + // TODO: test failing cases + // - expired token + // - "not before" in the future + // - subject/issuer mismatch + // - audience mismatch + // - wrong secret/signature + + let token = alg + .token(header, &claims, &key) + .expect("could not sign token"); let (auth, client, body) = warp::test::request() .method("POST") - .body("client_id=confidential&client_secret=secret&foo=baz&bar=foobar") + .header("Content-Type", mime::APPLICATION_WWW_FORM_URLENCODED.to_string()) + .body(serde_urlencoded::to_string(json!({ + "client_id": "confidential", + "client_assertion": token, + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "foo": "baz", + "bar": "foobar", + })).unwrap()) .filter(&filter) .await .unwrap(); - assert_eq!(auth, ClientAuthentication::ClientSecretPost); + assert_eq!(auth, ClientAuthenticationMethod::ClientSecretJwt); + assert_eq!(client.client_id, "confidential"); + assert_eq!(body.foo, "baz"); + assert_eq!(body.bar, "foobar"); + + // Without client_id + let res = warp::test::request() + .method("POST") + .header("Content-Type", mime::APPLICATION_WWW_FORM_URLENCODED.to_string()) + .body(serde_urlencoded::to_string(json!({ + "client_assertion": token, + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "foo": "baz", + "bar": "foobar", + })).unwrap()) + .filter(&filter) + .await; + assert!(res.is_ok()); + + // client_id mismatch + let res = warp::test::request() + .method("POST") + .body(serde_urlencoded::to_string(json!({ + "client_id": "confidential-2", + "client_assertion": token, + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "foo": "baz", + "bar": "foobar", + })).unwrap()) + .filter(&filter) + .await; + assert!(res.is_err()); + } + + #[tokio::test] + async fn client_secret_post() { + let filter = client_authentication::( + &oauth2_config(), + "https://example.com/token".to_string(), + ); + + let (auth, client, body) = warp::test::request() + .method("POST") + .header( + "Content-Type", + mime::APPLICATION_WWW_FORM_URLENCODED.to_string(), + ) + .body( + serde_urlencoded::to_string(json!({ + "client_id": "confidential", + "client_secret": CLIENT_SECRET, + "foo": "baz", + "bar": "foobar", + })) + .unwrap(), + ) + .filter(&filter) + .await + .unwrap(); + + assert_eq!(auth, ClientAuthenticationMethod::ClientSecretPost); assert_eq!(client.client_id, "confidential"); assert_eq!(body.foo, "baz"); assert_eq!(body.bar, "foobar"); @@ -192,17 +513,31 @@ mod tests { #[tokio::test] async fn client_secret_basic() { - let filter = client_authentication::(&oauth2_config()); + let filter = client_authentication::( + &oauth2_config(), + "https://example.com/token".to_string(), + ); + let auth = Authorization::basic("confidential", CLIENT_SECRET); let (auth, client, body) = warp::test::request() .method("POST") - .header("Authorization", "Basic Y29uZmlkZW50aWFsOnNlY3JldA==") - .body("foo=baz&bar=foobar") + .header( + "Content-Type", + mime::APPLICATION_WWW_FORM_URLENCODED.to_string(), + ) + .header("Authorization", auth.0.encode()) + .body( + serde_urlencoded::to_string(json!({ + "foo": "baz", + "bar": "foobar", + })) + .unwrap(), + ) .filter(&filter) .await .unwrap(); - assert_eq!(auth, ClientAuthentication::ClientSecretBasic); + assert_eq!(auth, ClientAuthenticationMethod::ClientSecretBasic); assert_eq!(client.client_id, "confidential"); assert_eq!(body.foo, "baz"); assert_eq!(body.bar, "foobar"); @@ -210,16 +545,30 @@ mod tests { #[tokio::test] async fn none() { - let filter = client_authentication::(&oauth2_config()); + let filter = client_authentication::( + &oauth2_config(), + "https://example.com/token".to_string(), + ); let (auth, client, body) = warp::test::request() .method("POST") - .body("client_id=public&foo=baz&bar=foobar") + .header( + "Content-Type", + mime::APPLICATION_WWW_FORM_URLENCODED.to_string(), + ) + .body( + serde_urlencoded::to_string(json!({ + "client_id": "public", + "foo": "baz", + "bar": "foobar", + })) + .unwrap(), + ) .filter(&filter) .await .unwrap(); - assert_eq!(auth, ClientAuthentication::None); + assert_eq!(auth, ClientAuthenticationMethod::None); assert_eq!(client.client_id, "public"); assert_eq!(body.foo, "baz"); assert_eq!(body.bar, "foobar"); diff --git a/crates/core/src/handlers/oauth2/authorization.rs b/crates/core/src/handlers/oauth2/authorization.rs index 67a6402d..2685df9a 100644 --- a/crates/core/src/handlers/oauth2/authorization.rs +++ b/crates/core/src/handlers/oauth2/authorization.rs @@ -113,6 +113,15 @@ where params: T, } + #[derive(Serialize)] + struct ParamsWithState { + #[serde(skip_serializing_if = "Option::is_none")] + state: Option, + + #[serde(flatten)] + params: T, + } + match response_mode { ResponseMode::Query => { let existing: Option> = redirect_uri @@ -159,7 +168,8 @@ where ))) } ResponseMode::FormPost => { - let ctx = FormPostContext::new(redirect_uri, params); + let merged = ParamsWithState { state, params }; + let ctx = FormPostContext::new(redirect_uri, merged); let rendered = templates.render_form_post(&ctx)?; Ok(Box::new(html(rendered))) } diff --git a/crates/core/src/handlers/oauth2/discovery.rs b/crates/core/src/handlers/oauth2/discovery.rs index 64d227fe..b93f017d 100644 --- a/crates/core/src/handlers/oauth2/discovery.rs +++ b/crates/core/src/handlers/oauth2/discovery.rs @@ -17,7 +17,7 @@ use std::collections::HashSet; use hyper::Method; use mas_config::OAuth2Config; use oauth2_types::{ - oidc::Metadata, + oidc::{Metadata, SigningAlgorithm}, pkce::CodeChallengeMethod, requests::{ClientAuthenticationMethod, GrantType, ResponseMode}, }; @@ -61,10 +61,19 @@ pub(super) fn filter( let mut s = HashSet::new(); s.insert(ClientAuthenticationMethod::ClientSecretBasic); s.insert(ClientAuthenticationMethod::ClientSecretPost); + s.insert(ClientAuthenticationMethod::ClientSecretJwt); s.insert(ClientAuthenticationMethod::None); s }); + let token_endpoint_auth_signing_alg_values_supported = Some({ + let mut s = HashSet::new(); + s.insert(SigningAlgorithm::Hs256); + s.insert(SigningAlgorithm::Hs384); + s.insert(SigningAlgorithm::Hs512); + s + }); + let code_challenge_methods_supported = Some({ let mut s = HashSet::new(); s.insert(CodeChallengeMethod::Plain); @@ -85,6 +94,7 @@ pub(super) fn filter( response_modes_supported, grant_types_supported, token_endpoint_auth_methods_supported, + token_endpoint_auth_signing_alg_values_supported, code_challenge_methods_supported, }; diff --git a/crates/core/src/handlers/oauth2/introspection.rs b/crates/core/src/handlers/oauth2/introspection.rs index 5757960b..e8a6316e 100644 --- a/crates/core/src/handlers/oauth2/introspection.rs +++ b/crates/core/src/handlers/oauth2/introspection.rs @@ -13,7 +13,9 @@ // limitations under the License. use hyper::Method; -use oauth2_types::requests::{IntrospectionRequest, IntrospectionResponse, TokenTypeHint}; +use oauth2_types::requests::{ + ClientAuthenticationMethod, IntrospectionRequest, IntrospectionResponse, TokenTypeHint, +}; use sqlx::{pool::PoolConnection, PgPool, Postgres}; use tracing::{info, warn}; use warp::{Filter, Rejection, Reply}; @@ -21,11 +23,7 @@ use warp::{Filter, Rejection, Reply}; use crate::{ config::{OAuth2ClientConfig, OAuth2Config}, errors::WrapError, - filters::{ - client::{client_authentication, ClientAuthentication}, - cors::cors, - database::connection, - }, + filters::{client::client_authentication, cors::cors, database::connection}, storage::oauth2::{ access_token::lookup_active_access_token, refresh_token::lookup_active_refresh_token, }, @@ -36,10 +34,16 @@ pub fn filter( pool: &PgPool, oauth2_config: &OAuth2Config, ) -> impl Filter + Clone + Send + Sync + 'static { + let audience = oauth2_config + .issuer + .join("/oauth2/introspect") + .unwrap() + .to_string(); + warp::path!("oauth2" / "introspect").and( warp::post() .and(connection(pool)) - .and(client_authentication(oauth2_config)) + .and(client_authentication(oauth2_config, audience)) .and_then(introspect) .recover(recover) .with(cors().allow_method(Method::POST)), @@ -63,7 +67,7 @@ const INACTIVE: IntrospectionResponse = IntrospectionResponse { async fn introspect( mut conn: PoolConnection, - auth: ClientAuthentication, + auth: ClientAuthenticationMethod, client: OAuth2ClientConfig, params: IntrospectionRequest, ) -> Result { diff --git a/crates/core/src/handlers/oauth2/token.rs b/crates/core/src/handlers/oauth2/token.rs index d33c896c..c779f39b 100644 --- a/crates/core/src/handlers/oauth2/token.rs +++ b/crates/core/src/handlers/oauth2/token.rs @@ -22,7 +22,8 @@ use mas_data_model::AuthorizationGrantStage; use oauth2_types::{ errors::{InvalidGrant, InvalidRequest, OAuth2Error, OAuth2ErrorCode, UnauthorizedClient}, requests::{ - AccessTokenRequest, AccessTokenResponse, AuthorizationCodeGrant, RefreshTokenGrant, + AccessTokenRequest, AccessTokenResponse, AuthorizationCodeGrant, + ClientAuthenticationMethod, RefreshTokenGrant, }, scope::OPENID, }; @@ -42,12 +43,7 @@ use warp::{ use crate::{ config::{KeySet, OAuth2ClientConfig, OAuth2Config}, errors::WrapError, - filters::{ - client::{client_authentication, ClientAuthentication}, - cors::cors, - database::connection, - with_keys, - }, + filters::{client::client_authentication, cors::cors, database::connection, with_keys}, reply::with_typed_header, storage::{ oauth2::{ @@ -97,10 +93,16 @@ pub fn filter( pool: &PgPool, oauth2_config: &OAuth2Config, ) -> impl Filter + Clone + Send + Sync + 'static { + let audience = oauth2_config + .issuer + .join("/oauth2/token") + .unwrap() + .to_string(); let issuer = oauth2_config.issuer.clone(); + warp::path!("oauth2" / "token").and( warp::post() - .and(client_authentication(oauth2_config)) + .and(client_authentication(oauth2_config, audience)) .and(with_keys(oauth2_config)) .and(warp::any().map(move || issuer.clone())) .and(connection(pool)) @@ -119,7 +121,7 @@ async fn recover(rejection: Rejection) -> Result { } async fn token( - _auth: ClientAuthentication, + _auth: ClientAuthenticationMethod, client: OAuth2ClientConfig, req: AccessTokenRequest, keys: KeySet, diff --git a/crates/oauth2-types/src/oidc.rs b/crates/oauth2-types/src/oidc.rs index 3d5aab34..dc5c3a7e 100644 --- a/crates/oauth2-types/src/oidc.rs +++ b/crates/oauth2-types/src/oidc.rs @@ -23,6 +23,28 @@ use crate::{ requests::{ClientAuthenticationMethod, GrantType, ResponseMode}, }; +#[derive(Serialize, Clone, Copy, PartialEq, Eq, Hash)] +#[serde(rename_all = "UPPERCASE")] +pub enum SigningAlgorithm { + #[serde(rename = "none")] + None, + Hs256, + Hs384, + Hs512, + Ps256, + Ps384, + Ps512, + Rs256, + Rs384, + Rs512, + Es256, + Es256K, + Es384, + Es512, + #[serde(rename = "EcDSA")] + EcDsa, +} + // TODO: https://datatracker.ietf.org/doc/html/rfc8414#section-2 #[skip_serializing_none] #[derive(Serialize, Clone)] @@ -65,6 +87,13 @@ pub struct Metadata { /// by this token endpoint. pub token_endpoint_auth_methods_supported: Option>, + /// JSON array containing a list of the JWS signing algorithms supported by + /// the Token Endpoint for the signature on the JWT used to authenticate + /// the Client at the Token Endpoint for the private_key_jwt and + /// client_secret_jwt authentication methods. Servers SHOULD support + /// RS256. The value none MUST NOT be used. + pub token_endpoint_auth_signing_alg_values_supported: Option>, + /// PKCE code challenge methods supported by this authorization server pub code_challenge_methods_supported: Option>, diff --git a/crates/oauth2-types/src/requests.rs b/crates/oauth2-types/src/requests.rs index 0a499d3e..594ea9cf 100644 --- a/crates/oauth2-types/src/requests.rs +++ b/crates/oauth2-types/src/requests.rs @@ -91,6 +91,16 @@ pub enum ClientAuthenticationMethod { None, ClientSecretPost, ClientSecretBasic, + ClientSecretJwt, + PrivateKeyJwt, +} + +impl ClientAuthenticationMethod { + #[must_use] + /// Check if the authentication method is for public client or not + pub fn public(&self) -> bool { + matches!(self, &Self::None) + } } #[derive(