6
6
7
7
use std:: ops:: Deref ;
8
8
9
+ use anyhow:: bail;
10
+ use camino:: Utf8PathBuf ;
9
11
use mas_iana:: oauth:: OAuthClientAuthenticationMethod ;
10
12
use mas_jose:: jwk:: PublicJsonWebKeySet ;
11
13
use schemars:: JsonSchema ;
12
14
use serde:: { Deserialize , Serialize , de:: Error } ;
15
+ use serde_with:: serde_as;
13
16
use ulid:: Ulid ;
14
17
use url:: Url ;
15
18
@@ -28,6 +31,66 @@ impl From<PublicJsonWebKeySet> for JwksOrJwksUri {
28
31
}
29
32
}
30
33
34
+ /// Client secret config option.
35
+ ///
36
+ /// It either holds the client secret value directly or references a file where
37
+ /// the client secret is stored.
38
+ #[ derive( Clone , Debug ) ]
39
+ pub enum ClientSecret {
40
+ File ( Utf8PathBuf ) ,
41
+ Value ( String ) ,
42
+ }
43
+
44
+ /// ClientSecret fields as serialized in JSON.
45
+ #[ derive( JsonSchema , Serialize , Deserialize , Clone , Debug ) ]
46
+ struct ClientSecretRaw {
47
+ /// Path to the file containing the client secret. The client secret is used
48
+ /// by the `client_secret_basic`, `client_secret_post` and
49
+ /// `client_secret_jwt` authentication methods.
50
+ #[ schemars( with = "Option<String>" ) ]
51
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
52
+ client_secret_file : Option < Utf8PathBuf > ,
53
+
54
+ /// Alternative to `client_secret_file`: Reads the client secret directly
55
+ /// from the config.
56
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
57
+ client_secret : Option < String > ,
58
+ }
59
+
60
+ impl TryFrom < ClientSecretRaw > for Option < ClientSecret > {
61
+ type Error = anyhow:: Error ;
62
+
63
+ fn try_from ( value : ClientSecretRaw ) -> Result < Self , Self :: Error > {
64
+ match ( value. client_secret , value. client_secret_file ) {
65
+ ( None , None ) => Ok ( None ) ,
66
+ ( None , Some ( path) ) => Ok ( Some ( ClientSecret :: File ( path) ) ) ,
67
+ ( Some ( client_secret) , None ) => Ok ( Some ( ClientSecret :: Value ( client_secret) ) ) ,
68
+ ( Some ( _) , Some ( _) ) => {
69
+ bail ! ( "Cannot specify both `client_secret` and `client_secret_file`" )
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ impl From < Option < ClientSecret > > for ClientSecretRaw {
76
+ fn from ( value : Option < ClientSecret > ) -> Self {
77
+ match value {
78
+ Some ( ClientSecret :: File ( path) ) => ClientSecretRaw {
79
+ client_secret_file : Some ( path) ,
80
+ client_secret : None ,
81
+ } ,
82
+ Some ( ClientSecret :: Value ( client_secret) ) => ClientSecretRaw {
83
+ client_secret_file : None ,
84
+ client_secret : Some ( client_secret) ,
85
+ } ,
86
+ None => ClientSecretRaw {
87
+ client_secret_file : None ,
88
+ client_secret : None ,
89
+ } ,
90
+ }
91
+ }
92
+ }
93
+
31
94
/// Authentication method used by clients
32
95
#[ derive( JsonSchema , Serialize , Deserialize , Copy , Clone , Debug ) ]
33
96
#[ serde( rename_all = "snake_case" ) ]
@@ -65,6 +128,7 @@ impl std::fmt::Display for ClientAuthMethodConfig {
65
128
}
66
129
67
130
/// An OAuth 2.0 client configuration
131
+ #[ serde_as]
68
132
#[ derive( Debug , Clone , Serialize , Deserialize , JsonSchema ) ]
69
133
pub struct ClientConfig {
70
134
/// The client ID
@@ -84,8 +148,10 @@ pub struct ClientConfig {
84
148
85
149
/// The client secret, used by the `client_secret_basic`,
86
150
/// `client_secret_post` and `client_secret_jwt` authentication methods
87
- #[ serde( skip_serializing_if = "Option::is_none" ) ]
88
- pub client_secret : Option < String > ,
151
+ #[ schemars( with = "ClientSecretRaw" ) ]
152
+ #[ serde_as( as = "serde_with::TryFromInto<ClientSecretRaw>" ) ]
153
+ #[ serde( flatten) ]
154
+ pub client_secret : Option < ClientSecret > ,
89
155
90
156
/// The JSON Web Key Set (JWKS) used by the `private_key_jwt` authentication
91
157
/// method. Mutually exclusive with `jwks_uri`
@@ -197,6 +263,21 @@ impl ClientConfig {
197
263
ClientAuthMethodConfig :: PrivateKeyJwt => OAuthClientAuthenticationMethod :: PrivateKeyJwt ,
198
264
}
199
265
}
266
+
267
+ /// Returns the client secret.
268
+ ///
269
+ /// If `client_secret_file` was given, the secret is read from that file.
270
+ ///
271
+ /// # Errors
272
+ ///
273
+ /// Returns an error when the client secret could not be read from file.
274
+ pub async fn client_secret ( & self ) -> anyhow:: Result < Option < String > > {
275
+ Ok ( match & self . client_secret {
276
+ Some ( ClientSecret :: File ( path) ) => Some ( tokio:: fs:: read_to_string ( path) . await ?) ,
277
+ Some ( ClientSecret :: Value ( client_secret) ) => Some ( client_secret. clone ( ) ) ,
278
+ None => None ,
279
+ } )
280
+ }
200
281
}
201
282
202
283
/// List of OAuth 2.0/OIDC clients config
@@ -258,75 +339,91 @@ mod tests {
258
339
Figment , Jail ,
259
340
providers:: { Format , Yaml } ,
260
341
} ;
342
+ use tokio:: { runtime:: Handle , task} ;
261
343
262
344
use super :: * ;
263
345
264
- #[ test]
265
- fn load_config ( ) {
266
- Jail :: expect_with ( |jail| {
267
- jail. create_file (
268
- "config.yaml" ,
269
- r#"
270
- clients:
271
- - client_id: 01GFWR28C4KNE04WG3HKXB7C9R
272
- client_auth_method: none
273
- redirect_uris:
274
- - https://exemple.fr/callback
275
-
276
- - client_id: 01GFWR32NCQ12B8Z0J8CPXRRB6
277
- client_auth_method: client_secret_basic
278
- client_secret: hello
279
-
280
- - client_id: 01GFWR3WHR93Y5HK389H28VHZ9
281
- client_auth_method: client_secret_post
282
- client_secret: hello
283
-
284
- - client_id: 01GFWR43R2ZZ8HX9CVBNW9TJWG
285
- client_auth_method: client_secret_jwt
286
- client_secret: hello
287
-
288
- - client_id: 01GFWR4BNFDCC4QDG6AMSP1VRR
289
- client_auth_method: private_key_jwt
290
- jwks:
291
- keys:
292
- - kid: "03e84aed4ef4431014e8617567864c4efaaaede9"
293
- kty: "RSA"
294
- alg: "RS256"
295
- use: "sig"
296
- e: "AQAB"
297
- n: "ma2uRyBeSEOatGuDpCiV9oIxlDWix_KypDYuhQfEzqi_BiF4fV266OWfyjcABbam59aJMNvOnKW3u_eZM-PhMCBij5MZ-vcBJ4GfxDJeKSn-GP_dJ09rpDcILh8HaWAnPmMoi4DC0nrfE241wPISvZaaZnGHkOrfN_EnA5DligLgVUbrA5rJhQ1aSEQO_gf1raEOW3DZ_ACU3qhtgO0ZBG3a5h7BPiRs2sXqb2UCmBBgwyvYLDebnpE7AotF6_xBIlR-Cykdap3GHVMXhrIpvU195HF30ZoBU4dMd-AeG6HgRt4Cqy1moGoDgMQfbmQ48Hlunv9_Vi2e2CLvYECcBw"
298
-
299
- - kid: "d01c1abe249269f72ef7ca2613a86c9f05e59567"
300
- kty: "RSA"
301
- alg: "RS256"
302
- use: "sig"
303
- e: "AQAB"
304
- n: "0hukqytPwrj1RbMYhYoepCi3CN5k7DwYkTe_Cmb7cP9_qv4ok78KdvFXt5AnQxCRwBD7-qTNkkfMWO2RxUMBdQD0ED6tsSb1n5dp0XY8dSWiBDCX8f6Hr-KolOpvMLZKRy01HdAWcM6RoL9ikbjYHUEW1C8IJnw3MzVHkpKFDL354aptdNLaAdTCBvKzU9WpXo10g-5ctzSlWWjQuecLMQ4G1mNdsR1LHhUENEnOvgT8cDkX0fJzLbEbyBYkdMgKggyVPEB1bg6evG4fTKawgnf0IDSPxIU-wdS9wdSP9ZCJJPLi5CEp-6t6rE_sb2dGcnzjCGlembC57VwpkUvyMw"
305
- "# ,
306
- ) ?;
307
-
308
- let config = Figment :: new ( )
309
- . merge ( Yaml :: file ( "config.yaml" ) )
310
- . extract_inner :: < ClientsConfig > ( "clients" ) ?;
311
-
312
- assert_eq ! ( config. 0 . len( ) , 5 ) ;
313
-
314
- assert_eq ! (
315
- config. 0 [ 0 ] . client_id,
316
- Ulid :: from_str( "01GFWR28C4KNE04WG3HKXB7C9R" ) . unwrap( )
317
- ) ;
318
- assert_eq ! (
319
- config. 0 [ 0 ] . redirect_uris,
320
- vec![ "https://exemple.fr/callback" . parse( ) . unwrap( ) ]
321
- ) ;
322
-
323
- assert_eq ! (
324
- config. 0 [ 1 ] . client_id,
325
- Ulid :: from_str( "01GFWR32NCQ12B8Z0J8CPXRRB6" ) . unwrap( )
326
- ) ;
327
- assert_eq ! ( config. 0 [ 1 ] . redirect_uris, Vec :: new( ) ) ;
328
-
329
- Ok ( ( ) )
330
- } ) ;
346
+ #[ tokio:: test]
347
+ async fn load_config ( ) {
348
+ task:: spawn_blocking ( || {
349
+ Jail :: expect_with ( |jail| {
350
+ jail. create_file (
351
+ "config.yaml" ,
352
+ r#"
353
+ clients:
354
+ - client_id: 01GFWR28C4KNE04WG3HKXB7C9R
355
+ client_auth_method: none
356
+ redirect_uris:
357
+ - https://exemple.fr/callback
358
+
359
+ - client_id: 01GFWR32NCQ12B8Z0J8CPXRRB6
360
+ client_auth_method: client_secret_basic
361
+ client_secret_file: secret
362
+
363
+ - client_id: 01GFWR3WHR93Y5HK389H28VHZ9
364
+ client_auth_method: client_secret_post
365
+ client_secret: c1!3n753c237
366
+
367
+ - client_id: 01GFWR43R2ZZ8HX9CVBNW9TJWG
368
+ client_auth_method: client_secret_jwt
369
+ client_secret_file: secret
370
+
371
+ - client_id: 01GFWR4BNFDCC4QDG6AMSP1VRR
372
+ client_auth_method: private_key_jwt
373
+ jwks:
374
+ keys:
375
+ - kid: "03e84aed4ef4431014e8617567864c4efaaaede9"
376
+ kty: "RSA"
377
+ alg: "RS256"
378
+ use: "sig"
379
+ e: "AQAB"
380
+ n: "ma2uRyBeSEOatGuDpCiV9oIxlDWix_KypDYuhQfEzqi_BiF4fV266OWfyjcABbam59aJMNvOnKW3u_eZM-PhMCBij5MZ-vcBJ4GfxDJeKSn-GP_dJ09rpDcILh8HaWAnPmMoi4DC0nrfE241wPISvZaaZnGHkOrfN_EnA5DligLgVUbrA5rJhQ1aSEQO_gf1raEOW3DZ_ACU3qhtgO0ZBG3a5h7BPiRs2sXqb2UCmBBgwyvYLDebnpE7AotF6_xBIlR-Cykdap3GHVMXhrIpvU195HF30ZoBU4dMd-AeG6HgRt4Cqy1moGoDgMQfbmQ48Hlunv9_Vi2e2CLvYECcBw"
381
+
382
+ - kid: "d01c1abe249269f72ef7ca2613a86c9f05e59567"
383
+ kty: "RSA"
384
+ alg: "RS256"
385
+ use: "sig"
386
+ e: "AQAB"
387
+ n: "0hukqytPwrj1RbMYhYoepCi3CN5k7DwYkTe_Cmb7cP9_qv4ok78KdvFXt5AnQxCRwBD7-qTNkkfMWO2RxUMBdQD0ED6tsSb1n5dp0XY8dSWiBDCX8f6Hr-KolOpvMLZKRy01HdAWcM6RoL9ikbjYHUEW1C8IJnw3MzVHkpKFDL354aptdNLaAdTCBvKzU9WpXo10g-5ctzSlWWjQuecLMQ4G1mNdsR1LHhUENEnOvgT8cDkX0fJzLbEbyBYkdMgKggyVPEB1bg6evG4fTKawgnf0IDSPxIU-wdS9wdSP9ZCJJPLi5CEp-6t6rE_sb2dGcnzjCGlembC57VwpkUvyMw"
388
+ "# ,
389
+ ) ?;
390
+ jail. create_file ( "secret" , r"c1!3n753c237" ) ?;
391
+
392
+ let config = Figment :: new ( )
393
+ . merge ( Yaml :: file ( "config.yaml" ) )
394
+ . extract_inner :: < ClientsConfig > ( "clients" ) ?;
395
+
396
+ assert_eq ! ( config. 0 . len( ) , 5 ) ;
397
+
398
+ assert_eq ! (
399
+ config. 0 [ 0 ] . client_id,
400
+ Ulid :: from_str( "01GFWR28C4KNE04WG3HKXB7C9R" ) . unwrap( )
401
+ ) ;
402
+ assert_eq ! (
403
+ config. 0 [ 0 ] . redirect_uris,
404
+ vec![ "https://exemple.fr/callback" . parse( ) . unwrap( ) ]
405
+ ) ;
406
+
407
+ assert_eq ! (
408
+ config. 0 [ 1 ] . client_id,
409
+ Ulid :: from_str( "01GFWR32NCQ12B8Z0J8CPXRRB6" ) . unwrap( )
410
+ ) ;
411
+ assert_eq ! ( config. 0 [ 1 ] . redirect_uris, Vec :: new( ) ) ;
412
+
413
+ assert ! ( matches!( config. 0 [ 0 ] . client_secret, None ) ) ;
414
+ assert ! ( matches!( config. 0 [ 1 ] . client_secret, Some ( ClientSecret :: File ( ref p) ) if p == "secret" ) ) ;
415
+ assert ! ( matches!( config. 0 [ 2 ] . client_secret, Some ( ClientSecret :: Value ( ref v) ) if v == "c1!3n753c237" ) ) ;
416
+ assert ! ( matches!( config. 0 [ 3 ] . client_secret, Some ( ClientSecret :: File ( ref p) ) if p == "secret" ) ) ;
417
+ assert ! ( matches!( config. 0 [ 4 ] . client_secret, None ) ) ;
418
+
419
+ Handle :: current ( ) . block_on ( async move {
420
+ assert_eq ! ( config. 0 [ 1 ] . client_secret( ) . await . unwrap( ) . unwrap( ) , "c1!3n753c237" ) ;
421
+ assert_eq ! ( config. 0 [ 2 ] . client_secret( ) . await . unwrap( ) . unwrap( ) , "c1!3n753c237" ) ;
422
+ assert_eq ! ( config. 0 [ 3 ] . client_secret( ) . await . unwrap( ) . unwrap( ) , "c1!3n753c237" ) ;
423
+ } ) ;
424
+
425
+ Ok ( ( ) )
426
+ } ) ;
427
+ } ) . await . unwrap ( ) ;
331
428
}
332
429
}
0 commit comments