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}