Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
281 lines
12 KiB
Go
281 lines
12 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package oidc
|
|
|
|
import (
|
|
"encoding/json"
|
|
"maps"
|
|
"net/url"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/ory/herodot"
|
|
"github.com/ory/kratos/identity"
|
|
"github.com/ory/x/urlx"
|
|
)
|
|
|
|
const (
|
|
// AccountLinkingModeVerifyWithExistingCredential requires the user to verify their identity
|
|
// with an existing credential before accounts are linked.
|
|
AccountLinkingModeVerifyWithExistingCredential = "confirm_with_existing_credential"
|
|
// AccountLinkingModeAutomatic silently links accounts if the provider verifies email ownership.
|
|
AccountLinkingModeAutomatic = "automatic"
|
|
|
|
// ProviderTypeApple is the provider type for Sign in with Apple.
|
|
ProviderTypeApple = "apple"
|
|
// ProviderTypeGoogle is the provider type for Sign in with Google.
|
|
ProviderTypeGoogle = "google"
|
|
|
|
// UpdateIdentityOnLoginNever disables identity updates on login (default).
|
|
UpdateIdentityOnLoginNever = "never"
|
|
// UpdateIdentityOnLoginAutomatic re-runs the Jsonnet claims mapper on every
|
|
// OIDC login and updates the identity's traits and metadata automatically.
|
|
UpdateIdentityOnLoginAutomatic = "automatic"
|
|
)
|
|
|
|
type Configuration struct {
|
|
// ID is the provider's ID
|
|
ID string `json:"id"`
|
|
|
|
// Provider is either "generic" for a generic OAuth 2.0 / OpenID Connect Provider or one of:
|
|
// - generic
|
|
// - google
|
|
// - github
|
|
// - github-app
|
|
// - gitlab
|
|
// - microsoft
|
|
// - discord
|
|
// - salesforce
|
|
// - slack
|
|
// - facebook
|
|
// - auth0
|
|
// - vk
|
|
// - yandex
|
|
// - apple
|
|
// - spotify
|
|
// - netid
|
|
// - dingtalk
|
|
// - linkedin
|
|
// - patreon
|
|
// - amazon
|
|
// - uaepass
|
|
Provider string `json:"provider"`
|
|
|
|
// Label represents an optional label which can be used in the UI generation.
|
|
Label string `json:"label"`
|
|
|
|
// ClientID is the application's Client ID.
|
|
ClientID string `json:"client_id"`
|
|
|
|
// ClientSecret is the application's secret.
|
|
ClientSecret string `json:"client_secret"`
|
|
|
|
// IssuerURL is the OpenID Connect Server URL. You can leave this empty if `provider` is not set to `generic`.
|
|
// If set, neither `auth_url` nor `token_url` are required.
|
|
IssuerURL string `json:"issuer_url"`
|
|
|
|
// AuthURL is the authorize url, typically something like: https://example.org/oauth2/auth
|
|
// Should only be used when the OAuth2 / OpenID Connect server is not supporting OpenID Connect Discovery and when
|
|
// `provider` is set to `generic`.
|
|
AuthURL string `json:"auth_url"`
|
|
|
|
// TokenURL is the token url, typically something like: https://example.org/oauth2/token
|
|
// Should only be used when the OAuth2 / OpenID Connect server is not supporting OpenID Connect Discovery and when
|
|
// `provider` is set to `generic`.
|
|
TokenURL string `json:"token_url"`
|
|
|
|
// Tenant is the Azure AD Tenant to use for authentication, and must be set when `provider` is set to `microsoft`.
|
|
// Can be either `common`, `organizations`, `consumers` for a multitenant application or a specific tenant like
|
|
// `8eaef023-2b34-4da1-9baa-8bc8c9d6a490` or `contoso.onmicrosoft.com`.
|
|
Tenant string `json:"microsoft_tenant"`
|
|
|
|
// SubjectSource is a flag which controls from which endpoint the subject identifier is taken by microsoft provider.
|
|
// Can be either `userinfo` or `me` or `oid`.
|
|
// If the value is `userinfo` then the subject identifier is taken from sub field of userinfo standard endpoint response.
|
|
// If the value is `me` then the `id` field of https://graph.microsoft.com/v1.0/me response is taken as subject.
|
|
// If the value is `oid` then the the oid (Object ID) is taken to identify users across different services.
|
|
// The default is `userinfo`.
|
|
SubjectSource string `json:"subject_source"`
|
|
|
|
// TeamId is the Apple Developer Team ID that's needed for the `apple` `provider` to work.
|
|
// It can be found Apple Developer website and combined with `apple_private_key` and `apple_private_key_id`
|
|
// is used to generate `client_secret`
|
|
TeamId string `json:"apple_team_id"`
|
|
|
|
// PrivateKeyId is the private Apple key identifier. Keys can be generated via developer.apple.com.
|
|
// This key should be generated with the `Sign In with Apple` option checked.
|
|
// This is needed when `provider` is set to `apple`
|
|
PrivateKeyId string `json:"apple_private_key_id"`
|
|
|
|
// PrivateKeyId is the Apple private key identifier that can be downloaded during key generation.
|
|
// This is needed when `provider` is set to `apple`
|
|
PrivateKey string `json:"apple_private_key"`
|
|
|
|
// Scope specifies optional requested permissions.
|
|
Scope []string `json:"scope"`
|
|
|
|
// Mapper specifies the JSONNet code snippet which uses the OpenID Connect Provider's data (e.g. GitHub or Google
|
|
// profile information) to hydrate the identity's data.
|
|
//
|
|
// It can be either a URL (file://, http(s)://, base64://) or an inline JSONNet code snippet.
|
|
Mapper string `json:"mapper_url"`
|
|
|
|
// RequestedClaims is a string encoded json object that specifies claims and optionally their properties that should be
|
|
// included in the id_token or returned from the UserInfo Endpoint.
|
|
//
|
|
// More information: https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter
|
|
RequestedClaims json.RawMessage `json:"requested_claims"`
|
|
|
|
// An optional organization ID that this provider belongs to.
|
|
// This parameter is only effective in the Ory Network.
|
|
OrganizationID string `json:"organization_id"`
|
|
|
|
// AdditionalIDTokenAudiences is a list of additional audiences allowed in the ID Token.
|
|
// This is only relevant in OIDC flows that submit an IDToken instead of using the callback from the OIDC provider.
|
|
AdditionalIDTokenAudiences []string `json:"additional_id_token_audiences"`
|
|
|
|
// ClaimsSource is a flag which controls where the claims are taken from when
|
|
// using the generic provider. Can be either `userinfo` (calls the userinfo
|
|
// endpoint to get the claims) or `id_token` (takes the claims from the id
|
|
// token). It defaults to `id_token`.
|
|
ClaimsSource string `json:"claims_source"`
|
|
|
|
// PKCE controls if the OpenID Connect OAuth2 flow should use PKCE (Proof Key for Code Exchange).
|
|
// Possible values are: `auto` (default), `never`, `force`.
|
|
// - `auto`: PKCE is used if the provider supports it. Requires setting `issuer_url`.
|
|
// - `never`: Disable PKCE entirely for this provider, even if the provider advertises support for it.
|
|
// - `force`: Always use PKCE, even if the provider does not advertise support for it. OAuth2 flows will fail if the provider does not support PKCE.
|
|
// IMPORTANT: If you set this to `force`, you must whitelist a different return URL for your OAuth2 client in the provider's configuration.
|
|
// Instead of <base-url>/self-service/methods/oidc/callback/<provider>, you must use <base-url>/self-service/methods/oidc/callback
|
|
// (Note the missing <provider> path segment and no trailing slash).
|
|
PKCE string `json:"pkce"`
|
|
|
|
// FedCMConfigURL is the URL to the FedCM IdP configuration file.
|
|
// This is only effective in the Ory Network.
|
|
FedCMConfigURL string `json:"fedcm_config_url"`
|
|
|
|
// NetIDTokenOriginHeader contains the orgin header to be used when exchanging a
|
|
// NetID FedCM token for an ID token.
|
|
NetIDTokenOriginHeader string `json:"net_id_token_origin_header"`
|
|
|
|
// AccountLinkingMode controls how account conflicts are resolved for this provider.
|
|
// Possible values: "confirm_with_existing_credential" (default), "automatic".
|
|
// This is only effective in the Ory Network.
|
|
AccountLinkingMode string `json:"account_linking_mode,omitempty"`
|
|
|
|
// UpdateIdentityOnLogin controls whether the identity is updated from
|
|
// OIDC claims on each login.
|
|
//
|
|
// Possible values:
|
|
// - "never" (default): the identity is not updated on login.
|
|
// - "automatic": re-runs the Jsonnet claims mapper on every OIDC login
|
|
// and updates the identity's traits and metadata automatically.
|
|
UpdateIdentityOnLogin string `json:"update_identity_on_login,omitempty"`
|
|
|
|
// AAL2ACRValues lists upstream OIDC `acr` claim values that elevate
|
|
// the resulting Kratos session to AAL2. When empty, the session AAL
|
|
// is always AAL1 regardless of the upstream `acr` claim.
|
|
AAL2ACRValues []string `json:"aal2_acr_values,omitempty"`
|
|
|
|
// AAL2AMRValues lists upstream OIDC `amr` claim values that elevate
|
|
// the resulting Kratos session to AAL2 when any of them appears in
|
|
// the upstream `amr` array. When empty, `amr` does not influence the
|
|
// session AAL.
|
|
AAL2AMRValues []string `json:"aal2_amr_values,omitempty"`
|
|
}
|
|
|
|
// AALForClaims returns the session AuthenticatorAssuranceLevel that the
|
|
// upstream OIDC response should be mapped to, based on the provider's
|
|
// AAL2ACRValues / AAL2AMRValues allowlists and the ACR / AMR claims
|
|
// reported by the upstream provider.
|
|
//
|
|
// The default is AAL1. AAL2 is only returned when either the upstream
|
|
// `acr` claim matches one of the configured AAL2ACRValues, or at least
|
|
// one of the upstream `amr` entries matches one of the configured
|
|
// AAL2AMRValues. An empty `acr` never matches, even if the allowlist
|
|
// contains an empty string.
|
|
func (c *Configuration) AALForClaims(claims *Claims) identity.AuthenticatorAssuranceLevel {
|
|
if claims == nil {
|
|
return identity.AuthenticatorAssuranceLevel1
|
|
}
|
|
if claims.ACR != "" && slices.Contains(c.AAL2ACRValues, claims.ACR) {
|
|
return identity.AuthenticatorAssuranceLevel2
|
|
}
|
|
for _, want := range c.AAL2AMRValues {
|
|
if slices.Contains(claims.AMR, want) {
|
|
return identity.AuthenticatorAssuranceLevel2
|
|
}
|
|
}
|
|
return identity.AuthenticatorAssuranceLevel1
|
|
}
|
|
|
|
func (p Configuration) Redir(public *url.URL) string {
|
|
if p.PKCE == "force" {
|
|
return urlx.AppendPaths(public, RouteCallbackGeneric).String()
|
|
}
|
|
|
|
if p.OrganizationID != "" {
|
|
route := RouteOrganizationCallback
|
|
route = strings.Replace(route, "{provider}", p.ID, 1)
|
|
route = strings.Replace(route, "{organization}", p.OrganizationID, 1)
|
|
return urlx.AppendPaths(public, route).String()
|
|
}
|
|
|
|
return urlx.AppendPaths(public, strings.Replace(RouteCallback, "{provider}", p.ID, 1)).String()
|
|
}
|
|
|
|
type ConfigurationCollection struct {
|
|
BaseRedirectURI string `json:"base_redirect_uri"`
|
|
Providers []Configuration `json:"providers"`
|
|
}
|
|
|
|
// !!! WARNING !!!
|
|
//
|
|
// If you add a provider here, please also add a test to
|
|
// provider_private_net_test.go
|
|
var supportedProviders = map[string]func(config *Configuration, reg Dependencies) Provider{
|
|
"generic": NewProviderGenericOIDC,
|
|
"google": NewProviderGoogle,
|
|
"github": NewProviderGitHub,
|
|
"github-app": NewProviderGitHubApp,
|
|
"gitlab": NewProviderGitLab,
|
|
"microsoft": NewProviderMicrosoft,
|
|
"discord": NewProviderDiscord,
|
|
"salesforce": NewProviderSalesforce,
|
|
"slack": NewProviderSlack,
|
|
"facebook": NewProviderFacebook,
|
|
"auth0": NewProviderAuth0,
|
|
"vk": NewProviderVK,
|
|
"yandex": NewProviderYandex,
|
|
"apple": NewProviderApple,
|
|
"spotify": NewProviderSpotify,
|
|
"netid": NewProviderNetID,
|
|
"dingtalk": NewProviderDingTalk,
|
|
"linkedin": NewProviderLinkedIn,
|
|
"linkedin_v2": NewProviderLinkedInV2,
|
|
"patreon": NewProviderPatreon,
|
|
"lark": NewProviderLark,
|
|
"x": NewProviderX,
|
|
"line": NewProviderLineV21,
|
|
"jackson": NewProviderJackson,
|
|
"fedcm-test": NewProviderTestFedcm,
|
|
"amazon": NewProviderAmazon,
|
|
"uaepass": NewProviderUAEPass,
|
|
}
|
|
|
|
func (c ConfigurationCollection) Provider(id string, reg Dependencies) (Provider, error) {
|
|
for _, p := range c.Providers {
|
|
if p.ID == id {
|
|
if f, ok := supportedProviders[p.Provider]; ok {
|
|
return f(&p, reg), nil
|
|
}
|
|
|
|
return nil, errors.Errorf("provider type %s is not supported, supported are: %v", p.Provider, maps.Keys(supportedProviders))
|
|
}
|
|
}
|
|
return nil, errors.WithStack(herodot.ErrNotFound().WithReasonf(`OpenID Connect Provider "%s" is unknown or has not been configured`, id))
|
|
}
|