mas_oidc_client/requests/jose.rs
1// Copyright 2024 New Vector Ltd.
2// Copyright 2022-2024 Kévin Commaille.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7//! Requests and method related to JSON Object Signing and Encryption.
8
9use std::collections::HashMap;
10
11use chrono::{DateTime, Utc};
12use mas_http::RequestBuilderExt;
13use mas_iana::jose::JsonWebSignatureAlg;
14use mas_jose::{
15 claims::{self, TimeOptions},
16 jwk::PublicJsonWebKeySet,
17 jwt::Jwt,
18};
19use serde_json::Value;
20use url::Url;
21
22use crate::{
23 error::{IdTokenError, JwksError, JwtVerificationError},
24 types::IdToken,
25};
26
27/// Fetch a JWKS at the given URL.
28///
29/// # Arguments
30///
31/// * `http_client` - The reqwest client to use for making HTTP requests.
32///
33/// * `jwks_uri` - The URL where the JWKS can be retrieved.
34///
35/// # Errors
36///
37/// Returns an error if the request fails or if the data is invalid.
38#[tracing::instrument(skip_all, fields(jwks_uri))]
39pub async fn fetch_jwks(
40 client: &reqwest::Client,
41 jwks_uri: &Url,
42) -> Result<PublicJsonWebKeySet, JwksError> {
43 tracing::debug!("Fetching JWKS...");
44
45 let response: PublicJsonWebKeySet = client
46 .get(jwks_uri.as_str())
47 .send_traced()
48 .await?
49 .error_for_status()?
50 .json()
51 .await?;
52
53 Ok(response)
54}
55
56/// The data required to verify a JWT.
57#[derive(Clone, Copy)]
58pub struct JwtVerificationData<'a> {
59 /// The URL of the issuer that generated the ID Token.
60 pub issuer: Option<&'a str>,
61
62 /// The issuer's JWKS.
63 pub jwks: &'a PublicJsonWebKeySet,
64
65 /// The ID obtained when registering the client.
66 pub client_id: &'a String,
67
68 /// The JWA that should have been used to sign the JWT, as set during
69 /// client registration.
70 pub signing_algorithm: &'a JsonWebSignatureAlg,
71}
72
73/// Decode and verify a signed JWT.
74///
75/// The following checks are performed:
76///
77/// * The signature is verified with the given JWKS.
78///
79/// * The `iss` claim must be present and match the issuer, if present
80///
81/// * The `aud` claim must be present and match the client ID.
82///
83/// * The `alg` in the header must match the signing algorithm.
84///
85/// # Arguments
86///
87/// * `jwt` - The serialized JWT to decode and verify.
88///
89/// * `jwks` - The JWKS that should contain the public key to verify the JWT's
90/// signature.
91///
92/// * `issuer` - The issuer of the JWT.
93///
94/// * `audience` - The audience that the JWT is intended for.
95///
96/// * `signing_algorithm` - The JWA that should have been used to sign the JWT.
97///
98/// # Errors
99///
100/// Returns an error if the data is invalid or verification fails.
101pub fn verify_signed_jwt<'a>(
102 jwt: &'a str,
103 verification_data: JwtVerificationData<'_>,
104) -> Result<Jwt<'a, HashMap<String, Value>>, JwtVerificationError> {
105 tracing::debug!("Validating JWT...");
106
107 let JwtVerificationData {
108 issuer,
109 jwks,
110 client_id,
111 signing_algorithm,
112 } = verification_data;
113
114 let jwt: Jwt<HashMap<String, Value>> = jwt.try_into()?;
115
116 jwt.verify_with_jwks(jwks)?;
117
118 let (header, mut claims) = jwt.clone().into_parts();
119
120 if let Some(issuer) = issuer {
121 // Must have the proper issuer.
122 claims::ISS.extract_required_with_options(&mut claims, issuer)?;
123 }
124
125 // Must have the proper audience.
126 claims::AUD.extract_required_with_options(&mut claims, client_id)?;
127
128 // Must use the proper algorithm.
129 if header.alg() != signing_algorithm {
130 return Err(JwtVerificationError::WrongSignatureAlg);
131 }
132
133 Ok(jwt)
134}
135
136/// Decode and verify an ID Token.
137///
138/// Besides the checks of [`verify_signed_jwt()`], the following checks are
139/// performed:
140///
141/// * The `exp` claim must be present and the token must not have expired.
142///
143/// * The `iat` claim must be present must be in the past.
144///
145/// * The `sub` claim must be present.
146///
147/// If an authorization ID token is provided, these extra checks are performed:
148///
149/// * The `sub` claims must match.
150///
151/// * The `auth_time` claims must match.
152///
153/// # Arguments
154///
155/// * `id_token` - The serialized ID Token to decode and verify.
156///
157/// * `verification_data` - The data necessary to verify the ID Token.
158///
159/// * `auth_id_token` - If the ID Token is not verified during an authorization
160/// request, the ID token that was returned from the latest authorization
161/// request.
162///
163/// # Errors
164///
165/// Returns an error if the data is invalid or verification fails.
166pub fn verify_id_token<'a>(
167 id_token: &'a str,
168 verification_data: JwtVerificationData<'_>,
169 auth_id_token: Option<&IdToken<'_>>,
170 now: DateTime<Utc>,
171) -> Result<IdToken<'a>, IdTokenError> {
172 let id_token = verify_signed_jwt(id_token, verification_data)?;
173
174 let mut claims = id_token.payload().clone();
175
176 let time_options = TimeOptions::new(now);
177 // Must not have expired.
178 claims::EXP.extract_required_with_options(&mut claims, &time_options)?;
179
180 // `iat` claim must be present.
181 claims::IAT.extract_required_with_options(&mut claims, time_options)?;
182
183 // Subject identifier must be present.
184 let sub = claims::SUB.extract_required(&mut claims)?;
185
186 // More checks if there is a previous ID token.
187 if let Some(auth_id_token) = auth_id_token {
188 let mut auth_claims = auth_id_token.payload().clone();
189
190 // Subject identifier must always be the same.
191 let auth_sub = claims::SUB.extract_required(&mut auth_claims)?;
192 if sub != auth_sub {
193 return Err(IdTokenError::WrongSubjectIdentifier);
194 }
195
196 // If the authentication time is present, it must be unchanged.
197 if let Some(auth_time) = claims::AUTH_TIME.extract_optional(&mut claims)? {
198 let prev_auth_time = claims::AUTH_TIME.extract_required(&mut auth_claims)?;
199
200 if prev_auth_time != auth_time {
201 return Err(IdTokenError::WrongAuthTime);
202 }
203 }
204 }
205
206 Ok(id_token)
207}