155 lines
3.7 KiB
Go
155 lines
3.7 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/coreos/go-oidc/v3/oidc"
|
|
"github.com/gin-gonic/gin"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
var (
|
|
oidcProvider *oidc.Provider
|
|
oauth2Cfg *oauth2.Config
|
|
authEnabled bool
|
|
)
|
|
|
|
func InitOIDC(ctx context.Context) error {
|
|
issuer := os.Getenv("OIDC_ISSUER")
|
|
if issuer == "" {
|
|
log.Println("OIDC_ISSUER not set; authentication disabled")
|
|
return nil
|
|
}
|
|
|
|
p, err := oidc.NewProvider(ctx, issuer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
oidcProvider = p
|
|
oauth2Cfg = &oauth2.Config{
|
|
ClientID: os.Getenv("OIDC_CLIENT_ID"),
|
|
ClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
|
|
RedirectURL: os.Getenv("OIDC_REDIRECT_URL"),
|
|
Endpoint: p.Endpoint(),
|
|
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
|
}
|
|
authEnabled = true
|
|
log.Println("OIDC authentication enabled")
|
|
return nil
|
|
}
|
|
|
|
func Enabled() bool { return authEnabled }
|
|
|
|
func HandleLogin(c *gin.Context) {
|
|
state, err := randomHex(16)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "state generation failed"})
|
|
return
|
|
}
|
|
if err := SaveState(c.Request.Context(), state); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "state save failed"})
|
|
return
|
|
}
|
|
c.Redirect(http.StatusFound, oauth2Cfg.AuthCodeURL(state))
|
|
}
|
|
|
|
func HandleCallback(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
if !ConsumeState(ctx, c.Query("state")) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid state"})
|
|
return
|
|
}
|
|
|
|
token, err := oauth2Cfg.Exchange(ctx, c.Query("code"))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "token exchange failed"})
|
|
return
|
|
}
|
|
|
|
rawIDToken, ok := token.Extra("id_token").(string)
|
|
if !ok {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "missing id_token"})
|
|
return
|
|
}
|
|
|
|
verifier := oidcProvider.Verifier(&oidc.Config{ClientID: oauth2Cfg.ClientID})
|
|
idToken, err := verifier.Verify(ctx, rawIDToken)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "token verification failed"})
|
|
return
|
|
}
|
|
|
|
var claims struct {
|
|
Sub string `json:"sub"`
|
|
Email string `json:"email"`
|
|
Name string `json:"name"`
|
|
}
|
|
if err := idToken.Claims(&claims); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "claims extraction failed"})
|
|
return
|
|
}
|
|
|
|
sessionID, err := SaveSession(ctx, &Session{
|
|
UserID: claims.Sub,
|
|
Email: claims.Email,
|
|
Name: claims.Name,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "session save failed"})
|
|
return
|
|
}
|
|
|
|
secure := c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https"
|
|
http.SetCookie(c.Writer, &http.Cookie{
|
|
Name: sessionCookieName,
|
|
Value: sessionID,
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
Secure: secure,
|
|
SameSite: http.SameSiteLaxMode,
|
|
MaxAge: int(sessionTTL.Seconds()),
|
|
})
|
|
|
|
frontendURL := os.Getenv("PUBLIC_HOST")
|
|
if frontendURL == "" {
|
|
frontendURL = "/"
|
|
}
|
|
c.Redirect(http.StatusFound, frontendURL)
|
|
}
|
|
|
|
func HandleLogout(c *gin.Context) {
|
|
if cookie, err := c.Request.Cookie(sessionCookieName); err == nil {
|
|
_ = DeleteSession(c.Request.Context(), cookie.Value)
|
|
}
|
|
http.SetCookie(c.Writer, &http.Cookie{
|
|
Name: sessionCookieName,
|
|
Value: "",
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
MaxAge: -1,
|
|
})
|
|
c.Redirect(http.StatusFound, "/")
|
|
}
|
|
|
|
func HandleMe(c *gin.Context) {
|
|
if !authEnabled {
|
|
c.JSON(http.StatusOK, gin.H{"auth_enabled": false})
|
|
return
|
|
}
|
|
cookie, err := c.Request.Cookie(sessionCookieName)
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
|
|
return
|
|
}
|
|
sess, err := GetSession(c.Request.Context(), cookie.Value)
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "session expired"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, sess)
|
|
}
|