package zitadel import ( "context" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strings" ) // IDTokenClaims holds selected OIDC id_token claims used by registration/login flows. type IDTokenClaims struct { Sub string Email string EmailVerified bool Name string Locale string } // AuthorizeURL builds the ZITADEL OIDC authorization URL for social registration/login. func (c *Client) AuthorizeURL(redirectURI, state, provider string) (string, error) { if c == nil { return "", ErrNotConfigured } if c.conf.OAuthClientID == "" { return "", fmt.Errorf("zitadel: oauth client id is required for authorize url") } if redirectURI == "" || state == "" { return "", fmt.Errorf("zitadel: redirect_uri and state are required") } q := url.Values{} q.Set("client_id", c.conf.OAuthClientID) q.Set("redirect_uri", redirectURI) q.Set("response_type", "code") q.Set("scope", "openid profile email") q.Set("state", state) if provider == "google" && c.conf.GoogleIdPID != "" { q.Set("idp_id", c.conf.GoogleIdPID) } return c.issuer + "/oauth/v2/authorize?" + q.Encode(), nil } // ExchangeAuthorizationCode exchanges an OAuth authorization code for tokens. func (c *Client) ExchangeAuthorizationCode(ctx context.Context, code, redirectURI string) (*TokenResult, error) { if c == nil { return nil, ErrNotConfigured } if code == "" || redirectURI == "" { return nil, fmt.Errorf("zitadel: code and redirect_uri are required") } if c.conf.OAuthClientID == "" || c.conf.OAuthClientSecret == "" { return nil, fmt.Errorf("zitadel: oauth client credentials are required for code exchange") } form := url.Values{} form.Set("grant_type", "authorization_code") form.Set("client_id", c.conf.OAuthClientID) form.Set("client_secret", c.conf.OAuthClientSecret) form.Set("code", code) form.Set("redirect_uri", redirectURI) return c.postToken(ctx, form) } // FetchUserInfo loads OIDC userinfo using an access token. func (c *Client) FetchUserInfo(ctx context.Context, accessToken string) (*IDTokenClaims, error) { if c == nil { return nil, ErrNotConfigured } if accessToken == "" { return nil, fmt.Errorf("zitadel: access token is required") } req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.issuer+"/oidc/v1/userinfo", http.NoBody) if err != nil { return nil, fmt.Errorf("zitadel: userinfo request: %w", err) } req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("Accept", "application/json") resp, err := c.http.Do(req) if err != nil { return nil, fmt.Errorf("zitadel: userinfo request: %w", err) } defer resp.Body.Close() raw, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("zitadel: read userinfo response: %w", err) } if resp.StatusCode == http.StatusUnauthorized { return nil, ErrInvalidCredentials } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("zitadel: userinfo request: status %d: %s", resp.StatusCode, truncateBody(raw)) } var info struct { Sub string `json:"sub"` Email string `json:"email"` EmailVerified bool `json:"email_verified"` Name string `json:"name"` Locale string `json:"locale"` } if err := json.Unmarshal(raw, &info); err != nil { return nil, fmt.Errorf("zitadel: decode userinfo response: %w", err) } if info.Sub == "" { return nil, errors.New("zitadel: userinfo missing sub") } return &IDTokenClaims{ Sub: info.Sub, Email: info.Email, EmailVerified: info.EmailVerified, Name: info.Name, Locale: info.Locale, }, nil } func ParseIDTokenClaims(idToken string) (*IDTokenClaims, error) { if idToken == "" { return nil, errors.New("zitadel: id_token is empty") } parts := strings.Split(idToken, ".") if len(parts) < 2 { return nil, errors.New("zitadel: malformed id_token") } payload, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { return nil, fmt.Errorf("zitadel: decode id_token payload: %w", err) } var raw struct { Sub string `json:"sub"` Email string `json:"email"` EmailVerified bool `json:"email_verified"` Name string `json:"name"` Locale string `json:"locale"` } if err := json.Unmarshal(payload, &raw); err != nil { return nil, fmt.Errorf("zitadel: unmarshal id_token payload: %w", err) } if raw.Sub == "" { return nil, errors.New("zitadel: id_token missing sub") } return &IDTokenClaims{ Sub: raw.Sub, Email: raw.Email, EmailVerified: raw.EmailVerified, Name: raw.Name, Locale: raw.Locale, }, nil }