@@ -76,6 +76,125 @@ func Test_BeginAuth(t *testing.T) {
7676 a .Contains (s .AuthURL , "state=test_state" )
7777 a .Contains (s .AuthURL , "redirect_uri=http%3A%2F%2Flocalhost%2Ffoo" )
7878 a .Contains (s .AuthURL , "scope=openid" )
79+
80+ // The mock server advertises ["plain","S256"] so PKCE must be used with S256.
81+ a .Equal ("S256" , provider .PKCEMethod )
82+ a .NotEmpty (s .CodeVerifier )
83+ a .Contains (s .AuthURL , "code_challenge=" )
84+ a .Contains (s .AuthURL , "code_challenge_method=S256" )
85+ }
86+
87+ func Test_BeginAuth_PKCE_S256_Challenge (t * testing.T ) {
88+ t .Parallel ()
89+ a := assert .New (t )
90+
91+ provider := openidConnectProvider ()
92+ session , err := provider .BeginAuth ("test_state" )
93+ a .NoError (err )
94+ s := session .(* Session )
95+
96+ // Verify that the code_challenge in the URL matches the S256 of the stored verifier.
97+ expected := generateS256Challenge (s .CodeVerifier )
98+ a .Contains (s .AuthURL , "code_challenge=" + expected )
99+ }
100+
101+ func Test_BeginAuth_NoPKCE_WhenNotAdvertised (t * testing.T ) {
102+ t .Parallel ()
103+ a := assert .New (t )
104+
105+ // Spin up a server that does NOT advertise code_challenge_methods_supported.
106+ noPKCEServer := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
107+ fmt .Fprintln (w , `{"issuer":"https://accounts.google.com","authorization_endpoint":"https://accounts.google.com/o/oauth2/v2/auth","token_endpoint":"https://www.googleapis.com/oauth2/v4/token","userinfo_endpoint":"https://www.googleapis.com/oauth2/v3/userinfo"}` )
108+ }))
109+ defer noPKCEServer .Close ()
110+
111+ provider , err := New (os .Getenv ("OPENID_CONNECT_KEY" ), os .Getenv ("OPENID_CONNECT_SECRET" ), "http://localhost/foo" , noPKCEServer .URL )
112+ a .NoError (err )
113+ a .Equal ("" , provider .PKCEMethod )
114+
115+ session , err := provider .BeginAuth ("test_state" )
116+ a .NoError (err )
117+ s := session .(* Session )
118+ a .Empty (s .CodeVerifier )
119+ a .NotContains (s .AuthURL , "code_challenge" )
120+ a .NotContains (s .AuthURL , "code_challenge_method" )
121+ }
122+
123+ func Test_SelectPKCEMethod (t * testing.T ) {
124+ t .Parallel ()
125+ a := assert .New (t )
126+
127+ a .Equal ("S256" , selectPKCEMethod ([]string {"plain" , "S256" }))
128+ a .Equal ("S256" , selectPKCEMethod ([]string {"S256" }))
129+ a .Equal ("plain" , selectPKCEMethod ([]string {"plain" }))
130+ a .Equal ("" , selectPKCEMethod ([]string {}))
131+ a .Equal ("" , selectPKCEMethod (nil ))
132+ a .Equal ("" , selectPKCEMethod ([]string {"other" }))
133+ }
134+
135+ func Test_GenerateCodeVerifier (t * testing.T ) {
136+ t .Parallel ()
137+ a := assert .New (t )
138+
139+ v , err := generateCodeVerifier ()
140+ a .NoError (err )
141+ // 32 bytes base64url-encoded without padding = 43 chars
142+ a .Equal (43 , len (v ))
143+ // All chars must be URL-safe base64 alphabet
144+ for _ , c := range v {
145+ a .Contains ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" , string (c ))
146+ }
147+
148+ // Two verifiers must differ (with overwhelming probability)
149+ v2 , _ := generateCodeVerifier ()
150+ a .NotEqual (v , v2 )
151+ }
152+
153+ func Test_GenerateS256Challenge (t * testing.T ) {
154+ t .Parallel ()
155+ a := assert .New (t )
156+
157+ // Known test vector from RFC 7636 Appendix B:
158+ // code_verifier = dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
159+ // code_challenge = E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
160+ a .Equal ("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" , generateS256Challenge ("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" ))
161+ }
162+
163+ func Test_New_PKCE_MethodSelectedFromDiscovery (t * testing.T ) {
164+ t .Parallel ()
165+ a := assert .New (t )
166+
167+ // Mock server advertises only "plain"
168+ plainOnlyServer := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
169+ fmt .Fprintln (w , `{"issuer":"https://accounts.google.com","authorization_endpoint":"https://accounts.google.com/o/oauth2/v2/auth","token_endpoint":"https://www.googleapis.com/oauth2/v4/token","userinfo_endpoint":"https://www.googleapis.com/oauth2/v3/userinfo","code_challenge_methods_supported":["plain"]}` )
170+ }))
171+ defer plainOnlyServer .Close ()
172+
173+ provider , err := New (os .Getenv ("OPENID_CONNECT_KEY" ), os .Getenv ("OPENID_CONNECT_SECRET" ), "http://localhost/foo" , plainOnlyServer .URL )
174+ a .NoError (err )
175+ a .Equal ("plain" , provider .PKCEMethod )
176+ }
177+
178+ func Test_BeginAuth_PKCE_PlainMethod (t * testing.T ) {
179+ t .Parallel ()
180+ a := assert .New (t )
181+
182+ // Mock server advertises only "plain"
183+ plainOnlyServer := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
184+ fmt .Fprintln (w , `{"issuer":"https://accounts.google.com","authorization_endpoint":"https://accounts.google.com/o/oauth2/v2/auth","token_endpoint":"https://www.googleapis.com/oauth2/v4/token","userinfo_endpoint":"https://www.googleapis.com/oauth2/v3/userinfo","code_challenge_methods_supported":["plain"]}` )
185+ }))
186+ defer plainOnlyServer .Close ()
187+
188+ provider , err := New (os .Getenv ("OPENID_CONNECT_KEY" ), os .Getenv ("OPENID_CONNECT_SECRET" ), "http://localhost/foo" , plainOnlyServer .URL )
189+ a .NoError (err )
190+
191+ session , err := provider .BeginAuth ("test_state" )
192+ a .NoError (err )
193+ s := session .(* Session )
194+ a .NotEmpty (s .CodeVerifier )
195+ // For "plain", challenge == verifier
196+ a .Contains (s .AuthURL , "code_challenge=" + s .CodeVerifier )
197+ a .Contains (s .AuthURL , "code_challenge_method=plain" )
79198}
80199
81200func Test_BeginAuth_AuthCodeOptions (t * testing.T ) {
0 commit comments