Skip to content

Commit 83bd337

Browse files
authored
Merge pull request #2 from database-playground/pan93412/dbp-10-allow-multiple-redirect-uris
feat(gauth): allow multiple redirect URIs
2 parents cc4aa18 + 749fb23 commit 83bd337

File tree

5 files changed

+86
-16
lines changed

5 files changed

+86
-16
lines changed

docs/config.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ Database Playground 使用 PostgreSQL 作為資料庫。
4646

4747
- `GAUTH_CLIENT_ID`:Google OAuth 的 Client ID
4848
- `GAUTH_CLIENT_SECRET`:Google OAuth 的 Client Secret
49-
- `GAUTH_REDIRECT_URI`:在完成 Google OAuth 流程後,要重新導向到的 URL,通常是指向前端
50-
- 舉例:你的前端會在進入起始連結前,記錄目前頁面的位址,然後在 `/auth/completed` endpoint 重新導向回使用者上次瀏覽的連結。這時,你可以將重新導向連結寫為 `https://app.yourdomain.tld/auth/completed`。如果你沒有這樣的 endpoint,寫上前端的首頁也是可以的。注意在起始連結帶入的 `state` 會被帶入這個 URI 中。
49+
- `GAUTH_REDIRECT_URIS`:在完成 Google OAuth 流程後,允許重新導向到的 URIs
50+
- 舉例:`https://admin.dbplay.app`
5151

5252
Google OAuth 的登入起始連結為 `https://backend.yourdomain.tld/api/auth/google/login`,可選擇性帶入 `state` 參數。
5353
Google OAuth 的回呼連結為 `https://backend.yourdomain.tld/api/auth/google/callback`

httpapi/auth/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ Auth 端點提供適合供網頁應用程式使用的認證 API。
2727

2828
## Google 登入
2929

30-
如果您要觸發 Google 登入的流程,請前往 `GET /api/auth/google/login`
30+
如果您要觸發 Google 登入的流程,請前往 `GET /api/auth/google/login`可以帶入 `redirect_uri` 參數來在登入完成後轉導到指定畫面。
3131

3232
這個頁面會重新導向到 Google 的登入頁面,登入後會回到 `POST /api/auth/google/callback` 並進行帳號登入和註冊手續。

httpapi/auth/gauth.go

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package authservice
22

33
import (
4+
"errors"
5+
"fmt"
46
"net/http"
57
"net/url"
68

@@ -16,7 +18,10 @@ import (
1618
"google.golang.org/api/option"
1719
)
1820

19-
const verifierCookieName = "Gauth-Verifier"
21+
const (
22+
verifierCookieName = "Gauth-Verifier"
23+
redirectCookieName = "Gauth-Redirect"
24+
)
2025

2126
// BuildOAuthConfig builds an oauth2.Config from a gauthConfig.
2227
func BuildOAuthConfig(gauthConfig config.GAuthConfig) *oauth2.Config {
@@ -32,13 +37,13 @@ func BuildOAuthConfig(gauthConfig config.GAuthConfig) *oauth2.Config {
3237
}
3338

3439
type GauthHandler struct {
35-
oauthConfig *oauth2.Config
36-
useraccount *useraccount.Context
37-
redirectURL string
40+
oauthConfig *oauth2.Config
41+
useraccount *useraccount.Context
42+
redirectURIs []string
3843
}
3944

40-
func NewGauthHandler(oauthConfig *oauth2.Config, useraccount *useraccount.Context, redirectURL string) *GauthHandler {
41-
return &GauthHandler{oauthConfig: oauthConfig, useraccount: useraccount, redirectURL: redirectURL}
45+
func NewGauthHandler(oauthConfig *oauth2.Config, useraccount *useraccount.Context, redirectURIs []string) *GauthHandler {
46+
return &GauthHandler{oauthConfig: oauthConfig, useraccount: useraccount, redirectURIs: redirectURIs}
4247
}
4348

4449
func (h *GauthHandler) Login(c *gin.Context) {
@@ -55,6 +60,11 @@ func (h *GauthHandler) Login(c *gin.Context) {
5560
return
5661
}
5762

63+
redirectURI := c.Query("redirect_uri")
64+
if redirectURI == "" {
65+
redirectURI = h.oauthConfig.RedirectURL
66+
}
67+
5868
callbackURL, err := url.Parse(h.oauthConfig.RedirectURL)
5969
if err != nil {
6070
c.JSON(http.StatusInternalServerError, gin.H{
@@ -74,6 +84,16 @@ func (h *GauthHandler) Login(c *gin.Context) {
7484
/* httpOnly */ true,
7585
)
7686

87+
c.SetCookie(
88+
/* name */ redirectCookieName,
89+
/* value */ redirectURI,
90+
/* maxAge */ 5*60, // 5 min
91+
/* path */ "/",
92+
/* domain */ "",
93+
/* secure */ true,
94+
/* httpOnly */ true,
95+
)
96+
7797
redirectURL := h.oauthConfig.AuthCodeURL(
7898
"",
7999
oauth2.AccessTypeOnline,
@@ -160,5 +180,55 @@ func (h *GauthHandler) Callback(c *gin.Context) {
160180
/* httpOnly */ true,
161181
)
162182

163-
c.Redirect(http.StatusTemporaryRedirect, h.redirectURL)
183+
// redirect to the original redirect URL
184+
redirectURL, err := c.Cookie(redirectCookieName)
185+
if err != nil {
186+
if errors.Is(err, http.ErrNoCookie) {
187+
c.JSON(http.StatusOK, gin.H{
188+
"success": true,
189+
})
190+
return
191+
}
192+
193+
c.JSON(http.StatusInternalServerError, gin.H{
194+
"error": "failed to get redirect URL",
195+
"detail": err.Error(),
196+
})
197+
return
198+
}
199+
200+
// check if the redirect URL is in the allowed redirect URIs
201+
userRedirectURL, err := url.Parse(redirectURL)
202+
if err != nil {
203+
c.JSON(http.StatusInternalServerError, gin.H{
204+
"error": "failed to parse redirect URL",
205+
"detail": err.Error(),
206+
})
207+
return
208+
}
209+
210+
for _, allowedRedirectURI := range h.redirectURIs {
211+
parsedAllowedRedirectURI, err := url.Parse(allowedRedirectURI)
212+
if err != nil {
213+
c.JSON(http.StatusInternalServerError, gin.H{
214+
"error": "failed to parse allowed redirect URI",
215+
"detail": err.Error(),
216+
})
217+
return
218+
}
219+
220+
matched := userRedirectURL.Scheme == parsedAllowedRedirectURI.Scheme &&
221+
userRedirectURL.Host == parsedAllowedRedirectURI.Host &&
222+
userRedirectURL.Path == parsedAllowedRedirectURI.Path
223+
224+
if matched {
225+
c.Redirect(http.StatusTemporaryRedirect, parsedAllowedRedirectURI.String())
226+
return
227+
}
228+
}
229+
230+
c.JSON(http.StatusBadRequest, gin.H{
231+
"error": "redirect URL is not allowed",
232+
"detail": fmt.Sprintf("redirect URL is not allowed: %s", redirectURL),
233+
})
164234
}

httpapi/auth/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func (s *AuthService) Register(router gin.IRouter) {
3434

3535
useraccount := useraccount.NewContext(s.entClient, s.storage)
3636

37-
gauthHandler := NewGauthHandler(oauthConfig, useraccount, s.config.GAuth.RedirectURL)
37+
gauthHandler := NewGauthHandler(oauthConfig, useraccount, s.config.GAuth.RedirectURIs)
3838

3939
gauth.GET("/login", gauthHandler.Login)
4040
gauth.GET("/callback", gauthHandler.Callback)

internal/config/models.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ func (c RedisConfig) Validate() error {
7171
}
7272

7373
type GAuthConfig struct {
74-
ClientID string `env:"CLIENT_ID"`
75-
ClientSecret string `env:"CLIENT_SECRET"`
76-
RedirectURL string `env:"REDIRECT_URL"`
74+
ClientID string `env:"CLIENT_ID"`
75+
ClientSecret string `env:"CLIENT_SECRET"`
76+
RedirectURIs []string `env:"REDIRECT_URIS"`
7777
}
7878

7979
func (c GAuthConfig) Validate() error {
@@ -83,8 +83,8 @@ func (c GAuthConfig) Validate() error {
8383
if c.ClientSecret == "" {
8484
return errors.New("GAUTH_CLIENT_SECRET is required")
8585
}
86-
if c.RedirectURL == "" {
87-
return errors.New("GAUTH_REDIRECT_URL is required")
86+
if len(c.RedirectURIs) == 0 {
87+
return errors.New("GAUTH_REDIRECT_URIS is required")
8888
}
8989

9090
return nil

0 commit comments

Comments
 (0)