Skip to main content

mas_oidc_client/types/
client_credentials.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//! Types and methods for client credentials.
9
10use std::{collections::HashMap, fmt};
11
12use base64ct::{Base64UrlUnpadded, Encoding};
13use chrono::{DateTime, Duration, Utc};
14use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
15use mas_jose::{
16    claims::{self, ClaimError},
17    constraints::Constrainable,
18    jwa::{AsymmetricSigningKey, SymmetricKey},
19    jwt::{JsonWebSignatureHeader, Jwt},
20};
21use mas_keystore::Keystore;
22use rand::RngExt;
23use serde::Serialize;
24use serde_json::Value;
25use url::Url;
26
27use crate::error::CredentialsError;
28
29/// The supported authentication methods of this library.
30///
31/// During client registration, make sure that you only use one of the values
32/// defined here.
33pub const CLIENT_SUPPORTED_AUTH_METHODS: &[OAuthClientAuthenticationMethod] = &[
34    OAuthClientAuthenticationMethod::None,
35    OAuthClientAuthenticationMethod::ClientSecretBasic,
36    OAuthClientAuthenticationMethod::ClientSecretPost,
37    OAuthClientAuthenticationMethod::ClientSecretJwt,
38    OAuthClientAuthenticationMethod::PrivateKeyJwt,
39];
40
41/// The credentials obtained during registration, to authenticate a client on
42/// endpoints that require it.
43#[derive(Clone)]
44pub enum ClientCredentials {
45    /// No client authentication is used.
46    ///
47    /// This is used if the client is public.
48    None {
49        /// The unique ID for the client.
50        client_id: String,
51    },
52
53    /// The client authentication is sent via the Authorization HTTP header.
54    ClientSecretBasic {
55        /// The unique ID for the client.
56        client_id: String,
57
58        /// The secret of the client.
59        client_secret: String,
60    },
61
62    /// The client authentication is sent with the body of the request.
63    ClientSecretPost {
64        /// The unique ID for the client.
65        client_id: String,
66
67        /// The secret of the client.
68        client_secret: String,
69    },
70
71    /// The client authentication uses a JWT signed with a key derived from the
72    /// client secret.
73    ClientSecretJwt {
74        /// The unique ID for the client.
75        client_id: String,
76
77        /// The secret of the client.
78        client_secret: String,
79
80        /// The algorithm used to sign the JWT.
81        signing_algorithm: JsonWebSignatureAlg,
82
83        /// The URL of the issuer's Token endpoint.
84        token_endpoint: Url,
85    },
86
87    /// The client authentication uses a JWT signed with a private key.
88    PrivateKeyJwt {
89        /// The unique ID for the client.
90        client_id: String,
91
92        /// The keystore used to sign the JWT
93        keystore: Keystore,
94
95        /// The algorithm used to sign the JWT.
96        signing_algorithm: JsonWebSignatureAlg,
97
98        /// The URL of the issuer's Token endpoint.
99        token_endpoint: Url,
100    },
101
102    /// The client authenticates like Sign in with Apple wants
103    SignInWithApple {
104        /// The unique ID for the client.
105        client_id: String,
106
107        /// The ECDSA key used to sign
108        key: elliptic_curve::SecretKey<p256::NistP256>,
109
110        /// The key ID
111        key_id: String,
112
113        /// The Apple Team ID
114        team_id: String,
115    },
116}
117
118impl ClientCredentials {
119    /// Get the client ID of these `ClientCredentials`.
120    #[must_use]
121    pub fn client_id(&self) -> &str {
122        match self {
123            ClientCredentials::None { client_id }
124            | ClientCredentials::ClientSecretBasic { client_id, .. }
125            | ClientCredentials::ClientSecretPost { client_id, .. }
126            | ClientCredentials::ClientSecretJwt { client_id, .. }
127            | ClientCredentials::PrivateKeyJwt { client_id, .. }
128            | ClientCredentials::SignInWithApple { client_id, .. } => client_id,
129        }
130    }
131
132    /// Apply these [`ClientCredentials`] to the given request with the given
133    /// form.
134    pub(crate) fn authenticated_form<T: Serialize>(
135        &self,
136        request: reqwest::RequestBuilder,
137        form: &T,
138        now: DateTime<Utc>,
139        rng: &mut impl RngExt,
140    ) -> Result<reqwest::RequestBuilder, CredentialsError> {
141        let request = match self {
142            ClientCredentials::None { client_id } => request.form(&RequestWithClientCredentials {
143                body: form,
144                client_id: Some(client_id),
145                client_secret: None,
146                client_assertion: None,
147                client_assertion_type: None,
148            }),
149
150            ClientCredentials::ClientSecretBasic {
151                client_id,
152                client_secret,
153            } => {
154                let username =
155                    form_urlencoded::byte_serialize(client_id.as_bytes()).collect::<String>();
156                let password =
157                    form_urlencoded::byte_serialize(client_secret.as_bytes()).collect::<String>();
158                request
159                    .basic_auth(username, Some(password))
160                    .form(&RequestWithClientCredentials {
161                        body: form,
162                        client_id: None,
163                        client_secret: None,
164                        client_assertion: None,
165                        client_assertion_type: None,
166                    })
167            }
168
169            ClientCredentials::ClientSecretPost {
170                client_id,
171                client_secret,
172            } => request.form(&RequestWithClientCredentials {
173                body: form,
174                client_id: Some(client_id),
175                client_secret: Some(client_secret),
176                client_assertion: None,
177                client_assertion_type: None,
178            }),
179
180            ClientCredentials::ClientSecretJwt {
181                client_id,
182                client_secret,
183                signing_algorithm,
184                token_endpoint,
185            } => {
186                let claims =
187                    prepare_claims(client_id.clone(), token_endpoint.to_string(), now, rng)?;
188                let key = SymmetricKey::new_for_alg(
189                    client_secret.as_bytes().to_vec(),
190                    signing_algorithm,
191                )?;
192                let header = JsonWebSignatureHeader::new(signing_algorithm.clone());
193
194                let jwt = Jwt::sign(header, claims, &key)?;
195
196                request.form(&RequestWithClientCredentials {
197                    body: form,
198                    client_id: None,
199                    client_secret: None,
200                    client_assertion: Some(jwt.as_str()),
201                    client_assertion_type: Some(JwtBearerClientAssertionType),
202                })
203            }
204
205            ClientCredentials::PrivateKeyJwt {
206                client_id,
207                keystore,
208                signing_algorithm,
209                token_endpoint,
210            } => {
211                let claims =
212                    prepare_claims(client_id.clone(), token_endpoint.to_string(), now, rng)?;
213
214                let key = keystore
215                    .signing_key_for_algorithm(signing_algorithm)
216                    .ok_or(CredentialsError::NoPrivateKeyFound)?;
217                let signer = key
218                    .params()
219                    .signing_key_for_alg(signing_algorithm)
220                    .map_err(|_| CredentialsError::JwtWrongAlgorithm)?;
221                let mut header = JsonWebSignatureHeader::new(signing_algorithm.clone());
222
223                if let Some(kid) = key.kid() {
224                    header = header.with_kid(kid);
225                }
226
227                let client_assertion = Jwt::sign(header, claims, &signer)?;
228
229                request.form(&RequestWithClientCredentials {
230                    body: form,
231                    client_id: None,
232                    client_secret: None,
233                    client_assertion: Some(client_assertion.as_str()),
234                    client_assertion_type: Some(JwtBearerClientAssertionType),
235                })
236            }
237
238            ClientCredentials::SignInWithApple {
239                client_id,
240                key,
241                key_id,
242                team_id,
243            } => {
244                // SIWA expects a signed JWT as client secret
245                // https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret
246                let signer = AsymmetricSigningKey::es256(key.clone());
247
248                let mut claims = HashMap::new();
249
250                claims::ISS.insert(&mut claims, team_id)?;
251                claims::SUB.insert(&mut claims, client_id)?;
252                claims::AUD.insert(&mut claims, "https://appleid.apple.com".to_owned())?;
253                claims::IAT.insert(&mut claims, now)?;
254                claims::EXP.insert(&mut claims, now + Duration::microseconds(60 * 1000 * 1000))?;
255
256                let header =
257                    JsonWebSignatureHeader::new(JsonWebSignatureAlg::Es256).with_kid(key_id);
258
259                let client_secret = Jwt::sign(header, claims, &signer)?;
260
261                request.form(&RequestWithClientCredentials {
262                    body: form,
263                    client_id: Some(client_id),
264                    client_secret: Some(client_secret.as_str()),
265                    client_assertion: None,
266                    client_assertion_type: None,
267                })
268            }
269        };
270
271        Ok(request)
272    }
273}
274
275impl fmt::Debug for ClientCredentials {
276    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277        match self {
278            Self::None { client_id } => f
279                .debug_struct("None")
280                .field("client_id", client_id)
281                .finish(),
282            Self::ClientSecretBasic { client_id, .. } => f
283                .debug_struct("ClientSecretBasic")
284                .field("client_id", client_id)
285                .finish_non_exhaustive(),
286            Self::ClientSecretPost { client_id, .. } => f
287                .debug_struct("ClientSecretPost")
288                .field("client_id", client_id)
289                .finish_non_exhaustive(),
290            Self::ClientSecretJwt {
291                client_id,
292                signing_algorithm,
293                token_endpoint,
294                ..
295            } => f
296                .debug_struct("ClientSecretJwt")
297                .field("client_id", client_id)
298                .field("signing_algorithm", signing_algorithm)
299                .field("token_endpoint", token_endpoint)
300                .finish_non_exhaustive(),
301            Self::PrivateKeyJwt {
302                client_id,
303                signing_algorithm,
304                token_endpoint,
305                ..
306            } => f
307                .debug_struct("PrivateKeyJwt")
308                .field("client_id", client_id)
309                .field("signing_algorithm", signing_algorithm)
310                .field("token_endpoint", token_endpoint)
311                .finish_non_exhaustive(),
312            Self::SignInWithApple {
313                client_id,
314                key_id,
315                team_id,
316                ..
317            } => f
318                .debug_struct("SignInWithApple")
319                .field("client_id", client_id)
320                .field("key_id", key_id)
321                .field("team_id", team_id)
322                .finish_non_exhaustive(),
323        }
324    }
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
328#[serde(rename = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")]
329struct JwtBearerClientAssertionType;
330
331fn prepare_claims(
332    iss: String,
333    aud: String,
334    now: DateTime<Utc>,
335    rng: &mut impl RngExt,
336) -> Result<HashMap<String, Value>, ClaimError> {
337    let mut claims = HashMap::new();
338
339    claims::ISS.insert(&mut claims, iss.clone())?;
340    claims::SUB.insert(&mut claims, iss)?;
341    claims::AUD.insert(&mut claims, aud)?;
342    claims::IAT.insert(&mut claims, now)?;
343    claims::EXP.insert(
344        &mut claims,
345        now + Duration::microseconds(5 * 60 * 1000 * 1000),
346    )?;
347
348    let mut jti = [0u8; 16];
349    rng.fill(&mut jti);
350    let jti = Base64UrlUnpadded::encode_string(&jti);
351    claims::JTI.insert(&mut claims, jti)?;
352
353    Ok(claims)
354}
355
356/// A request with client credentials added to it.
357#[derive(Clone, Serialize)]
358struct RequestWithClientCredentials<'a, T> {
359    #[serde(flatten)]
360    body: T,
361
362    #[serde(skip_serializing_if = "Option::is_none")]
363    client_id: Option<&'a str>,
364    #[serde(skip_serializing_if = "Option::is_none")]
365    client_secret: Option<&'a str>,
366    #[serde(skip_serializing_if = "Option::is_none")]
367    client_assertion: Option<&'a str>,
368    #[serde(skip_serializing_if = "Option::is_none")]
369    client_assertion_type: Option<JwtBearerClientAssertionType>,
370}