Skip to main content

mas_oidc_client/requests/
refresh_token.rs

1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2022-2024 Kévin Commaille.
4//
5// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
6// Please see LICENSE files in the repository root for full details.
7
8//! Requests for using [Refresh Tokens].
9//!
10//! [Refresh Tokens]: https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens
11
12use chrono::{DateTime, Utc};
13use mas_jose::claims::{self, TokenHash};
14use oauth2_types::{
15    requests::{AccessTokenRequest, AccessTokenResponse, RefreshTokenGrant},
16    scope::Scope,
17};
18use rand::RngExt;
19use url::Url;
20
21use super::jose::JwtVerificationData;
22use crate::{
23    error::{IdTokenError, TokenRefreshError},
24    requests::{jose::verify_id_token, token::request_access_token},
25    types::{IdToken, client_credentials::ClientCredentials},
26};
27
28/// Exchange an authorization code for an access token.
29///
30/// This should be used as the first step for logging in, and to request a
31/// token with a new scope.
32///
33/// # Arguments
34///
35/// * `http_client` - The reqwest client to use for making HTTP requests.
36///
37/// * `client_credentials` - The credentials obtained when registering the
38///   client.
39///
40/// * `token_endpoint` - The URL of the issuer's Token endpoint.
41///
42/// * `refresh_token` - The token used to refresh the access token returned at
43///   the Token endpoint.
44///
45/// * `scope` - The scope of the access token. The requested scope must not
46///   include any scope not originally granted to the access token, and if
47///   omitted is treated as equal to the scope originally granted by the issuer.
48///
49/// * `id_token_verification_data` - The data required to verify the ID Token in
50///   the response.
51///
52///   The signing algorithm corresponds to the `id_token_signed_response_alg`
53/// field in the client metadata.
54///
55///   If it is not provided, the ID Token won't be verified.
56///
57/// * `auth_id_token` - If an ID Token is expected in the response, the ID token
58///   that was returned from the latest authorization request.
59///
60/// * `now` - The current time.
61///
62/// * `rng` - A random number generator.
63///
64/// # Errors
65///
66/// Returns an error if the request fails, the response is invalid or the
67/// verification of the ID Token fails.
68#[expect(clippy::too_many_arguments)]
69#[tracing::instrument(skip_all, fields(token_endpoint))]
70pub async fn refresh_access_token(
71    http_client: &reqwest::Client,
72    client_credentials: ClientCredentials,
73    token_endpoint: &Url,
74    refresh_token: String,
75    scope: Option<Scope>,
76    id_token_verification_data: Option<JwtVerificationData<'_>>,
77    auth_id_token: Option<&IdToken<'_>>,
78    now: DateTime<Utc>,
79    rng: &mut impl RngExt,
80) -> Result<(AccessTokenResponse, Option<IdToken<'static>>), TokenRefreshError> {
81    tracing::debug!("Refreshing access token…");
82
83    let token_response = request_access_token(
84        http_client,
85        client_credentials,
86        token_endpoint,
87        AccessTokenRequest::RefreshToken(RefreshTokenGrant {
88            refresh_token,
89            scope,
90        }),
91        now,
92        rng,
93    )
94    .await?;
95
96    let id_token = if let Some((verification_data, id_token)) =
97        id_token_verification_data.zip(token_response.id_token.as_ref())
98    {
99        let auth_id_token = auth_id_token.ok_or(IdTokenError::MissingAuthIdToken)?;
100        let signing_alg = verification_data.signing_algorithm;
101
102        let id_token = verify_id_token(id_token, verification_data, Some(auth_id_token), now)?;
103
104        let mut claims = id_token.payload().clone();
105
106        // Access token hash must match.
107        claims::AT_HASH
108            .extract_optional_with_options(
109                &mut claims,
110                TokenHash::new(signing_alg, &token_response.access_token),
111            )
112            .map_err(IdTokenError::from)?;
113
114        Some(id_token.into_owned())
115    } else {
116        None
117    };
118
119    Ok((token_response, id_token))
120}