@@ -11,6 +11,7 @@ import (
1111 "fmt"
1212 "net/http"
1313 "net/url"
14+ "slices"
1415 "time"
1516
1617 "github.com/database-playground/backend-v2/internal/auth"
@@ -100,6 +101,43 @@ func generateCodeChallenge(verifier string) string {
100101 return base64 .RawURLEncoding .EncodeToString (h [:])
101102}
102103
104+ // redirectWithError redirects to the redirect URI with error parameters
105+ func redirectWithError (c * gin.Context , redirectURI , errorCode , errorDescription , state string ) {
106+ if redirectURI == "" {
107+ // If no redirect URI is available, fall back to JSON response
108+ c .JSON (http .StatusBadRequest , OAuth2Error {
109+ Error : errorCode ,
110+ ErrorDescription : errorDescription ,
111+ State : state ,
112+ })
113+ return
114+ }
115+
116+ redirectURL , err := url .Parse (redirectURI )
117+ if err != nil {
118+ // If redirect URI is invalid, fall back to JSON response
119+ c .JSON (http .StatusBadRequest , OAuth2Error {
120+ Error : "invalid_request" ,
121+ ErrorDescription : "Invalid redirect URI" ,
122+ State : state ,
123+ })
124+ return
125+ }
126+
127+ // Add error parameters to query string
128+ q := redirectURL .Query ()
129+ q .Set ("error" , errorCode )
130+ if errorDescription != "" {
131+ q .Set ("error_description" , errorDescription )
132+ }
133+ if state != "" {
134+ q .Set ("state" , state )
135+ }
136+ redirectURL .RawQuery = q .Encode ()
137+
138+ c .Redirect (http .StatusFound , redirectURL .String ())
139+ }
140+
103141// Authorize handles the OAuth 2.0 authorization request (RFC 6749 Section 4.1.1)
104142func (h * GauthHandler ) Authorize (c * gin.Context ) {
105143 // Lax since we are using a cookie to store the verifier
@@ -108,72 +146,52 @@ func (h *GauthHandler) Authorize(c *gin.Context) {
108146
109147 // Validate required OAuth 2.0 parameters
110148 responseType := c .Query ("response_type" )
149+ redirectURI := c .Query ("redirect_uri" )
150+ state := c .Query ("state" )
151+
111152 if responseType != "code" {
112- c .JSON (http .StatusBadRequest , OAuth2Error {
113- Error : "invalid_request" ,
114- ErrorDescription : "response_type must be 'code'" ,
115- State : c .Query ("state" ),
116- })
153+ redirectWithError (c , redirectURI , "invalid_request" , "response_type must be 'code'" , state )
117154 return
118155 }
119156
120- redirectURI := c .Query ("redirect_uri" )
121157 if redirectURI == "" {
122158 c .JSON (http .StatusBadRequest , OAuth2Error {
123159 Error : "invalid_request" ,
124160 ErrorDescription : "redirect_uri is required" ,
125- State : c . Query ( " state" ) ,
161+ State : state ,
126162 })
127163 return
128164 }
129165
130- // Validate PKCE parameters (RFC 7636)
131- codeChallenge := c .Query ("code_challenge" )
132- codeChallengeMethod := c .Query ("code_challenge_method" )
133- if err := validatePKCE (codeChallenge , codeChallengeMethod ); err != nil {
166+ // Check if redirect URI is allowed first, before other validations
167+ allowed := slices .Contains (h .redirectURIs , redirectURI )
168+ if ! allowed {
134169 c .JSON (http .StatusBadRequest , OAuth2Error {
135170 Error : "invalid_request" ,
136- ErrorDescription : err . Error () ,
137- State : c . Query ( " state" ) ,
171+ ErrorDescription : "Bad redirect URI." ,
172+ State : state ,
138173 })
139174 return
140175 }
141176
142- // Check if redirect URI is allowed
143- allowed := false
144- for _ , allowedURI := range h .redirectURIs {
145- if redirectURI == allowedURI {
146- allowed = true
147- break
148- }
149- }
150- if ! allowed {
151- c .JSON (http .StatusBadRequest , OAuth2Error {
152- Error : "invalid_request" ,
153- ErrorDescription : "Bad redirect URI." ,
154- State : c .Query ("state" ),
155- })
177+ // Validate PKCE parameters (RFC 7636)
178+ codeChallenge := c .Query ("code_challenge" )
179+ codeChallengeMethod := c .Query ("code_challenge_method" )
180+ if err := validatePKCE (codeChallenge , codeChallengeMethod ); err != nil {
181+ redirectWithError (c , redirectURI , "invalid_request" , err .Error (), state )
156182 return
157183 }
158184
159185 // Generate internal code verifier for Google OAuth
160186 verifier , err := generateCodeVerifier ()
161187 if err != nil {
162- c .JSON (http .StatusInternalServerError , OAuth2Error {
163- Error : "server_error" ,
164- ErrorDescription : "Failed to generate verifier" ,
165- State : c .Query ("state" ),
166- })
188+ redirectWithError (c , redirectURI , "server_error" , "Failed to generate verifier" , state )
167189 return
168190 }
169191
170192 callbackURL , err := url .Parse (h .oauthConfig .RedirectURL )
171193 if err != nil {
172- c .JSON (http .StatusInternalServerError , OAuth2Error {
173- Error : "server_error" ,
174- ErrorDescription : "Failed to parse redirect URL" ,
175- State : c .Query ("state" ),
176- })
194+ redirectWithError (c , redirectURI , "server_error" , "Failed to parse redirect URL" , state )
177195 return
178196 }
179197
@@ -222,11 +240,7 @@ func (h *GauthHandler) Authorize(c *gin.Context) {
222240 // Generate state for Google OAuth (internal state)
223241 internalState , err := authutil .GenerateToken ()
224242 if err != nil {
225- c .JSON (http .StatusInternalServerError , OAuth2Error {
226- Error : "server_error" ,
227- ErrorDescription : "Failed to generate state" ,
228- State : c .Query ("state" ),
229- })
243+ redirectWithError (c , redirectURI , "server_error" , "Failed to generate state" , state )
230244 return
231245 }
232246
@@ -341,27 +355,21 @@ func (h *GauthHandler) decryptAuthCode(encryptedCode string) (*AuthorizationCode
341355func (h * GauthHandler ) Callback (c * gin.Context ) {
342356 c .SetSameSite (http .SameSiteStrictMode )
343357
358+ // Get stored parameters early for error handling
359+ redirectURI , _ := c .Cookie (redirectCookieName )
360+ state , _ := c .Cookie (stateCookieName )
361+
344362 // Get stored verifier for Google OAuth
345363 verifier , err := c .Cookie (verifierCookieName )
346364 if err != nil {
347- state , _ := c .Cookie (stateCookieName )
348- c .JSON (http .StatusUnauthorized , OAuth2Error {
349- Error : "invalid_request" ,
350- ErrorDescription : "Missing verifier cookie" ,
351- State : state ,
352- })
365+ redirectWithError (c , redirectURI , "invalid_request" , "Missing verifier cookie" , state )
353366 return
354367 }
355368
356369 // Exchange Google authorization code for token
357370 oauthToken , err := h .oauthConfig .Exchange (c .Request .Context (), c .Query ("code" ), oauth2 .VerifierOption (verifier ))
358371 if err != nil {
359- state , _ := c .Cookie (stateCookieName )
360- c .JSON (http .StatusInternalServerError , OAuth2Error {
361- Error : "server_error" ,
362- ErrorDescription : "Failed to exchange code with Google" ,
363- State : state ,
364- })
372+ redirectWithError (c , redirectURI , "server_error" , "Failed to exchange code with Google" , state )
365373 return
366374 }
367375
@@ -371,23 +379,13 @@ func (h *GauthHandler) Callback(c *gin.Context) {
371379 option .WithTokenSource (h .oauthConfig .TokenSource (c .Request .Context (), oauthToken )),
372380 )
373381 if err != nil {
374- state , _ := c .Cookie (stateCookieName )
375- c .JSON (http .StatusInternalServerError , OAuth2Error {
376- Error : "server_error" ,
377- ErrorDescription : "Failed to create Google client" ,
378- State : state ,
379- })
382+ redirectWithError (c , redirectURI , "server_error" , "Failed to create Google client" , state )
380383 return
381384 }
382385
383386 user , err := client .Userinfo .Get ().Do ()
384387 if err != nil {
385- state , _ := c .Cookie (stateCookieName )
386- c .JSON (http .StatusInternalServerError , OAuth2Error {
387- Error : "server_error" ,
388- ErrorDescription : "Failed to get user info from Google" ,
389- State : state ,
390- })
388+ redirectWithError (c , redirectURI , "server_error" , "Failed to get user info from Google" , state )
391389 return
392390 }
393391
@@ -398,19 +396,12 @@ func (h *GauthHandler) Callback(c *gin.Context) {
398396 Avatar : user .Picture ,
399397 })
400398 if err != nil {
401- state , _ := c .Cookie (stateCookieName )
402- c .JSON (http .StatusInternalServerError , OAuth2Error {
403- Error : "server_error" ,
404- ErrorDescription : "Failed to register user" ,
405- State : state ,
406- })
399+ redirectWithError (c , redirectURI , "server_error" , "Failed to register user" , state )
407400 return
408401 }
409402
410- // Get stored parameters
411- redirectURI , err := c .Cookie (redirectCookieName )
412- if err != nil {
413- state , _ := c .Cookie (stateCookieName )
403+ // Validate that we have the redirect URI (already retrieved at the beginning)
404+ if redirectURI == "" {
414405 c .JSON (http .StatusInternalServerError , OAuth2Error {
415406 Error : "server_error" ,
416407 ErrorDescription : "Missing redirect URI" ,
@@ -421,34 +412,23 @@ func (h *GauthHandler) Callback(c *gin.Context) {
421412
422413 codeChallenge , err := c .Cookie (codeCookieName )
423414 if err != nil {
424- state , _ := c .Cookie (stateCookieName )
425- c .JSON (http .StatusInternalServerError , OAuth2Error {
426- Error : "server_error" ,
427- ErrorDescription : "Missing code challenge" ,
428- State : state ,
429- })
415+ redirectWithError (c , redirectURI , "server_error" , "Missing code challenge" , state )
430416 return
431417 }
432418
433- clientState , _ := c .Cookie (stateCookieName )
434-
435419 // Create authorization code data
436420 authCodeData := & AuthorizationCodeData {
437421 UserID : entUser .ID ,
438422 RedirectURI : redirectURI ,
439423 CodeChallenge : codeChallenge ,
440- State : clientState ,
424+ State : state ,
441425 ExpiresAt : time .Now ().Add (10 * time .Minute ),
442426 }
443427
444428 // Encrypt authorization code
445429 authCode , err := h .encryptAuthCode (authCodeData )
446430 if err != nil {
447- c .JSON (http .StatusInternalServerError , OAuth2Error {
448- Error : "server_error" ,
449- ErrorDescription : "Failed to generate authorization code" ,
450- State : clientState ,
451- })
431+ redirectWithError (c , redirectURI , "server_error" , "Failed to generate authorization code" , state )
452432 return
453433 }
454434
@@ -461,19 +441,15 @@ func (h *GauthHandler) Callback(c *gin.Context) {
461441 // Redirect to client with authorization code
462442 redirectURL , err := url .Parse (redirectURI )
463443 if err != nil {
464- c .JSON (http .StatusInternalServerError , OAuth2Error {
465- Error : "server_error" ,
466- ErrorDescription : "Invalid redirect URI" ,
467- State : clientState ,
468- })
444+ redirectWithError (c , redirectURI , "server_error" , "Invalid redirect URI" , state )
469445 return
470446 }
471447
472448 // Add query parameters
473449 q := redirectURL .Query ()
474450 q .Set ("code" , authCode )
475- if clientState != "" {
476- q .Set ("state" , clientState )
451+ if state != "" {
452+ q .Set ("state" , state )
477453 }
478454 redirectURL .RawQuery = q .Encode ()
479455
0 commit comments