Skip to content

Commit 15d5d1b

Browse files
committed
refactor(auth): update specification
1 parent 34c5d47 commit 15d5d1b

File tree

2 files changed

+78
-107
lines changed

2 files changed

+78
-107
lines changed

httpapi/auth/README.md

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,6 @@ code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
2222

2323
登入完成後,會自動跳轉到 `redirect_uri` 上,接著您可以在 redirect URI(下稱 callback)中取回 token。
2424

25-
如果失敗,則會回傳符合 [RFC 6749 的錯誤回傳值](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1),如:
26-
27-
```json
28-
{
29-
"error": "invalid_request",
30-
"error_description": "Bad redirect URI.",
31-
"state": ""
32-
}
33-
```
34-
3525
### Callback 會收到的參數
3626

3727
在驗證完成後,瀏覽器會跳轉到 `redirect_uri`,並帶入以下的查詢字串:
@@ -43,6 +33,11 @@ code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
4333

4434
接著您可以使用〈取回 token〉API 來取得 token。
4535

36+
如果登入失敗,則是會帶入以下的查詢字串:
37+
38+
- `error`:錯誤代碼
39+
- `error_description`:錯誤描述
40+
4641
## 取回 token
4742

4843
使用 `POST /api/auth/v2/token` 取回 token。

httpapi/auth/gauth.go

Lines changed: 73 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
104142
func (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
341355
func (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

Comments
 (0)