-
-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This allows the API to trigger an OAuth workflow to create the JWT for authentication. For now the workflow is triggered by manually visiting `/api/login/oidc` on the frontend app until the UI repo is updated to add support.
- Loading branch information
Showing
10 changed files
with
460 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
package auth | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/coreos/go-oidc/v3/oidc" | ||
"github.com/golang-jwt/jwt/v5" | ||
U "github.com/yusing/go-proxy/internal/api/v1/utils" | ||
"github.com/yusing/go-proxy/internal/common" | ||
E "github.com/yusing/go-proxy/internal/error" | ||
"golang.org/x/oauth2" | ||
) | ||
|
||
var ( | ||
oauthConfig *oauth2.Config | ||
oidcProvider *oidc.Provider | ||
oidcVerifier *oidc.IDTokenVerifier | ||
) | ||
|
||
// InitOIDC initializes the OIDC provider | ||
func InitOIDC(issuerURL, clientID, clientSecret, redirectURL string) error { | ||
if issuerURL == "" { | ||
return nil // OIDC not configured | ||
} | ||
|
||
provider, err := oidc.NewProvider(context.Background(), issuerURL) | ||
if err != nil { | ||
return fmt.Errorf("failed to initialize OIDC provider: %w", err) | ||
} | ||
|
||
oidcProvider = provider | ||
oidcVerifier = provider.Verifier(&oidc.Config{ | ||
ClientID: clientID, | ||
}) | ||
|
||
oauthConfig = &oauth2.Config{ | ||
ClientID: clientID, | ||
ClientSecret: clientSecret, | ||
RedirectURL: redirectURL, | ||
Endpoint: provider.Endpoint(), | ||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// OIDCLoginHandler initiates the OIDC login flow | ||
func OIDCLoginHandler(w http.ResponseWriter, r *http.Request) { | ||
if oauthConfig == nil { | ||
U.HandleErr(w, r, E.New("OIDC not configured"), http.StatusNotImplemented) | ||
return | ||
} | ||
|
||
state := common.GenerateRandomString(32) | ||
http.SetCookie(w, &http.Cookie{ | ||
Name: "oauth_state", | ||
Value: state, | ||
MaxAge: 300, | ||
HttpOnly: true, | ||
SameSite: http.SameSiteStrictMode, | ||
Path: "/", | ||
}) | ||
|
||
url := oauthConfig.AuthCodeURL(state) | ||
http.Redirect(w, r, url, http.StatusTemporaryRedirect) | ||
} | ||
|
||
// OIDCCallbackHandler handles the OIDC callback | ||
func OIDCCallbackHandler(w http.ResponseWriter, r *http.Request) { | ||
if oauthConfig == nil { | ||
U.HandleErr(w, r, E.New("OIDC not configured"), http.StatusNotImplemented) | ||
return | ||
} | ||
|
||
// For testing purposes, skip provider verification | ||
if common.IsTest { | ||
handleTestCallback(w, r) | ||
return | ||
} | ||
|
||
if oidcProvider == nil { | ||
U.HandleErr(w, r, E.New("OIDC not configured"), http.StatusNotImplemented) | ||
return | ||
} | ||
|
||
state, err := r.Cookie("oauth_state") | ||
if err != nil { | ||
U.HandleErr(w, r, E.New("missing state cookie"), http.StatusBadRequest) | ||
return | ||
} | ||
|
||
if r.URL.Query().Get("state") != state.Value { | ||
U.HandleErr(w, r, E.New("invalid oauth state"), http.StatusBadRequest) | ||
return | ||
} | ||
|
||
code := r.URL.Query().Get("code") | ||
oauth2Token, err := oauthConfig.Exchange(r.Context(), code) | ||
if err != nil { | ||
U.HandleErr(w, r, fmt.Errorf("failed to exchange token: %w", err), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
rawIDToken, ok := oauth2Token.Extra("id_token").(string) | ||
if !ok { | ||
U.HandleErr(w, r, E.New("missing id_token"), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
idToken, err := oidcVerifier.Verify(r.Context(), rawIDToken) | ||
if err != nil { | ||
U.HandleErr(w, r, fmt.Errorf("failed to verify ID token: %w", err), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
var claims struct { | ||
Email string `json:"email"` | ||
Username string `json:"preferred_username"` | ||
} | ||
if err := idToken.Claims(&claims); err != nil { | ||
U.HandleErr(w, r, fmt.Errorf("failed to parse claims: %w", err), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
if err := setAuthenticatedCookie(w, r, claims.Username); err != nil { | ||
U.HandleErr(w, r, err, http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
// Redirect to home page | ||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) | ||
} | ||
|
||
// handleTestCallback handles OIDC callback in test environment | ||
func handleTestCallback(w http.ResponseWriter, r *http.Request) { | ||
state, err := r.Cookie("oauth_state") | ||
if err != nil { | ||
U.HandleErr(w, r, E.New("missing state cookie"), http.StatusBadRequest) | ||
return | ||
} | ||
|
||
if r.URL.Query().Get("state") != state.Value { | ||
U.HandleErr(w, r, E.New("invalid oauth state"), http.StatusBadRequest) | ||
return | ||
} | ||
|
||
// Create test JWT token | ||
expiresAt := time.Now().Add(common.APIJWTTokenTTL) | ||
jwtClaims := &Claims{ | ||
Username: "test-user", | ||
RegisteredClaims: jwt.RegisteredClaims{ | ||
ExpiresAt: jwt.NewNumericDate(expiresAt), | ||
}, | ||
} | ||
|
||
token := jwt.NewWithClaims(jwt.SigningMethodHS512, jwtClaims) | ||
tokenStr, err := token.SignedString(common.APIJWTSecret) | ||
if err != nil { | ||
U.HandleErr(w, r, err, http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
http.SetCookie(w, &http.Cookie{ | ||
Name: "token", | ||
Value: tokenStr, | ||
Expires: expiresAt, | ||
HttpOnly: true, | ||
SameSite: http.SameSiteStrictMode, | ||
Path: "/", | ||
}) | ||
|
||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) | ||
} |
Oops, something went wrong.