mas_oidc_client/types/
client_credentials.rs

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