package service import ( "baron-sso-backend/internal/repository" "context" "crypto/rand" "encoding/base64" "fmt" "time" "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2" ) type FederationService struct { repo repository.FederationRepository hydraSvc *HydraAdminService redisSvc *RedisService } func NewFederationService(repo repository.FederationRepository, hydraSvc *HydraAdminService, redisSvc *RedisService) *FederationService { return &FederationService{repo: repo, hydraSvc: hydraSvc, redisSvc: redisSvc} } func (s *FederationService) InitiateOIDCLogin(ctx context.Context, providerID, loginChallenge string) (string, error) { provider, err := s.repo.FindProviderByID(ctx, providerID) if err != nil { return "", fmt.Errorf("failed to find provider: %w", err) } if provider == nil || provider.IssuerURL == nil || provider.OIDCClientID == nil || provider.OIDCClientSecret == nil || provider.Scopes == nil { return "", fmt.Errorf("OIDC configuration for provider %s is incomplete", providerID) } oidcProvider, err := oidc.NewProvider(ctx, *provider.IssuerURL) if err != nil { return "", fmt.Errorf("failed to create OIDC provider: %w", err) } config := oauth2.Config{ ClientID: *provider.OIDCClientID, ClientSecret: *provider.OIDCClientSecret, Endpoint: oidcProvider.Endpoint(), RedirectURL: "http://localhost:8080/api/v1/federation/oidc/callback", // This should be configurable Scopes: []string{*provider.Scopes}, } state, err := generateState() if err != nil { return "", fmt.Errorf("failed to generate state: %w", err) } // Store state and login_challenge in Redis redisKey := fmt.Sprintf("oidc_state:%s", state) if err := s.redisSvc.Set(redisKey, loginChallenge, 10*time.Minute); err != nil { return "", fmt.Errorf("failed to save state to Redis: %w", err) } return config.AuthCodeURL(state), nil } func (s *FederationService) HandleOIDCCallback(ctx context.Context, code, state string) (string, error) { // 1. Retrieve login_challenge from Redis redisKey := fmt.Sprintf("oidc_state:%s", state) loginChallenge, err := s.redisSvc.Get(redisKey) if err != nil { return "", fmt.Errorf("failed to get state from Redis or state expired: %w", err) } // Delete the state from Redis now that it's been used s.redisSvc.Delete(redisKey) // TODO: Finish the rest of the callback logic // 2. Exchange code for token // 3. Verify ID token // 4. JIT Provisioning // 5. Accept Hydra Login Request fmt.Println("Login challenge found:", loginChallenge) return "http://localhost:3000/login?login_successful=true", nil // Placeholder } func generateState() (string, error) { b := make([]byte, 32) _, err := rand.Read(b) if err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(b), nil }