1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// Copyright 2022 Kévin Commaille.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Requests for obtaining [Claims] about an end-user.
//!
//! [Claims]: https://openid.net/specs/openid-connect-core-1_0.html#Claims

use std::collections::HashMap;

use bytes::Bytes;
use headers::{Authorization, ContentType, HeaderMapExt, HeaderValue};
use http::header::ACCEPT;
use mas_http::CatchHttpCodesLayer;
use mas_jose::claims;
use mime::Mime;
use serde_json::Value;
use tower::{Layer, Service, ServiceExt};
use url::Url;

use super::jose::JwtVerificationData;
use crate::{
    error::{IdTokenError, UserInfoError},
    http_service::HttpService,
    requests::jose::verify_signed_jwt,
    types::IdToken,
    utils::{http_all_error_status_codes, http_error_mapper},
};

/// Obtain information about an authenticated end-user.
///
/// Returns a map of claims with their value, that should be extracted with
/// one of the [`Claim`] methods.
///
/// # Arguments
///
/// * `http_service` - The service to use for making HTTP requests.
///
/// * `userinfo_endpoint` - The URL of the issuer's User Info endpoint.
///
/// * `access_token` - The access token of the end-user.
///
/// * `jwt_verification_data` - The data required to verify the response if a
///   signed response was requested during client registration.
///
///   The signing algorithm corresponds to the `userinfo_signed_response_alg`
///   field in the client metadata.
///
/// * `auth_id_token` - The ID token that was returned from the latest
///   authorization request.
///
/// # Errors
///
/// Returns an error if the request fails, the response is invalid or the
/// validation of the signed response fails.
///
/// [`Claim`]: mas_jose::claims::Claim
#[tracing::instrument(skip_all, fields(userinfo_endpoint))]
pub async fn fetch_userinfo(
    http_service: &HttpService,
    userinfo_endpoint: &Url,
    access_token: &str,
    jwt_verification_data: Option<JwtVerificationData<'_>>,
    auth_id_token: &IdToken<'_>,
) -> Result<HashMap<String, Value>, UserInfoError> {
    tracing::debug!("Obtaining user info…");

    let mut userinfo_request = http::Request::get(userinfo_endpoint.as_str());

    let expected_content_type = if jwt_verification_data.is_some() {
        "application/jwt"
    } else {
        mime::APPLICATION_JSON.as_ref()
    };

    if let Some(headers) = userinfo_request.headers_mut() {
        headers.typed_insert(Authorization::bearer(access_token)?);
        headers.insert(ACCEPT, HeaderValue::from_static(expected_content_type));
    }

    let userinfo_request = userinfo_request.body(Bytes::new())?;

    let service = CatchHttpCodesLayer::new(http_all_error_status_codes(), http_error_mapper)
        .layer(http_service.clone());

    let userinfo_response = service
        .ready_oneshot()
        .await?
        .call(userinfo_request)
        .await?;

    let content_type: Mime = userinfo_response
        .headers()
        .typed_try_get::<ContentType>()
        .map_err(|_| UserInfoError::InvalidResponseContentTypeValue)?
        .ok_or(UserInfoError::MissingResponseContentType)?
        .into();

    if content_type.essence_str() != expected_content_type {
        return Err(UserInfoError::UnexpectedResponseContentType {
            expected: expected_content_type.to_owned(),
            got: content_type.to_string(),
        });
    }

    let response_body = std::str::from_utf8(userinfo_response.body())?;

    let mut claims = if let Some(verification_data) = jwt_verification_data {
        verify_signed_jwt(response_body, verification_data)
            .map_err(IdTokenError::from)?
            .into_parts()
            .1
    } else {
        serde_json::from_str(response_body)?
    };

    let mut auth_claims = auth_id_token.payload().clone();

    // Subject identifier must always be the same.
    let sub = claims::SUB
        .extract_required(&mut claims)
        .map_err(IdTokenError::from)?;
    let auth_sub = claims::SUB
        .extract_required(&mut auth_claims)
        .map_err(IdTokenError::from)?;
    if sub != auth_sub {
        return Err(IdTokenError::WrongSubjectIdentifier.into());
    }

    Ok(claims)
}