From b098326d72738f4e2ab2cab9b22caeb0a411e79f Mon Sep 17 00:00:00 2001 From: Arne Luenser Date: Tue, 3 Sep 2024 14:11:41 +0200 Subject: [PATCH] feat: client-side PKCE This change introduces a new configuration for OIDC providers: pkce with values auto (default), never, force. When auto is specified or the field is omitted, Kratos will perform autodiscovery and perform PKCE when the server advertises support for it. This requires the issuer_url to be set for the provider. never completely disables PKCE support. This is only theoretically useful: when a provider advertises PKCE support but doesn't actually implement it. force always sends a PKCE challenge in the initial redirect URL, regardless of what the provider advertises. This setting is useful when the provider offers PKCE but doesn't advertise it in his ./well-known/openid-configuration. Important: When setting pkce: force, you must whitelist a different return URL for your OAuth2 client in the provider's configuration. Instead of /self-service/methods/oidc/callback/, you must use /self-service/methods/oidc/callback (note missing last path segment). This is to enable the use of the same OAuth client ID+secret when configuring several Kratos OIDC providers, without having to whitelist individual redirect_uris for each Kratos provider config. --- Makefile | 21 +- buf.gen.yaml | 12 + buf.yaml | 9 + cipher/chacha20.go | 6 + cmd/identities/get_test.go | 9 +- cmd/identities/helpers_test.go | 2 +- gen/oidc/v1/state.pb.go | 183 +++++++++++++ go.mod | 2 +- go.sum | 4 +- internal/driver.go | 13 +- proto/oidc/v1/state.proto | 10 + ...dc_credentials-case=should_fail_login.json | 66 +++++ ...entials-case=should_fail_registration.json | 66 +++++ ...egistration_id_first_strategy_enabled.json | 66 +++++ ...rd_credentials-case=should_fail_login.json | 66 +++++ ...entials-case=should_fail_registration.json | 66 +++++ ...egistration_id_first_strategy_enabled.json | 66 +++++ ...ategy-method=TestPopulateSignUpMethod.json | 69 +++++ ...dc_credentials-case=should_fail_login.json | 254 ++++++++++++++++++ ...entials-case=should_fail_registration.json | 254 ++++++++++++++++++ ...egistration_id_first_strategy_enabled.json | 254 ++++++++++++++++++ ...rd_credentials-case=should_fail_login.json | 254 ++++++++++++++++++ ...entials-case=should_fail_registration.json | 254 ++++++++++++++++++ ...egistration_id_first_strategy_enabled.json | 254 ++++++++++++++++++ ...dc_credentials-case=should_fail_login.json | 83 ++++++ ...entials-case=should_fail_registration.json | 83 ++++++ ...egistration_id_first_strategy_enabled.json | 83 ++++++ ...rd_credentials-case=should_fail_login.json | 100 +++++++ ...entials-case=should_fail_registration.json | 100 +++++++ ...egistration_id_first_strategy_enabled.json | 100 +++++++ ...State-method=TestPopulateSignUpMethod.json | 202 ++++++++++++++ selfservice/strategy/oidc/pkce.go | 79 ++++++ selfservice/strategy/oidc/pkce_test.go | 90 +++++++ selfservice/strategy/oidc/provider_config.go | 14 + selfservice/strategy/oidc/state.go | 118 ++++++++ selfservice/strategy/oidc/state_test.go | 59 ++++ selfservice/strategy/oidc/strategy.go | 133 ++++----- .../strategy/oidc/strategy_helper_test.go | 16 +- selfservice/strategy/oidc/strategy_login.go | 10 +- .../strategy/oidc/strategy_registration.go | 10 +- .../strategy/oidc/strategy_settings.go | 9 +- .../strategy/oidc/strategy_state_test.go | 24 -- selfservice/strategy/oidc/strategy_test.go | 212 ++++++++++++++- 43 files changed, 3643 insertions(+), 142 deletions(-) create mode 100644 buf.gen.yaml create mode 100644 buf.yaml create mode 100644 gen/oidc/v1/state.pb.go create mode 100644 proto/oidc/v1/state.proto create mode 100644 selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json create mode 100644 selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json create mode 100644 selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json create mode 100644 selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json create mode 100644 selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json create mode 100644 selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json create mode 100644 selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json create mode 100644 selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json create mode 100644 selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json create mode 100644 selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json create mode 100644 selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json create mode 100644 selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json create mode 100644 selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-method=TestPopulateSignUpMethod.json create mode 100644 selfservice/strategy/oidc/pkce.go create mode 100644 selfservice/strategy/oidc/pkce_test.go create mode 100644 selfservice/strategy/oidc/state.go create mode 100644 selfservice/strategy/oidc/state_test.go delete mode 100644 selfservice/strategy/oidc/strategy_state_test.go diff --git a/Makefile b/Makefile index c82068cf3aa6..4924072c8111 100644 --- a/Makefile +++ b/Makefile @@ -57,14 +57,28 @@ docs/swagger: curl https://raw.githubusercontent.com/ory/meta/master/install.sh | bash -s -- -b .bin ory v0.2.2 touch -a -m .bin/ory +.bin/buf: Makefile + curl -sSL \ + "https://github.com/bufbuild/buf/releases/download/v1.39.0/buf-$(shell uname -s)-$(shell uname -m).tar.gz" | \ + tar -xvzf - -C ".bin/" --strip-components=2 buf/bin/buf buf/bin/protoc-gen-buf-breaking buf/bin/protoc-gen-buf-lint + touch -a -m .bin/buf + .PHONY: lint lint: .bin/golangci-lint - golangci-lint run -v --timeout 10m ./... + .bin/golangci-lint run -v --timeout 10m ./... + .bin/buf lint .PHONY: mocks mocks: .bin/mockgen mockgen -mock_names Manager=MockLoginExecutorDependencies -package internal -destination internal/hook_login_executor_dependencies.go github.com/ory/kratos/selfservice loginExecutorDependencies +.PHONY: proto +proto: gen/oidc/v1/state.pb.go + +gen/oidc/v1/state.pb.go: proto/oidc/v1/state.proto buf.yaml buf.gen.yaml .bin/buf .bin/goimports + .bin/buf generate + .bin/goimports -w gen/ + .PHONY: install install: go install -tags sqlite . @@ -162,11 +176,12 @@ authors: # updates the AUTHORS file # Formats the code .PHONY: format -format: .bin/goimports .bin/ory node_modules - .bin/ory dev headers copyright --exclude=internal/httpclient --exclude=internal/client-go --exclude test/e2e/proxy/node_modules --exclude test/e2e/node_modules --exclude node_modules +format: .bin/goimports .bin/ory node_modules .bin/buf + .bin/ory dev headers copyright --exclude=gen --exclude=internal/httpclient --exclude=internal/client-go --exclude test/e2e/proxy/node_modules --exclude test/e2e/node_modules --exclude node_modules goimports -w -local github.com/ory . npm exec -- prettier --write 'test/e2e/**/*{.ts,.js}' npm exec -- prettier --write '.github' + .bin/buf format --write # Build local docker image .PHONY: docker diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 000000000000..bcc94c85856e --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,12 @@ +version: v2 +managed: + enabled: true + override: + - file_option: go_package_prefix + value: github.com/ory/kratos +plugins: + - remote: buf.build/protocolbuffers/go + out: gen + opt: paths=source_relative +inputs: + - directory: proto diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 000000000000..227c4a6c6faf --- /dev/null +++ b/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +modules: + - path: proto +lint: + use: + - DEFAULT +breaking: + use: + - FILE diff --git a/cipher/chacha20.go b/cipher/chacha20.go index 46cf1efc85d9..9c35e4237369 100644 --- a/cipher/chacha20.go +++ b/cipher/chacha20.go @@ -8,6 +8,7 @@ import ( "crypto/rand" "encoding/hex" "io" + "math" "github.com/pkg/errors" "golang.org/x/crypto/chacha20poly1305" @@ -43,6 +44,11 @@ func (c *XChaCha20Poly1305) Encrypt(ctx context.Context, message []byte) (string return "", herodot.ErrInternalServerError.WithWrap(err).WithReason("Unable to generate key") } + // Make sure the size calculation does not overflow. + if len(message) > math.MaxInt-aead.NonceSize()-aead.Overhead() { + return "", errors.WithStack(herodot.ErrInternalServerError.WithReason("plaintext too large")) + } + nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(message)+aead.Overhead()) _, err = io.ReadFull(rand.Reader, nonce) if err != nil { diff --git a/cmd/identities/get_test.go b/cmd/identities/get_test.go index 03a1291d5872..5cbaad0e9cb8 100644 --- a/cmd/identities/get_test.go +++ b/cmd/identities/get_test.go @@ -5,7 +5,6 @@ package identities_test import ( "context" - "encoding/hex" "encoding/json" "testing" @@ -63,10 +62,12 @@ func TestGetCmd(t *testing.T) { return out } transform := func(token string) string { - if !encrypt { - return token + if encrypt { + s, err := reg.Cipher(context.Background()).Encrypt(context.Background(), []byte(token)) + require.NoError(t, err) + return s } - return hex.EncodeToString([]byte(token)) + return token } return identity.Credentials{ Type: identity.CredentialsTypeOIDC, diff --git a/cmd/identities/helpers_test.go b/cmd/identities/helpers_test.go index 5997b32c7623..a6571e813abc 100644 --- a/cmd/identities/helpers_test.go +++ b/cmd/identities/helpers_test.go @@ -21,7 +21,7 @@ import ( "github.com/ory/kratos/internal/testhelpers" ) -func setup(t *testing.T, newCmd func() *cobra.Command) (driver.Registry, *cmdx.CommandExecuter) { +func setup(t *testing.T, newCmd func() *cobra.Command) (*driver.RegistryDefault, *cmdx.CommandExecuter) { conf, reg := internal.NewFastRegistryWithMocks(t) _, admin := testhelpers.NewKratosServerWithCSRF(t, reg) testhelpers.SetDefaultIdentitySchema(conf, "file://./stubs/identity.schema.json") diff --git a/gen/oidc/v1/state.pb.go b/gen/oidc/v1/state.pb.go new file mode 100644 index 000000000000..ce3ab14d52b1 --- /dev/null +++ b/gen/oidc/v1/state.pb.go @@ -0,0 +1,183 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc (unknown) +// source: oidc/v1/state.proto + +package oidcv1 + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type State struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FlowId []byte `protobuf:"bytes,1,opt,name=flow_id,json=flowId,proto3" json:"flow_id,omitempty"` + SessionTokenExchangeCodeSha512 []byte `protobuf:"bytes,2,opt,name=session_token_exchange_code_sha512,json=sessionTokenExchangeCodeSha512,proto3" json:"session_token_exchange_code_sha512,omitempty"` + ProviderId string `protobuf:"bytes,3,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"` + PkceVerifier string `protobuf:"bytes,4,opt,name=pkce_verifier,json=pkceVerifier,proto3" json:"pkce_verifier,omitempty"` +} + +func (x *State) Reset() { + *x = State{} + if protoimpl.UnsafeEnabled { + mi := &file_oidc_v1_state_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *State) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*State) ProtoMessage() {} + +func (x *State) ProtoReflect() protoreflect.Message { + mi := &file_oidc_v1_state_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use State.ProtoReflect.Descriptor instead. +func (*State) Descriptor() ([]byte, []int) { + return file_oidc_v1_state_proto_rawDescGZIP(), []int{0} +} + +func (x *State) GetFlowId() []byte { + if x != nil { + return x.FlowId + } + return nil +} + +func (x *State) GetSessionTokenExchangeCodeSha512() []byte { + if x != nil { + return x.SessionTokenExchangeCodeSha512 + } + return nil +} + +func (x *State) GetProviderId() string { + if x != nil { + return x.ProviderId + } + return "" +} + +func (x *State) GetPkceVerifier() string { + if x != nil { + return x.PkceVerifier + } + return "" +} + +var File_oidc_v1_state_proto protoreflect.FileDescriptor + +var file_oidc_v1_state_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x6f, 0x69, 0x64, 0x63, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x6f, 0x69, 0x64, 0x63, 0x2e, 0x76, 0x31, 0x22, 0xb2, + 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x6c, 0x6f, 0x77, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x66, 0x6c, 0x6f, 0x77, 0x49, + 0x64, 0x12, 0x4a, 0x0a, 0x22, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x5f, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, + 0x5f, 0x73, 0x68, 0x61, 0x35, 0x31, 0x32, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x1e, 0x73, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x78, 0x63, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x53, 0x68, 0x61, 0x35, 0x31, 0x32, 0x12, 0x1f, 0x0a, + 0x0b, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, + 0x0a, 0x0d, 0x70, 0x6b, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x6b, 0x63, 0x65, 0x56, 0x65, 0x72, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x42, 0x7c, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x2e, + 0x76, 0x31, 0x42, 0x0a, 0x53, 0x74, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, + 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x72, 0x79, + 0x2f, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x2f, 0x76, 0x31, 0x3b, + 0x6f, 0x69, 0x64, 0x63, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x4f, + 0x69, 0x64, 0x63, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x07, 0x4f, 0x69, 0x64, 0x63, 0x5c, 0x56, 0x31, + 0xe2, 0x02, 0x13, 0x4f, 0x69, 0x64, 0x63, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x08, 0x4f, 0x69, 0x64, 0x63, 0x3a, 0x3a, 0x56, + 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_oidc_v1_state_proto_rawDescOnce sync.Once + file_oidc_v1_state_proto_rawDescData = file_oidc_v1_state_proto_rawDesc +) + +func file_oidc_v1_state_proto_rawDescGZIP() []byte { + file_oidc_v1_state_proto_rawDescOnce.Do(func() { + file_oidc_v1_state_proto_rawDescData = protoimpl.X.CompressGZIP(file_oidc_v1_state_proto_rawDescData) + }) + return file_oidc_v1_state_proto_rawDescData +} + +var file_oidc_v1_state_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_oidc_v1_state_proto_goTypes = []any{ + (*State)(nil), // 0: oidc.v1.State +} +var file_oidc_v1_state_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_oidc_v1_state_proto_init() } +func file_oidc_v1_state_proto_init() { + if File_oidc_v1_state_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_oidc_v1_state_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*State); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_oidc_v1_state_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_oidc_v1_state_proto_goTypes, + DependencyIndexes: file_oidc_v1_state_proto_depIdxs, + MessageInfos: file_oidc_v1_state_proto_msgTypes, + }.Build() + File_oidc_v1_state_proto = out.File + file_oidc_v1_state_proto_rawDesc = nil + file_oidc_v1_state_proto_goTypes = nil + file_oidc_v1_state_proto_depIdxs = nil +} diff --git a/go.mod b/go.mod index f37e321b2d76..82261d0d6687 100644 --- a/go.mod +++ b/go.mod @@ -315,7 +315,7 @@ require ( golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/protobuf v1.34.1 // indirect + google.golang.org/protobuf v1.34.2 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect diff --git a/go.sum b/go.sum index be40836fdc95..3ee607215770 100644 --- a/go.sum +++ b/go.sum @@ -1315,8 +1315,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= diff --git a/internal/driver.go b/internal/driver.go index 521be82264d1..0e7e514ce5e5 100644 --- a/internal/driver.go +++ b/internal/driver.go @@ -12,6 +12,7 @@ import ( confighelpers "github.com/ory/kratos/driver/config/testhelpers" "github.com/ory/x/contextx" + "github.com/ory/x/randx" "github.com/sirupsen/logrus" @@ -86,10 +87,14 @@ func NewFastRegistryWithMocks(t *testing.T, opts ...configx.OptionModifier) (*co // NewRegistryDefaultWithDSN returns a more standard registry without mocks. Good for e2e and advanced integration testing! func NewRegistryDefaultWithDSN(t testing.TB, dsn string, opts ...configx.OptionModifier) (*config.Config, *driver.RegistryDefault) { ctx := context.Background() - c := NewConfigurationWithDefaults(t, append(opts, configx.WithValues(map[string]interface{}{ - config.ViperKeyDSN: stringsx.Coalesce(dsn, dbal.NewSQLiteTestDatabase(t)+"&lock=false&max_conns=1"), - "dev": true, - }))...) + c := NewConfigurationWithDefaults(t, append([]configx.OptionModifier{configx.WithValues(map[string]interface{}{ + config.ViperKeyDSN: stringsx.Coalesce(dsn, dbal.NewSQLiteTestDatabase(t)+"&lock=false&max_conns=1"), + "dev": true, + config.ViperKeySecretsCipher: []string{randx.MustString(32, randx.AlphaNum)}, + config.ViperKeySecretsCookie: []string{randx.MustString(32, randx.AlphaNum)}, + config.ViperKeySecretsDefault: []string{randx.MustString(32, randx.AlphaNum)}, + config.ViperKeyCipherAlgorithm: "xchacha20-poly1305", + })}, opts...)...) reg, err := driver.NewRegistryFromDSN(ctx, c, logrusx.New("", "", logrusx.ForceLevel(logrus.ErrorLevel))) require.NoError(t, err) pool := jsonnetsecure.NewProcessPool(runtime.GOMAXPROCS(0)) diff --git a/proto/oidc/v1/state.proto b/proto/oidc/v1/state.proto new file mode 100644 index 000000000000..255f7f118e05 --- /dev/null +++ b/proto/oidc/v1/state.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package oidc.v1; + +message State { + bytes flow_id = 1; + bytes session_token_exchange_code_sha512 = 2; + string provider_id = 3; + string pkce_verifier = 4; +} diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json index 8b4f5fad2b43..cfcda57ec4e1 100644 --- a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json @@ -5,6 +5,28 @@ "ui": { "method": "POST", "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, { "type": "input", "group": "oidc", @@ -27,6 +49,28 @@ } } }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, { "type": "input", "group": "oidc", @@ -49,6 +93,28 @@ } } }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, { "type": "input", "group": "oidc", diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json index 8b4f5fad2b43..cfcda57ec4e1 100644 --- a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json @@ -5,6 +5,28 @@ "ui": { "method": "POST", "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, { "type": "input", "group": "oidc", @@ -27,6 +49,28 @@ } } }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, { "type": "input", "group": "oidc", @@ -49,6 +93,28 @@ } } }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, { "type": "input", "group": "oidc", diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json index 8b4f5fad2b43..cfcda57ec4e1 100644 --- a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json @@ -5,6 +5,28 @@ "ui": { "method": "POST", "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, { "type": "input", "group": "oidc", @@ -27,6 +49,28 @@ } } }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, { "type": "input", "group": "oidc", @@ -49,6 +93,28 @@ } } }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, { "type": "input", "group": "oidc", diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json index 112edcf3999b..5fbb69e1fcc6 100644 --- a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json @@ -5,6 +5,28 @@ "ui": { "method": "POST", "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, { "type": "input", "group": "oidc", @@ -27,6 +49,28 @@ } } }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, { "type": "input", "group": "oidc", @@ -49,6 +93,28 @@ } } }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, { "type": "input", "group": "oidc", diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json index 112edcf3999b..5fbb69e1fcc6 100644 --- a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json @@ -5,6 +5,28 @@ "ui": { "method": "POST", "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, { "type": "input", "group": "oidc", @@ -27,6 +49,28 @@ } } }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, { "type": "input", "group": "oidc", @@ -49,6 +93,28 @@ } } }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, { "type": "input", "group": "oidc", diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json index 112edcf3999b..5fbb69e1fcc6 100644 --- a/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json @@ -5,6 +5,28 @@ "ui": { "method": "POST", "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, { "type": "input", "group": "oidc", @@ -27,6 +49,28 @@ } } }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, { "type": "input", "group": "oidc", @@ -49,6 +93,28 @@ } } }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, { "type": "input", "group": "oidc", diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateSignUpMethod.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateSignUpMethod.json index 4177f37350e2..2177c514d3fd 100644 --- a/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateSignUpMethod.json +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateSignUpMethod.json @@ -106,6 +106,75 @@ } } }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE", + "provider_id": "neverPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE", + "provider_id": "autoPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE", + "provider_id": "forcePKCE" + } + } + } + }, { "type": "input", "group": "oidc", diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json new file mode 100644 index 000000000000..cfcda57ec4e1 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json @@ -0,0 +1,254 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "claimsViaUserInfo", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with claimsViaUserInfo", + "type": "info", + "context": { + "provider": "claimsViaUserInfo" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "invalid-issuer", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with invalid-issuer", + "type": "info", + "context": { + "provider": "invalid-issuer" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "valid2", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with valid2", + "type": "info", + "context": { + "provider": "valid2" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": [], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "duplicate_identifier": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json new file mode 100644 index 000000000000..cfcda57ec4e1 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json @@ -0,0 +1,254 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "claimsViaUserInfo", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with claimsViaUserInfo", + "type": "info", + "context": { + "provider": "claimsViaUserInfo" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "invalid-issuer", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with invalid-issuer", + "type": "info", + "context": { + "provider": "invalid-issuer" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "valid2", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with valid2", + "type": "info", + "context": { + "provider": "valid2" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": [], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "duplicate_identifier": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json new file mode 100644 index 000000000000..cfcda57ec4e1 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json @@ -0,0 +1,254 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "claimsViaUserInfo", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with claimsViaUserInfo", + "type": "info", + "context": { + "provider": "claimsViaUserInfo" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "invalid-issuer", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with invalid-issuer", + "type": "info", + "context": { + "provider": "invalid-issuer" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "valid2", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with valid2", + "type": "info", + "context": { + "provider": "valid2" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": [], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "duplicate_identifier": "email-exist-with-oidc-strategy-lh-false@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json new file mode 100644 index 000000000000..5fbb69e1fcc6 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json @@ -0,0 +1,254 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "claimsViaUserInfo", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with claimsViaUserInfo", + "type": "info", + "context": { + "provider": "claimsViaUserInfo" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "invalid-issuer", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with invalid-issuer", + "type": "info", + "context": { + "provider": "invalid-issuer" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "valid2", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with valid2", + "type": "info", + "context": { + "provider": "valid2" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-password-strategy-lh-false@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-password-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": [], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-password-strategy-lh-false@ory.sh", + "duplicate_identifier": "email-exist-with-password-strategy-lh-false@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json new file mode 100644 index 000000000000..5fbb69e1fcc6 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json @@ -0,0 +1,254 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "claimsViaUserInfo", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with claimsViaUserInfo", + "type": "info", + "context": { + "provider": "claimsViaUserInfo" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "invalid-issuer", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with invalid-issuer", + "type": "info", + "context": { + "provider": "invalid-issuer" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "valid2", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with valid2", + "type": "info", + "context": { + "provider": "valid2" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-password-strategy-lh-false@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-password-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": [], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-password-strategy-lh-false@ory.sh", + "duplicate_identifier": "email-exist-with-password-strategy-lh-false@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json new file mode 100644 index 000000000000..5fbb69e1fcc6 --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=false-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json @@ -0,0 +1,254 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "claimsViaUserInfo", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with claimsViaUserInfo", + "type": "info", + "context": { + "provider": "claimsViaUserInfo" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "invalid-issuer", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with invalid-issuer", + "type": "info", + "context": { + "provider": "invalid-issuer" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "valid2", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with valid2", + "type": "info", + "context": { + "provider": "valid2" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-password-strategy-lh-false@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-password-strategy-lh-false@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-false@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": [], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-password-strategy-lh-false@ory.sh", + "duplicate_identifier": "email-exist-with-password-strategy-lh-false@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json new file mode 100644 index 000000000000..77bef5d097ae --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_login.json @@ -0,0 +1,83 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": ["oidc"], + "available_providers": ["secondProvider"], + "duplicateIdentifier": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "duplicate_identifier": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json new file mode 100644 index 000000000000..77bef5d097ae --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration.json @@ -0,0 +1,83 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": ["oidc"], + "available_providers": ["secondProvider"], + "duplicateIdentifier": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "duplicate_identifier": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json new file mode 100644 index 000000000000..77bef5d097ae --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_oidc_credentials-case=should_fail_registration_id_first_strategy_enabled.json @@ -0,0 +1,83 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010018, + "text": "Confirm with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-oidc-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-oidc-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": ["oidc"], + "available_providers": ["secondProvider"], + "duplicateIdentifier": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "duplicate_identifier": "email-exist-with-oidc-strategy-lh-true@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json new file mode 100644 index 000000000000..93317c6e479a --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_login.json @@ -0,0 +1,100 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-password-strategy-lh-true@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-password-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": ["password"], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-password-strategy-lh-true@ory.sh", + "duplicate_identifier": "email-exist-with-password-strategy-lh-true@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json new file mode 100644 index 000000000000..93317c6e479a --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration.json @@ -0,0 +1,100 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-password-strategy-lh-true@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-password-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": ["password"], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-password-strategy-lh-true@ory.sh", + "duplicate_identifier": "email-exist-with-password-strategy-lh-true@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json new file mode 100644 index 000000000000..93317c6e479a --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-login-hints-enabled=true-case=should_fail_to_register_and_return_fresh_login_flow_if_email_is_already_being_used_by_password_credentials-case=should_fail_registration_id_first_strategy_enabled.json @@ -0,0 +1,100 @@ +{ + "organization_id": null, + "type": "browser", + "active": "oidc", + "ui": { + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "default", + "attributes": { + "name": "identifier", + "type": "hidden", + "value": "email-exist-with-password-strategy-lh-true@ory.sh", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070004, + "text": "ID", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "password", + "type": "password", + "required": true, + "autocomplete": "current-password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1070001, + "text": "Password", + "type": "info" + } + } + }, + { + "type": "input", + "group": "password", + "attributes": { + "name": "method", + "type": "submit", + "value": "password", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1010022, + "text": "Sign in with password", + "type": "info" + } + } + } + ], + "messages": [ + { + "id": 1010016, + "text": "You tried to sign in with \"email-exist-with-password-strategy-lh-true@ory.sh\", but that email is already used by another account. Sign in to your account with one of the options below to add your account \"email-exist-with-password-strategy-lh-true@ory.sh\" at \"generic\" as another way to sign in.", + "type": "info", + "context": { + "available_credential_types": ["password"], + "available_providers": [], + "duplicateIdentifier": "email-exist-with-password-strategy-lh-true@ory.sh", + "duplicate_identifier": "email-exist-with-password-strategy-lh-true@ory.sh", + "provider": "generic" + } + } + ] + }, + "refresh": false, + "requested_aal": "aal1", + "state": "choose_method" +} + diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-method=TestPopulateSignUpMethod.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-method=TestPopulateSignUpMethod.json new file mode 100644 index 000000000000..2177c514d3fd --- /dev/null +++ b/selfservice/strategy/oidc/.snapshots/TestStrategy-newStyleState-method=TestPopulateSignUpMethod.json @@ -0,0 +1,202 @@ +{ + "method": "POST", + "nodes": [ + { + "type": "input", + "group": "default", + "attributes": { + "name": "csrf_token", + "type": "hidden", + "required": true, + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": {} + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "valid", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with valid", + "type": "info", + "context": { + "provider": "valid", + "provider_id": "valid" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "valid2", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with valid2", + "type": "info", + "context": { + "provider": "valid2", + "provider_id": "valid2" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "secondProvider", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with secondProvider", + "type": "info", + "context": { + "provider": "secondProvider", + "provider_id": "secondProvider" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "claimsViaUserInfo", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with claimsViaUserInfo", + "type": "info", + "context": { + "provider": "claimsViaUserInfo", + "provider_id": "claimsViaUserInfo" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "neverPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with neverPKCE", + "type": "info", + "context": { + "provider": "neverPKCE", + "provider_id": "neverPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "autoPKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with autoPKCE", + "type": "info", + "context": { + "provider": "autoPKCE", + "provider_id": "autoPKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "forcePKCE", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with forcePKCE", + "type": "info", + "context": { + "provider": "forcePKCE", + "provider_id": "forcePKCE" + } + } + } + }, + { + "type": "input", + "group": "oidc", + "attributes": { + "name": "provider", + "type": "submit", + "value": "invalid-issuer", + "disabled": false, + "node_type": "input" + }, + "messages": [], + "meta": { + "label": { + "id": 1040002, + "text": "Sign up with invalid-issuer", + "type": "info", + "context": { + "provider": "invalid-issuer", + "provider_id": "invalid-issuer" + } + } + } + } + ] +} diff --git a/selfservice/strategy/oidc/pkce.go b/selfservice/strategy/oidc/pkce.go new file mode 100644 index 000000000000..2b397c8702b7 --- /dev/null +++ b/selfservice/strategy/oidc/pkce.go @@ -0,0 +1,79 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "slices" + + gooidc "github.com/coreos/go-oidc/v3/oidc" + "github.com/pkg/errors" + "golang.org/x/oauth2" + + oidcv1 "github.com/ory/kratos/gen/oidc/v1" + "github.com/ory/kratos/x" +) + +type pkceDependencies interface { + x.LoggingProvider + x.HTTPClientProvider +} + +func PKCEChallenge(s *oidcv1.State) []oauth2.AuthCodeOption { + if s.GetPkceVerifier() == "" { + return nil + } + return []oauth2.AuthCodeOption{oauth2.S256ChallengeOption(s.GetPkceVerifier())} +} + +func PKCEVerifier(s *oidcv1.State) []oauth2.AuthCodeOption { + if s.GetPkceVerifier() == "" { + return nil + } + return []oauth2.AuthCodeOption{oauth2.VerifierOption(s.GetPkceVerifier())} +} + +func maybePKCE(ctx context.Context, d pkceDependencies, _p Provider) (verifier string) { + if _p.Config().PKCE == "never" { + return "" + } + + p, ok := _p.(OAuth2Provider) + if !ok { + return "" + } + + if p.Config().PKCE != "force" { + // autodiscover PKCE support + pkceSupported, err := discoverPKCE(ctx, d, p) + if err != nil { + d.Logger().WithError(err).Warnf("Failed to autodiscover PKCE support for provider %q. Continuing without PKCE.", p.Config().ID) + return "" + } + if !pkceSupported { + d.Logger().Infof("Provider %q does not advertise support for PKCE. Continuing without PKCE.", p.Config().ID) + return "" + } + } + return oauth2.GenerateVerifier() +} + +func discoverPKCE(ctx context.Context, d pkceDependencies, p OAuth2Provider) (pkceSupported bool, err error) { + if p.Config().IssuerURL == "" { + return false, errors.New("Issuer URL must be set to autodiscover PKCE support") + } + + ctx = gooidc.ClientContext(ctx, d.HTTPClient(ctx).HTTPClient) + gp, err := gooidc.NewProvider(ctx, p.Config().IssuerURL) + if err != nil { + return false, errors.Wrap(err, "failed to initialize provider") + } + var claims struct { + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"` + } + if err := gp.Claims(&claims); err != nil { + return false, errors.Wrap(err, "failed to deserialize provider claims") + } + return slices.Contains(claims.CodeChallengeMethodsSupported, "S256"), nil +} diff --git a/selfservice/strategy/oidc/pkce_test.go b/selfservice/strategy/oidc/pkce_test.go new file mode 100644 index 000000000000..7b42dfd2a8eb --- /dev/null +++ b/selfservice/strategy/oidc/pkce_test.go @@ -0,0 +1,90 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/x" +) + +func TestPKCESupport(t *testing.T) { + supported := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, `{"issuer": "http://%s", "code_challenge_methods_supported":["S256"]}`, r.Host) + })) + t.Cleanup(supported.Close) + notSupported := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, `{"issuer": "http://%s", "code_challenge_methods_supported": ["plain"]}`, r.Host) + })) + t.Cleanup(notSupported.Close) + + conf, reg := internal.NewFastRegistryWithMocks(t) + _ = conf + strat := oidc.NewStrategy(reg) + oidc.TestHookEnableNewStyleState(t) + + for _, tc := range []struct { + c *oidc.Configuration + pkce bool + }{ + {c: &oidc.Configuration{IssuerURL: supported.URL, PKCE: "force"}, pkce: true}, + {c: &oidc.Configuration{IssuerURL: supported.URL, PKCE: "never"}, pkce: false}, + {c: &oidc.Configuration{IssuerURL: supported.URL, PKCE: "auto"}, pkce: true}, + {c: &oidc.Configuration{IssuerURL: supported.URL, PKCE: ""}, pkce: true}, // same as auto + + {c: &oidc.Configuration{IssuerURL: notSupported.URL, PKCE: "force"}, pkce: true}, + {c: &oidc.Configuration{IssuerURL: notSupported.URL, PKCE: "never"}, pkce: false}, + {c: &oidc.Configuration{IssuerURL: notSupported.URL, PKCE: "auto"}, pkce: false}, + {c: &oidc.Configuration{IssuerURL: notSupported.URL, PKCE: ""}, pkce: false}, // same as auto + + {c: &oidc.Configuration{IssuerURL: "", PKCE: "force"}, pkce: true}, + {c: &oidc.Configuration{IssuerURL: "", PKCE: "never"}, pkce: false}, + {c: &oidc.Configuration{IssuerURL: "", PKCE: "auto"}, pkce: false}, + {c: &oidc.Configuration{IssuerURL: "", PKCE: ""}, pkce: false}, // same as auto + + } { + provider := oidc.NewProviderGenericOIDC(tc.c, reg) + + stateParam, pkce, err := strat.GenerateState(context.Background(), provider, x.NewUUID()) + require.NoError(t, err) + require.NotEmpty(t, stateParam) + + state, err := oidc.DecryptState(context.Background(), reg.Cipher(context.Background()), stateParam) + require.NoError(t, err) + + if tc.pkce { + require.NotEmpty(t, pkce) + require.NotEmpty(t, oidc.PKCEVerifier(state)) + } else { + require.Empty(t, pkce) + require.Empty(t, oidc.PKCEVerifier(state)) + } + } + + t.Run("OAuth1", func(t *testing.T) { + for _, provider := range []oidc.Provider{ + oidc.NewProviderX(&oidc.Configuration{IssuerURL: supported.URL, PKCE: "force"}, reg), + oidc.NewProviderX(&oidc.Configuration{IssuerURL: supported.URL, PKCE: "never"}, reg), + oidc.NewProviderX(&oidc.Configuration{IssuerURL: supported.URL, PKCE: "auto"}, reg), + } { + stateParam, pkce, err := strat.GenerateState(context.Background(), provider, x.NewUUID()) + require.NoError(t, err) + require.NotEmpty(t, stateParam) + assert.Empty(t, pkce) + + state, err := oidc.DecryptState(context.Background(), reg.Cipher(context.Background()), stateParam) + require.NoError(t, err) + assert.Empty(t, oidc.PKCEVerifier(state)) + } + }) +} diff --git a/selfservice/strategy/oidc/provider_config.go b/selfservice/strategy/oidc/provider_config.go index f3db2e120e01..7e2b0b19dbfb 100644 --- a/selfservice/strategy/oidc/provider_config.go +++ b/selfservice/strategy/oidc/provider_config.go @@ -119,9 +119,23 @@ type Configuration struct { // 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 /self-service/methods/oidc/callback/, you must use /self-service/methods/oidc/callback + // (Note the missing path segment and no trailing slash). + PKCE string `json:"pkce"` } 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) diff --git a/selfservice/strategy/oidc/state.go b/selfservice/strategy/oidc/state.go new file mode 100644 index 000000000000..02ccdf59f0bf --- /dev/null +++ b/selfservice/strategy/oidc/state.go @@ -0,0 +1,118 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "bytes" + "context" + "crypto/sha512" + "crypto/subtle" + "encoding/base64" + "fmt" + "testing" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "golang.org/x/oauth2" + "google.golang.org/protobuf/proto" + + "github.com/ory/herodot" + "github.com/ory/kratos/cipher" + oidcv1 "github.com/ory/kratos/gen/oidc/v1" + "github.com/ory/kratos/x" +) + +func EncryptState(ctx context.Context, c cipher.Cipher, state *oidcv1.State) (ciphertext string, err error) { + m, err := proto.Marshal(state) + if err != nil { + return "", herodot.ErrInternalServerError.WithReasonf("Unable to marshal state: %s", err) + } + return c.Encrypt(ctx, m) +} + +func DecryptState(ctx context.Context, c cipher.Cipher, ciphertext string) (*oidcv1.State, error) { + plaintext, err := c.Decrypt(ctx, ciphertext) + if err != nil { + return nil, herodot.ErrBadRequest.WithReasonf("Unable to decrypt state: %s", err) + } + var state oidcv1.State + if err := proto.Unmarshal(plaintext, &state); err != nil { + return nil, herodot.ErrBadRequest.WithReasonf("Unable to unmarshal state: %s", err) + } + return &state, nil +} + +func legacyString(s *oidcv1.State) string { + flowID := uuid.FromBytesOrNil(s.GetFlowId()) + code := s.GetSessionTokenExchangeCodeSha512() + return base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", flowID.String(), code))) +} + +var newStyleState = false + +func TestHookEnableNewStyleState(t *testing.T) { + prev := newStyleState + newStyleState = true + t.Cleanup(func() { + newStyleState = prev + }) +} + +func TestHookNewStyleStateEnabled(*testing.T) bool { + return newStyleState +} + +func (s *Strategy) GenerateState(ctx context.Context, p Provider, flowID uuid.UUID) (stateParam string, pkce []oauth2.AuthCodeOption, err error) { + state := oidcv1.State{ + FlowId: flowID.Bytes(), + SessionTokenExchangeCodeSha512: x.NewUUID().Bytes(), + ProviderId: p.Config().ID, + PkceVerifier: maybePKCE(ctx, s.d, p), + } + if code, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(ctx, flowID); hasCode { + sum := sha512.Sum512([]byte(code.InitCode)) + state.SessionTokenExchangeCodeSha512 = sum[:] + } + + // TODO: compatibility: remove later + if !newStyleState { + state.PkceVerifier = "" + return legacyString(&state), nil, nil // compat: disable later + } + // END TODO + + param, err := EncryptState(ctx, s.d.Cipher(ctx), &state) + if err != nil { + return "", nil, herodot.ErrInternalServerError.WithReason("Unable to encrypt state").WithWrap(err) + } + return param, PKCEChallenge(&state), nil +} + +func codeMatches(s *oidcv1.State, code string) bool { + sum := sha512.Sum512([]byte(code)) + return subtle.ConstantTimeCompare(s.GetSessionTokenExchangeCodeSha512(), sum[:]) == 1 +} + +func ParseStateCompatiblity(ctx context.Context, c cipher.Cipher, s string) (*oidcv1.State, error) { + // new-style: encrypted + state, err := DecryptState(ctx, c, s) + if err == nil { + return state, nil + } + // old-style: unencrypted + raw, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + return nil, err + } + if id, data, ok := bytes.Cut(raw, []byte(":")); !ok { + return nil, errors.New("state has invalid format (1)") + } else if flowID, err := uuid.FromString(string(id)); err != nil { + return nil, errors.New("state has invalid format (2)") + } else { + return &oidcv1.State{ + FlowId: flowID.Bytes(), + SessionTokenExchangeCodeSha512: data, + }, nil + } +} diff --git a/selfservice/strategy/oidc/state_test.go b/selfservice/strategy/oidc/state_test.go new file mode 100644 index 000000000000..2d21eaa4aef9 --- /dev/null +++ b/selfservice/strategy/oidc/state_test.go @@ -0,0 +1,59 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/cipher" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/x" +) + +func TestGenerateState(t *testing.T) { + conf, reg := internal.NewFastRegistryWithMocks(t) + _ = conf + strat := oidc.NewStrategy(reg) + ctx := context.Background() + ciph := reg.Cipher(ctx) + _, ok := ciph.(*cipher.Noop) + require.False(t, ok) + + var expectProvider string + assertions := func(t *testing.T) { + flowID := x.NewUUID() + + stateParam, pkce, err := strat.GenerateState(ctx, &testProvider{}, flowID) + require.NoError(t, err) + require.NotEmpty(t, stateParam) + assert.Empty(t, pkce) + + state, err := oidc.ParseStateCompatiblity(ctx, ciph, stateParam) + require.NoError(t, err) + assert.Equal(t, flowID.Bytes(), state.FlowId) + assert.Empty(t, oidc.PKCEVerifier(state)) + assert.Equal(t, expectProvider, state.ProviderId) + } + + t.Run("case=old-style", func(t *testing.T) { + expectProvider = "" + assertions(t) + }) + t.Run("case=new-style", func(t *testing.T) { + oidc.TestHookEnableNewStyleState(t) + expectProvider = "test-provider" + assertions(t) + }) +} + +type testProvider struct{} + +func (t *testProvider) Config() *oidc.Configuration { + return &oidc.Configuration{ID: "test-provider", PKCE: "never"} +} diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index b2cc02bbe786..fdf849bf1db0 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -6,10 +6,7 @@ package oidc import ( "bytes" "context" - "crypto/sha512" - "encoding/base64" "encoding/json" - "fmt" "net/http" "net/url" "path/filepath" @@ -27,6 +24,7 @@ import ( "golang.org/x/oauth2" "github.com/ory/kratos/cipher" + oidcv1 "github.com/ory/kratos/gen/oidc/v1" "github.com/ory/kratos/selfservice/sessiontokenexchange" "github.com/ory/x/jsonnetsecure" "github.com/ory/x/otelx" @@ -142,42 +140,6 @@ type AuthCodeContainer struct { TransientPayload json.RawMessage `json:"transient_payload"` } -type State struct { - FlowID string - Data []byte -} - -func (s *State) String() string { - return base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", s.FlowID, s.Data))) -} - -func generateState(flowID string) *State { - return &State{ - FlowID: flowID, - Data: x.NewUUID().Bytes(), - } -} - -func (s *State) setCode(code string) { - s.Data = sha512.New().Sum([]byte(code)) -} - -func (s *State) codeMatches(code string) bool { - return bytes.Equal(s.Data, sha512.New().Sum([]byte(code))) -} - -func parseState(s string) (*State, error) { - raw, err := base64.RawURLEncoding.DecodeString(s) - if err != nil { - return nil, err - } - if id, data, ok := bytes.Cut(raw, []byte(":")); !ok { - return nil, errors.New("state has invalid format") - } else { - return &State{FlowID: string(id), Data: data}, nil - } -} - func (s *Strategy) CountActiveFirstFactorCredentials(_ context.Context, cc map[identity.CredentialsType]identity.Credentials) (count int, err error) { for _, c := range cc { if c.Type == s.ID() && gjson.ValidBytes(c.Config) { @@ -212,6 +174,9 @@ func (s *Strategy) setRoutes(r *x.RouterPublic) { if handle, _, _ := r.Lookup("GET", RouteCallback); handle == nil { r.GET(RouteCallback, wrappedHandleCallback) } + if handle, _, _ := r.Lookup("GET", RouteCallbackGeneric); handle == nil { + r.GET(RouteCallbackGeneric, wrappedHandleCallback) + } // Apple can use the POST request method when calling the callback if handle, _, _ := r.Lookup("POST", RouteCallback); handle == nil { @@ -293,7 +258,7 @@ func (s *Strategy) validateFlow(ctx context.Context, r *http.Request, rid uuid.U return ar, err // this must return the error } -func (s *Strategy) ValidateCallback(w http.ResponseWriter, r *http.Request) (flow.Flow, *AuthCodeContainer, error) { +func (s *Strategy) ValidateCallback(w http.ResponseWriter, r *http.Request, ps httprouter.Params) (flow.Flow, *oidcv1.State, *AuthCodeContainer, error) { var ( codeParam = stringsx.Coalesce(r.URL.Query().Get("code"), r.URL.Query().Get("authCode")) stateParam = r.URL.Query().Get("state") @@ -301,21 +266,36 @@ func (s *Strategy) ValidateCallback(w http.ResponseWriter, r *http.Request) (flo ) if stateParam == "" { - return nil, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the OpenID Provider did not return the state query parameter.`)) + return nil, nil, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the OpenID Provider did not return the state query parameter.`)) } - state, err := parseState(stateParam) + state, err := ParseStateCompatiblity(r.Context(), s.d.Cipher(r.Context()), stateParam) if err != nil { - return nil, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the state parameter was invalid.`)) + return nil, nil, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the state parameter is invalid.`)) + } + + if providerFromURL := ps.ByName("provider"); providerFromURL != "" { + // We're serving an OIDC callback URL with provider in the URL. + if state.ProviderId == "" { + // provider in URL, but not in state: compatiblity mode, remove this fallback later + state.ProviderId = providerFromURL + } else if state.ProviderId != providerFromURL { + // provider in state, but URL with different provider -> something's fishy + return nil, nil, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow: provider mismatch between internal state and URL.`)) + } + } + if state.ProviderId == "" { + // weird: provider neither in the state nor in the URL + return nil, nil, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow: provider could not be retrieved from state nor URL.`)) } - f, err := s.validateFlow(r.Context(), r, x.ParseUUID(state.FlowID)) + f, err := s.validateFlow(r.Context(), r, uuid.FromBytesOrNil(state.FlowId)) if err != nil { - return nil, nil, err + return nil, state, nil, err } tokenCode, hasSessionTokenCode, err := s.d.SessionTokenExchangePersister().CodeForFlow(r.Context(), f.GetID()) if err != nil { - return nil, nil, err + return nil, state, nil, err } cntnr := AuthCodeContainer{} @@ -324,29 +304,29 @@ func (s *Strategy) ValidateCallback(w http.ResponseWriter, r *http.Request) (flo continuity.WithPayload(&cntnr), continuity.WithExpireInsteadOfDelete(time.Minute), ); err != nil { - return nil, nil, err + return nil, state, nil, err } if stateParam != cntnr.State { - return nil, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the query state parameter does not match the state parameter from the session cookie.`)) + return nil, state, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the query state parameter does not match the state parameter from the session cookie.`)) } } else { // We need to validate the tokenCode here - if !state.codeMatches(tokenCode.InitCode) { - return nil, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the query state parameter does not match the state parameter from the code.`)) + if !codeMatches(state, tokenCode.InitCode) { + return nil, state, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the query state parameter does not match the state parameter from the code.`)) } cntnr.State = stateParam - cntnr.FlowID = state.FlowID + cntnr.FlowID = uuid.FromBytesOrNil(state.FlowId).String() } if errorParam != "" { - return f, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the OpenID Provider returned error "%s": %s`, r.URL.Query().Get("error"), r.URL.Query().Get("error_description"))) + return f, state, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the OpenID Provider returned error "%s": %s`, r.URL.Query().Get("error"), r.URL.Query().Get("error_description"))) } if codeParam == "" { - return f, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the OpenID Provider did not return the code query parameter.`)) + return f, state, &cntnr, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the OpenID Provider did not return the code query parameter.`)) } - return f, &cntnr, nil + return f, state, &cntnr, nil } func registrationOrLoginFlowID(flow any) (uuid.UUID, bool) { @@ -393,7 +373,6 @@ func (s *Strategy) alreadyAuthenticated(w http.ResponseWriter, r *http.Request, func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { var ( code = stringsx.Coalesce(r.URL.Query().Get("code"), r.URL.Query().Get("authCode")) - pid = ps.ByName("provider") err error ) @@ -402,25 +381,25 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt defer otelx.End(span, &err) r = r.WithContext(ctx) - req, cntnr, err := s.ValidateCallback(w, r) + req, state, cntnr, err := s.ValidateCallback(w, r, ps) if err != nil { if req != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + s.forwardError(w, r, req, s.handleError(w, r, req, state.ProviderId, nil, err)) } else { - s.d.SelfServiceErrorManager().Forward(ctx, w, r, s.handleError(w, r, nil, pid, nil, err)) + s.d.SelfServiceErrorManager().Forward(ctx, w, r, s.handleError(w, r, nil, "", nil, err)) } return } if authenticated, err := s.alreadyAuthenticated(w, r, req); err != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + s.forwardError(w, r, req, s.handleError(w, r, req, state.ProviderId, nil, err)) } else if authenticated { return } - provider, err := s.provider(r.Context(), pid) + provider, err := s.provider(r.Context(), state.ProviderId) if err != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + s.forwardError(w, r, req, s.handleError(w, r, req, state.ProviderId, nil, err)) return } @@ -428,39 +407,39 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt var et *identity.CredentialsOIDCEncryptedTokens switch p := provider.(type) { case OAuth2Provider: - token, err := s.ExchangeCode(r.Context(), provider, code) + token, err := s.ExchangeCode(r.Context(), provider, code, PKCEVerifier(state)) if err != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + s.forwardError(w, r, req, s.handleError(w, r, req, state.ProviderId, nil, err)) return } et, err = s.encryptOAuth2Tokens(r.Context(), token) if err != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + s.forwardError(w, r, req, s.handleError(w, r, req, state.ProviderId, nil, err)) return } claims, err = p.Claims(r.Context(), token, r.URL.Query()) if err != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + s.forwardError(w, r, req, s.handleError(w, r, req, state.ProviderId, nil, err)) return } case OAuth1Provider: token, err := p.ExchangeToken(r.Context(), r) if err != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + s.forwardError(w, r, req, s.handleError(w, r, req, state.ProviderId, nil, err)) return } claims, err = p.Claims(r.Context(), token) if err != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + s.forwardError(w, r, req, s.handleError(w, r, req, state.ProviderId, nil, err)) return } } if err = claims.Validate(); err != nil { - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, err)) + s.forwardError(w, r, req, s.handleError(w, r, req, state.ProviderId, nil, err)) return } @@ -497,22 +476,22 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt a.TransientPayload = cntnr.TransientPayload sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r) if err != nil { - s.forwardError(w, r, a, s.handleError(w, r, a, pid, nil, err)) + s.forwardError(w, r, a, s.handleError(w, r, a, state.ProviderId, nil, err)) return } if err := s.linkProvider(w, r, &settings.UpdateContext{Session: sess, Flow: a}, et, claims, provider); err != nil { - s.forwardError(w, r, a, s.handleError(w, r, a, pid, nil, err)) + s.forwardError(w, r, a, s.handleError(w, r, a, state.ProviderId, nil, err)) return } return default: - s.forwardError(w, r, req, s.handleError(w, r, req, pid, nil, errors.WithStack(x.PseudoPanic. + s.forwardError(w, r, req, s.handleError(w, r, req, state.ProviderId, nil, errors.WithStack(x.PseudoPanic. WithDetailf("cause", "Unexpected type in OpenID Connect flow: %T", a)))) return } } -func (s *Strategy) ExchangeCode(ctx context.Context, provider Provider, code string) (token *oauth2.Token, err error) { +func (s *Strategy) ExchangeCode(ctx context.Context, provider Provider, code string, opts []oauth2.AuthCodeOption) (token *oauth2.Token, err error) { ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "strategy.oidc.ExchangeCode") defer otelx.End(span, &err) span.SetAttributes(attribute.String("provider_id", provider.Config().ID)) @@ -530,7 +509,7 @@ func (s *Strategy) ExchangeCode(ctx context.Context, provider Provider, code str client := s.d.HTTPClient(ctx) ctx = context.WithValue(ctx, oauth2.HTTPClient, client.HTTPClient) - token, err = te.Exchange(ctx, code) + token, err = te.Exchange(ctx, code, opts...) return token, err default: return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The chosen provider is not capable of exchanging an OAuth 2.0 code for an access token.")) @@ -823,17 +802,19 @@ func (s *Strategy) linkCredentials(ctx context.Context, i *identity.Identity, to return nil } -func getAuthRedirectURL(ctx context.Context, provider Provider, req ider, state *State, upstreamParameters map[string]string) (codeURL string, err error) { +func getAuthRedirectURL(ctx context.Context, provider Provider, req ider, state string, upstreamParameters map[string]string, opts []oauth2.AuthCodeOption) (codeURL string, err error) { switch p := provider.(type) { case OAuth2Provider: c, err := p.OAuth2(ctx) if err != nil { return "", err } + opts = append(opts, UpstreamParameters(upstreamParameters)...) + opts = append(opts, p.AuthCodeURLOptions(req)...) - return c.AuthCodeURL(state.String(), append(UpstreamParameters(upstreamParameters), p.AuthCodeURLOptions(req)...)...), nil + return c.AuthCodeURL(state, opts...), nil case OAuth1Provider: - return p.AuthURL(ctx, state.String()) + return p.AuthURL(ctx, state) default: return "", errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The provider %s does not support the OAuth 2.0 or OAuth 1.0 protocol", provider.Config().Provider)) } diff --git a/selfservice/strategy/oidc/strategy_helper_test.go b/selfservice/strategy/oidc/strategy_helper_test.go index cee924ee5d91..2b542cb20e57 100644 --- a/selfservice/strategy/oidc/strategy_helper_test.go +++ b/selfservice/strategy/oidc/strategy_helper_test.go @@ -10,6 +10,7 @@ import ( "encoding/json" "fmt" "io" + "net" "net/http" "net/http/httptest" "net/url" @@ -203,17 +204,12 @@ func newHydraIntegration(t *testing.T, remote *string, subject *string, claims * parsed, err := url.ParseRequestURI(addr) require.NoError(t, err) - //#nosec G112 - server := &http.Server{Addr: ":" + parsed.Port(), Handler: router} - go func(t *testing.T) { - if err := server.ListenAndServe(); err != http.ErrServerClosed { - require.NoError(t, err) - } else if err == nil { - require.NoError(t, server.Close()) - } - }(t) + listener, err := net.Listen("tcp", ":"+parsed.Port()) + require.NoError(t, err, "port busy?") + server := &http.Server{Handler: router} + go server.Serve(listener) t.Cleanup(func() { - require.NoError(t, server.Close()) + assert.NoError(t, server.Close()) }) return server, addr } diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index ffa643742ec3..61a3605a19c3 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -247,13 +247,13 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, errors.WithStack(flow.ErrCompletedByStrategy) } - state := generateState(f.ID.String()) - if code, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(ctx, f.ID); hasCode { - state.setCode(code.InitCode) + state, pkce, err := s.GenerateState(ctx, provider, f.ID) + if err != nil { + return nil, s.handleError(w, r, f, pid, nil, err) } if err := s.d.ContinuityManager().Pause(ctx, w, r, sessionName, continuity.WithPayload(&AuthCodeContainer{ - State: state.String(), + State: state, FlowID: f.ID.String(), Traits: p.Traits, TransientPayload: f.TransientPayload, @@ -272,7 +272,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return nil, err } - codeURL, err := getAuthRedirectURL(ctx, provider, f, state, up) + codeURL, err := getAuthRedirectURL(ctx, provider, f, state, up, pkce) if err != nil { return nil, s.handleError(w, r, f, pid, nil, err) } diff --git a/selfservice/strategy/oidc/strategy_registration.go b/selfservice/strategy/oidc/strategy_registration.go index 765baafbe902..88c4b51d76de 100644 --- a/selfservice/strategy/oidc/strategy_registration.go +++ b/selfservice/strategy/oidc/strategy_registration.go @@ -212,13 +212,13 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat return errors.WithStack(flow.ErrCompletedByStrategy) } - state := generateState(f.ID.String()) - if code, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(ctx, f.ID); hasCode { - state.setCode(code.InitCode) + state, pkce, err := s.GenerateState(ctx, provider, f.ID) + if err != nil { + return s.handleError(w, r, f, pid, nil, err) } if err := s.d.ContinuityManager().Pause(ctx, w, r, sessionName, continuity.WithPayload(&AuthCodeContainer{ - State: state.String(), + State: state, FlowID: f.ID.String(), Traits: p.Traits, TransientPayload: f.TransientPayload, @@ -232,7 +232,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat return err } - codeURL, err := getAuthRedirectURL(ctx, provider, f, state, up) + codeURL, err := getAuthRedirectURL(ctx, provider, f, state, up, pkce) if err != nil { return s.handleError(w, r, f, pid, nil, err) } diff --git a/selfservice/strategy/oidc/strategy_settings.go b/selfservice/strategy/oidc/strategy_settings.go index 5cba6f304bf8..a86998ac9e3f 100644 --- a/selfservice/strategy/oidc/strategy_settings.go +++ b/selfservice/strategy/oidc/strategy_settings.go @@ -379,10 +379,13 @@ func (s *Strategy) initLinkProvider(w http.ResponseWriter, r *http.Request, ctxU return s.handleSettingsError(w, r, ctxUpdate, p, err) } - state := generateState(ctxUpdate.Flow.ID.String()) + state, pkce, err := s.GenerateState(ctx, provider, ctxUpdate.Flow.ID) + if err != nil { + return s.handleSettingsError(w, r, ctxUpdate, p, err) + } if err := s.d.ContinuityManager().Pause(ctx, w, r, sessionName, continuity.WithPayload(&AuthCodeContainer{ - State: state.String(), + State: state, FlowID: ctxUpdate.Flow.ID.String(), Traits: p.Traits, }), @@ -395,7 +398,7 @@ func (s *Strategy) initLinkProvider(w http.ResponseWriter, r *http.Request, ctxU return err } - codeURL, err := getAuthRedirectURL(ctx, provider, req, state, up) + codeURL, err := getAuthRedirectURL(ctx, provider, req, state, up, pkce) if err != nil { return s.handleSettingsError(w, r, ctxUpdate, p, err) } diff --git a/selfservice/strategy/oidc/strategy_state_test.go b/selfservice/strategy/oidc/strategy_state_test.go deleted file mode 100644 index 28302400861d..000000000000 --- a/selfservice/strategy/oidc/strategy_state_test.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright © 2023 Ory Corp -// SPDX-License-Identifier: Apache-2.0 - -package oidc - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/ory/kratos/x" -) - -func TestGenerateState(t *testing.T) { - flowID := x.NewUUID().String() - state := generateState(flowID).String() - assert.NotEmpty(t, state) - - s, err := parseState(state) - require.NoError(t, err) - assert.Equal(t, flowID, s.FlowID) - assert.NotEmpty(t, s.Data) -} diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index 36275d214886..0fdaecb5a1f6 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -20,7 +20,9 @@ import ( "time" "github.com/davecgh/go-spew/spew" + "github.com/pkg/errors" "github.com/samber/lo" + "golang.org/x/oauth2" "github.com/ory/kratos/selfservice/hook/hooktest" "github.com/ory/x/sqlxx" @@ -59,6 +61,15 @@ import ( ) func TestStrategy(t *testing.T) { + t.Run("newStyleState", func(t *testing.T) { + oidc.TestHookEnableNewStyleState(t) + testStrategy(t) + }) + + testStrategy(t) +} + +func testStrategy(t *testing.T) { ctx := context.Background() if testing.Short() { t.Skip() @@ -88,6 +99,15 @@ func TestStrategy(t *testing.T) { newOIDCProvider(t, ts, remotePublic, remoteAdmin, "claimsViaUserInfo", func(c *oidc.Configuration) { c.ClaimsSource = oidc.ClaimsSourceUserInfo }), + newOIDCProvider(t, ts, remotePublic, remoteAdmin, "neverPKCE", func(c *oidc.Configuration) { + c.PKCE = "never" + }), + newOIDCProvider(t, ts, remotePublic, remoteAdmin, "autoPKCE", func(c *oidc.Configuration) { + c.PKCE = "auto" + }), + newOIDCProvider(t, ts, remotePublic, remoteAdmin, "forcePKCE", func(c *oidc.Configuration) { + c.PKCE = "force" + }), oidc.Configuration{ Provider: "generic", ID: "invalid-issuer", @@ -471,6 +491,186 @@ func TestStrategy(t *testing.T) { return id } + t.Run("case=force PKCE", func(t *testing.T) { + if !oidc.TestHookNewStyleStateEnabled(t) { + t.Skip("This test is not compatible with the old state handling") + } + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "forcePKCE") + subject = "force-pkce@ory.sh" + scope = []string{"openid", "offline"} + var redirects []*http.Request + res, body := makeRequestWithCookieJar(t, "forcePKCE", action, url.Values{}, nil, func(_ *http.Request, via []*http.Request) error { + redirects = via + return nil + }) + require.GreaterOrEqual(t, len(redirects), 3) + assert.Contains(t, redirects[1].URL.String(), "/oauth2/auth") + assert.Contains(t, redirects[1].URL.String(), "code_challenge_method=S256") + assert.Contains(t, redirects[1].URL.String(), "code_challenge=") + assert.Equal(t, redirects[len(redirects)-1].URL.Path, "/self-service/methods/oidc/callback") + + assertIdentity(t, res, body) + expectTokens(t, "forcePKCE", body) + assert.Equal(t, "forcePKCE", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + }) + t.Run("case=force PKCE, invalid verifier", func(t *testing.T) { + if !oidc.TestHookNewStyleStateEnabled(t) { + t.Skip("This test is not compatible with the old state handling") + } + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "forcePKCE") + subject = "force-pkce@ory.sh" + scope = []string{"openid", "offline"} + verifierFalsified := false + res, body := makeRequestWithCookieJar(t, "forcePKCE", action, url.Values{}, nil, func(req *http.Request, via []*http.Request) error { + if req.URL.Path == "/oauth2/auth" && !verifierFalsified { + q := req.URL.Query() + require.NotEmpty(t, q.Get("code_challenge")) + require.Equal(t, "S256", q.Get("code_challenge_method")) + q.Set("code_challenge", oauth2.S256ChallengeFromVerifier(oauth2.GenerateVerifier())) + req.URL.RawQuery = q.Encode() + verifierFalsified = true + } + return nil + }) + require.True(t, verifierFalsified) + assertSystemErrorWithMessage(t, res, body, http.StatusInternalServerError, "The PKCE code challenge did not match the code verifier.") + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()) + }) + t.Run("case=force PKCE, code challenge params removed from initial redirect", func(t *testing.T) { + if !oidc.TestHookNewStyleStateEnabled(t) { + t.Skip("This test is not compatible with the old state handling") + } + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "forcePKCE") + subject = "force-pkce@ory.sh" + scope = []string{"openid", "offline"} + challengeParamsRemoved := false + res, body := makeRequestWithCookieJar(t, "forcePKCE", action, url.Values{}, nil, func(req *http.Request, via []*http.Request) error { + if req.URL.Path == "/oauth2/auth" && !challengeParamsRemoved { + q := req.URL.Query() + require.NotEmpty(t, q.Get("code_challenge")) + require.Equal(t, "S256", q.Get("code_challenge_method")) + q.Del("code_challenge") + q.Del("code_challenge_method") + req.URL.RawQuery = q.Encode() + challengeParamsRemoved = true + } + return nil + }) + require.True(t, challengeParamsRemoved) + assertSystemErrorWithMessage(t, res, body, http.StatusInternalServerError, "The PKCE code challenge did not match the code verifier.") + assert.Contains(t, res.Request.URL.String(), conf.SelfServiceFlowErrorURL(ctx).String()) + }) + t.Run("case=PKCE prevents authorization code injection attacks", func(t *testing.T) { + if !oidc.TestHookNewStyleStateEnabled(t) { + t.Skip("This test is not compatible with the old state handling") + } + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "forcePKCE") + subject = "attacker@ory.sh" + scope = []string{"openid", "offline"} + var code string + _, err := testhelpers.NewClientWithCookieJar(t, nil, func(req *http.Request, via []*http.Request) error { + if req.URL.Query().Has("code") { + code = req.URL.Query().Get("code") + return errors.New("code intercepted") + } + return nil + }).PostForm(action, url.Values{"provider": {"forcePKCE"}}) + require.ErrorContains(t, err, "code intercepted") + require.NotEmpty(t, code) // code now contains a valid authorization code + + r2 := newBrowserLoginFlow(t, returnTS.URL, time.Minute) + action = assertFormValues(t, r2.ID, "forcePKCE") + jar, err := cookiejar.New(nil) // must capture the continuity cookie + require.NoError(t, err) + var redirectURI, state string + _, err = testhelpers.NewClientWithCookieJar(t, jar, func(req *http.Request, via []*http.Request) error { + if req.URL.Path == "/oauth2/auth" { + redirectURI = req.URL.Query().Get("redirect_uri") + state = req.URL.Query().Get("state") + return errors.New("stop before redirect to Authorization URL") + } + return nil + }).PostForm(action, url.Values{"provider": {"forcePKCE"}}) + require.ErrorContains(t, err, "stop") + require.NotEmpty(t, redirectURI) + require.NotEmpty(t, state) + res, err := testhelpers.NewClientWithCookieJar(t, jar, nil).Get(redirectURI + "?code=" + code + "&state=" + state) + require.NoError(t, err) + body := x.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + assertSystemErrorWithMessage(t, res, body, http.StatusInternalServerError, "The PKCE code challenge did not match the code verifier.") + }) + t.Run("case=confused providers are detected", func(t *testing.T) { + if !oidc.TestHookNewStyleStateEnabled(t) { + t.Skip("This test is not compatible with the old state handling") + } + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "valid") + subject = "attacker@ory.sh" + scope = []string{"openid", "offline"} + redirectConfused := false + res, err := testhelpers.NewClientWithCookieJar(t, nil, func(req *http.Request, via []*http.Request) error { + if req.URL.Query().Has("code") { + req.URL.Path = strings.Replace(req.URL.Path, "valid", "valid2", 1) + redirectConfused = true + } + return nil + }).PostForm(action, url.Values{"provider": {"valid"}}) + require.True(t, redirectConfused) + require.NoError(t, err) + body := x.MustReadAll(res.Body) + require.NoError(t, res.Body.Close()) + + assertSystemErrorWithReason(t, res, body, http.StatusBadRequest, "provider mismatch between internal state and URL") + }) + t.Run("case=automatic PKCE", func(t *testing.T) { + if !oidc.TestHookNewStyleStateEnabled(t) { + t.Skip("This test is not compatible with the old state handling") + } + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "autoPKCE") + subject = "auto-pkce@ory.sh" + scope = []string{"openid", "offline"} + var redirects []*http.Request + res, body := makeRequestWithCookieJar(t, "autoPKCE", action, url.Values{}, nil, func(_ *http.Request, via []*http.Request) error { + redirects = via + return nil + }) + require.GreaterOrEqual(t, len(redirects), 3) + assert.Contains(t, redirects[1].URL.String(), "/oauth2/auth") + assert.Contains(t, redirects[1].URL.String(), "code_challenge_method=S256") + assert.Contains(t, redirects[1].URL.String(), "code_challenge=") + assert.Equal(t, redirects[len(redirects)-1].URL.Path, "/self-service/methods/oidc/callback/autoPKCE") + + assertIdentity(t, res, body) + expectTokens(t, "autoPKCE", body) + assert.Equal(t, "autoPKCE", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + }) + t.Run("case=disabled PKCE", func(t *testing.T) { + r := newBrowserRegistrationFlow(t, returnTS.URL, time.Minute) + action := assertFormValues(t, r.ID, "neverPKCE") + subject = "never-pkce@ory.sh" + scope = []string{"openid", "offline"} + var redirects []*http.Request + res, body := makeRequestWithCookieJar(t, "neverPKCE", action, url.Values{}, nil, func(_ *http.Request, via []*http.Request) error { + redirects = via + return nil + }) + require.GreaterOrEqual(t, len(redirects), 3) + assert.Contains(t, redirects[1].URL.String(), "/oauth2/auth") + assert.NotContains(t, redirects[1].URL.String(), "code_challenge_method=") + assert.NotContains(t, redirects[1].URL.String(), "code_challenge=") + assert.Equal(t, redirects[len(redirects)-1].URL.Path, "/self-service/methods/oidc/callback/neverPKCE") + + assertIdentity(t, res, body) + expectTokens(t, "neverPKCE", body) + assert.Equal(t, "neverPKCE", gjson.GetBytes(body, "authentication_methods.0.provider").String(), "%s", body) + }) + t.Run("case=register and then login", func(t *testing.T) { postRegistrationWebhook := hooktest.NewServer() t.Cleanup(postRegistrationWebhook.Close) @@ -1037,7 +1237,14 @@ func TestStrategy(t *testing.T) { for _, tc := range []struct{ name, provider string }{ {name: "idtoken", provider: "valid"}, {name: "userinfo", provider: "claimsViaUserInfo"}, + {name: "disable-pkce", provider: "neverPKCE"}, + {name: "auto-pkce", provider: "autoPKCE"}, + {name: "force-pkce", provider: "forcePKCE"}, } { + if !oidc.TestHookNewStyleStateEnabled(t) && tc.name == "force-pkce" { + t.Log("Skipping test because old state handling is enabled") + continue + } subject = fmt.Sprintf("incomplete-data@%s.ory.sh", tc.name) scope = []string{"openid"} claims = idTokenClaims{} @@ -1696,10 +1903,7 @@ func TestPostEndpointRedirect(t *testing.T) { func findCsrfTokenPath(t *testing.T, body []byte) string { nodes := gjson.GetBytes(body, "ui.nodes").Array() index := slices.IndexFunc(nodes, func(n gjson.Result) bool { - if n.Get("attributes.name").String() == "csrf_token" { - return true - } - return false + return n.Get("attributes.name").String() == "csrf_token" }) require.GreaterOrEqual(t, index, 0) return fmt.Sprintf("ui.nodes.%v.attributes.value", index)