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..66ef044639ca --- /dev/null +++ b/selfservice/strategy/oidc/state.go @@ -0,0 +1,114 @@ +// 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, 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 { + state.SessionTokenExchangeCodeSha512 = sha512.New().Sum([]byte(code.InitCode)) + } + + // 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 { + return subtle.ConstantTimeCompare(s.GetSessionTokenExchangeCodeSha512(), sha512.New().Sum([]byte(code))) == 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") + } else { + return &oidcv1.State{ + FlowId: id, + 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)