// Package identity adapts OpenID Connect providers (Google, generic OIDC) to // the IdentityProvider port. Google is just an OIDC issuer, so one // implementation covers both required login methods. package identity import ( "context" "errors" "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2" "github.com/snip/backend/internal/port" ) // GoogleIssuer is Google's OIDC discovery issuer. const GoogleIssuer = "https://accounts.google.com" type OIDCProvider struct { name string oauth *oauth2.Config verifier *oidc.IDTokenVerifier } // NewOIDCProvider performs OIDC discovery against the issuer (one network call // at startup) and returns a ready provider. func NewOIDCProvider(ctx context.Context, name, issuer, clientID, clientSecret, redirectURL string, extraScopes []string) (*OIDCProvider, error) { provider, err := oidc.NewProvider(ctx, issuer) if err != nil { return nil, err } scopes := append([]string{oidc.ScopeOpenID, "email", "profile"}, extraScopes...) return &OIDCProvider{ name: name, oauth: &oauth2.Config{ ClientID: clientID, ClientSecret: clientSecret, Endpoint: provider.Endpoint(), RedirectURL: redirectURL, Scopes: scopes, }, verifier: provider.Verifier(&oidc.Config{ClientID: clientID}), }, nil } func (p *OIDCProvider) Name() string { return p.name } func (p *OIDCProvider) AuthURL(state string) string { return p.oauth.AuthCodeURL(state, oauth2.AccessTypeOffline) } func (p *OIDCProvider) Exchange(ctx context.Context, code string) (*port.Identity, error) { tok, err := p.oauth.Exchange(ctx, code) if err != nil { return nil, err } rawID, ok := tok.Extra("id_token").(string) if !ok { return nil, errors.New("oidc: response missing id_token") } idToken, err := p.verifier.Verify(ctx, rawID) if err != nil { return nil, err } var claims struct { Sub string `json:"sub"` Email string `json:"email"` Name string `json:"name"` } if err := idToken.Claims(&claims); err != nil { return nil, err } return &port.Identity{Subject: claims.Sub, Email: claims.Email, Name: claims.Name}, nil }