11package controller_test
22
33import (
4+ "crypto/sha256"
5+ "encoding/base64"
46 "encoding/json"
57 "net/http/httptest"
68 "net/url"
@@ -15,11 +17,13 @@ import (
1517 "github.com/steveiliop56/tinyauth/internal/controller"
1618 "github.com/steveiliop56/tinyauth/internal/repository"
1719 "github.com/steveiliop56/tinyauth/internal/service"
20+ "github.com/steveiliop56/tinyauth/internal/utils/tlog"
1821 "github.com/stretchr/testify/assert"
1922 "github.com/stretchr/testify/require"
2023)
2124
2225func TestOIDCController (t * testing.T ) {
26+ tlog .NewTestLogger ().Init ()
2327 tempDir := t .TempDir ()
2428
2529 oidcServiceCfg := service.OIDCServiceConfig {
@@ -431,6 +435,227 @@ func TestOIDCController(t *testing.T) {
431435 assert .False (t , ok , "Did not expect email claim in userinfo response" )
432436 },
433437 },
438+ {
439+ description : "Ensure plain PKCE succeeds" ,
440+ middlewares : []gin.HandlerFunc {
441+ simpleCtx ,
442+ },
443+ run : func (t * testing.T , router * gin.Engine , recorder * httptest.ResponseRecorder ) {
444+ reqBody := service.AuthorizeRequest {
445+ Scope : "openid" ,
446+ ResponseType : "code" ,
447+ ClientID : "some-client-id" ,
448+ RedirectURI : "https://test.example.com/callback" ,
449+ State : "some-state" ,
450+ Nonce : "some-nonce" ,
451+ CodeChallenge : "some-challenge" ,
452+ // Not setting a code challenge method should default to "plain"
453+ CodeChallengeMethod : "" ,
454+ }
455+ reqBodyBytes , err := json .Marshal (reqBody )
456+ assert .NoError (t , err )
457+
458+ req := httptest .NewRequest ("POST" , "/api/oidc/authorize" , strings .NewReader (string (reqBodyBytes )))
459+ req .Header .Set ("Content-Type" , "application/json" )
460+ router .ServeHTTP (recorder , req )
461+ assert .Equal (t , 200 , recorder .Code )
462+
463+ var res map [string ]any
464+ err = json .Unmarshal (recorder .Body .Bytes (), & res )
465+ assert .NoError (t , err )
466+
467+ redirectURI := res ["redirect_uri" ].(string )
468+ url , err := url .Parse (redirectURI )
469+ assert .NoError (t , err )
470+
471+ queryParams := url .Query ()
472+ assert .Equal (t , queryParams .Get ("state" ), "some-state" )
473+
474+ code := queryParams .Get ("code" )
475+ assert .NotEmpty (t , code )
476+
477+ // Now exchange the code for a token
478+ recorder = httptest .NewRecorder ()
479+ tokenReqBody := controller.TokenRequest {
480+ GrantType : "authorization_code" ,
481+ Code : code ,
482+ RedirectURI : "https://test.example.com/callback" ,
483+ CodeVerifier : "some-challenge" ,
484+ }
485+ reqBodyEncoded , err := query .Values (tokenReqBody )
486+ assert .NoError (t , err )
487+
488+ req = httptest .NewRequest ("POST" , "/api/oidc/token" , strings .NewReader (reqBodyEncoded .Encode ()))
489+ req .Header .Set ("Content-Type" , "application/x-www-form-urlencoded" )
490+ req .SetBasicAuth ("some-client-id" , "some-client-secret" )
491+ router .ServeHTTP (recorder , req )
492+
493+ assert .Equal (t , 200 , recorder .Code )
494+ },
495+ },
496+ {
497+ description : "Ensure S256 PKCE succeeds" ,
498+ middlewares : []gin.HandlerFunc {
499+ simpleCtx ,
500+ },
501+ run : func (t * testing.T , router * gin.Engine , recorder * httptest.ResponseRecorder ) {
502+ hasher := sha256 .New ()
503+ hasher .Write ([]byte ("some-challenge" ))
504+ codeChallenge := hasher .Sum (nil )
505+ codeChallengeEncoded := base64 .RawURLEncoding .EncodeToString (codeChallenge )
506+ reqBody := service.AuthorizeRequest {
507+ Scope : "openid" ,
508+ ResponseType : "code" ,
509+ ClientID : "some-client-id" ,
510+ RedirectURI : "https://test.example.com/callback" ,
511+ State : "some-state" ,
512+ Nonce : "some-nonce" ,
513+ CodeChallenge : codeChallengeEncoded ,
514+ CodeChallengeMethod : "S256" ,
515+ }
516+ reqBodyBytes , err := json .Marshal (reqBody )
517+ assert .NoError (t , err )
518+
519+ req := httptest .NewRequest ("POST" , "/api/oidc/authorize" , strings .NewReader (string (reqBodyBytes )))
520+ req .Header .Set ("Content-Type" , "application/json" )
521+ router .ServeHTTP (recorder , req )
522+ assert .Equal (t , 200 , recorder .Code )
523+
524+ var res map [string ]any
525+ err = json .Unmarshal (recorder .Body .Bytes (), & res )
526+ assert .NoError (t , err )
527+
528+ redirectURI := res ["redirect_uri" ].(string )
529+ url , err := url .Parse (redirectURI )
530+ assert .NoError (t , err )
531+
532+ queryParams := url .Query ()
533+ assert .Equal (t , queryParams .Get ("state" ), "some-state" )
534+
535+ code := queryParams .Get ("code" )
536+ assert .NotEmpty (t , code )
537+
538+ // Now exchange the code for a token
539+ recorder = httptest .NewRecorder ()
540+ tokenReqBody := controller.TokenRequest {
541+ GrantType : "authorization_code" ,
542+ Code : code ,
543+ RedirectURI : "https://test.example.com/callback" ,
544+ CodeVerifier : "some-challenge" ,
545+ }
546+ reqBodyEncoded , err := query .Values (tokenReqBody )
547+ assert .NoError (t , err )
548+
549+ req = httptest .NewRequest ("POST" , "/api/oidc/token" , strings .NewReader (reqBodyEncoded .Encode ()))
550+ req .Header .Set ("Content-Type" , "application/x-www-form-urlencoded" )
551+ req .SetBasicAuth ("some-client-id" , "some-client-secret" )
552+ router .ServeHTTP (recorder , req )
553+
554+ assert .Equal (t , 200 , recorder .Code )
555+ },
556+ },
557+ {
558+ description : "Ensure request with invalid PKCE fails" ,
559+ middlewares : []gin.HandlerFunc {
560+ simpleCtx ,
561+ },
562+ run : func (t * testing.T , router * gin.Engine , recorder * httptest.ResponseRecorder ) {
563+ hasher := sha256 .New ()
564+ hasher .Write ([]byte ("some-challenge" ))
565+ codeChallenge := hasher .Sum (nil )
566+ codeChallengeEncoded := base64 .RawURLEncoding .EncodeToString (codeChallenge )
567+ reqBody := service.AuthorizeRequest {
568+ Scope : "openid" ,
569+ ResponseType : "code" ,
570+ ClientID : "some-client-id" ,
571+ RedirectURI : "https://test.example.com/callback" ,
572+ State : "some-state" ,
573+ Nonce : "some-nonce" ,
574+ CodeChallenge : codeChallengeEncoded ,
575+ CodeChallengeMethod : "S256" ,
576+ }
577+ reqBodyBytes , err := json .Marshal (reqBody )
578+ assert .NoError (t , err )
579+
580+ req := httptest .NewRequest ("POST" , "/api/oidc/authorize" , strings .NewReader (string (reqBodyBytes )))
581+ req .Header .Set ("Content-Type" , "application/json" )
582+ router .ServeHTTP (recorder , req )
583+ assert .Equal (t , 200 , recorder .Code )
584+
585+ var res map [string ]any
586+ err = json .Unmarshal (recorder .Body .Bytes (), & res )
587+ assert .NoError (t , err )
588+
589+ redirectURI := res ["redirect_uri" ].(string )
590+ url , err := url .Parse (redirectURI )
591+ assert .NoError (t , err )
592+
593+ queryParams := url .Query ()
594+ assert .Equal (t , queryParams .Get ("state" ), "some-state" )
595+
596+ code := queryParams .Get ("code" )
597+ assert .NotEmpty (t , code )
598+
599+ // Now exchange the code for a token
600+ recorder = httptest .NewRecorder ()
601+ tokenReqBody := controller.TokenRequest {
602+ GrantType : "authorization_code" ,
603+ Code : code ,
604+ RedirectURI : "https://test.example.com/callback" ,
605+ CodeVerifier : "some-challenge-1" ,
606+ }
607+ reqBodyEncoded , err := query .Values (tokenReqBody )
608+ assert .NoError (t , err )
609+
610+ req = httptest .NewRequest ("POST" , "/api/oidc/token" , strings .NewReader (reqBodyEncoded .Encode ()))
611+ req .Header .Set ("Content-Type" , "application/x-www-form-urlencoded" )
612+ req .SetBasicAuth ("some-client-id" , "some-client-secret" )
613+ router .ServeHTTP (recorder , req )
614+
615+ assert .Equal (t , 400 , recorder .Code )
616+ },
617+ },
618+ {
619+ description : "Ensure request with invalid challenge method fails" ,
620+ middlewares : []gin.HandlerFunc {
621+ simpleCtx ,
622+ },
623+ run : func (t * testing.T , router * gin.Engine , recorder * httptest.ResponseRecorder ) {
624+ hasher := sha256 .New ()
625+ hasher .Write ([]byte ("some-challenge" ))
626+ codeChallenge := hasher .Sum (nil )
627+ codeChallengeEncoded := base64 .RawURLEncoding .EncodeToString (codeChallenge )
628+ reqBody := service.AuthorizeRequest {
629+ Scope : "openid" ,
630+ ResponseType : "code" ,
631+ ClientID : "some-client-id" ,
632+ RedirectURI : "https://test.example.com/callback" ,
633+ State : "some-state" ,
634+ Nonce : "some-nonce" ,
635+ CodeChallenge : codeChallengeEncoded ,
636+ CodeChallengeMethod : "foo" ,
637+ }
638+ reqBodyBytes , err := json .Marshal (reqBody )
639+ assert .NoError (t , err )
640+
641+ req := httptest .NewRequest ("POST" , "/api/oidc/authorize" , strings .NewReader (string (reqBodyBytes )))
642+ req .Header .Set ("Content-Type" , "application/json" )
643+ router .ServeHTTP (recorder , req )
644+ assert .Equal (t , 200 , recorder .Code )
645+
646+ var res map [string ]any
647+ err = json .Unmarshal (recorder .Body .Bytes (), & res )
648+ assert .NoError (t , err )
649+
650+ redirectURI := res ["redirect_uri" ].(string )
651+ url , err := url .Parse (redirectURI )
652+ assert .NoError (t , err )
653+
654+ queryParams := url .Query ()
655+ error := queryParams .Get ("error" )
656+ assert .NotEmpty (t , error )
657+ },
658+ },
434659 }
435660
436661 app := bootstrap .NewBootstrapApp (config.Config {})
0 commit comments