@@ -5,6 +5,8 @@ package auth
55
66import (
77 "context"
8+ "crypto/ecdsa"
9+ "crypto/elliptic"
810 "crypto/rand"
911 "crypto/rsa"
1012 "crypto/x509"
@@ -37,6 +39,51 @@ func testLogger() *otelzap.Logger {
3739 return otelzap .New (zapLogger )
3840}
3941
42+ func TestValidateOAuth2SigningAlgorithms (t * testing.T ) {
43+ tests := []struct {
44+ name string
45+ input []string
46+ expectErr string
47+ }{
48+ {
49+ name : "rejects missing algorithms" ,
50+ expectErr : "OAuth2 signing algorithms are required" ,
51+ },
52+ {
53+ name : "accepts configured algorithms" ,
54+ input : []string {"RS256" , "ES256" , "RS256" },
55+ },
56+ {
57+ name : "rejects algorithms with whitespace" ,
58+ input : []string {"RS256" , " ES256" },
59+ expectErr : `unsupported OAuth2 signing algorithm " ES256"` ,
60+ },
61+ {
62+ name : "rejects empty algorithm" ,
63+ input : []string {"RS256" , "" },
64+ expectErr : `unsupported OAuth2 signing algorithm ""` ,
65+ },
66+ {
67+ name : "rejects unsupported algorithm" ,
68+ input : []string {"HS256" },
69+ expectErr : `unsupported OAuth2 signing algorithm "HS256"` ,
70+ },
71+ }
72+
73+ for _ , tt := range tests {
74+ t .Run (tt .name , func (t * testing.T ) {
75+ err := validateOAuth2SigningAlgorithms (tt .input )
76+ if tt .expectErr != "" {
77+ require .Error (t , err )
78+ assert .Contains (t , err .Error (), tt .expectErr )
79+ return
80+ }
81+
82+ require .NoError (t , err )
83+ })
84+ }
85+ }
86+
4087// TestOAuth2Authentication tests OAuth2/JWKS authentication with mock server
4188func TestOAuth2Authentication (t * testing.T ) {
4289 // Generate RSA key pair for JWT signing
@@ -88,6 +135,7 @@ func TestOAuth2Authentication(t *testing.T) {
88135 jwksServer .URL ,
89136 "https://auth.example.com/" ,
90137 "test-audience" ,
138+ []string {gojwt .SigningMethodRS256 .Alg ()},
91139 pm ,
92140 testLogger (),
93141 testServiceName ,
@@ -287,6 +335,7 @@ func TestOAuth2RejectsUnexpectedSigningMethod(t *testing.T) {
287335 jwksServer .URL ,
288336 "https://auth.example.com/" ,
289337 "test-audience" ,
338+ []string {gojwt .SigningMethodRS256 .Alg ()},
290339 pm ,
291340 testLogger (),
292341 testServiceName ,
@@ -310,6 +359,68 @@ func TestOAuth2RejectsUnexpectedSigningMethod(t *testing.T) {
310359 assert .Contains (t , err .Error (), "signing method HS256 is invalid" )
311360}
312361
362+ func TestOAuth2AllowsConfiguredES256SigningMethod (t * testing.T ) {
363+ privateKey , err := ecdsa .GenerateKey (elliptic .P256 (), rand .Reader )
364+ require .NoError (t , err )
365+
366+ jwkSet := jwkset .NewMemoryStorage ()
367+ jwk , err := jwkset .NewJWKFromKey (privateKey , jwkset.JWKOptions {
368+ Metadata : jwkset.JWKMetadataOptions {
369+ KID : "test-key-1" ,
370+ },
371+ })
372+ require .NoError (t , err )
373+ require .NoError (t , jwkSet .KeyWrite (context .Background (), jwk ))
374+
375+ jwksServer := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
376+ jwks , err := jwkSet .JSONPublic (context .Background ())
377+ if err != nil {
378+ http .Error (w , "Failed to get JWKS" , http .StatusInternalServerError )
379+ return
380+ }
381+ w .Header ().Set ("Content-Type" , "application/json" )
382+ if _ , err := w .Write (jwks ); err != nil {
383+ http .Error (w , "Failed to write JWKS" , http .StatusInternalServerError )
384+ }
385+ }))
386+ defer jwksServer .Close ()
387+
388+ permFile := createTestPermissionsFile (t )
389+ defer os .Remove (permFile )
390+
391+ pm , err := config .NewPermissionsManager (permFile , testLogger ())
392+ require .NoError (t , err )
393+ defer pm .Close ()
394+
395+ oauth2Auth , err := NewOAuth2Authenticator (
396+ jwksServer .URL ,
397+ "https://auth.example.com/" ,
398+ "test-audience" ,
399+ []string {gojwt .SigningMethodRS256 .Alg (), gojwt .SigningMethodES256 .Alg ()},
400+ pm ,
401+ testLogger (),
402+ testServiceName ,
403+ )
404+ require .NoError (t , err )
405+ defer oauth2Auth .Close ()
406+
407+ token := gojwt .NewWithClaims (gojwt .SigningMethodES256 , gojwt.MapClaims {
408+ "iss" : "https://auth.example.com/" ,
409+ "sub" : "user@example.com" ,
410+ "aud" : "test-audience" ,
411+ "exp" : time .Now ().Add (1 * time .Hour ).Unix (),
412+ "scope" : "mqtt" ,
413+ })
414+ token .Header ["kid" ] = "test-key-1"
415+ tokenString , err := token .SignedString (privateKey )
416+ require .NoError (t , err )
417+
418+ profile , err := oauth2Auth .Authenticate (context .Background (), tokenString )
419+ require .NoError (t , err )
420+ require .NotNil (t , profile )
421+ assert .Equal (t , "APP1" , profile .Account )
422+ }
423+
313424// TestOAuth2RequiredScope tests per-client required scope validation
314425func TestOAuth2RequiredScope (t * testing.T ) {
315426 // Generate RSA key pair for JWT signing
@@ -382,6 +493,7 @@ func TestOAuth2RequiredScope(t *testing.T) {
382493 jwksServer .URL ,
383494 "https://auth.example.com/" ,
384495 "test-audience" ,
496+ []string {gojwt .SigningMethodRS256 .Alg ()},
385497 pm ,
386498 testLogger (),
387499 testServiceName ,
0 commit comments