Files
upstream-ory-kratos/selfservice/strategy/oidc/strategy_settings.go
upstream-mirror c60cc7ca75
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
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

700 lines
23 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
"bytes"
"cmp"
"context"
_ "embed"
"encoding/json"
"fmt"
"net/http"
"slices"
"time"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"github.com/tidwall/sjson"
"go.opentelemetry.io/otel/attribute"
"github.com/ory/herodot"
"github.com/ory/jsonschema/v3"
"github.com/ory/x/decoderx"
"github.com/ory/x/otelx"
"github.com/ory/x/sqlxx"
"github.com/ory/kratos/continuity"
oidcv1 "github.com/ory/kratos/gen/oidc/v1"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/settings"
"github.com/ory/kratos/selfservice/strategy"
"github.com/ory/kratos/session"
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x"
)
//go:embed .schema/settings.schema.json
var settingsSchema []byte
var (
_ settings.Strategy = new(Strategy)
UnknownConnectionValidationError = &jsonschema.ValidationError{
Message: "can not unlink non-existing OpenID Connect connection", InstancePtr: "#/",
}
)
var ConnectionExistValidationError = &jsonschema.ValidationError{
Message: "can not link unknown or already existing OpenID Connect connection", InstancePtr: "#/",
}
var UnlinkAllFirstFactorConnectionsError = &jsonschema.ValidationError{
Message: "can not unlink OpenID Connect connection because it is the last remaining first factor credential", InstancePtr: "#/",
}
func (s *Strategy) SettingsStrategyID() string {
return s.ID().String()
}
func (s *Strategy) decoderSettings(ctx context.Context, p *updateSettingsFlowWithOidcMethod, r *http.Request, settingsFlow *settings.Flow) error {
schema := flow.IdentitySchema(settingsFlow.Identity.SchemaID)
ds, err := schema.URL(ctx, s.d.Config())
if err != nil {
return err
}
raw, err := sjson.SetBytes(settingsSchema,
"properties.traits.$ref", ds.String()+"#/properties/traits")
if err != nil {
return errors.WithStack(err)
}
compiler, err := decoderx.HTTPRawJSONSchemaCompiler(raw)
if err != nil {
return errors.WithStack(err)
}
if err := decoderx.Decode(r, &p, compiler,
decoderx.HTTPKeepRequestBody(true),
decoderx.HTTPDecoderUseQueryAndBody(),
decoderx.HTTPDecoderSetValidatePayloads(false),
decoderx.HTTPDecoderAllowedMethods("POST", "GET"),
decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *Strategy) linkedProviders(conf *ConfigurationCollection, confidential *identity.Identity) ([]Provider, error) {
creds, ok := confidential.GetCredentials(s.ID())
if !ok {
return nil, nil
}
var available identity.CredentialsOIDC
if err := json.Unmarshal(creds.Config, &available); err != nil {
return nil, errors.WithStack(err)
}
var result []Provider
for _, p := range available.Providers {
prov, err := conf.Provider(p.Provider, s.d)
if errors.Is(err, herodot.ErrNotFound()) {
continue
} else if err != nil {
return nil, err
}
result = append(result, prov)
}
return result, nil
}
func (s *Strategy) linkableProviders(conf *ConfigurationCollection, confidential *identity.Identity) ([]Provider, error) {
var available identity.CredentialsOIDC
creds, ok := confidential.GetCredentials(s.ID())
if ok {
if err := json.Unmarshal(creds.Config, &available); err != nil {
return nil, errors.WithStack(err)
}
}
var result []Provider
for _, p := range conf.Providers {
var found bool
for _, pp := range available.Providers {
if pp.Provider == p.ID {
found = true
break
}
}
if !found {
prov, err := conf.Provider(p.ID, s.d)
if err != nil {
return nil, err
}
result = append(result, prov)
}
}
return result, nil
}
// settingsNodeGroup returns the UI node group for this strategy's settings
// link/unlink nodes. SAML nodes use the SAML group so a sibling strategy's
// group-scoped removal does not clobber them (both strategies share the OSS
// PopulateSettingsMethod, which names every link/unlink node "link"/"unlink").
func (s *Strategy) settingsNodeGroup() node.UiNodeGroup {
if s.credType == identity.CredentialsTypeSAML {
return node.SAMLGroup
}
return node.OpenIDConnectGroup
}
func (s *Strategy) PopulateSettingsMethod(ctx context.Context, r *http.Request, id *identity.Identity, sr *settings.Flow) (err error) {
ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.Strategy.PopulateSettingsMethod")
defer otelx.End(span, &err)
conf, err := s.Config(ctx)
if err != nil {
return err
}
linkable, err := s.linkableProviders(conf, id)
if err != nil {
return err
}
linked, err := s.linkedProviders(conf, id)
if err != nil {
return err
}
group := s.settingsNodeGroup()
sr.UI.GetNodes().RemoveInGroup(group, "unlink", "link")
sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
if sr.OrganizationID.Valid {
boundToFlowOrg := id.OrganizationID.Valid && id.OrganizationID.UUID == sr.OrganizationID.UUID
// Org-scoped settings: render only providers belonging to this org.
// Already-linked providers render as disabled unlink nodes (the org
// owns the binding, so the user cannot remove it). Unlinked providers
// render as link nodes — disabled when the identity is already bound
// to this org, active otherwise (first-time bind).
linkedIDs := make(map[string]struct{}, len(linked))
for _, p := range linked {
linkedIDs[p.Config().ID] = struct{}{}
}
for _, p := range conf.Providers {
if !flowOrgMatchesProvider(sr, p.OrganizationID) {
continue
}
_, alreadyLinked := linkedIDs[p.ID]
var n *node.Node
if alreadyLinked {
n = NewUnlinkNode(p.ID, cmp.Or(p.Label, p.ID))
} else {
n = NewLinkNode(p.ID, cmp.Or(p.Label, p.ID))
}
n.Group = group
if alreadyLinked || boundToFlowOrg {
if attrs, ok := n.Attributes.(*node.InputAttributes); ok {
attrs.Disabled = true
}
}
sr.UI.GetNodes().Append(n)
}
if boundToFlowOrg {
// Cloud OIDC and SAML org strategies both delegate here; dedupe so
// the managed-by-organization message appears exactly once.
hasMsg := slices.ContainsFunc(sr.UI.Messages, func(m text.Message) bool {
return m.ID == text.InfoSelfServiceSettingsManagedByOrganization
})
if !hasMsg {
sr.UI.Messages.Add(text.NewInfoSelfServiceSettingsManagedByOrganization())
}
}
return nil
}
// Non-org settings flow: existing behavior.
for _, l := range linkable {
// We do not want to offer to link SSO providers in the regular settings flow.
if l.Config().OrganizationID != "" {
continue
}
sr.UI.GetNodes().Append(NewLinkNode(l.Config().ID, cmp.Or(l.Config().Label, l.Config().ID)))
}
count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(ctx, id)
if err != nil {
return err
}
if count > 1 {
// This means that we're able to remove a connection because it is the last configured credential. If it is
// removed, the identity is no longer able to sign in.
for _, l := range linked {
sr.UI.GetNodes().Append(NewUnlinkNode(l.Config().ID, cmp.Or(l.Config().Label, l.Config().ID)))
}
}
return nil
}
// Update Settings Flow with OpenID Connect Method
//
// swagger:model updateSettingsFlowWithOidcMethod
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type updateSettingsFlowWithOidcMethod struct {
// Method
//
// Should be set to profile when trying to update a profile.
//
// required: true
Method string `json:"method"`
// Link this provider
//
// Either this or `unlink` must be set.
//
// type: string
// in: body
Link string `json:"link"`
// Unlink this provider
//
// Either this or `link` must be set.
//
// type: string
// in: body
Unlink string `json:"unlink"`
// Flow ID is the flow's ID.
//
// in: query
FlowID string `json:"flow"`
// The identity's traits
//
// in: body
Traits json.RawMessage `json:"traits"`
// UpstreamParameters are the parameters that are passed to the upstream identity provider.
//
// These parameters are optional and depend on what the upstream identity provider supports.
// Supported parameters are:
// - `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`.
// - `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`.
// - `acr_values` (string): The `acr_values` specifies the Authentication Context Class Reference values for the authorization request.
//
// required: false
UpstreamParameters json.RawMessage `json:"upstream_parameters"`
// Transient data to pass along to any webhooks
//
// required: false
TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"`
}
func (p *updateSettingsFlowWithOidcMethod) GetFlowID() uuid.UUID {
return x.ParseUUID(p.FlowID)
}
func (p *updateSettingsFlowWithOidcMethod) SetFlowID(rid uuid.UUID) {
p.FlowID = rid.String()
}
func (s *Strategy) Settings(ctx context.Context, w http.ResponseWriter, r *http.Request, f *settings.Flow, ss *session.Session) (_ *settings.UpdateContext, err error) {
ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.Strategy.Settings")
defer otelx.End(span, &err)
var p updateSettingsFlowWithOidcMethod
if err := s.decoderSettings(ctx, &p, r, f); err != nil {
return nil, err
}
f.TransientPayload = p.TransientPayload
if f.OrganizationID.Valid && len(p.Unlink) > 0 {
return nil, errors.WithStack(herodot.ErrBadRequest().WithReason("Cannot unlink an organization-scoped credential through the settings flow."))
}
ctxUpdate, err := settings.PrepareUpdate(s.d, w, r, f, ss, settings.ContinuityKey(s.SettingsStrategyID()), &p)
if errors.Is(err, settings.ErrContinuePreviousAction) {
if !s.d.Config().SelfServiceStrategy(ctx, s.SettingsStrategyID()).Enabled {
return nil, s.handleMethodNotAllowedError(errors.WithStack(herodot.ErrNotFound().WithReason(strategy.EndpointDisabledMessage)))
}
if len(p.Link) > 0 {
if err := s.initLinkProvider(ctx, w, r, ctxUpdate, &p); err != nil {
return nil, err
}
return ctxUpdate, nil
} else if len(p.Unlink) > 0 {
if err := s.unlinkProvider(ctx, w, r, ctxUpdate, &p); err != nil {
return nil, err
}
return ctxUpdate, nil
}
return nil, s.handleSettingsError(ctx, w, r, ctxUpdate, &p, errors.WithStack(herodot.ErrBadRequest().WithReason("Expected either link or unlink to be set when continuing flow but both are unset.")))
} else if err != nil {
return nil, s.handleSettingsError(ctx, w, r, ctxUpdate, &p, err)
}
if len(p.Link)+len(p.Unlink) == 0 {
span.SetAttributes(attribute.String("not_responsible_reason", "neither link nor unlink set"))
return nil, errors.WithStack(flow.ErrStrategyNotResponsible)
}
if !s.d.Config().SelfServiceStrategy(ctx, s.SettingsStrategyID()).Enabled {
return nil, s.handleMethodNotAllowedError(errors.WithStack(herodot.ErrNotFound().WithReason(strategy.EndpointDisabledMessage)))
}
switch l, u := len(p.Link), len(p.Unlink); {
case l > 0 && u > 0:
return nil, s.handleSettingsError(ctx, w, r, ctxUpdate, &p, errors.WithStack(&jsonschema.ValidationError{
Message: "it is not possible to link and unlink providers in the same request",
InstancePtr: "#/",
}))
case l > 0:
if err := s.initLinkProvider(ctx, w, r, ctxUpdate, &p); err != nil {
return nil, err
}
return ctxUpdate, nil
case u > 0:
if err := s.unlinkProvider(ctx, w, r, ctxUpdate, &p); err != nil {
return nil, err
}
return ctxUpdate, nil
}
// this case should never be reached as we previously checked whether link and unlink are both empty
return nil, errors.WithStack(flow.ErrStrategyNotResponsible)
}
func (s *Strategy) isLinkable(ctx context.Context, ctxUpdate *settings.UpdateContext, toLink string) (*identity.Identity, error) {
providers, err := s.Config(ctx)
if err != nil {
return nil, err
}
i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.Identity.ID)
if err != nil {
return nil, err
}
linkable, err := s.linkableProviders(providers, i)
if err != nil {
return nil, err
}
var found bool
for _, available := range linkable {
if toLink == available.Config().ID {
// The provider must match the flow's organization scope. An
// org-scoped flow may link only providers of that organization; a
// non-org flow may link only providers with no organization. We
// reuse ConnectionExistValidationError rather than a distinct
// "wrong organization" message so an attacker cannot probe which
// providers another organization has configured.
if !flowOrgMatchesProvider(ctxUpdate.Flow, available.Config().OrganizationID) {
return nil, errors.WithStack(ConnectionExistValidationError)
}
found = true
break
}
}
if !found {
return nil, errors.WithStack(ConnectionExistValidationError)
}
return i, nil
}
// flowOrgMatchesProvider reports whether a provider with organization id
// providerOrg may be linked through flow f. An org-scoped flow accepts only
// providers of that organization; a non-org flow accepts only providers with
// no organization.
func flowOrgMatchesProvider(f *settings.Flow, providerOrg string) bool {
if !f.OrganizationID.Valid {
return providerOrg == ""
}
providerOrgID, err := uuid.FromString(providerOrg)
if err != nil {
return false
}
return providerOrgID == f.OrganizationID.UUID
}
func (s *Strategy) initLinkProvider(ctx context.Context, w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithOidcMethod) error {
if _, err := s.isLinkable(ctx, ctxUpdate, p.Link); err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
}
if !s.d.SessionManager().IsPrivileged(ctx, ctxUpdate.Session) {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, errors.WithStack(settings.NewFlowNeedsReAuth()))
}
provider, err := s.Provider(ctx, p.Link)
if err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
}
req, err := s.validateFlow(ctx, r, ctxUpdate.Flow.ID, oidcv1.FlowKind_FLOW_KIND_SETTINGS)
if err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
}
state, pkce, err := s.GenerateState(ctx, provider, ctxUpdate.Flow, x.RequestBaseURL(r))
if err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
}
if _, err := s.d.ContinuityManager().Pause(ctx, w, r, sessionName,
continuity.NewCookieReferenceStore(s.d.ContinuityCookieManager(ctx)),
continuity.WithPayload(&AuthCodeContainer{
State: state,
FlowID: ctxUpdate.Flow.ID.String(),
Traits: p.Traits,
}),
continuity.WithLifespan(time.Minute*30)); err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
}
var up map[string]string
if err := json.NewDecoder(bytes.NewBuffer(p.UpstreamParameters)).Decode(&up); err != nil {
return err
}
codeURL, err := getAuthRedirectURL(ctx, provider, req, state, up, pkce)
if err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
}
if x.IsJSONRequest(r) {
s.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(codeURL))
} else {
http.Redirect(w, r, codeURL, http.StatusSeeOther)
}
return errors.WithStack(flow.ErrCompletedByStrategy)
}
func (s *Strategy) linkProvider(ctx context.Context, w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider) error {
p := &updateSettingsFlowWithOidcMethod{
Link: provider.Config().ID, FlowID: ctxUpdate.Flow.ID.String(),
}
if !s.d.SessionManager().IsPrivileged(ctx, ctxUpdate.Session) {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, errors.WithStack(settings.NewFlowNeedsReAuth()))
}
i, err := s.isLinkable(ctx, ctxUpdate, p.Link)
if err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
}
if err := s.linkCredentials(ctx, i, token, provider.Config().ID, claims.Subject, provider.Config().OrganizationID); err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
}
// Add authentication method to session after
// linking with 3rd party OIDC provider
if err := s.d.SessionManager().SessionAddAuthenticationMethods(
ctx,
ctxUpdate.Session.ID,
session.AuthenticationMethod{
Method: s.ID(),
AAL: provider.Config().AALForClaims(claims),
Provider: provider.Config().ID,
Organization: provider.Config().OrganizationID,
UpstreamACR: claims.ACR,
UpstreamAMR: claims.AMR,
}); err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
}
if err := s.d.SettingsHookExecutor().PostSettingsHook(ctx, w, r, s.SettingsStrategyID(), ctxUpdate, i, settings.WithCallback(func(ctxUpdate *settings.UpdateContext) error {
// Credential population is done by PostSettingsHook on ctxUpdate.Session.Identity
return s.PopulateSettingsMethod(ctx, r, ctxUpdate.Session.Identity, ctxUpdate.Flow)
})); err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
}
return nil
}
func (s *Strategy) unlinkProvider(ctx context.Context, w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithOidcMethod) error {
if !s.d.SessionManager().IsPrivileged(ctx, ctxUpdate.Session) {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, errors.WithStack(settings.NewFlowNeedsReAuth()))
}
providers, err := s.Config(ctx)
if err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
}
i, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, ctxUpdate.Session.Identity.ID)
if err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
}
availableProviders, err := s.linkedProviders(providers, i)
if err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
}
var cc identity.CredentialsOIDC
creds, err := i.ParseCredentials(s.ID(), &cc)
if err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
}
count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(ctx, i)
if err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
}
if count < 2 {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, errors.WithStack(UnlinkAllFirstFactorConnectionsError))
}
var found bool
var updatedProviders []identity.CredentialsOIDCProvider
var updatedIdentifiers []string
for _, available := range availableProviders {
if p.Unlink == available.Config().ID {
for _, link := range cc.Providers {
if link.Provider != p.Unlink {
updatedIdentifiers = append(updatedIdentifiers, identity.OIDCUniqueID(link.Provider, link.Subject))
updatedProviders = append(updatedProviders, link)
} else {
found = true
}
}
}
}
if !found {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, errors.WithStack(UnknownConnectionValidationError))
}
creds.Identifiers = updatedIdentifiers
creds.Config, err = json.Marshal(&identity.CredentialsOIDC{Providers: updatedProviders})
if err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, errors.WithStack(err))
}
i.Credentials[s.ID()] = *creds
if err := s.d.SettingsHookExecutor().PostSettingsHook(ctx, w, r, s.SettingsStrategyID(), ctxUpdate, i, settings.WithCallback(func(ctxUpdate *settings.UpdateContext) error {
// Credential population is done by PostSettingsHook on ctxUpdate.Session.Identity
return s.PopulateSettingsMethod(ctx, r, ctxUpdate.Session.Identity, ctxUpdate.Flow)
})); err != nil {
return s.handleSettingsError(ctx, w, r, ctxUpdate, p, err)
}
return errors.WithStack(flow.ErrCompletedByStrategy)
}
func (s *Strategy) handleSettingsError(ctx context.Context, w http.ResponseWriter, r *http.Request, ctxUpdate *settings.UpdateContext, p *updateSettingsFlowWithOidcMethod, err error) error {
// Do not pause flow if the flow type is an API flow as we can't save cookies in those flows.
if e := new(settings.FlowNeedsReAuth); errors.As(err, &e) && ctxUpdate.Flow != nil && ctxUpdate.Flow.Type == flow.TypeBrowser {
if _, err := s.d.ContinuityManager().Pause(ctx, w, r,
settings.ContinuityKey(s.SettingsStrategyID()), continuity.NewCookieReferenceStore(s.d.ContinuityCookieManager(ctx)), settings.ContinuityOptions(p, ctxUpdate.Session.Identity)...); err != nil {
return err
}
}
if ctxUpdate.Flow != nil {
ctxUpdate.Flow.UI.ResetMessages()
ctxUpdate.Flow.UI.SetCSRF(s.d.GenerateCSRFToken(r))
}
return err
}
func (s *Strategy) Link(ctx context.Context, i *identity.Identity, credentialsConfig sqlxx.JSONRawMessage) (err error) {
ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.Strategy.Link")
defer otelx.End(span, &err)
var credentialsOIDCConfig identity.CredentialsOIDC
if err := json.Unmarshal(credentialsConfig, &credentialsOIDCConfig); err != nil {
return err
}
if len(credentialsOIDCConfig.Providers) != 1 {
return errors.New("no oidc provider was set")
}
credentialsOIDCProvider := credentialsOIDCConfig.Providers[0]
if err := s.linkCredentials(
ctx,
i,
// The tokens in this credential are coming from the existing identity. Hence, the values are already encrypted.
credentialsOIDCProvider.GetTokens(),
credentialsOIDCProvider.Provider,
credentialsOIDCProvider.Subject,
credentialsOIDCProvider.Organization,
); err != nil {
return err
}
if err := s.d.IdentityManager().Update(ctx, i, identity.ManagerAllowWriteProtectedTraits); err != nil {
return err
}
return nil
}
func (s *Strategy) CompletedLogin(sess *session.Session, data *flow.DuplicateCredentialsData) error {
var credentialsOIDCConfig identity.CredentialsOIDC
if err := json.Unmarshal(data.CredentialsConfig, &credentialsOIDCConfig); err != nil {
return err
}
if len(credentialsOIDCConfig.Providers) != 1 {
return errors.New("no oidc provider was set")
}
credentialsOIDCProvider := credentialsOIDCConfig.Providers[0]
sess.CompletedLoginForWithProvider(
s.ID(),
identity.AuthenticatorAssuranceLevel1,
credentialsOIDCProvider.Provider,
credentialsOIDCProvider.Organization,
)
return nil
}
func (s *Strategy) SetDuplicateCredentials(f flow.InternalContexter, duplicateIdentifier string, credentials identity.Credentials, provider string) error {
var credentialsOIDCConfig identity.CredentialsOIDC
if err := json.Unmarshal(credentials.Config, &credentialsOIDCConfig); err != nil {
return err
}
// We want to only set the provider in the credentials config that was used to authenticate the user.
for _, p := range credentialsOIDCConfig.Providers {
if p.Provider == provider {
credentialsOIDCConfig.Providers = []identity.CredentialsOIDCProvider{p}
config, err := json.Marshal(credentialsOIDCConfig)
if err != nil {
return err
}
return flow.SetDuplicateCredentials(f, flow.DuplicateCredentialsData{
CredentialsType: s.ID(),
CredentialsConfig: config,
DuplicateIdentifier: duplicateIdentifier,
})
}
}
return fmt.Errorf("provider %q not found in credentials", provider)
}