1use 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
28pub const CLIENT_SUPPORTED_AUTH_METHODS: &[OAuthClientAuthenticationMethod] = &[
33 OAuthClientAuthenticationMethod::None,
34 OAuthClientAuthenticationMethod::ClientSecretBasic,
35 OAuthClientAuthenticationMethod::ClientSecretPost,
36 OAuthClientAuthenticationMethod::ClientSecretJwt,
37 OAuthClientAuthenticationMethod::PrivateKeyJwt,
38];
39
40#[derive(Clone)]
43pub enum ClientCredentials {
44 None {
48 client_id: String,
50 },
51
52 ClientSecretBasic {
54 client_id: String,
56
57 client_secret: String,
59 },
60
61 ClientSecretPost {
63 client_id: String,
65
66 client_secret: String,
68 },
69
70 ClientSecretJwt {
73 client_id: String,
75
76 client_secret: String,
78
79 signing_algorithm: JsonWebSignatureAlg,
81
82 token_endpoint: Url,
84 },
85
86 PrivateKeyJwt {
88 client_id: String,
90
91 keystore: Keystore,
93
94 signing_algorithm: JsonWebSignatureAlg,
96
97 token_endpoint: Url,
99 },
100
101 SignInWithApple {
103 client_id: String,
105
106 key: elliptic_curve::SecretKey<p256::NistP256>,
108
109 key_id: String,
111
112 team_id: String,
114 },
115}
116
117impl ClientCredentials {
118 #[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 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 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#[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}