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
// Copyright 2024 New Vector Ltd.
// Copyright 2023, 2024 Kévin Commaille.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.

//! Requests for [RP-Initiated Logout].
//!
//! [RP-Initiated Logout]: https://openid.net/specs/openid-connect-rpinitiated-1_0.html

use language_tags::LanguageTag;
use oauth2_types::oidc::RpInitiatedLogoutRequest;
use rand::{
    distributions::{Alphanumeric, DistString},
    Rng,
};
use url::Url;

/// The data necessary to build a logout request.
#[derive(Default, Clone)]
pub struct LogoutData {
    /// ID Token previously issued by the OP to the RP.
    ///
    /// Recommended, used as a hint about the End-User's current authenticated
    /// session with the Client.
    pub id_token_hint: Option<String>,

    /// Hint to the Authorization Server about the End-User that is logging out.
    ///
    /// The value and meaning of this parameter is left up to the OP's
    /// discretion. For instance, the value might contain an email address,
    /// phone number, username, or session identifier pertaining to the RP's
    /// session with the OP for the End-User.
    pub logout_hint: Option<String>,

    /// OAuth 2.0 Client Identifier valid at the Authorization Server.
    ///
    /// The most common use case for this parameter is to specify the Client
    /// Identifier when `post_logout_redirect_uri` is used but `id_token_hint`
    /// is not. Another use is for symmetrically encrypted ID Tokens used as
    /// `id_token_hint` values that require the Client Identifier to be
    /// specified by other means, so that the ID Tokens can be decrypted by
    /// the OP.
    pub client_id: Option<String>,

    /// URI to which the RP is requesting that the End-User's User Agent be
    /// redirected after a logout has been performed.
    ///
    /// The value MUST have been previously registered with the OP, using the
    /// `post_logout_redirect_uris` registration parameter.
    pub post_logout_redirect_uri: Option<Url>,

    /// The End-User's preferred languages and scripts for the user interface,
    /// ordered by preference.
    pub ui_locales: Option<Vec<LanguageTag>>,
}

/// Build the URL for initiating logout at the logout endpoint.
///
/// # Arguments
///
/// * `end_session_endpoint` - The URL of the issuer's logout endpoint.
///
/// * `logout_data` - The data necessary to build the logout request.
///
/// * `rng` - A random number generator.
///
/// # Returns
///
/// A URL to be opened in a web browser where the end-user will be able to
/// logout of their session, and an optional `state` string.
///
/// The `state` will only be set if `post_logout_redirect_uri` is set. It should
/// be present in the query when the end user is redirected to the
/// `post_logout_redirect_uri`.
///
/// # Errors
///
/// Returns an error if preparing the URL fails.
///
/// [`VerifiedClientMetadata`]: oauth2_types::registration::VerifiedClientMetadata
/// [`ClientErrorCode`]: oauth2_types::errors::ClientErrorCode
pub fn build_end_session_url(
    mut end_session_endpoint: Url,
    logout_data: LogoutData,
    rng: &mut impl Rng,
) -> Result<(Url, Option<String>), serde_urlencoded::ser::Error> {
    let LogoutData {
        id_token_hint,
        logout_hint,
        client_id,
        post_logout_redirect_uri,
        ui_locales,
    } = logout_data;

    let state = if post_logout_redirect_uri.is_some() {
        Some(Alphanumeric.sample_string(rng, 16))
    } else {
        None
    };

    let logout_request = RpInitiatedLogoutRequest {
        id_token_hint,
        logout_hint,
        client_id,
        post_logout_redirect_uri,
        state: state.clone(),
        ui_locales,
    };

    let logout_query = serde_urlencoded::to_string(logout_request)?;

    // Add our parameters to the query, because the URL might already have one.
    let mut full_query = end_session_endpoint
        .query()
        .map(ToOwned::to_owned)
        .unwrap_or_default();
    if !full_query.is_empty() {
        full_query.push('&');
    }
    full_query.push_str(&logout_query);

    end_session_endpoint.set_query(Some(&full_query));

    Ok((end_session_endpoint, state))
}