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.
67//! Requests for obtaining [Claims] about an end-user.
8//!
9//! [Claims]: https://openid.net/specs/openid-connect-core-1_0.html#Claims
1011use std::collections::HashMap;
1213use headers::{ContentType, HeaderMapExt, HeaderValue};
14use http::header::ACCEPT;
15use mas_http::RequestBuilderExt;
16use mime::Mime;
17use serde_json::Value;
18use url::Url;
1920use super::jose::JwtVerificationData;
21use crate::{
22 error::{IdTokenError, ResponseExt, UserInfoError},
23 requests::jose::verify_signed_jwt,
24};
2526/// Obtain information about an authenticated end-user.
27///
28/// Returns a map of claims with their value, that should be extracted with
29/// one of the [`Claim`] methods.
30///
31/// # Arguments
32///
33/// * `http_client` - The reqwest client to use for making HTTP requests.
34///
35/// * `userinfo_endpoint` - The URL of the issuer's User Info endpoint.
36///
37/// * `access_token` - The access token of the end-user.
38///
39/// * `jwt_verification_data` - The data required to verify the response if a
40/// signed response was requested during client registration.
41///
42/// The signing algorithm corresponds to the `userinfo_signed_response_alg`
43/// field in the client metadata.
44///
45/// * `auth_id_token` - The ID token that was returned from the latest
46/// authorization request.
47///
48/// # Errors
49///
50/// Returns an error if the request fails, the response is invalid or the
51/// validation of the signed response fails.
52///
53/// [`Claim`]: mas_jose::claims::Claim
54#[tracing::instrument(skip_all, fields(userinfo_endpoint))]
55pub async fn fetch_userinfo(
56 http_client: &reqwest::Client,
57 userinfo_endpoint: &Url,
58 access_token: &str,
59 jwt_verification_data: Option<JwtVerificationData<'_>>,
60) -> Result<HashMap<String, Value>, UserInfoError> {
61tracing::debug!("Obtaining user info…");
6263let expected_content_type = if jwt_verification_data.is_some() {
64"application/jwt"
65} else {
66 mime::APPLICATION_JSON.as_ref()
67 };
6869let userinfo_request = http_client
70 .get(userinfo_endpoint.as_str())
71 .bearer_auth(access_token)
72 .header(ACCEPT, HeaderValue::from_static(expected_content_type));
7374let userinfo_response = userinfo_request
75 .send_traced()
76 .await?
77.error_from_oauth2_error_response()
78 .await?;
7980let content_type: Mime = userinfo_response
81 .headers()
82 .typed_try_get::<ContentType>()
83 .map_err(|_| UserInfoError::InvalidResponseContentTypeValue)?
84.ok_or(UserInfoError::MissingResponseContentType)?
85.into();
8687if content_type.essence_str() != expected_content_type {
88return Err(UserInfoError::UnexpectedResponseContentType {
89 expected: expected_content_type.to_owned(),
90 got: content_type.to_string(),
91 });
92 }
9394let claims = if let Some(verification_data) = jwt_verification_data {
95let response_body = userinfo_response.text().await?;
96 verify_signed_jwt(&response_body, verification_data)
97 .map_err(IdTokenError::from)?
98.into_parts()
99 .1
100} else {
101 userinfo_response.json().await?
102};
103104Ok(claims)
105}