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 #[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 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#[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}