package auth import ( "context" "crypto/rand" "encoding/base64" "fmt" "time" "go.b0esche.cloud/backend/internal/config" "go.b0esche.cloud/backend/internal/database" "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2" ) type OIDCService struct { provider *oidc.Provider oauth2Config oauth2.Config db *database.DB // Assume we have a DB wrapper } func NewOIDCService(cfg *config.Config, db *database.DB) (*OIDCService, error) { ctx := context.Background() provider, err := oidc.NewProvider(ctx, cfg.OIDCIssuerURL) if err != nil { return nil, fmt.Errorf("failed to get OIDC provider: %w", err) } oauth2Config := oauth2.Config{ ClientID: cfg.OIDCClientID, ClientSecret: cfg.OIDCClientSecret, RedirectURL: cfg.OIDCRedirectURL, // Add to config Endpoint: provider.Endpoint(), Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, } return &OIDCService{ provider: provider, oauth2Config: oauth2Config, db: db, }, nil } func (s *OIDCService) LoginURL(state string) string { return s.oauth2Config.AuthCodeURL(state) } func (s *OIDCService) HandleCallback(ctx context.Context, code, state string) (*database.User, *database.Session, error) { oauth2Token, err := s.oauth2Config.Exchange(ctx, code) if err != nil { return nil, nil, fmt.Errorf("failed to exchange code: %w", err) } rawIDToken, ok := oauth2Token.Extra("id_token").(string) if !ok { return nil, nil, fmt.Errorf("no id_token in token response") } idToken, err := s.provider.Verifier(&oidc.Config{ClientID: s.oauth2Config.ClientID}).Verify(ctx, rawIDToken) if err != nil { return nil, nil, fmt.Errorf("failed to verify ID token: %w", err) } var claims struct { Sub string `json:"sub"` Email string `json:"email"` EmailVerified bool `json:"email_verified"` Name string `json:"name"` } if err := idToken.Claims(&claims); err != nil { return nil, nil, fmt.Errorf("failed to parse claims: %w", err) } user, err := s.db.GetOrCreateUser(ctx, claims.Sub, claims.Email, claims.Name) if err != nil { return nil, nil, err } session, err := s.db.CreateSession(ctx, user.ID, time.Now().Add(15*time.Minute)) if err != nil { return nil, nil, err } return user, session, nil } // GenerateState generates a secure random state string func GenerateState() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } return base64.URLEncoding.EncodeToString(b), nil }