chatai/auth_v2.169.0/internal/api/provider/oidc.go

411 lines
9.8 KiB
Go

package provider
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/golang-jwt/jwt/v5"
)
type ParseIDTokenOptions struct {
SkipAccessTokenCheck bool
AccessToken string
}
// OverrideVerifiers can be used to set a custom verifier for an OIDC provider
// (identified by the provider's Endpoint().AuthURL string). Should only be
// used in tests.
var OverrideVerifiers = make(map[string]func(context.Context, *oidc.Config) *oidc.IDTokenVerifier)
// OverrideClock can be used to set a custom clock function to be used when
// parsing ID tokens. Should only be used in tests.
var OverrideClock func() time.Time
func ParseIDToken(ctx context.Context, provider *oidc.Provider, config *oidc.Config, idToken string, options ParseIDTokenOptions) (*oidc.IDToken, *UserProvidedData, error) {
if config == nil {
config = &oidc.Config{
// aud claim check to be performed by other flows
SkipClientIDCheck: true,
}
}
if OverrideClock != nil {
clonedConfig := *config
clonedConfig.Now = OverrideClock
config = &clonedConfig
}
verifier := provider.VerifierContext(ctx, config)
overrideVerifier, ok := OverrideVerifiers[provider.Endpoint().AuthURL]
if ok && overrideVerifier != nil {
verifier = overrideVerifier(ctx, config)
}
token, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, nil, err
}
var data *UserProvidedData
switch token.Issuer {
case IssuerGoogle:
token, data, err = parseGoogleIDToken(token)
case IssuerApple:
token, data, err = parseAppleIDToken(token)
case IssuerLinkedin:
token, data, err = parseLinkedinIDToken(token)
case IssuerKakao:
token, data, err = parseKakaoIDToken(token)
case IssuerVercelMarketplace:
token, data, err = parseVercelMarketplaceIDToken(token)
default:
if IsAzureIssuer(token.Issuer) {
token, data, err = parseAzureIDToken(token)
} else {
token, data, err = parseGenericIDToken(token)
}
}
if err != nil {
return nil, nil, err
}
if !options.SkipAccessTokenCheck && token.AccessTokenHash != "" {
if err := token.VerifyAccessToken(options.AccessToken); err != nil {
return nil, nil, err
}
}
return token, data, nil
}
func parseGoogleIDToken(token *oidc.IDToken) (*oidc.IDToken, *UserProvidedData, error) {
var claims googleUser
if err := token.Claims(&claims); err != nil {
return nil, nil, err
}
var data UserProvidedData
if claims.Email != "" {
data.Emails = append(data.Emails, Email{
Email: claims.Email,
Verified: claims.IsEmailVerified(),
Primary: true,
})
}
data.Metadata = &Claims{
Issuer: claims.Issuer,
Subject: claims.Subject,
Name: claims.Name,
Picture: claims.AvatarURL,
// To be deprecated
AvatarURL: claims.AvatarURL,
FullName: claims.Name,
ProviderId: claims.Subject,
}
if claims.HostedDomain != "" {
data.Metadata.CustomClaims = map[string]any{
"hd": claims.HostedDomain,
}
}
return token, &data, nil
}
type AppleIDTokenClaims struct {
jwt.RegisteredClaims
Email string `json:"email"`
AuthTime *float64 `json:"auth_time"`
IsPrivateEmail *IsPrivateEmail `json:"is_private_email"`
}
func parseAppleIDToken(token *oidc.IDToken) (*oidc.IDToken, *UserProvidedData, error) {
var claims AppleIDTokenClaims
if err := token.Claims(&claims); err != nil {
return nil, nil, err
}
var data UserProvidedData
data.Emails = append(data.Emails, Email{
Email: claims.Email,
Verified: true,
Primary: true,
})
data.Metadata = &Claims{
Issuer: token.Issuer,
Subject: token.Subject,
ProviderId: token.Subject,
CustomClaims: make(map[string]any),
}
if claims.IsPrivateEmail != nil {
data.Metadata.CustomClaims["is_private_email"] = *claims.IsPrivateEmail
}
if claims.AuthTime != nil {
data.Metadata.CustomClaims["auth_time"] = *claims.AuthTime
}
if len(data.Metadata.CustomClaims) < 1 {
data.Metadata.CustomClaims = nil
}
return token, &data, nil
}
type LinkedinIDTokenClaims struct {
jwt.RegisteredClaims
Email string `json:"email"`
EmailVerified string `json:"email_verified"`
FamilyName string `json:"family_name"`
GivenName string `json:"given_name"`
Locale string `json:"locale"`
Picture string `json:"picture"`
}
func parseLinkedinIDToken(token *oidc.IDToken) (*oidc.IDToken, *UserProvidedData, error) {
var claims LinkedinIDTokenClaims
if err := token.Claims(&claims); err != nil {
return nil, nil, err
}
var data UserProvidedData
emailVerified, err := strconv.ParseBool(claims.EmailVerified)
if err != nil {
return nil, nil, err
}
if claims.Email != "" {
data.Emails = append(data.Emails, Email{
Email: claims.Email,
Verified: emailVerified,
Primary: true,
})
}
data.Metadata = &Claims{
Issuer: token.Issuer,
Subject: token.Subject,
Name: strings.TrimSpace(claims.GivenName + " " + claims.FamilyName),
GivenName: claims.GivenName,
FamilyName: claims.FamilyName,
Locale: claims.Locale,
Picture: claims.Picture,
ProviderId: token.Subject,
}
return token, &data, nil
}
type AzureIDTokenClaims struct {
jwt.RegisteredClaims
Email string `json:"email"`
Name string `json:"name"`
PreferredUsername string `json:"preferred_username"`
XMicrosoftEmailDomainOwnerVerified any `json:"xms_edov"`
}
func (c *AzureIDTokenClaims) IsEmailVerified() bool {
emailVerified := false
edov := c.XMicrosoftEmailDomainOwnerVerified
// If xms_edov is not set, and an email is present or xms_edov is true,
// only then is the email regarded as verified.
// https://learn.microsoft.com/en-us/azure/active-directory/develop/migrate-off-email-claim-authorization#using-the-xms_edov-optional-claim-to-determine-email-verification-status-and-migrate-users
if edov == nil {
// An email is provided, but xms_edov is not -- probably not
// configured, so we must assume the email is verified as Azure
// will only send out a potentially unverified email address in
// single-tenanat apps.
emailVerified = c.Email != ""
} else {
edovBool := false
// Azure can't be trusted with how they encode the xms_edov
// claim. Sometimes it's "xms_edov": "1", sometimes "xms_edov": true.
switch v := edov.(type) {
case bool:
edovBool = v
case string:
edovBool = v == "1" || v == "true"
default:
edovBool = false
}
emailVerified = c.Email != "" && edovBool
}
return emailVerified
}
// removeAzureClaimsFromCustomClaims contains the list of claims to be removed
// from the CustomClaims map. See:
// https://learn.microsoft.com/en-us/azure/active-directory/develop/id-token-claims-reference
var removeAzureClaimsFromCustomClaims = []string{
"aud",
"iss",
"iat",
"nbf",
"exp",
"c_hash",
"at_hash",
"aio",
"nonce",
"rh",
"uti",
"jti",
"ver",
"sub",
"name",
"preferred_username",
}
func parseAzureIDToken(token *oidc.IDToken) (*oidc.IDToken, *UserProvidedData, error) {
var data UserProvidedData
var azureClaims AzureIDTokenClaims
if err := token.Claims(&azureClaims); err != nil {
return nil, nil, err
}
data.Metadata = &Claims{
Issuer: token.Issuer,
Subject: token.Subject,
ProviderId: token.Subject,
PreferredUsername: azureClaims.PreferredUsername,
FullName: azureClaims.Name,
CustomClaims: make(map[string]any),
}
if azureClaims.Email != "" {
data.Emails = []Email{{
Email: azureClaims.Email,
Verified: azureClaims.IsEmailVerified(),
Primary: true,
}}
}
if err := token.Claims(&data.Metadata.CustomClaims); err != nil {
return nil, nil, err
}
if data.Metadata.CustomClaims != nil {
for _, claim := range removeAzureClaimsFromCustomClaims {
delete(data.Metadata.CustomClaims, claim)
}
}
return token, &data, nil
}
type KakaoIDTokenClaims struct {
jwt.RegisteredClaims
Email string `json:"email"`
Nickname string `json:"nickname"`
Picture string `json:"picture"`
}
func parseKakaoIDToken(token *oidc.IDToken) (*oidc.IDToken, *UserProvidedData, error) {
var claims KakaoIDTokenClaims
if err := token.Claims(&claims); err != nil {
return nil, nil, err
}
var data UserProvidedData
if claims.Email != "" {
data.Emails = append(data.Emails, Email{
Email: claims.Email,
Verified: true,
Primary: true,
})
}
data.Metadata = &Claims{
Issuer: token.Issuer,
Subject: token.Subject,
Name: claims.Nickname,
PreferredUsername: claims.Nickname,
ProviderId: token.Subject,
Picture: claims.Picture,
}
return token, &data, nil
}
type VercelMarketplaceIDTokenClaims struct {
jwt.RegisteredClaims
UserEmail string `json:"user_email"`
UserName string `json:"user_name"`
UserAvatarUrl string `json:"user_avatar_url"`
}
func parseVercelMarketplaceIDToken(token *oidc.IDToken) (*oidc.IDToken, *UserProvidedData, error) {
var claims VercelMarketplaceIDTokenClaims
if err := token.Claims(&claims); err != nil {
return nil, nil, err
}
var data UserProvidedData
data.Emails = append(data.Emails, Email{
Email: claims.UserEmail,
Verified: true,
Primary: true,
})
data.Metadata = &Claims{
Issuer: token.Issuer,
Subject: token.Subject,
ProviderId: token.Subject,
Name: claims.UserName,
Picture: claims.UserAvatarUrl,
}
return token, &data, nil
}
func parseGenericIDToken(token *oidc.IDToken) (*oidc.IDToken, *UserProvidedData, error) {
var data UserProvidedData
if err := token.Claims(&data.Metadata); err != nil {
return nil, nil, err
}
if data.Metadata.Email != "" {
data.Emails = append(data.Emails, Email{
Email: data.Metadata.Email,
Verified: data.Metadata.EmailVerified,
Primary: true,
})
}
if len(data.Emails) <= 0 {
return nil, nil, fmt.Errorf("provider: Generic OIDC ID token from issuer %q must contain an email address", token.Issuer)
}
return token, &data, nil
}