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) }