Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: link oidc credentials when login #3563

Merged
merged 36 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
be7259f
feat: link credentials when login
splaunov Apr 10, 2023
29a6667
feat: link credentials when login - add LoginAndLinkCredentials metho…
splaunov Apr 11, 2023
5b93315
feat(saml): add linkCredentialsFlow parameter to update flow request …
splaunov Apr 25, 2023
a38a5b1
fix: panic when linked flow does not exist (CORE-2017)
splaunov Apr 27, 2023
4d2551e
feat: block linking credentials if they do not match identifier of lo…
splaunov May 2, 2023
c06f46b
feat: link credentials when second login is OIDC (CORE-2041)
splaunov May 8, 2023
1a7c291
Merge remote-tracking branch 'origin/master' into feature/link-creden…
hperl Oct 9, 2023
f7c76be
chore: format
hperl Oct 9, 2023
bc0bcb3
fix: tests
hperl Oct 10, 2023
93f4168
cleanup
hperl Oct 20, 2023
7d89d54
Merge remote-tracking branch 'origin/master' into hperl/link-credenti…
hperl Oct 20, 2023
76fa5b0
fix: simplify linking flow
hperl Oct 23, 2023
5cb2ea8
fix: cleanup
hperl Oct 23, 2023
c540231
chore: cleanup and test fixes
hperl Oct 23, 2023
14493bd
fix: add no_org_ui param
hperl Oct 24, 2023
e5a3504
feat: handle org ID
hperl Oct 24, 2023
cd14390
fix: write org id to amr
hperl Oct 24, 2023
070aecf
Merge remote-tracking branch 'origin/master' into hperl/link-credenti…
hperl Oct 25, 2023
39b7bb6
fix: linter errors
hperl Oct 26, 2023
5bff9ef
add consent label
hperl Oct 30, 2023
1297952
handle conflicts in recoverable, verifiable addresses
hperl Oct 30, 2023
8ad4526
Merge remote-tracking branch 'origin/master' into hperl/link-credenti…
hperl Oct 30, 2023
6140287
chore: formatting
hperl Oct 30, 2023
d812e73
Merge remote-tracking branch 'origin/master' into hperl/link-credenti…
hperl Oct 31, 2023
81a31cb
chore: code review
aeneasr Oct 31, 2023
d844c6e
test: add Cypress e2e test
hperl Oct 31, 2023
c7c205b
test: add ConflictingIdentity tests
hperl Oct 31, 2023
e747d50
test: add test for maybeLinkCredentials
hperl Oct 31, 2023
3b1ba9b
fix: add SetInternalContext
hperl Oct 31, 2023
c13a823
fix: error message
hperl Oct 31, 2023
bc26f78
fix: uncomment test
hperl Nov 6, 2023
737cf57
Merge branch 'master' into hperl/link-credentials-when-login
hperl Nov 6, 2023
771bb95
fix: add dup credentials error info
hperl Nov 7, 2023
9f32943
fix: tests
hperl Nov 7, 2023
5b9bda5
Merge remote-tracking branch 'origin/master' into hperl/link-credenti…
hperl Nov 7, 2023
51ed466
fix: tests
hperl Nov 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/clidoc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ func init() {
"NewInfoSelfServiceLoginCode": text.NewInfoSelfServiceLoginCode(),
"NewErrorValidationRegistrationRetrySuccessful": text.NewErrorValidationRegistrationRetrySuccessful(),
"NewInfoSelfServiceRegistrationRegisterCode": text.NewInfoSelfServiceRegistrationRegisterCode(),
"NewInfoSelfServiceLoginLinkCredentials": text.NewInfoSelfServiceLoginLinkCredentials(),
"NewErrorValidationLoginLinkedCredentialsDoNotMatch": text.NewErrorValidationLoginLinkedCredentialsDoNotMatch(),
}
}

Expand Down
10 changes: 10 additions & 0 deletions schema/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,13 @@ func NewLoginCodeInvalid() error {
Messages: new(text.Messages).Add(text.NewErrorValidationLoginCodeInvalidOrAlreadyUsed()),
})
}

func NewLinkedCredentialsDoNotMatch() error {
return errors.WithStack(&ValidationError{
ValidationError: &jsonschema.ValidationError{
Message: `linked credentials do not match`,
hperl marked this conversation as resolved.
Show resolved Hide resolved
InstancePtr: "#/",
},
Messages: new(text.Messages).Add(text.NewErrorValidationLoginLinkedCredentialsDoNotMatch()),
})
}
15 changes: 12 additions & 3 deletions selfservice/flow/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,27 @@ import (
"net/http"
"net/url"

"github.com/gofrs/uuid"
"github.com/pkg/errors"

"github.com/ory/herodot"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/ui/container"
"github.com/ory/kratos/x"

"github.com/gofrs/uuid"

"github.com/ory/x/sqlxx"
"github.com/ory/x/urlx"
)

const InternalContextDuplicateCredentialsPath = "registration_duplicate_credentials"
const InternalContextLinkCredentialsPath = "link_credentials"

type RegistrationDuplicateCredentials struct {
CredentialsType identity.CredentialsType
CredentialsConfig sqlxx.JSONRawMessage
DuplicateIdentifier string
}

func AppendFlowTo(src *url.URL, id uuid.UUID) *url.URL {
return urlx.CopyWithQuery(src, url.Values{"flow": {id.String()}})
}
Expand Down
10 changes: 10 additions & 0 deletions selfservice/flow/login/.schema/link_credentials.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$id": "https://schemas.ory.sh/kratos/selfservice/flow/link_credentials.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"linkCredentialsFlow": {
"type": "string"
}
}
}
39 changes: 39 additions & 0 deletions selfservice/flow/login/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
package login

import (
_ "embed"
"encoding/json"
"net/http"
"net/url"
"time"

"github.com/tidwall/gjson"
"github.com/tidwall/sjson"

"github.com/gofrs/uuid"
"github.com/julienschmidt/httprouter"
"github.com/pkg/errors"
Expand All @@ -32,6 +37,9 @@ import (
"github.com/ory/x/urlx"
)

//go:embed .schema/link_credentials.schema.json
var linkCredentialsSchema []byte

const (
RouteInitBrowserFlow = "/self-service/login/browser"
RouteInitAPIFlow = "/self-service/login/api"
Expand Down Expand Up @@ -772,6 +780,37 @@ continueLogin:
return
}

internalContextDuplicateCredentials := gjson.GetBytes(f.InternalContext, flow.InternalContextDuplicateCredentialsPath)
if internalContextDuplicateCredentials.IsObject() {
// If return_to was set before, we need to preserve it.
var opts []FlowOption
if len(f.ReturnTo) > 0 {
opts = append(opts, WithFlowReturnTo(f.ReturnTo))
}
opts = append(opts, func(newFlow *Flow) {
newFlow.UI.Messages.Add(text.NewInfoSelfServiceLoginLinkCredentials())
var linkCredentials flow.RegistrationDuplicateCredentials
if err := json.Unmarshal([]byte(internalContextDuplicateCredentials.Raw), &linkCredentials); err != nil {
h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err)
return
}
newFlow.InternalContext, err = sjson.SetBytes(newFlow.InternalContext, flow.InternalContextLinkCredentialsPath,
linkCredentials)
if err != nil {
h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err)
return
}
})
loginFlow, _, err := h.NewLoginFlow(w, r, flow.TypeBrowser, opts...)
if err != nil {
h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err)
return
}

http.Redirect(w, r, loginFlow.AppendTo(h.d.Config().SelfServiceFlowLoginUI(r.Context())).String(), http.StatusSeeOther)
return
}

var i *identity.Identity
var group node.UiNodeGroup
for _, ss := range h.d.AllLoginStrategies() {
Expand Down
102 changes: 102 additions & 0 deletions selfservice/flow/login/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ package login

import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/gofrs/uuid"
"github.com/tidwall/gjson"

"github.com/ory/kratos/schema"
"github.com/ory/x/decoderx"

"go.opentelemetry.io/otel/trace"

"github.com/ory/kratos/x/events"
Expand Down Expand Up @@ -46,6 +53,7 @@ type (
executorDependencies interface {
config.Provider
hydra.Provider
identity.PrivilegedPoolProvider
session.ManagementProvider
session.PersistenceProvider
x.CSRFTokenGeneratorProvider
Expand All @@ -54,7 +62,9 @@ type (
x.TracingProvider
sessiontokenexchange.PersistenceProvider

FlowPersistenceProvider
HooksProvider
StrategyProvider
}
HookExecutor struct {
d executorDependencies
Expand Down Expand Up @@ -128,6 +138,10 @@ func (e *HookExecutor) PostLoginHook(
r = r.WithContext(ctx)
defer otelx.End(span, &err)

if err := e.linkCredentials(r, s, i, a); err != nil {
return err
}

if err := s.Activate(r, i, e.d.Config(), time.Now().UTC()); err != nil {
return err
}
Expand Down Expand Up @@ -301,3 +315,91 @@ func (e *HookExecutor) PreLoginHook(w http.ResponseWriter, r *http.Request, a *F

return nil
}

func (e *HookExecutor) linkCredentials(r *http.Request, s *session.Session, i *identity.Identity, f *Flow) error {
var lc flow.RegistrationDuplicateCredentials

if r.Method == "POST" {
var p struct {
FlowID string `json:"linkCredentialsFlow" form:"linkCredentialsFlow"`
}

if err := decoderx.NewHTTP().Decode(r, &p,
decoderx.HTTPDecoderSetValidatePayloads(true),
decoderx.MustHTTPRawJSONSchemaCompiler(linkCredentialsSchema),
decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil {
return err
}

if p.FlowID != "" {
linkCredentialsFlowID, innerErr := uuid.FromString(p.FlowID)
if innerErr != nil {
return innerErr
}
linkCredentialsFlow, innerErr := e.d.LoginFlowPersister().GetLoginFlow(r.Context(), linkCredentialsFlowID)
if innerErr != nil {
return innerErr
}
innerErr = e.getInternalContextLinkCredentials(linkCredentialsFlow, flow.InternalContextDuplicateCredentialsPath, &lc)
if innerErr != nil {
return innerErr
}
}
}

if lc.CredentialsType == "" {
err := e.getInternalContextLinkCredentials(f, flow.InternalContextLinkCredentialsPath, &lc)
if err != nil {
return err
}
}

if lc.CredentialsType != "" {
if err := e.checkDuplecateCredentialsIdentifierMatch(r.Context(), i.ID, lc.DuplicateIdentifier); err != nil {
return err
}
strategy, err := e.d.AllLoginStrategies().Strategy(lc.CredentialsType)
if err != nil {
return err
}

linkableStrategy, ok := strategy.(LinkableStrategy)
if !ok {
return errors.New(fmt.Sprintf("Strategy is not linkable: %T", linkableStrategy))
}

if err := linkableStrategy.Link(r.Context(), i, lc.CredentialsConfig); err != nil {
return err
}

method := strategy.CompletedAuthenticationMethod(r.Context())
s.CompletedLoginFor(method.Method, method.AAL)
}

return nil
}

func (e *HookExecutor) getInternalContextLinkCredentials(f *Flow, internalContextPath string, lc *flow.RegistrationDuplicateCredentials) error {
internalContextLinkCredentials := gjson.GetBytes(f.InternalContext, internalContextPath)
if internalContextLinkCredentials.IsObject() {
if err := json.Unmarshal([]byte(internalContextLinkCredentials.Raw), lc); err != nil {
return err
}
}
return nil
}

func (e *HookExecutor) checkDuplecateCredentialsIdentifierMatch(ctx context.Context, identityID uuid.UUID, match string) error {
i, err := e.d.PrivilegedIdentityPool().GetIdentityConfidential(ctx, identityID)
if err != nil {
return err
}
for _, credentials := range i.Credentials {
for _, identifier := range credentials.Identifiers {
if identifier == match {
return nil
}
}
}
return schema.NewLinkedCredentialsDoNotMatch()
}
6 changes: 6 additions & 0 deletions selfservice/flow/login/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"context"
"net/http"

"github.com/ory/x/sqlxx"

"github.com/gofrs/uuid"

"github.com/ory/kratos/session"
Expand All @@ -29,6 +31,10 @@ type Strategy interface {

type Strategies []Strategy

type LinkableStrategy interface {
Link(ctx context.Context, i *identity.Identity, credentials sqlxx.JSONRawMessage) error
}

func (s Strategies) Strategy(id identity.CredentialsType) (Strategy, error) {
ids := make([]identity.CredentialsType, len(s))
for k, ss := range s {
Expand Down
48 changes: 37 additions & 11 deletions selfservice/flow/registration/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,20 @@ import (
"time"

"github.com/gobuffalo/pop/v6"

"github.com/gofrs/uuid"
"github.com/pkg/errors"
"github.com/tidwall/gjson"

"github.com/ory/x/sqlxx"
"github.com/tidwall/sjson"

hydraclientgo "github.com/ory/hydra-client-go/v2"

"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/hydra"
"github.com/ory/kratos/ui/container"

"github.com/gofrs/uuid"
"github.com/pkg/errors"

"github.com/ory/x/urlx"

"github.com/ory/kratos/identity"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/ui/container"
"github.com/ory/kratos/x"
"github.com/ory/x/sqlxx"
"github.com/ory/x/urlx"
)

// swagger:model registrationFlow
Expand Down Expand Up @@ -267,3 +262,34 @@ func (f *Flow) GetFlowName() flow.FlowName {
func (f *Flow) SetState(state State) {
f.State = state
}

const internalContextOuterLoginFlowPath = "outer_flow"

type OuterLoginFlow struct {
ID string
}

func (f *Flow) GetOuterLoginFlowID() (*uuid.UUID, error) {
la := gjson.GetBytes(f.InternalContext, internalContextOuterLoginFlowPath)
if !la.IsObject() {
return nil, nil
}
var internalContextOuterFlow OuterLoginFlow
if err := json.Unmarshal([]byte(la.Raw), &internalContextOuterFlow); err != nil {
return nil, err
}
id, err := uuid.FromString(internalContextOuterFlow.ID)
if err != nil {
return nil, err
}
return &id, nil
}

func (f *Flow) SetOuterLoginFlowID(flowID uuid.UUID) error {
hperl marked this conversation as resolved.
Show resolved Hide resolved
var err error = nil
f.InternalContext, err = sjson.SetBytes(f.InternalContext, internalContextOuterLoginFlowPath,
OuterLoginFlow{
ID: flowID.String(),
})
return err
}
Loading