diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f36f008c5e32..e7c488f0d496 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -28,7 +28,7 @@ jobs: - sdk-generate services: postgres: - image: postgres:11.8 + image: postgres:14 env: POSTGRES_DB: postgres POSTGRES_PASSWORD: test @@ -111,7 +111,7 @@ jobs: - sdk-generate services: postgres: - image: postgres:11.8 + image: postgres:14 env: POSTGRES_DB: postgres POSTGRES_PASSWORD: test @@ -119,7 +119,7 @@ jobs: ports: - 5432:5432 mysql: - image: mysql:5.7 + image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: test ports: @@ -222,7 +222,7 @@ jobs: - sdk-generate services: postgres: - image: postgres:11.8 + image: postgres:14 env: POSTGRES_DB: postgres POSTGRES_PASSWORD: test @@ -230,7 +230,7 @@ jobs: ports: - 5432:5432 mysql: - image: mysql:5.7 + image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: test ports: diff --git a/Makefile b/Makefile index 212cc06a9299..4924072c8111 100644 --- a/Makefile +++ b/Makefile @@ -32,9 +32,8 @@ $(call make-lint-dependency) echo "deprecated usage, use docs/cli instead" go build -o .bin/clidoc ./cmd/clidoc/. -.PHONY: .bin/yq -.bin/yq: - go build -o .bin/yq github.com/mikefarah/yq/v4 +.bin/yq: Makefile + GOBIN=$(PWD)/.bin go install github.com/mikefarah/yq/v4@v4.44.3 .PHONY: docs/cli docs/cli: @@ -58,17 +57,31 @@ 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: - GO111MODULE=on go install -tags sqlite . + go install -tags sqlite . .PHONY: test-resetdb test-resetdb: @@ -163,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 0836132e4e2b..7289a142b67b 100644 --- a/go.mod +++ b/go.mod @@ -58,7 +58,6 @@ require ( github.com/luna-duclos/instrumentedsql v1.1.3 github.com/mailhog/MailHog v1.0.1 github.com/mattn/goveralls v0.0.12 - github.com/mikefarah/yq/v4 v4.44.2 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe github.com/ory/analytics-go/v5 v5.0.1 @@ -124,8 +123,6 @@ require ( github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect - github.com/a8m/envsubst v1.4.2 // indirect - github.com/alecthomas/participle/v2 v2.1.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/avast/retry-go/v4 v4.3.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect @@ -136,14 +133,12 @@ require ( github.com/cockroachdb/cockroach-go/v2 v2.3.5 github.com/containerd/continuity v0.4.3 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/dimchansky/utfbom v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v26.1.4+incompatible // indirect github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect - github.com/elliotchance/orderedmap v1.6.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/felixge/fgprof v0.9.3 // indirect @@ -218,7 +213,6 @@ require ( github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jandelgado/gcov2lcov v1.0.5 // indirect github.com/jessevdk/go-flags v1.5.0 // indirect - github.com/jinzhu/copier v0.4.0 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect @@ -300,7 +294,6 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect - github.com/yuin/gopher-lua v1.1.1 // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.47.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.21.0 // indirect @@ -319,11 +312,10 @@ 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 - gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect diff --git a/go.sum b/go.sum index ed56323915dc..ef1b1a497d6c 100644 --- a/go.sum +++ b/go.sum @@ -53,14 +53,6 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg= -github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY= -github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= -github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= -github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= -github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -140,8 +132,6 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= -github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8= @@ -154,8 +144,6 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elliotchance/orderedmap v1.6.0 h1:xjn+kbbKXeDq6v9RVE+WYwRbYfAZKvlWfcJNxM8pvEw= -github.com/elliotchance/orderedmap v1.6.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -413,8 +401,6 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -467,8 +453,6 @@ github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPa github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= -github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= @@ -592,8 +576,6 @@ github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8m github.com/microcosm-cc/bluemonday v1.0.22/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= -github.com/mikefarah/yq/v4 v4.44.2 h1:J+ezWCDTg+SUs0jXdcE0HIPH1+rEr0Tbn9Y1SwiWtH0= -github.com/mikefarah/yq/v4 v4.44.2/go.mod h1:9bnz36uZJDEyxdIjRronBcqStS953k3y3DrSRXr4F/w= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -675,7 +657,6 @@ github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1H github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 h1:jYi87L8j62qkXzaYHAQAhEapgukhenIMZRBKTNRLHJ4= github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -835,8 +816,6 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= -github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/zmb3/spotify/v2 v2.4.2 h1:j3yNN5lKVEMZQItJF4MHCSZbfNWmXO+KaC+3RFaLlLc= github.com/zmb3/spotify/v2 v2.4.2/go.mod h1:XOV7BrThayFYB9AAfB+L0Q0wyxBuLCARk4fI/ZXCBW8= go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= @@ -1248,8 +1227,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= @@ -1269,8 +1248,6 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= -gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE= -gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= diff --git a/go_mod_indirect_pins.go b/go_mod_indirect_pins.go index 2c884ac441e7..15d9aecaf52f 100644 --- a/go_mod_indirect_pins.go +++ b/go_mod_indirect_pins.go @@ -7,12 +7,10 @@ package main import ( + _ "github.com/cortesi/modd/cmd/modd" _ "github.com/go-swagger/go-swagger/cmd/swagger" + _ "github.com/mailhog/MailHog" _ "github.com/mattn/goveralls" _ "github.com/ory/go-acc" - - _ "github.com/cortesi/modd/cmd/modd" - _ "github.com/mailhog/MailHog" - _ "github.com/mikefarah/yq/v4" ) diff --git a/internal/client-go/.openapi-generator/FILES b/internal/client-go/.openapi-generator/FILES index 5eaa392f30a9..118cf9b06463 100644 --- a/internal/client-go/.openapi-generator/FILES +++ b/internal/client-go/.openapi-generator/FILES @@ -42,6 +42,7 @@ docs/Identity.md docs/IdentityAPI.md docs/IdentityCredentials.md docs/IdentityCredentialsCode.md +docs/IdentityCredentialsCodeAddress.md docs/IdentityCredentialsOidc.md docs/IdentityCredentialsOidcProvider.md docs/IdentityCredentialsPassword.md @@ -165,6 +166,7 @@ model_health_status.go model_identity.go model_identity_credentials.go model_identity_credentials_code.go +model_identity_credentials_code_address.go model_identity_credentials_oidc.go model_identity_credentials_oidc_provider.go model_identity_credentials_password.go diff --git a/internal/client-go/README.md b/internal/client-go/README.md index 6e2097d9a320..97593523117a 100644 --- a/internal/client-go/README.md +++ b/internal/client-go/README.md @@ -166,6 +166,7 @@ Class | Method | HTTP request | Description - [Identity](docs/Identity.md) - [IdentityCredentials](docs/IdentityCredentials.md) - [IdentityCredentialsCode](docs/IdentityCredentialsCode.md) + - [IdentityCredentialsCodeAddress](docs/IdentityCredentialsCodeAddress.md) - [IdentityCredentialsOidc](docs/IdentityCredentialsOidc.md) - [IdentityCredentialsOidcProvider](docs/IdentityCredentialsOidcProvider.md) - [IdentityCredentialsPassword](docs/IdentityCredentialsPassword.md) diff --git a/internal/client-go/go.sum b/internal/client-go/go.sum index 6cc3f5911d11..c966c8ddfd0d 100644 --- a/internal/client-go/go.sum +++ b/internal/client-go/go.sum @@ -4,7 +4,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/client-go/model_identity_credentials_code.go b/internal/client-go/model_identity_credentials_code.go index 75857f31c272..53fefb6719eb 100644 --- a/internal/client-go/model_identity_credentials_code.go +++ b/internal/client-go/model_identity_credentials_code.go @@ -13,14 +13,11 @@ package client import ( "encoding/json" - "time" ) // IdentityCredentialsCode CredentialsCode represents a one time login/registration code type IdentityCredentialsCode struct { - // The type of the address for this code - AddressType *string `json:"address_type,omitempty"` - UsedAt NullableTime `json:"used_at,omitempty"` + Addresses []IdentityCredentialsCodeAddress `json:"addresses,omitempty"` } // NewIdentityCredentialsCode instantiates a new IdentityCredentialsCode object @@ -40,88 +37,42 @@ func NewIdentityCredentialsCodeWithDefaults() *IdentityCredentialsCode { return &this } -// GetAddressType returns the AddressType field value if set, zero value otherwise. -func (o *IdentityCredentialsCode) GetAddressType() string { - if o == nil || o.AddressType == nil { - var ret string +// GetAddresses returns the Addresses field value if set, zero value otherwise. +func (o *IdentityCredentialsCode) GetAddresses() []IdentityCredentialsCodeAddress { + if o == nil || o.Addresses == nil { + var ret []IdentityCredentialsCodeAddress return ret } - return *o.AddressType + return o.Addresses } -// GetAddressTypeOk returns a tuple with the AddressType field value if set, nil otherwise +// GetAddressesOk returns a tuple with the Addresses field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *IdentityCredentialsCode) GetAddressTypeOk() (*string, bool) { - if o == nil || o.AddressType == nil { +func (o *IdentityCredentialsCode) GetAddressesOk() ([]IdentityCredentialsCodeAddress, bool) { + if o == nil || o.Addresses == nil { return nil, false } - return o.AddressType, true + return o.Addresses, true } -// HasAddressType returns a boolean if a field has been set. -func (o *IdentityCredentialsCode) HasAddressType() bool { - if o != nil && o.AddressType != nil { +// HasAddresses returns a boolean if a field has been set. +func (o *IdentityCredentialsCode) HasAddresses() bool { + if o != nil && o.Addresses != nil { return true } return false } -// SetAddressType gets a reference to the given string and assigns it to the AddressType field. -func (o *IdentityCredentialsCode) SetAddressType(v string) { - o.AddressType = &v -} - -// GetUsedAt returns the UsedAt field value if set, zero value otherwise (both if not set or set to explicit null). -func (o *IdentityCredentialsCode) GetUsedAt() time.Time { - if o == nil || o.UsedAt.Get() == nil { - var ret time.Time - return ret - } - return *o.UsedAt.Get() -} - -// GetUsedAtOk returns a tuple with the UsedAt field value if set, nil otherwise -// and a boolean to check if the value has been set. -// NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *IdentityCredentialsCode) GetUsedAtOk() (*time.Time, bool) { - if o == nil { - return nil, false - } - return o.UsedAt.Get(), o.UsedAt.IsSet() -} - -// HasUsedAt returns a boolean if a field has been set. -func (o *IdentityCredentialsCode) HasUsedAt() bool { - if o != nil && o.UsedAt.IsSet() { - return true - } - - return false -} - -// SetUsedAt gets a reference to the given NullableTime and assigns it to the UsedAt field. -func (o *IdentityCredentialsCode) SetUsedAt(v time.Time) { - o.UsedAt.Set(&v) -} - -// SetUsedAtNil sets the value for UsedAt to be an explicit nil -func (o *IdentityCredentialsCode) SetUsedAtNil() { - o.UsedAt.Set(nil) -} - -// UnsetUsedAt ensures that no value is present for UsedAt, not even an explicit nil -func (o *IdentityCredentialsCode) UnsetUsedAt() { - o.UsedAt.Unset() +// SetAddresses gets a reference to the given []IdentityCredentialsCodeAddress and assigns it to the Addresses field. +func (o *IdentityCredentialsCode) SetAddresses(v []IdentityCredentialsCodeAddress) { + o.Addresses = v } func (o IdentityCredentialsCode) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} - if o.AddressType != nil { - toSerialize["address_type"] = o.AddressType - } - if o.UsedAt.IsSet() { - toSerialize["used_at"] = o.UsedAt.Get() + if o.Addresses != nil { + toSerialize["addresses"] = o.Addresses } return json.Marshal(toSerialize) } diff --git a/internal/client-go/model_identity_credentials_code_address.go b/internal/client-go/model_identity_credentials_code_address.go new file mode 100644 index 000000000000..c739045e79e0 --- /dev/null +++ b/internal/client-go/model_identity_credentials_code_address.go @@ -0,0 +1,151 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// IdentityCredentialsCodeAddress struct for IdentityCredentialsCodeAddress +type IdentityCredentialsCodeAddress struct { + // The address for this code + Address *string `json:"address,omitempty"` + Channel *string `json:"channel,omitempty"` +} + +// NewIdentityCredentialsCodeAddress instantiates a new IdentityCredentialsCodeAddress object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewIdentityCredentialsCodeAddress() *IdentityCredentialsCodeAddress { + this := IdentityCredentialsCodeAddress{} + return &this +} + +// NewIdentityCredentialsCodeAddressWithDefaults instantiates a new IdentityCredentialsCodeAddress object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewIdentityCredentialsCodeAddressWithDefaults() *IdentityCredentialsCodeAddress { + this := IdentityCredentialsCodeAddress{} + return &this +} + +// GetAddress returns the Address field value if set, zero value otherwise. +func (o *IdentityCredentialsCodeAddress) GetAddress() string { + if o == nil || o.Address == nil { + var ret string + return ret + } + return *o.Address +} + +// GetAddressOk returns a tuple with the Address field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsCodeAddress) GetAddressOk() (*string, bool) { + if o == nil || o.Address == nil { + return nil, false + } + return o.Address, true +} + +// HasAddress returns a boolean if a field has been set. +func (o *IdentityCredentialsCodeAddress) HasAddress() bool { + if o != nil && o.Address != nil { + return true + } + + return false +} + +// SetAddress gets a reference to the given string and assigns it to the Address field. +func (o *IdentityCredentialsCodeAddress) SetAddress(v string) { + o.Address = &v +} + +// GetChannel returns the Channel field value if set, zero value otherwise. +func (o *IdentityCredentialsCodeAddress) GetChannel() string { + if o == nil || o.Channel == nil { + var ret string + return ret + } + return *o.Channel +} + +// GetChannelOk returns a tuple with the Channel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsCodeAddress) GetChannelOk() (*string, bool) { + if o == nil || o.Channel == nil { + return nil, false + } + return o.Channel, true +} + +// HasChannel returns a boolean if a field has been set. +func (o *IdentityCredentialsCodeAddress) HasChannel() bool { + if o != nil && o.Channel != nil { + return true + } + + return false +} + +// SetChannel gets a reference to the given string and assigns it to the Channel field. +func (o *IdentityCredentialsCodeAddress) SetChannel(v string) { + o.Channel = &v +} + +func (o IdentityCredentialsCodeAddress) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Address != nil { + toSerialize["address"] = o.Address + } + if o.Channel != nil { + toSerialize["channel"] = o.Channel + } + return json.Marshal(toSerialize) +} + +type NullableIdentityCredentialsCodeAddress struct { + value *IdentityCredentialsCodeAddress + isSet bool +} + +func (v NullableIdentityCredentialsCodeAddress) Get() *IdentityCredentialsCodeAddress { + return v.value +} + +func (v *NullableIdentityCredentialsCodeAddress) Set(val *IdentityCredentialsCodeAddress) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsCodeAddress) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsCodeAddress) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsCodeAddress(val *IdentityCredentialsCodeAddress) *NullableIdentityCredentialsCodeAddress { + return &NullableIdentityCredentialsCodeAddress{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsCodeAddress) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsCodeAddress) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/client-go/model_update_login_flow_with_code_method.go b/internal/client-go/model_update_login_flow_with_code_method.go index 5833200a3ce9..06272618da90 100644 --- a/internal/client-go/model_update_login_flow_with_code_method.go +++ b/internal/client-go/model_update_login_flow_with_code_method.go @@ -17,6 +17,8 @@ import ( // UpdateLoginFlowWithCodeMethod Update Login flow using the code method type UpdateLoginFlowWithCodeMethod struct { + // Address is the address to send the code to, in case that there are multiple addresses. This field is only used in two-factor flows and is ineffective for passwordless flows. + Address *string `json:"address,omitempty"` // Code is the 6 digits code sent to the user Code *string `json:"code,omitempty"` // CSRFToken is the anti-CSRF token @@ -50,6 +52,38 @@ func NewUpdateLoginFlowWithCodeMethodWithDefaults() *UpdateLoginFlowWithCodeMeth return &this } +// GetAddress returns the Address field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetAddress() string { + if o == nil || o.Address == nil { + var ret string + return ret + } + return *o.Address +} + +// GetAddressOk returns a tuple with the Address field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetAddressOk() (*string, bool) { + if o == nil || o.Address == nil { + return nil, false + } + return o.Address, true +} + +// HasAddress returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasAddress() bool { + if o != nil && o.Address != nil { + return true + } + + return false +} + +// SetAddress gets a reference to the given string and assigns it to the Address field. +func (o *UpdateLoginFlowWithCodeMethod) SetAddress(v string) { + o.Address = &v +} + // GetCode returns the Code field value if set, zero value otherwise. func (o *UpdateLoginFlowWithCodeMethod) GetCode() string { if o == nil || o.Code == nil { @@ -228,6 +262,9 @@ func (o *UpdateLoginFlowWithCodeMethod) SetTransientPayload(v map[string]interfa func (o UpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} + if o.Address != nil { + toSerialize["address"] = o.Address + } if o.Code != nil { toSerialize["code"] = o.Code } 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/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES index 5eaa392f30a9..118cf9b06463 100644 --- a/internal/httpclient/.openapi-generator/FILES +++ b/internal/httpclient/.openapi-generator/FILES @@ -42,6 +42,7 @@ docs/Identity.md docs/IdentityAPI.md docs/IdentityCredentials.md docs/IdentityCredentialsCode.md +docs/IdentityCredentialsCodeAddress.md docs/IdentityCredentialsOidc.md docs/IdentityCredentialsOidcProvider.md docs/IdentityCredentialsPassword.md @@ -165,6 +166,7 @@ model_health_status.go model_identity.go model_identity_credentials.go model_identity_credentials_code.go +model_identity_credentials_code_address.go model_identity_credentials_oidc.go model_identity_credentials_oidc_provider.go model_identity_credentials_password.go diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index 6e2097d9a320..97593523117a 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -166,6 +166,7 @@ Class | Method | HTTP request | Description - [Identity](docs/Identity.md) - [IdentityCredentials](docs/IdentityCredentials.md) - [IdentityCredentialsCode](docs/IdentityCredentialsCode.md) + - [IdentityCredentialsCodeAddress](docs/IdentityCredentialsCodeAddress.md) - [IdentityCredentialsOidc](docs/IdentityCredentialsOidc.md) - [IdentityCredentialsOidcProvider](docs/IdentityCredentialsOidcProvider.md) - [IdentityCredentialsPassword](docs/IdentityCredentialsPassword.md) diff --git a/internal/httpclient/model_identity_credentials_code.go b/internal/httpclient/model_identity_credentials_code.go index 75857f31c272..53fefb6719eb 100644 --- a/internal/httpclient/model_identity_credentials_code.go +++ b/internal/httpclient/model_identity_credentials_code.go @@ -13,14 +13,11 @@ package client import ( "encoding/json" - "time" ) // IdentityCredentialsCode CredentialsCode represents a one time login/registration code type IdentityCredentialsCode struct { - // The type of the address for this code - AddressType *string `json:"address_type,omitempty"` - UsedAt NullableTime `json:"used_at,omitempty"` + Addresses []IdentityCredentialsCodeAddress `json:"addresses,omitempty"` } // NewIdentityCredentialsCode instantiates a new IdentityCredentialsCode object @@ -40,88 +37,42 @@ func NewIdentityCredentialsCodeWithDefaults() *IdentityCredentialsCode { return &this } -// GetAddressType returns the AddressType field value if set, zero value otherwise. -func (o *IdentityCredentialsCode) GetAddressType() string { - if o == nil || o.AddressType == nil { - var ret string +// GetAddresses returns the Addresses field value if set, zero value otherwise. +func (o *IdentityCredentialsCode) GetAddresses() []IdentityCredentialsCodeAddress { + if o == nil || o.Addresses == nil { + var ret []IdentityCredentialsCodeAddress return ret } - return *o.AddressType + return o.Addresses } -// GetAddressTypeOk returns a tuple with the AddressType field value if set, nil otherwise +// GetAddressesOk returns a tuple with the Addresses field value if set, nil otherwise // and a boolean to check if the value has been set. -func (o *IdentityCredentialsCode) GetAddressTypeOk() (*string, bool) { - if o == nil || o.AddressType == nil { +func (o *IdentityCredentialsCode) GetAddressesOk() ([]IdentityCredentialsCodeAddress, bool) { + if o == nil || o.Addresses == nil { return nil, false } - return o.AddressType, true + return o.Addresses, true } -// HasAddressType returns a boolean if a field has been set. -func (o *IdentityCredentialsCode) HasAddressType() bool { - if o != nil && o.AddressType != nil { +// HasAddresses returns a boolean if a field has been set. +func (o *IdentityCredentialsCode) HasAddresses() bool { + if o != nil && o.Addresses != nil { return true } return false } -// SetAddressType gets a reference to the given string and assigns it to the AddressType field. -func (o *IdentityCredentialsCode) SetAddressType(v string) { - o.AddressType = &v -} - -// GetUsedAt returns the UsedAt field value if set, zero value otherwise (both if not set or set to explicit null). -func (o *IdentityCredentialsCode) GetUsedAt() time.Time { - if o == nil || o.UsedAt.Get() == nil { - var ret time.Time - return ret - } - return *o.UsedAt.Get() -} - -// GetUsedAtOk returns a tuple with the UsedAt field value if set, nil otherwise -// and a boolean to check if the value has been set. -// NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *IdentityCredentialsCode) GetUsedAtOk() (*time.Time, bool) { - if o == nil { - return nil, false - } - return o.UsedAt.Get(), o.UsedAt.IsSet() -} - -// HasUsedAt returns a boolean if a field has been set. -func (o *IdentityCredentialsCode) HasUsedAt() bool { - if o != nil && o.UsedAt.IsSet() { - return true - } - - return false -} - -// SetUsedAt gets a reference to the given NullableTime and assigns it to the UsedAt field. -func (o *IdentityCredentialsCode) SetUsedAt(v time.Time) { - o.UsedAt.Set(&v) -} - -// SetUsedAtNil sets the value for UsedAt to be an explicit nil -func (o *IdentityCredentialsCode) SetUsedAtNil() { - o.UsedAt.Set(nil) -} - -// UnsetUsedAt ensures that no value is present for UsedAt, not even an explicit nil -func (o *IdentityCredentialsCode) UnsetUsedAt() { - o.UsedAt.Unset() +// SetAddresses gets a reference to the given []IdentityCredentialsCodeAddress and assigns it to the Addresses field. +func (o *IdentityCredentialsCode) SetAddresses(v []IdentityCredentialsCodeAddress) { + o.Addresses = v } func (o IdentityCredentialsCode) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} - if o.AddressType != nil { - toSerialize["address_type"] = o.AddressType - } - if o.UsedAt.IsSet() { - toSerialize["used_at"] = o.UsedAt.Get() + if o.Addresses != nil { + toSerialize["addresses"] = o.Addresses } return json.Marshal(toSerialize) } diff --git a/internal/httpclient/model_identity_credentials_code_address.go b/internal/httpclient/model_identity_credentials_code_address.go new file mode 100644 index 000000000000..c739045e79e0 --- /dev/null +++ b/internal/httpclient/model_identity_credentials_code_address.go @@ -0,0 +1,151 @@ +/* + * Ory Identities API + * + * This is the API specification for Ory Identities with features such as registration, login, recovery, account verification, profile settings, password reset, identity management, session management, email and sms delivery, and more. + * + * API version: + * Contact: office@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// IdentityCredentialsCodeAddress struct for IdentityCredentialsCodeAddress +type IdentityCredentialsCodeAddress struct { + // The address for this code + Address *string `json:"address,omitempty"` + Channel *string `json:"channel,omitempty"` +} + +// NewIdentityCredentialsCodeAddress instantiates a new IdentityCredentialsCodeAddress object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewIdentityCredentialsCodeAddress() *IdentityCredentialsCodeAddress { + this := IdentityCredentialsCodeAddress{} + return &this +} + +// NewIdentityCredentialsCodeAddressWithDefaults instantiates a new IdentityCredentialsCodeAddress object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewIdentityCredentialsCodeAddressWithDefaults() *IdentityCredentialsCodeAddress { + this := IdentityCredentialsCodeAddress{} + return &this +} + +// GetAddress returns the Address field value if set, zero value otherwise. +func (o *IdentityCredentialsCodeAddress) GetAddress() string { + if o == nil || o.Address == nil { + var ret string + return ret + } + return *o.Address +} + +// GetAddressOk returns a tuple with the Address field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsCodeAddress) GetAddressOk() (*string, bool) { + if o == nil || o.Address == nil { + return nil, false + } + return o.Address, true +} + +// HasAddress returns a boolean if a field has been set. +func (o *IdentityCredentialsCodeAddress) HasAddress() bool { + if o != nil && o.Address != nil { + return true + } + + return false +} + +// SetAddress gets a reference to the given string and assigns it to the Address field. +func (o *IdentityCredentialsCodeAddress) SetAddress(v string) { + o.Address = &v +} + +// GetChannel returns the Channel field value if set, zero value otherwise. +func (o *IdentityCredentialsCodeAddress) GetChannel() string { + if o == nil || o.Channel == nil { + var ret string + return ret + } + return *o.Channel +} + +// GetChannelOk returns a tuple with the Channel field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *IdentityCredentialsCodeAddress) GetChannelOk() (*string, bool) { + if o == nil || o.Channel == nil { + return nil, false + } + return o.Channel, true +} + +// HasChannel returns a boolean if a field has been set. +func (o *IdentityCredentialsCodeAddress) HasChannel() bool { + if o != nil && o.Channel != nil { + return true + } + + return false +} + +// SetChannel gets a reference to the given string and assigns it to the Channel field. +func (o *IdentityCredentialsCodeAddress) SetChannel(v string) { + o.Channel = &v +} + +func (o IdentityCredentialsCodeAddress) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Address != nil { + toSerialize["address"] = o.Address + } + if o.Channel != nil { + toSerialize["channel"] = o.Channel + } + return json.Marshal(toSerialize) +} + +type NullableIdentityCredentialsCodeAddress struct { + value *IdentityCredentialsCodeAddress + isSet bool +} + +func (v NullableIdentityCredentialsCodeAddress) Get() *IdentityCredentialsCodeAddress { + return v.value +} + +func (v *NullableIdentityCredentialsCodeAddress) Set(val *IdentityCredentialsCodeAddress) { + v.value = val + v.isSet = true +} + +func (v NullableIdentityCredentialsCodeAddress) IsSet() bool { + return v.isSet +} + +func (v *NullableIdentityCredentialsCodeAddress) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIdentityCredentialsCodeAddress(val *IdentityCredentialsCodeAddress) *NullableIdentityCredentialsCodeAddress { + return &NullableIdentityCredentialsCodeAddress{value: val, isSet: true} +} + +func (v NullableIdentityCredentialsCodeAddress) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIdentityCredentialsCodeAddress) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_update_login_flow_with_code_method.go b/internal/httpclient/model_update_login_flow_with_code_method.go index 5833200a3ce9..06272618da90 100644 --- a/internal/httpclient/model_update_login_flow_with_code_method.go +++ b/internal/httpclient/model_update_login_flow_with_code_method.go @@ -17,6 +17,8 @@ import ( // UpdateLoginFlowWithCodeMethod Update Login flow using the code method type UpdateLoginFlowWithCodeMethod struct { + // Address is the address to send the code to, in case that there are multiple addresses. This field is only used in two-factor flows and is ineffective for passwordless flows. + Address *string `json:"address,omitempty"` // Code is the 6 digits code sent to the user Code *string `json:"code,omitempty"` // CSRFToken is the anti-CSRF token @@ -50,6 +52,38 @@ func NewUpdateLoginFlowWithCodeMethodWithDefaults() *UpdateLoginFlowWithCodeMeth return &this } +// GetAddress returns the Address field value if set, zero value otherwise. +func (o *UpdateLoginFlowWithCodeMethod) GetAddress() string { + if o == nil || o.Address == nil { + var ret string + return ret + } + return *o.Address +} + +// GetAddressOk returns a tuple with the Address field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *UpdateLoginFlowWithCodeMethod) GetAddressOk() (*string, bool) { + if o == nil || o.Address == nil { + return nil, false + } + return o.Address, true +} + +// HasAddress returns a boolean if a field has been set. +func (o *UpdateLoginFlowWithCodeMethod) HasAddress() bool { + if o != nil && o.Address != nil { + return true + } + + return false +} + +// SetAddress gets a reference to the given string and assigns it to the Address field. +func (o *UpdateLoginFlowWithCodeMethod) SetAddress(v string) { + o.Address = &v +} + // GetCode returns the Code field value if set, zero value otherwise. func (o *UpdateLoginFlowWithCodeMethod) GetCode() string { if o == nil || o.Code == nil { @@ -228,6 +262,9 @@ func (o *UpdateLoginFlowWithCodeMethod) SetTransientPayload(v map[string]interfa func (o UpdateLoginFlowWithCodeMethod) MarshalJSON() ([]byte, error) { toSerialize := map[string]interface{}{} + if o.Address != nil { + toSerialize["address"] = o.Address + } if o.Code != nil { toSerialize["code"] = o.Code } diff --git a/internal/testhelpers/http.go b/internal/testhelpers/http.go index 5523de9d5368..f46c1cc62968 100644 --- a/internal/testhelpers/http.go +++ b/internal/testhelpers/http.go @@ -20,26 +20,34 @@ func NewDebugClient(t *testing.T) *http.Client { return &http.Client{Transport: NewTransportWithLogger(http.DefaultTransport, t)} } -func NewClientWithCookieJar(t *testing.T, jar *cookiejar.Jar, debugRedirects bool) *http.Client { +func NewClientWithCookieJar(t *testing.T, jar *cookiejar.Jar, checkRedirect CheckRedirectFunc) *http.Client { if jar == nil { j, err := cookiejar.New(nil) jar = j require.NoError(t, err) } + if checkRedirect == nil { + checkRedirect = DebugRedirects(t) + } return &http.Client{ - Jar: jar, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - if debugRedirects { - t.Logf("Redirect: %s", req.URL.String()) - } - if len(via) >= 20 { - for k, v := range via { - t.Logf("Failed with redirect (%d): %s", k, v.URL.String()) - } - return errors.New("stopped after 20 redirects") + Jar: jar, + CheckRedirect: checkRedirect, + } +} + +type CheckRedirectFunc func(req *http.Request, via []*http.Request) error + +func DebugRedirects(t *testing.T) CheckRedirectFunc { + return func(req *http.Request, via []*http.Request) error { + t.Logf("Redirect: %s", req.URL.String()) + + if len(via) >= 20 { + for k, v := range via { + t.Logf("Failed with redirect (%d): %s", k, v.URL.String()) } - return nil - }, + return errors.New("stopped after 20 redirects") + } + return nil } } 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/quickstart-mysql.yml b/quickstart-mysql.yml index 7412e22c55df..f0cda5c4ea69 100644 --- a/quickstart-mysql.yml +++ b/quickstart-mysql.yml @@ -1,4 +1,4 @@ -version: '3.7' +version: "3.7" services: kratos-migrate: @@ -10,7 +10,7 @@ services: - DSN=mysql://root:secret@tcp(mysqld:3306)/mysql?max_conns=20&max_idle_conns=4 mysqld: - image: mysql:5.7 + image: mysql:8.0 ports: - "3306:3306" environment: diff --git a/quickstart-postgres.yml b/quickstart-postgres.yml index 8c0dee47a2f3..93cad3a2ffc5 100644 --- a/quickstart-postgres.yml +++ b/quickstart-postgres.yml @@ -1,4 +1,4 @@ -version: '3.7' +version: "3.7" services: kratos-migrate: @@ -10,7 +10,7 @@ services: - DSN=postgres://kratos:secret@postgresd:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 postgresd: - image: postgres:11.8 + image: postgres:14 ports: - "5432:5432" environment: diff --git a/script/testenv.sh b/script/testenv.sh index 15977c10fc8b..becd47b98c7a 100755 --- a/script/testenv.sh +++ b/script/testenv.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash docker rm -f kratos_test_database_mysql kratos_test_database_postgres kratos_test_database_cockroach kratos_test_hydra || true -docker run --platform linux/amd64 --name kratos_test_database_mysql -p 3444:3306 -e MYSQL_ROOT_PASSWORD=secret -d mysql:8.0.34 -docker run --platform linux/amd64 --name kratos_test_database_postgres -p 3445:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=postgres -d postgres:11.8 postgres -c log_statement=all -docker run --platform linux/amd64 --name kratos_test_database_cockroach -p 3446:26257 -p 3447:8080 -d cockroachdb/cockroach:v22.2.6 start-single-node --insecure -docker run --platform linux/amd64 --name kratos_test_hydra -p 4444:4444 -p 4445:4445 -d -e DSN=memory -e URLS_SELF_ISSUER=http://localhost:4444/ -e URLS_LOGIN=http://localhost:4446/login -e URLS_CONSENT=http://localhost:4446/consent oryd/hydra:v2.0.2 serve all --dev +docker run --name kratos_test_database_mysql -p 3444:3306 -e MYSQL_ROOT_PASSWORD=secret -d mysql:8.0 +docker run --name kratos_test_database_postgres -p 3445:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=postgres -d postgres:14 postgres -c log_statement=all +docker run --name kratos_test_database_cockroach -p 3446:26257 -p 3447:8080 -d cockroachdb/cockroach:v22.2.6 start-single-node --insecure +docker run --name kratos_test_hydra -p 4444:4444 -p 4445:4445 -d -e DSN=memory -e URLS_SELF_ISSUER=http://localhost:4444/ -e URLS_LOGIN=http://localhost:4446/login -e URLS_CONSENT=http://localhost:4446/consent oryd/hydra:v2.0.2 serve all --dev source script/test-envs.sh diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go index cb734b1bf4a1..63a143ef9596 100644 --- a/selfservice/strategy/code/strategy_login.go +++ b/selfservice/strategy/code/strategy_login.go @@ -231,7 +231,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, } return nil, nil case flow.StateEmailSent: - i, err := s.loginVerifyCode(ctx, r, f, &p, sess) + i, err := s.loginVerifyCode(ctx, f, &p, sess) if err != nil { return nil, s.HandleLoginError(r, f, &p, err) } @@ -437,7 +437,7 @@ func maybeNormalizeEmail(input string) string { return input } -func (s *Strategy) loginVerifyCode(ctx context.Context, r *http.Request, f *login.Flow, p *updateLoginFlowWithCodeMethod, sess *session.Session) (_ *identity.Identity, err error) { +func (s *Strategy) loginVerifyCode(ctx context.Context, f *login.Flow, p *updateLoginFlowWithCodeMethod, sess *session.Session) (_ *identity.Identity, err error) { ctx, span := s.deps.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.code.strategy.loginVerifyCode") defer otelx.End(span, &err) 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/provider_facebook.go b/selfservice/strategy/oidc/provider_facebook.go index abf9806cce05..8bbca9b24e83 100644 --- a/selfservice/strategy/oidc/provider_facebook.go +++ b/selfservice/strategy/oidc/provider_facebook.go @@ -41,9 +41,9 @@ func NewProviderFacebook( } } -func (g *ProviderFacebook) generateAppSecretProof(ctx context.Context, exchange *oauth2.Token) string { +func (g *ProviderFacebook) generateAppSecretProof(token *oauth2.Token) string { secret := g.config.ClientSecret - data := exchange.AccessToken + data := token.AccessToken h := hmac.New(sha256.New, []byte(secret)) h.Write([]byte(data)) @@ -62,19 +62,19 @@ func (g *ProviderFacebook) OAuth2(ctx context.Context) (*oauth2.Config, error) { return g.oauth2ConfigFromEndpoint(ctx, endpoint), nil } -func (g *ProviderFacebook) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { +func (g *ProviderFacebook) Claims(ctx context.Context, token *oauth2.Token, query url.Values) (*Claims, error) { o, err := g.OAuth2(ctx) if err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) } - appSecretProof := g.generateAppSecretProof(ctx, exchange) + appSecretProof := g.generateAppSecretProof(token) u, err := url.Parse(fmt.Sprintf("https://graph.facebook.com/me?fields=id,name,first_name,last_name,middle_name,email,picture,birthday,gender&appsecret_proof=%s", appSecretProof)) if err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) } - ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), o, exchange) + ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), o, token) req, err := retryablehttp.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) diff --git a/selfservice/strategy/oidc/provider_test.go b/selfservice/strategy/oidc/provider_test.go index a5733d2e95f8..208421ad2ab0 100644 --- a/selfservice/strategy/oidc/provider_test.go +++ b/selfservice/strategy/oidc/provider_test.go @@ -32,13 +32,13 @@ func NewTestProvider(c *Configuration, reg Dependencies) Provider { } } -func RegisterTestProvider(id string) func() { +func RegisterTestProvider(t *testing.T, id string) { supportedProviders[id] = func(c *Configuration, reg Dependencies) Provider { return NewTestProvider(c, reg) } - return func() { + t.Cleanup(func() { delete(supportedProviders, id) - } + }) } var _ IDTokenVerifier = new(TestProvider) diff --git a/selfservice/strategy/oidc/state.go b/selfservice/strategy/oidc/state.go new file mode 100644 index 000000000000..72a52c24ad2e --- /dev/null +++ b/selfservice/strategy/oidc/state.go @@ -0,0 +1,118 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "bytes" + "context" + "crypto/sha512" + "crypto/subtle" + "encoding/base64" + "fmt" + "testing" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "golang.org/x/oauth2" + "google.golang.org/protobuf/proto" + + "github.com/ory/herodot" + "github.com/ory/kratos/cipher" + oidcv1 "github.com/ory/kratos/gen/oidc/v1" + "github.com/ory/kratos/x" +) + +func encryptState(ctx context.Context, c cipher.Cipher, state *oidcv1.State) (ciphertext string, err error) { + m, err := proto.Marshal(state) + if err != nil { + return "", herodot.ErrInternalServerError.WithReasonf("Unable to marshal state: %s", err) + } + return c.Encrypt(ctx, m) +} + +func DecryptState(ctx context.Context, c cipher.Cipher, ciphertext string) (*oidcv1.State, error) { + plaintext, err := c.Decrypt(ctx, ciphertext) + if err != nil { + return nil, herodot.ErrBadRequest.WithReasonf("Unable to decrypt state: %s", err) + } + var state oidcv1.State + if err := proto.Unmarshal(plaintext, &state); err != nil { + return nil, herodot.ErrBadRequest.WithReasonf("Unable to unmarshal state: %s", err) + } + return &state, nil +} + +func legacyString(s *oidcv1.State) string { + flowID := uuid.FromBytesOrNil(s.GetFlowId()) + code := s.GetSessionTokenExchangeCodeSha512() + return base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", flowID.String(), code))) +} + +var newStyleState = false + +func TestHookEnableNewStyleState(t *testing.T) { + prev := newStyleState + newStyleState = true + t.Cleanup(func() { + newStyleState = prev + }) +} + +func TestHookNewStyleStateEnabled(*testing.T) bool { + return newStyleState +} + +func (s *Strategy) GenerateState(ctx context.Context, p Provider, flowID uuid.UUID) (stateParam string, pkce []oauth2.AuthCodeOption, err error) { + state := oidcv1.State{ + FlowId: flowID.Bytes(), + SessionTokenExchangeCodeSha512: x.NewUUID().Bytes(), + ProviderId: p.Config().ID, + PkceVerifier: maybePKCE(ctx, s.d, p), + } + if code, hasCode, _ := s.d.SessionTokenExchangePersister().CodeForFlow(ctx, flowID); hasCode { + sum := sha512.Sum512([]byte(code.InitCode)) + state.SessionTokenExchangeCodeSha512 = sum[:] + } + + // TODO: compatibility: remove later + if !newStyleState { + state.PkceVerifier = "" + return legacyString(&state), nil, nil // compat: disable later + } + // END TODO + + param, err := encryptState(ctx, s.d.Cipher(ctx), &state) + if err != nil { + return "", nil, herodot.ErrInternalServerError.WithReason("Unable to encrypt state").WithWrap(err) + } + return param, PKCEChallenge(&state), nil +} + +func codeMatches(s *oidcv1.State, code string) bool { + sum := sha512.Sum512([]byte(code)) + return subtle.ConstantTimeCompare(s.GetSessionTokenExchangeCodeSha512(), sum[:]) == 1 +} + +func ParseStateCompatiblity(ctx context.Context, c cipher.Cipher, s string) (*oidcv1.State, error) { + // new-style: encrypted + state, err := DecryptState(ctx, c, s) + if err == nil { + return state, nil + } + // old-style: unencrypted + raw, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + return nil, err + } + if id, data, ok := bytes.Cut(raw, []byte(":")); !ok { + return nil, errors.New("state has invalid format (1)") + } else if flowID, err := uuid.FromString(string(id)); err != nil { + return nil, errors.New("state has invalid format (2)") + } else { + return &oidcv1.State{ + FlowId: flowID.Bytes(), + SessionTokenExchangeCodeSha512: data, + }, nil + } +} diff --git a/selfservice/strategy/oidc/state_test.go b/selfservice/strategy/oidc/state_test.go new file mode 100644 index 000000000000..2d21eaa4aef9 --- /dev/null +++ b/selfservice/strategy/oidc/state_test.go @@ -0,0 +1,59 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/cipher" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/strategy/oidc" + "github.com/ory/kratos/x" +) + +func TestGenerateState(t *testing.T) { + conf, reg := internal.NewFastRegistryWithMocks(t) + _ = conf + strat := oidc.NewStrategy(reg) + ctx := context.Background() + ciph := reg.Cipher(ctx) + _, ok := ciph.(*cipher.Noop) + require.False(t, ok) + + var expectProvider string + assertions := func(t *testing.T) { + flowID := x.NewUUID() + + stateParam, pkce, err := strat.GenerateState(ctx, &testProvider{}, flowID) + require.NoError(t, err) + require.NotEmpty(t, stateParam) + assert.Empty(t, pkce) + + state, err := oidc.ParseStateCompatiblity(ctx, ciph, stateParam) + require.NoError(t, err) + assert.Equal(t, flowID.Bytes(), state.FlowId) + assert.Empty(t, oidc.PKCEVerifier(state)) + assert.Equal(t, expectProvider, state.ProviderId) + } + + t.Run("case=old-style", func(t *testing.T) { + expectProvider = "" + assertions(t) + }) + t.Run("case=new-style", func(t *testing.T) { + oidc.TestHookEnableNewStyleState(t) + expectProvider = "test-provider" + assertions(t) + }) +} + +type testProvider struct{} + +func (t *testProvider) Config() *oidc.Configuration { + return &oidc.Configuration{ID: "test-provider", PKCE: "never"} +} diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index fa493a528c2c..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" @@ -67,6 +65,7 @@ const ( RouteAuth = RouteBase + "/auth/:flow" RouteCallback = RouteBase + "/callback/:provider" + RouteCallbackGeneric = RouteBase + "/callback" RouteOrganizationCallback = RouteBase + "/organization/:organization/callback/:provider" ) @@ -141,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) { @@ -211,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 { @@ -292,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") @@ -300,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{} @@ -323,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) { @@ -392,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 ) @@ -401,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(ctx, 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(ctx, 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(ctx, 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(ctx, w, r, req, pid, nil, err)) + s.forwardError(w, r, req, s.handleError(w, r, req, state.ProviderId, nil, err)) return } @@ -427,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(ctx, 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(ctx, 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(ctx, 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(ctx, 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(ctx, 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(ctx, w, r, req, pid, nil, err)) + s.forwardError(w, r, req, s.handleError(w, r, req, state.ProviderId, nil, err)) return } @@ -483,7 +463,7 @@ func (s *Strategy) HandleCallback(w http.ResponseWriter, r *http.Request, ps htt case *registration.Flow: a.Active = s.ID() a.TransientPayload = cntnr.TransientPayload - if ff, err := s.processRegistration(ctx, w, r, a, et, claims, provider, cntnr, ""); err != nil { + if ff, err := s.processRegistration(ctx, w, r, a, et, claims, provider, cntnr); err != nil { if ff != nil { s.forwardError(w, r, ff, err) return @@ -496,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(ctx, 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(ctx, 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(ctx, 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)) @@ -529,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.")) @@ -589,7 +569,7 @@ func (s *Strategy) forwardError(w http.ResponseWriter, r *http.Request, f flow.F } } -func (s *Strategy) handleError(ctx context.Context, w http.ResponseWriter, r *http.Request, f flow.Flow, usedProviderID string, traits []byte, err error) error { +func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Flow, usedProviderID string, traits []byte, err error) error { switch rf := f.(type) { case *login.Flow: return err @@ -609,7 +589,7 @@ func (s *Strategy) handleError(ctx context.Context, w http.ResponseWriter, r *ht rf.UI.Messages.Add(text.NewErrorValidationDuplicateCredentialsOnOIDCLink()) } - lf, err := s.registrationToLogin(w, r, rf, usedProviderID) + lf, err := s.registrationToLogin(w, r, rf) if err != nil { return err } @@ -749,7 +729,7 @@ func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.Au } } -func (s *Strategy) processIDToken(w http.ResponseWriter, r *http.Request, provider Provider, idToken, idTokenNonce string) (*Claims, error) { +func (s *Strategy) processIDToken(r *http.Request, provider Provider, idToken, idTokenNonce string) (*Claims, error) { verifier, ok := provider.(IDTokenVerifier) if !ok { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The provider %s does not support id_token verification", provider.Config().Provider)) @@ -822,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 7b39729cc6af..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" @@ -76,19 +77,43 @@ func (token *idTokenClaims) MarshalJSON() ([]byte, error) { }) } -func createClient(t *testing.T, remote string, redir string) (id, secret string) { +func createClient(t *testing.T, remote string, redir []string) (id, secret string) { require.NoError(t, resilience.Retry(logrusx.New("", ""), time.Second*10, time.Minute*2, func() error { var b bytes.Buffer require.NoError(t, json.NewEncoder(&b).Encode(&struct { - Scope string `json:"scope"` - GrantTypes []string `json:"grant_types"` - ResponseTypes []string `json:"response_types"` - RedirectURIs []string `json:"redirect_uris"` + Scope string `json:"scope"` + GrantTypes []string `json:"grant_types"` + ResponseTypes []string `json:"response_types"` + RedirectURIs []string `json:"redirect_uris"` + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"` }{ GrantTypes: []string{"authorization_code", "refresh_token"}, ResponseTypes: []string{"code"}, Scope: "offline offline_access openid", - RedirectURIs: []string{redir}, + RedirectURIs: redir, + + // This is a workaround to prevent golang.org/x/oauth2 from + // swallowing the actual error messages from failed token exchanges. + // + // The library first attempts to use the Authorization header to + // pass Client ID+secret during token exchange (client_secret_basic + // in Hydra terminology). If that fails (with any error), it tries + // again with the Client ID+secret passed in the HTTP POST body + // (client_secret_post in Hydra). If that also fails, this second + // error is returned. + // + // Now, if the the client was indeed configured to use + // client_secret_basic, but the token exchange fails for another + // reason, the error message will be swallowed and replaced with + // "invalid_client". + // + // Manually setting this to client_secret_post means that during + // tests, all token exchanges will first fail with `invalid_client` + // and then be retried with the correct method. This is the only way + // to get the actual error message from the server, however. + // + // https://github.com/golang/oauth2/blob/5fd42413edb3b1699004a31b72e485e0e4ba1b13/internal/token.go#L227-L242 + TokenEndpointAuthMethod: "client_secret_post", })) res, err := http.Post(remote+"/admin/clients", "application/json", &b) @@ -179,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 } @@ -317,7 +337,7 @@ func newOIDCProvider( id string, opts ...func(*oidc.Configuration), ) oidc.Configuration { - clientID, secret := createClient(t, hydraAdmin, kratos.URL+oidc.RouteBase+"/callback/"+id) + clientID, secret := createClient(t, hydraAdmin, []string{kratos.URL + oidc.RouteBase + "/callback/" + id, kratos.URL + oidc.RouteCallbackGeneric}) cfg := oidc.Configuration{ Provider: "generic", diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go index eb004c534621..61a3605a19c3 100644 --- a/selfservice/strategy/oidc/strategy_login.go +++ b/selfservice/strategy/oidc/strategy_login.go @@ -11,33 +11,23 @@ import ( "strings" "time" - "github.com/ory/kratos/selfservice/strategy/idfirst" - "github.com/ory/x/stringsx" - - "github.com/ory/kratos/selfservice/flowhelpers" - - "github.com/julienschmidt/httprouter" - - "github.com/ory/kratos/session" - - "github.com/ory/kratos/ui/node" - "github.com/ory/x/otelx" - "github.com/ory/x/sqlcon" - - "github.com/ory/kratos/selfservice/flow/registration" - - "github.com/ory/kratos/text" - - "github.com/ory/kratos/continuity" - "github.com/pkg/errors" "github.com/ory/herodot" - + "github.com/ory/kratos/continuity" "github.com/ory/kratos/identity" "github.com/ory/kratos/selfservice/flow" "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/flowhelpers" + "github.com/ory/kratos/selfservice/strategy/idfirst" + "github.com/ory/kratos/session" + "github.com/ory/kratos/text" + "github.com/ory/kratos/ui/node" "github.com/ory/kratos/x" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + "github.com/ory/x/stringsx" ) var ( @@ -142,12 +132,12 @@ func (s *Strategy) processLogin(ctx context.Context, w http.ResponseWriter, r *h registrationFlow, err := s.d.RegistrationHandler().NewRegistrationFlow(w, r, loginFlow.Type, opts...) if err != nil { - return nil, s.handleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err) + return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, err) } err = s.d.SessionTokenExchangePersister().MoveToNewFlow(ctx, loginFlow.ID, registrationFlow.ID) if err != nil { - return nil, s.handleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err) + return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, err) } registrationFlow.OrganizationID = loginFlow.OrganizationID @@ -158,37 +148,36 @@ func (s *Strategy) processLogin(ctx context.Context, w http.ResponseWriter, r *h registrationFlow.Active = s.ID() if err != nil { - return nil, s.handleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err) + return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, err) } - if _, err := s.processRegistration(ctx, w, r, registrationFlow, token, claims, provider, container, loginFlow.IDToken); err != nil { + if _, err := s.processRegistration(ctx, w, r, registrationFlow, token, claims, provider, container); err != nil { return registrationFlow, err } return nil, nil } - return nil, s.handleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err) + return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, err) } var oidcCredentials identity.CredentialsOIDC if err := json.NewDecoder(bytes.NewBuffer(c.Config)).Decode(&oidcCredentials); err != nil { - return nil, s.handleError(ctx, w, r, loginFlow, provider.Config().ID, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()))) + return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()))) } sess := session.NewInactiveSession() - sess.CompletedLoginForWithProvider(s.ID(), identity.AuthenticatorAssuranceLevel1, provider.Config().ID, - httprouter.ParamsFromContext(ctx).ByName("organization")) + sess.CompletedLoginForWithProvider(s.ID(), identity.AuthenticatorAssuranceLevel1, provider.Config().ID, provider.Config().OrganizationID) for _, c := range oidcCredentials.Providers { if c.Subject == claims.Subject && c.Provider == provider.Config().ID { if err = s.d.LoginHookExecutor().PostLoginHook(w, r, node.OpenIDConnectGroup, loginFlow, i, sess, provider.Config().ID); err != nil { - return nil, s.handleError(ctx, w, r, loginFlow, provider.Config().ID, nil, err) + return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, err) } return nil, nil } } - return nil, s.handleError(ctx, w, r, loginFlow, provider.Config().ID, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to find matching OpenID Connect Credentials.").WithDebugf(`Unable to find credentials that match the given provider "%s" and subject "%s".`, provider.Config().ID, claims.Subject))) + return nil, s.handleError(w, r, loginFlow, provider.Config().ID, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to find matching OpenID Connect Credentials.").WithDebugf(`Unable to find credentials that match the given provider "%s" and subject "%s".`, provider.Config().ID, claims.Subject))) } func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, _ *session.Session) (i *identity.Identity, err error) { @@ -201,7 +190,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, var p UpdateLoginFlowWithOidcMethod if err := s.newLinkDecoder(&p, r); err != nil { - return nil, s.handleError(ctx, w, r, f, "", nil, err) + return nil, s.handleError(w, r, f, "", nil, err) } f.IDToken = p.IDToken @@ -224,58 +213,58 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, } if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { - return nil, s.handleError(ctx, w, r, f, pid, nil, err) + return nil, s.handleError(w, r, f, pid, nil, err) } provider, err := s.provider(ctx, pid) if err != nil { - return nil, s.handleError(ctx, w, r, f, pid, nil, err) + return nil, s.handleError(w, r, f, pid, nil, err) } req, err := s.validateFlow(ctx, r, f.ID) if err != nil { - return nil, s.handleError(ctx, w, r, f, pid, nil, err) + return nil, s.handleError(w, r, f, pid, nil, err) } if authenticated, err := s.alreadyAuthenticated(w, r, req); err != nil { - return nil, s.handleError(ctx, w, r, f, pid, nil, err) + return nil, s.handleError(w, r, f, pid, nil, err) } else if authenticated { return i, nil } if p.IDToken != "" { - claims, err := s.processIDToken(w, r, provider, p.IDToken, p.IDTokenNonce) + claims, err := s.processIDToken(r, provider, p.IDToken, p.IDTokenNonce) if err != nil { - return nil, s.handleError(ctx, w, r, f, pid, nil, err) + return nil, s.handleError(w, r, f, pid, nil, err) } _, err = s.processLogin(ctx, w, r, f, nil, claims, provider, &AuthCodeContainer{ FlowID: f.ID.String(), Traits: p.Traits, }) if err != nil { - return nil, s.handleError(ctx, w, r, f, pid, nil, err) + return nil, s.handleError(w, r, f, pid, nil, err) } 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, }), continuity.WithLifespan(time.Minute*30)); err != nil { - return nil, s.handleError(ctx, w, r, f, pid, nil, err) + return nil, s.handleError(w, r, f, pid, nil, err) } f.Active = s.ID() if err = s.d.LoginFlowPersister().UpdateLoginFlow(ctx, f); err != nil { - return nil, s.handleError(ctx, w, r, f, pid, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) + return nil, s.handleError(w, r, f, pid, nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error()))) } var up map[string]string @@ -283,9 +272,9 @@ 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(ctx, w, r, f, pid, nil, err) + return nil, s.handleError(w, r, f, pid, nil, err) } if x.IsJSONRequest(r) { diff --git a/selfservice/strategy/oidc/strategy_registration.go b/selfservice/strategy/oidc/strategy_registration.go index 9a060c9296f5..88c4b51d76de 100644 --- a/selfservice/strategy/oidc/strategy_registration.go +++ b/selfservice/strategy/oidc/strategy_registration.go @@ -13,7 +13,6 @@ import ( "github.com/dgraph-io/ristretto" "github.com/gofrs/uuid" - "github.com/julienschmidt/httprouter" "github.com/pkg/errors" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -155,7 +154,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat var p UpdateRegistrationFlowWithOidcMethod if err := s.newLinkDecoder(&p, r); err != nil { - return s.handleError(ctx, w, r, f, "", nil, err) + return s.handleError(w, r, f, "", nil, err) } pid := p.Provider // this can come from both url query and post body @@ -178,54 +177,54 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat } if err := flow.MethodEnabledAndAllowed(ctx, f.GetFlowName(), s.SettingsStrategyID(), s.SettingsStrategyID(), s.d); err != nil { - return s.handleError(ctx, w, r, f, pid, nil, err) + return s.handleError(w, r, f, pid, nil, err) } provider, err := s.provider(ctx, pid) if err != nil { - return s.handleError(ctx, w, r, f, pid, nil, err) + return s.handleError(w, r, f, pid, nil, err) } req, err := s.validateFlow(ctx, r, f.ID) if err != nil { - return s.handleError(ctx, w, r, f, pid, nil, err) + return s.handleError(w, r, f, pid, nil, err) } if authenticated, err := s.alreadyAuthenticated(w, r, req); err != nil { - return s.handleError(ctx, w, r, f, pid, nil, err) + return s.handleError(w, r, f, pid, nil, err) } else if authenticated { return errors.WithStack(registration.ErrAlreadyLoggedIn) } if p.IDToken != "" { - claims, err := s.processIDToken(w, r, provider, p.IDToken, p.IDTokenNonce) + claims, err := s.processIDToken(r, provider, p.IDToken, p.IDTokenNonce) if err != nil { - return s.handleError(ctx, w, r, f, pid, nil, err) + return s.handleError(w, r, f, pid, nil, err) } _, err = s.processRegistration(ctx, w, r, f, nil, claims, provider, &AuthCodeContainer{ FlowID: f.ID.String(), Traits: p.Traits, TransientPayload: f.TransientPayload, - }, p.IDToken) + }) if err != nil { - return s.handleError(ctx, w, r, f, pid, nil, err) + return s.handleError(w, r, f, pid, nil, err) } 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, }), continuity.WithLifespan(time.Minute*30)); err != nil { - return s.handleError(ctx, w, r, f, pid, nil, err) + return s.handleError(w, r, f, pid, nil, err) } var up map[string]string @@ -233,9 +232,9 @@ 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(ctx, w, r, f, pid, nil, err) + return s.handleError(w, r, f, pid, nil, err) } if x.IsJSONRequest(r) { s.d.Writer().WriteError(w, r, flow.NewBrowserLocationChangeRequiredError(codeURL)) @@ -246,7 +245,7 @@ func (s *Strategy) Register(w http.ResponseWriter, r *http.Request, f *registrat return errors.WithStack(flow.ErrCompletedByStrategy) } -func (s *Strategy) registrationToLogin(w http.ResponseWriter, r *http.Request, rf *registration.Flow, providerID string) (*login.Flow, error) { +func (s *Strategy) registrationToLogin(w http.ResponseWriter, r *http.Request, rf *registration.Flow) (*login.Flow, error) { // If return_to was set before, we need to preserve it. var opts []login.FlowOption if len(rf.ReturnTo) > 0 { @@ -279,7 +278,7 @@ func (s *Strategy) registrationToLogin(w http.ResponseWriter, r *http.Request, r return lf, nil } -func (s *Strategy) processRegistration(ctx context.Context, w http.ResponseWriter, r *http.Request, rf *registration.Flow, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider, container *AuthCodeContainer, idToken string) (_ *login.Flow, err error) { +func (s *Strategy) processRegistration(ctx context.Context, w http.ResponseWriter, r *http.Request, rf *registration.Flow, token *identity.CredentialsOIDCEncryptedTokens, claims *Claims, provider Provider, container *AuthCodeContainer) (_ *login.Flow, err error) { ctx, span := s.d.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.strategy.processRegistration") defer otelx.End(span, &err) @@ -297,13 +296,13 @@ func (s *Strategy) processRegistration(ctx context.Context, w http.ResponseWrite WithField("subject", claims.Subject). Debug("Received successful OpenID Connect callback but user is already registered. Re-initializing login flow now.") - lf, err := s.registrationToLogin(w, r, rf, provider.Config().ID) + lf, err := s.registrationToLogin(w, r, rf) if err != nil { - return nil, s.handleError(ctx, w, r, rf, provider.Config().ID, nil, err) + return nil, s.handleError(w, r, rf, provider.Config().ID, nil, err) } if _, err := s.processLogin(ctx, w, r, lf, token, claims, provider, container); err != nil { - return lf, s.handleError(ctx, w, r, rf, provider.Config().ID, nil, err) + return lf, s.handleError(w, r, rf, provider.Config().ID, nil, err) } return nil, nil @@ -312,17 +311,17 @@ func (s *Strategy) processRegistration(ctx context.Context, w http.ResponseWrite fetch := fetcher.NewFetcher(fetcher.WithClient(s.d.HTTPClient(r.Context())), fetcher.WithCache(jsonnetCache, 60*time.Minute)) jsonnetMapperSnippet, err := fetch.FetchContext(r.Context(), provider.Config().Mapper) if err != nil { - return nil, s.handleError(ctx, w, r, rf, provider.Config().ID, nil, err) + return nil, s.handleError(w, r, rf, provider.Config().ID, nil, err) } - i, va, err := s.createIdentity(ctx, w, r, rf, claims, provider, container, jsonnetMapperSnippet.Bytes()) + i, va, err := s.createIdentity(w, r, rf, claims, provider, container, jsonnetMapperSnippet.Bytes()) if err != nil { - return nil, s.handleError(ctx, w, r, rf, provider.Config().ID, nil, err) + return nil, s.handleError(w, r, rf, provider.Config().ID, nil, err) } // Validate the identity itself if err := s.d.IdentityValidator().Validate(r.Context(), i); err != nil { - return nil, s.handleError(ctx, w, r, rf, provider.Config().ID, i.Traits, err) + return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) } for n := range i.VerifiableAddresses { @@ -339,54 +338,54 @@ func (s *Strategy) processRegistration(ctx context.Context, w http.ResponseWrite creds, err := identity.NewCredentialsOIDC(token, provider.Config().ID, claims.Subject, provider.Config().OrganizationID) if err != nil { - return nil, s.handleError(ctx, w, r, rf, provider.Config().ID, i.Traits, err) + return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) } i.SetCredentials(s.ID(), *creds) if err := s.d.RegistrationExecutor().PostRegistrationHook(w, r, identity.CredentialsTypeOIDC, provider.Config().ID, rf, i); err != nil { - return nil, s.handleError(ctx, w, r, rf, provider.Config().ID, i.Traits, err) + return nil, s.handleError(w, r, rf, provider.Config().ID, i.Traits, err) } return nil, nil } -func (s *Strategy) createIdentity(ctx context.Context, w http.ResponseWriter, r *http.Request, a *registration.Flow, claims *Claims, provider Provider, container *AuthCodeContainer, jsonnetSnippet []byte) (*identity.Identity, []VerifiedAddress, error) { +func (s *Strategy) createIdentity(w http.ResponseWriter, r *http.Request, a *registration.Flow, claims *Claims, provider Provider, container *AuthCodeContainer, jsonnetSnippet []byte) (*identity.Identity, []VerifiedAddress, error) { var jsonClaims bytes.Buffer if err := json.NewEncoder(&jsonClaims).Encode(claims); err != nil { - return nil, nil, s.handleError(ctx, w, r, a, provider.Config().ID, nil, err) + return nil, nil, s.handleError(w, r, a, provider.Config().ID, nil, err) } vm, err := s.d.JsonnetVM(r.Context()) if err != nil { - return nil, nil, s.handleError(ctx, w, r, a, provider.Config().ID, nil, err) + return nil, nil, s.handleError(w, r, a, provider.Config().ID, nil, err) } vm.ExtCode("claims", jsonClaims.String()) evaluated, err := vm.EvaluateAnonymousSnippet(provider.Config().Mapper, string(jsonnetSnippet)) if err != nil { - return nil, nil, s.handleError(ctx, w, r, a, provider.Config().ID, nil, err) + return nil, nil, s.handleError(w, r, a, provider.Config().ID, nil, err) } i := identity.NewIdentity(s.d.Config().DefaultIdentityTraitsSchemaID(r.Context())) - if err := s.setTraits(ctx, w, r, a, claims, provider, container, evaluated, i); err != nil { - return nil, nil, s.handleError(ctx, w, r, a, provider.Config().ID, i.Traits, err) + if err := s.setTraits(w, r, a, provider, container, evaluated, i); err != nil { + return nil, nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err) } if err := s.setMetadata(evaluated, i, PublicMetadata); err != nil { - return nil, nil, s.handleError(ctx, w, r, a, provider.Config().ID, i.Traits, err) + return nil, nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err) } if err := s.setMetadata(evaluated, i, AdminMetadata); err != nil { - return nil, nil, s.handleError(ctx, w, r, a, provider.Config().ID, i.Traits, err) + return nil, nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err) } va, err := s.extractVerifiedAddresses(evaluated) if err != nil { - return nil, nil, s.handleError(ctx, w, r, a, provider.Config().ID, i.Traits, err) + return nil, nil, s.handleError(w, r, a, provider.Config().ID, i.Traits, err) } - if orgID := httprouter.ParamsFromContext(r.Context()).ByName("organization"); orgID != "" { - i.OrganizationID = uuid.NullUUID{UUID: x.ParseUUID(orgID), Valid: true} + if orgID, err := uuid.FromString(provider.Config().OrganizationID); err == nil { + i.OrganizationID = uuid.NullUUID{UUID: orgID, Valid: true} } s.d.Logger(). @@ -399,7 +398,7 @@ func (s *Strategy) createIdentity(ctx context.Context, w http.ResponseWriter, r return i, va, nil } -func (s *Strategy) setTraits(ctx context.Context, w http.ResponseWriter, r *http.Request, a *registration.Flow, claims *Claims, provider Provider, container *AuthCodeContainer, evaluated string, i *identity.Identity) error { +func (s *Strategy) setTraits(w http.ResponseWriter, r *http.Request, a *registration.Flow, provider Provider, container *AuthCodeContainer, evaluated string, i *identity.Identity) error { jsonTraits := gjson.Get(evaluated, "identity.traits") if !jsonTraits.IsObject() { return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("OpenID Connect Jsonnet mapper did not return an object for key identity.traits. Please check your Jsonnet code!")) @@ -408,7 +407,7 @@ func (s *Strategy) setTraits(ctx context.Context, w http.ResponseWriter, r *http if container != nil { traits, err := merge(container.Traits, json.RawMessage(jsonTraits.Raw)) if err != nil { - return s.handleError(ctx, w, r, a, provider.Config().ID, nil, err) + return s.handleError(w, r, a, provider.Config().ID, nil, err) } i.Traits = traits 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 ce87a6a28fbf..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", @@ -151,9 +171,9 @@ func TestStrategy(t *testing.T) { return ts.URL + login.RouteSubmitFlow + "?flow=" + flowID.String() } - makeRequestWithCookieJar := func(t *testing.T, provider string, action string, fv url.Values, jar *cookiejar.Jar) (*http.Response, []byte) { + makeRequestWithCookieJar := func(t *testing.T, provider string, action string, fv url.Values, jar *cookiejar.Jar, checkRedirect testhelpers.CheckRedirectFunc) (*http.Response, []byte) { fv.Set("provider", provider) - res, err := testhelpers.NewClientWithCookieJar(t, jar, false).PostForm(action, fv) + res, err := testhelpers.NewClientWithCookieJar(t, jar, checkRedirect).PostForm(action, fv) require.NoError(t, err, action) body, err := io.ReadAll(res.Body) @@ -166,12 +186,12 @@ func TestStrategy(t *testing.T) { } makeRequest := func(t *testing.T, provider string, action string, fv url.Values) (*http.Response, []byte) { - return makeRequestWithCookieJar(t, provider, action, fv, nil) + return makeRequestWithCookieJar(t, provider, action, fv, nil, nil) } makeJSONRequest := func(t *testing.T, provider string, action string, fv url.Values) (*http.Response, []byte) { fv.Set("provider", provider) - client := testhelpers.NewClientWithCookieJar(t, nil, false) + client := testhelpers.NewClientWithCookieJar(t, nil, nil) req, err := http.NewRequest("POST", action, strings.NewReader(fv.Encode())) require.NoError(t, err) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") @@ -198,7 +218,7 @@ func TestStrategy(t *testing.T) { var changeLocation flow.BrowserLocationChangeRequiredError require.NoError(t, json.NewDecoder(res.Body).Decode(&changeLocation)) - res, err = testhelpers.NewClientWithCookieJar(t, nil, true).Get(changeLocation.RedirectBrowserTo) + res, err = testhelpers.NewClientWithCookieJar(t, nil, nil).Get(changeLocation.RedirectBrowserTo) require.NoError(t, err) returnToURL = res.Request.URL @@ -240,7 +260,7 @@ func TestStrategy(t *testing.T) { // assert ui error (redirect to login/registration ui endpoint) assertUIError := func(t *testing.T, res *http.Response, body []byte, reason string) { - require.Contains(t, res.Request.URL.String(), uiTS.URL, "status: %d, body: %s", res.StatusCode, body) + require.Contains(t, res.Request.URL.String(), uiTS.URL, "Redirect does not point to UI server. Status: %d, body: %s", res.StatusCode, body) assert.Contains(t, gjson.GetBytes(body, "ui.messages.0.text").String(), reason, "%s", prettyJSON(t, body)) } @@ -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) @@ -570,7 +770,7 @@ func TestStrategy(t *testing.T) { // We essentially run into this bit: // // if authenticated, err := s.alreadyAuthenticated(w, r, req); err != nil { - // s.forwardError(w, r, req, s.handleError(ctx, w, , r, req, pid, nil, err)) + // s.forwardError(w, r, req, s.handleError(w, , r, req, pid, nil, err)) // } else if authenticated { // return <-- we end up here on the second call // } @@ -606,10 +806,7 @@ func TestStrategy(t *testing.T) { require.NoError(t, err) require.NoError(t, res.Body.Close()) - // The reason for `invalid_client` here is that the code was already used and the session was already authenticated. The invalid_client - // happens because of the way Golang's OAuth2 library is trying out different auth methods when a token request fails, which obfuscates - // the underlying error. - assert.Contains(t, string(body), "invalid_client", "%s", body) + assert.Contains(t, string(body), "The authorization code has already been used", "%s", body) }) }) @@ -756,7 +953,7 @@ func TestStrategy(t *testing.T) { Mapper: "file://./stub/oidc.facebook.jsonnet", }, ) - t.Cleanup(oidc.RegisterTestProvider("test-provider")) + oidc.RegisterTestProvider(t, "test-provider") cl := http.Client{} @@ -983,7 +1180,7 @@ func TestStrategy(t *testing.T) { action := assertFormValues(t, r.ID, "valid") fv := url.Values{} fv.Set("provider", "valid") - res, err := testhelpers.NewClientWithCookieJar(t, nil, false).PostForm(action, fv) + res, err := testhelpers.NewClientWithCookieJar(t, nil, nil).PostForm(action, fv) require.NoError(t, err) // Expect to be returned to the hydra instance, that instantiated the request assert.Equal(t, hydra.FakePostLoginURL, res.Request.URL.String()) @@ -1040,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{} @@ -1160,10 +1364,10 @@ func TestStrategy(t *testing.T) { fv := url.Values{"traits.name": {"valid-name"}} jar, _ := cookiejar.New(nil) r1 := newBrowserLoginFlow(t, returnTS.URL, time.Minute) - res1, body1 := makeRequestWithCookieJar(t, "valid", assertFormValues(t, r1.ID, "valid"), fv, jar) + res1, body1 := makeRequestWithCookieJar(t, "valid", assertFormValues(t, r1.ID, "valid"), fv, jar, nil) assertIdentity(t, res1, body1) r2 := newBrowserLoginFlow(t, returnTS.URL, time.Minute) - res2, body2 := makeRequestWithCookieJar(t, "valid", assertFormValues(t, r2.ID, "valid"), fv, jar) + res2, body2 := makeRequestWithCookieJar(t, "valid", assertFormValues(t, r2.ID, "valid"), fv, jar, nil) assertIdentity(t, res2, body2) assert.Equal(t, body1, body2) }) @@ -1175,11 +1379,11 @@ func TestStrategy(t *testing.T) { fv := url.Values{"traits.name": {"valid-name"}} jar, _ := cookiejar.New(nil) r1 := newBrowserLoginFlow(t, returnTS.URL, time.Minute) - res1, body1 := makeRequestWithCookieJar(t, "valid", assertFormValues(t, r1.ID, "valid"), fv, jar) + res1, body1 := makeRequestWithCookieJar(t, "valid", assertFormValues(t, r1.ID, "valid"), fv, jar, nil) assertIdentity(t, res1, body1) r2 := newBrowserLoginFlow(t, returnTS.URL, time.Minute) require.NoError(t, reg.LoginFlowPersister().ForceLoginFlow(context.Background(), r2.ID)) - res2, body2 := makeRequestWithCookieJar(t, "valid", assertFormValues(t, r2.ID, "valid"), fv, jar) + res2, body2 := makeRequestWithCookieJar(t, "valid", assertFormValues(t, r2.ID, "valid"), fv, jar, nil) assertIdentity(t, res2, body2) assert.NotEqual(t, gjson.GetBytes(body1, "id"), gjson.GetBytes(body2, "id")) authAt1, err := time.Parse(time.RFC3339, gjson.GetBytes(body1, "authenticated_at").String()) @@ -1380,7 +1584,7 @@ func TestStrategy(t *testing.T) { require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i2)) }) - client := testhelpers.NewClientWithCookieJar(t, nil, false) + client := testhelpers.NewClientWithCookieJar(t, nil, nil) loginFlow := newLoginFlow(t, returnTS.URL, time.Minute, flow.TypeBrowser) var linkingLoginFlow struct { @@ -1461,7 +1665,7 @@ func TestStrategy(t *testing.T) { }) subject = email1 - client := testhelpers.NewClientWithCookieJar(t, nil, false) + client := testhelpers.NewClientWithCookieJar(t, nil, nil) loginFlow := newLoginFlow(t, returnTS.URL, time.Minute, flow.TypeBrowser) var linkingLoginFlow struct{ ID string } t.Run("step=should fail login and start a new login", func(t *testing.T) { @@ -1595,7 +1799,7 @@ func TestCountActiveFirstFactorCredentials(t *testing.T) { for _, v := range tc.in { in[v.Type] = v } - actual, err := strategy.CountActiveFirstFactorCredentials(nil, in) + actual, err := strategy.CountActiveFirstFactorCredentials(context.Background(), in) require.NoError(t, err) assert.Equal(t, tc.expected, actual) }) @@ -1699,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) diff --git a/selfservice/strategy/password/op_login_test.go b/selfservice/strategy/password/op_login_test.go index 6d8492a2ff3e..1fad1cce1c18 100644 --- a/selfservice/strategy/password/op_login_test.go +++ b/selfservice/strategy/password/op_login_test.go @@ -227,7 +227,7 @@ func TestOAuth2Provider(t *testing.T) { t.Cleanup(func() { conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) }) - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) @@ -298,7 +298,7 @@ func TestOAuth2Provider(t *testing.T) { // to SessionPersistentCookie=false, the user is not // remembered from the previous OAuth2 flow. // The user must then re-authenticate and re-consent. - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) @@ -398,7 +398,7 @@ func TestOAuth2Provider(t *testing.T) { // The user must then only re-consent. conf.MustSet(ctx, config.ViperKeySessionPersistentCookie, true) - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier, pwd := x.NewUUID().String(), "password" @@ -488,7 +488,7 @@ func TestOAuth2Provider(t *testing.T) { }) t.Run("should prompt login even with session with OAuth flow", func(t *testing.T) { - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) @@ -559,7 +559,7 @@ func TestOAuth2Provider(t *testing.T) { }) t.Run("first party clients can skip consent", func(t *testing.T) { - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) @@ -628,7 +628,7 @@ func TestOAuth2Provider(t *testing.T) { }) t.Run("oauth flow with consent remember, skips consent", func(t *testing.T) { - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) @@ -719,7 +719,7 @@ func TestOAuth2Provider(t *testing.T) { }) t.Run("should fail when Hydra session subject doesn't match the subject authenticated by Kratos", func(t *testing.T) { - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier, pwd := x.NewUUID().String(), "password" createIdentity(ctx, reg, t, identifier, pwd) diff --git a/selfservice/strategy/password/op_registration_test.go b/selfservice/strategy/password/op_registration_test.go index 7a0f38270b9a..dc2df971b9f9 100644 --- a/selfservice/strategy/password/op_registration_test.go +++ b/selfservice/strategy/password/op_registration_test.go @@ -261,7 +261,7 @@ func TestOAuth2ProviderRegistration(t *testing.T) { require.Equal(t, http.StatusOK, res.StatusCode) } - registerNewAccount := func(t *testing.T, ctx context.Context, browserClient *http.Client, identifier, password string) { + registerNewAccount := func(t *testing.T, browserClient *http.Client, identifier, password string) { // we need to create a new session directly with kratos f := testhelpers.InitializeRegistrationFlowViaBrowser(t, browserClient, kratosPublicTS, false, false, false) require.NotNil(t, f) @@ -310,7 +310,7 @@ func TestOAuth2ProviderRegistration(t *testing.T) { Scopes: scopes, RedirectURL: clientAppTS.URL, } - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier := x.NewUUID().String() password := x.NewUUID().String() @@ -387,7 +387,7 @@ func TestOAuth2ProviderRegistration(t *testing.T) { RedirectURL: clientAppTS.URL, } - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier := x.NewUUID().String() password := x.NewUUID().String() @@ -418,7 +418,7 @@ func TestOAuth2ProviderRegistration(t *testing.T) { state: &clientAS, }) - registerNewAccount(t, ctx, browserClient, identifier, password) + registerNewAccount(t, browserClient, identifier, password) require.ElementsMatch(t, []callTrace{ RegistrationUI, @@ -472,7 +472,7 @@ func TestOAuth2ProviderRegistration(t *testing.T) { RedirectURL: clientAppTS.URL, } - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier := x.NewUUID().String() password := x.NewUUID().String() @@ -579,7 +579,7 @@ func TestOAuth2ProviderRegistration(t *testing.T) { RedirectURL: clientAppTS.URL, } - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier := x.NewUUID().String() password := x.NewUUID().String() @@ -659,7 +659,7 @@ func TestOAuth2ProviderRegistration(t *testing.T) { RedirectURL: clientAppTS.URL, } - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) identifier := x.NewUUID().String() password := x.NewUUID().String() @@ -776,7 +776,7 @@ func TestOAuth2ProviderRegistration(t *testing.T) { RedirectURL: clientAppTS.URL, } - browserClient := testhelpers.NewClientWithCookieJar(t, nil, false) + browserClient := testhelpers.NewClientWithCookieJar(t, nil, nil) ct := make([]callTrace, 0) diff --git a/selfservice/strategy/webauthn/login.go b/selfservice/strategy/webauthn/login.go index d3a46ddc5085..e2a0ef2a3145 100644 --- a/selfservice/strategy/webauthn/login.go +++ b/selfservice/strategy/webauthn/login.go @@ -184,7 +184,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, return s.loginPasswordless(ctx, w, r, f, &p) } - return s.loginMultiFactor(ctx, w, r, f, sess.IdentityID, &p) + return s.loginMultiFactor(ctx, r, f, sess.IdentityID, &p) } func (s *Strategy) loginPasswordless(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, p *updateLoginFlowWithWebAuthnMethod) (i *identity.Identity, err error) { @@ -300,7 +300,7 @@ func (s *Strategy) loginAuthenticate(ctx context.Context, r *http.Request, f *lo return i, nil } -func (s *Strategy) loginMultiFactor(ctx context.Context, w http.ResponseWriter, r *http.Request, f *login.Flow, identityID uuid.UUID, p *updateLoginFlowWithWebAuthnMethod) (*identity.Identity, error) { +func (s *Strategy) loginMultiFactor(ctx context.Context, r *http.Request, f *login.Flow, identityID uuid.UUID, p *updateLoginFlowWithWebAuthnMethod) (*identity.Identity, error) { if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel2); err != nil { return nil, err } diff --git a/spec/api.json b/spec/api.json index 8a4dbefe714e..c8f296913fa0 100644 --- a/spec/api.json +++ b/spec/api.json @@ -2,7 +2,7 @@ "components": { "responses": { "emptyResponse": { - "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 201." + "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 204." }, "identitySchemas": { "content": { @@ -81,6 +81,9 @@ } }, "schemas": { + "CodeChannel": { + "type": "string" + }, "DefaultError": {}, "Duration": { "description": "A Duration represents the elapsed time between two instants\nas an int64 nanosecond count. The representation limits the\nlargest representable duration to approximately 290 years.", @@ -1071,12 +1074,23 @@ "identityCredentialsCode": { "description": "CredentialsCode represents a one time login/registration code", "properties": { - "address_type": { - "description": "The type of the address for this code", + "addresses": { + "items": { + "$ref": "#/components/schemas/identityCredentialsCodeAddress" + }, + "type": "array" + } + }, + "type": "object" + }, + "identityCredentialsCodeAddress": { + "properties": { + "address": { + "description": "The address for this code", "type": "string" }, - "used_at": { - "$ref": "#/components/schemas/NullTime" + "channel": { + "$ref": "#/components/schemas/CodeChannel" } }, "type": "object" @@ -2723,6 +2737,10 @@ "updateLoginFlowWithCodeMethod": { "description": "Update Login flow using the code method", "properties": { + "address": { + "description": "Address is the address to send the code to, in case that there are multiple addresses. This field\nis only used in two-factor flows and is ineffective for passwordless flows.", + "type": "string" + }, "code": { "description": "Code is the 6 digits code sent to the user", "type": "string" @@ -5611,7 +5629,7 @@ } }, { - "description": "Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows.", + "description": "Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows.\n\nDEPRECATED: This field is deprecated. Please remove it from your requests. The user will now see a choice\nof MFA credentials to choose from to perform the second factor instead.", "in": "query", "name": "via", "schema": { @@ -5711,7 +5729,7 @@ } }, { - "description": "Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows.", + "description": "Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows.\n\nDEPRECATED: This field is deprecated. Please remove it from your requests. The user will now see a choice\nof MFA credentials to choose from to perform the second factor instead.", "in": "query", "name": "via", "schema": { diff --git a/spec/swagger.json b/spec/swagger.json index de605124627b..2952837d4c09 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -1604,7 +1604,7 @@ }, { "type": "string", - "description": "Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows.", + "description": "Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows.\n\nDEPRECATED: This field is deprecated. Please remove it from your requests. The user will now see a choice\nof MFA credentials to choose from to perform the second factor instead.", "name": "via", "in": "query" } @@ -1685,7 +1685,7 @@ }, { "type": "string", - "description": "Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows.", + "description": "Via should contain the identity's credential the code should be sent to. Only relevant in aal2 flows.\n\nDEPRECATED: This field is deprecated. Please remove it from your requests. The user will now see a choice\nof MFA credentials to choose from to perform the second factor instead.", "name": "via", "in": "query" } @@ -3245,6 +3245,9 @@ } }, "definitions": { + "CodeChannel": { + "type": "string" + }, "DefaultError": {}, "Duration": { "description": "A Duration represents the elapsed time between two instants\nas an int64 nanosecond count. The representation limits the\nlargest representable duration to approximately 290 years.", @@ -3259,20 +3262,6 @@ "type": "object", "title": "JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger." }, - "NullTime": { - "description": "NullTime implements the [Scanner] interface so\nit can be used as a scan destination, similar to [NullString].", - "type": "object", - "title": "NullTime represents a [time.Time] that may be null.", - "properties": { - "Time": { - "type": "string", - "format": "date-time" - }, - "Valid": { - "type": "boolean" - } - } - }, "NullUUID": { "description": "NullUUID can be used with the standard sql package to represent a\nUUID value that can be NULL in the database.", "type": "object", @@ -4197,12 +4186,23 @@ "description": "CredentialsCode represents a one time login/registration code", "type": "object", "properties": { - "address_type": { - "description": "The type of the address for this code", + "addresses": { + "type": "array", + "items": { + "$ref": "#/definitions/identityCredentialsCodeAddress" + } + } + } + }, + "identityCredentialsCodeAddress": { + "type": "object", + "properties": { + "address": { + "description": "The address for this code", "type": "string" }, - "used_at": { - "$ref": "#/definitions/NullTime" + "channel": { + "$ref": "#/definitions/CodeChannel" } } }, @@ -5765,6 +5765,10 @@ "csrf_token" ], "properties": { + "address": { + "description": "Address is the address to send the code to, in case that there are multiple addresses. This field\nis only used in two-factor flows and is ineffective for passwordless flows.", + "type": "string" + }, "code": { "description": "Code is the 6 digits code sent to the user", "type": "string" @@ -6678,7 +6682,7 @@ }, "responses": { "emptyResponse": { - "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 201." + "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 204." }, "identitySchemas": { "description": "List Identity JSON Schemas Response", diff --git a/test/e2e/run.sh b/test/e2e/run.sh index 62b5330adafc..a0a6d449fc6c 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -72,8 +72,8 @@ prepare() { if [ -z ${TEST_DATABASE_POSTGRESQL+x} ]; then docker rm -f kratos_test_database_mysql kratos_test_database_postgres kratos_test_database_cockroach || true - docker run --platform linux/amd64 --name kratos_test_database_mysql -p 3444:3306 -e MYSQL_ROOT_PASSWORD=secret -d mysql:5.7 - docker run --name kratos_test_database_postgres -p 3445:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=postgres -d postgres:9.6 postgres -c log_statement=all + docker run --name kratos_test_database_mysql -p 3444:3306 -e MYSQL_ROOT_PASSWORD=secret -d mysql:8.0 + docker run --name kratos_test_database_postgres -p 3445:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=postgres -d postgres:14 postgres -c log_statement=all docker run --name kratos_test_database_cockroach -p 3446:26257 -d cockroachdb/cockroach:v22.2.6 start-single-node --insecure export TEST_DATABASE_MYSQL="mysql://root:secret@(localhost:3444)/mysql?parseTime=true&multiStatements=true" @@ -249,8 +249,8 @@ run() { export DSN=${1} - nc -zv localhost 4434 && exit 1 - nc -zv localhost 4433 && exit 1 + nc -zv localhost 4434 && (echo "Port 4434 unavailable, used by" ; lsof -i:4434 ; exit 1) + nc -zv localhost 4433 && (echo "Port 4433 unavailable, used by" ; lsof -i:4433 ; exit 1) ls -la . for profile in code email mobile oidc recovery recovery-mfa verification mfa spa network passwordless passkey webhooks oidc-provider oidc-provider-mfa two-steps; do diff --git a/x/swagger/swagger_types_global.go b/x/swagger/swagger_types_global.go index 2175ff1eca4e..8057ee495dfa 100644 --- a/x/swagger/swagger_types_global.go +++ b/x/swagger/swagger_types_global.go @@ -10,9 +10,6 @@ import "github.com/ory/herodot" // The standard Ory JSON API error format. // // swagger:model errorGeneric -// -//nolint:deadcode,unused -//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions type ErrorGeneric struct { // Contains error details // @@ -23,10 +20,7 @@ type ErrorGeneric struct { // swagger:model genericError type GenericError struct{ herodot.DefaultError } -// Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 201. +// Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 204. // // swagger:response emptyResponse -// -//nolint:deadcode,unused -//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions -type emptyResponse struct{} +type _ struct{}