mas_oidc_client/types/
client_credentials.rs

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.
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    #[allow(clippy::too_many_lines)]
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 Rng,
140    ) -> Result<reqwest::RequestBuilder, CredentialsError> {
141        let request = match self {
142            ClientCredentials::None { client_id } => request.form(&RequestWithClientCredentials {
143                body: form,
144                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,
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,
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,
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,
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,
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 Rng,
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    client_id: &'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}