Include nonce parameter in id_tokens
Some checks failed
trivy / Check (push) Has been cancelled

This commit is contained in:
mdecimus 2024-10-03 15:07:38 +02:00
parent cce2d9c915
commit 292d1cc048
5 changed files with 57 additions and 28 deletions

View file

@ -78,12 +78,20 @@ pub struct Userinfo {
pub updated_at: Option<i64>, pub updated_at: Option<i64>,
} }
#[derive(Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct Nonce {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub nonce: Option<String>,
}
impl Server { impl Server {
pub fn issue_id_token( pub fn issue_id_token(
&self, &self,
subject: impl Into<String>, subject: impl Into<String>,
issuer: impl Into<String>, issuer: impl Into<String>,
audience: impl Into<String>, audience: impl Into<String>,
nonce: Option<impl Into<String>>,
) -> trc::Result<String> { ) -> trc::Result<String> {
let now = now() as i64; let now = now() as i64;
@ -93,7 +101,7 @@ impl Server {
key_id: Some("default".into()), key_id: Some("default".into()),
..Default::default() ..Default::default()
}), }),
ClaimsSet::<()> { ClaimsSet::<Nonce> {
registered: RegisteredClaims { registered: RegisteredClaims {
issuer: Some(issuer.into()), issuer: Some(issuer.into()),
subject: Some(subject.into()), subject: Some(subject.into()),
@ -103,7 +111,9 @@ impl Server {
expiry: Some((now + self.core.oauth.oidc_expiry_id_token as i64).into()), expiry: Some((now + self.core.oauth.oidc_expiry_id_token as i64).into()),
..Default::default() ..Default::default()
}, },
private: (), private: Nonce {
nonce: nonce.map(Into::into),
},
}, },
) )
.into_encoded(&self.core.oauth.oidc_signing_secret) .into_encoded(&self.core.oauth.oidc_signing_secret)

View file

@ -262,7 +262,8 @@ impl OAuthApiHandler for Server {
user_code, user_code,
expires_in: self.core.oauth.oauth_expiry_user_code, expires_in: self.core.oauth.oauth_expiry_user_code,
interval: 5, interval: 5,
}).no_cache() })
.no_cache()
.into_http_response()) .into_http_response())
} }

View file

@ -41,6 +41,7 @@ pub trait TokenHandler: Sync + Send {
account_id: u32, account_id: u32,
client_id: &str, client_id: &str,
issuer: String, issuer: String,
nonce: Option<String>,
with_refresh_token: bool, with_refresh_token: bool,
) -> impl Future<Output = trc::Result<OAuthResponse>> + Send; ) -> impl Future<Output = trc::Result<OAuthResponse>> + Send;
} }
@ -63,10 +64,11 @@ impl TokenHandler for Server {
.await; .await;
if grant_type.eq_ignore_ascii_case("authorization_code") { if grant_type.eq_ignore_ascii_case("authorization_code") {
response = if let (Some(code), Some(client_id), Some(redirect_uri)) = ( response = if let (Some(code), Some(client_id), Some(redirect_uri), nonce) = (
params.get("code"), params.get("code"),
params.get("client_id"), params.get("client_id"),
params.get("redirect_uri"), params.get("redirect_uri"),
params.get("nonce"),
) { ) {
// Obtain code // Obtain code
match self match self
@ -100,15 +102,21 @@ impl TokenHandler for Server {
.await?; .await?;
// Issue token // Issue token
self.issue_token(oauth.account_id, &oauth.client_id, issuer, true) self.issue_token(
.await oauth.account_id,
.map(TokenResponse::Granted) &oauth.client_id,
.map_err(|err| { issuer,
trc::AuthEvent::Error nonce.map(Into::into),
.into_err() true,
.details(err) )
.caused_by(trc::location!()) .await
})? .map(TokenResponse::Granted)
.map_err(|err| {
trc::AuthEvent::Error
.into_err()
.details(err)
.caused_by(trc::location!())
})?
} }
} else { } else {
TokenResponse::error(ErrorType::InvalidGrant) TokenResponse::error(ErrorType::InvalidGrant)
@ -122,9 +130,11 @@ impl TokenHandler for Server {
} else if grant_type.eq_ignore_ascii_case("urn:ietf:params:oauth:grant-type:device_code") { } else if grant_type.eq_ignore_ascii_case("urn:ietf:params:oauth:grant-type:device_code") {
response = TokenResponse::error(ErrorType::ExpiredToken); response = TokenResponse::error(ErrorType::ExpiredToken);
if let (Some(device_code), Some(client_id)) = if let (Some(device_code), Some(client_id), nonce) = (
(params.get("device_code"), params.get("client_id")) params.get("device_code"),
{ params.get("client_id"),
params.get("nonce"),
) {
// Obtain code // Obtain code
if let Some(auth_code) = self if let Some(auth_code) = self
.core .core
@ -157,6 +167,7 @@ impl TokenHandler for Server {
oauth.account_id, oauth.account_id,
&oauth.client_id, &oauth.client_id,
issuer, issuer,
nonce.map(Into::into),
true, true,
) )
.await .await
@ -190,6 +201,7 @@ impl TokenHandler for Server {
token_info.account_id, token_info.account_id,
&token_info.client_id, &token_info.client_id,
issuer, issuer,
None,
token_info.expires_in token_info.expires_in
<= self.core.oauth.oauth_expiry_refresh_token_renew, <= self.core.oauth.oauth_expiry_refresh_token_renew,
) )
@ -251,6 +263,7 @@ impl TokenHandler for Server {
account_id: u32, account_id: u32,
client_id: &str, client_id: &str,
issuer: String, issuer: String,
nonce: Option<String>,
with_refresh_token: bool, with_refresh_token: bool,
) -> trc::Result<OAuthResponse> { ) -> trc::Result<OAuthResponse> {
Ok(OAuthResponse { Ok(OAuthResponse {
@ -276,7 +289,7 @@ impl TokenHandler for Server {
} else { } else {
None None
}, },
id_token: match self.issue_id_token(account_id.to_string(), issuer, client_id) { id_token: match self.issue_id_token(account_id.to_string(), issuer, client_id, nonce) {
Ok(id_token) => Some(id_token), Ok(id_token) => Some(id_token),
Err(err) => { Err(err) => {
trc::error!(err); trc::error!(err);

View file

@ -11,6 +11,7 @@ use biscuit::{jwk::JWKSet, SingleOrMultiple, JWT};
use bytes::Bytes; use bytes::Bytes;
use common::auth::oauth::{ use common::auth::oauth::{
introspect::OAuthIntrospect, introspect::OAuthIntrospect,
oidc::Nonce,
registration::{ClientRegistrationRequest, ClientRegistrationResponse}, registration::{ClientRegistrationRequest, ClientRegistrationResponse},
}; };
use imap_proto::ResponseType; use imap_proto::ResponseType;
@ -135,6 +136,7 @@ pub async fn test(params: &mut JMAPTest) {
// Obtain token // Obtain token
token_params.insert("redirect_uri".to_string(), "https://localhost".to_string()); token_params.insert("redirect_uri".to_string(), "https://localhost".to_string());
token_params.insert("nonce".to_string(), "abc1234".to_string());
let (token, refresh_token, id_token) = let (token, refresh_token, id_token) =
unwrap_oidc_token_response(post(&metadata.token_endpoint, &token_params).await); unwrap_oidc_token_response(post(&metadata.token_endpoint, &token_params).await);
@ -154,16 +156,19 @@ pub async fn test(params: &mut JMAPTest) {
.is_empty()); .is_empty());
// Verify ID token using the JWK set // Verify ID token using the JWK set
let id_token = JWT::<(), biscuit::Empty>::new_encoded(&id_token) let id_token = JWT::<Nonce, biscuit::Empty>::new_encoded(&id_token)
.decode_with_jwks(&jwk_set, None) .decode_with_jwks(&jwk_set, None)
.unwrap(); .unwrap();
let claims = &id_token.payload().unwrap().registered; let claims = id_token.payload().unwrap();
assert_eq!(claims.issuer, Some(oidc_metadata.issuer)); let registered_claims = &claims.registered;
assert_eq!(claims.subject, Some(john_int_id.to_string())); let private_claims = &claims.private;
assert_eq!(registered_claims.issuer, Some(oidc_metadata.issuer));
assert_eq!(registered_claims.subject, Some(john_int_id.to_string()));
assert_eq!( assert_eq!(
claims.audience, registered_claims.audience,
Some(SingleOrMultiple::Single(client_id.to_string())) Some(SingleOrMultiple::Single(client_id.to_string()))
); );
assert_eq!(private_claims.nonce, Some("abc1234".to_string()));
// Introspect token // Introspect token
let access_introspect: OAuthIntrospect = post_with_auth::<OAuthIntrospect>( let access_introspect: OAuthIntrospect = post_with_auth::<OAuthIntrospect>(

View file

@ -370,8 +370,8 @@ pub async fn jmap_tests() {
) )
.await; .await;
webhooks::test(&mut params).await; /*webhooks::test(&mut params).await;
/*email_query::test(&mut params, delete).await; email_query::test(&mut params, delete).await;
email_get::test(&mut params).await; email_get::test(&mut params).await;
email_set::test(&mut params).await; email_set::test(&mut params).await;
email_parse::test(&mut params).await; email_parse::test(&mut params).await;
@ -384,9 +384,9 @@ pub async fn jmap_tests() {
mailbox::test(&mut params).await; mailbox::test(&mut params).await;
delivery::test(&mut params).await; delivery::test(&mut params).await;
auth_acl::test(&mut params).await; auth_acl::test(&mut params).await;
auth_limits::test(&mut params).await; auth_limits::test(&mut params).await;*/
auth_oauth::test(&mut params).await; auth_oauth::test(&mut params).await;
event_source::test(&mut params).await; /*event_source::test(&mut params).await;
push_subscription::test(&mut params).await; push_subscription::test(&mut params).await;
sieve_script::test(&mut params).await; sieve_script::test(&mut params).await;
vacation_response::test(&mut params).await; vacation_response::test(&mut params).await;
@ -394,10 +394,10 @@ pub async fn jmap_tests() {
websocket::test(&mut params).await; websocket::test(&mut params).await;
quota::test(&mut params).await; quota::test(&mut params).await;
crypto::test(&mut params).await; crypto::test(&mut params).await;
blob::test(&mut params).await;*/ blob::test(&mut params).await;
permissions::test(&params).await; permissions::test(&params).await;
purge::test(&mut params).await; purge::test(&mut params).await;
enterprise::test(&mut params).await; enterprise::test(&mut params).await;*/
if delete { if delete {
params.temp_dir.delete(); params.temp_dir.delete();