diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 26df4ffc97a3..9c2ee0555b42 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -88,13 +88,12 @@ jobs:
- run: npm install
name: Install node deps
- name: Run golangci-lint
- uses: golangci/golangci-lint-action@v4
+ uses: golangci/golangci-lint-action@v6
env:
GOGC: 100
with:
args: --timeout 10m0s
- version: v1.56.2
- skip-pkg-cache: true
+ version: v1.59.1
- name: Build Kratos
run: make install
- name: Run go-acc (tests)
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index a4d098e9826a..1c5519d95843 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -39,7 +39,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v1
+ uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/codeql-config.yml
@@ -51,7 +51,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@v1
+ uses: github/codeql-action/autobuild@v2
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -65,4 +65,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v1
+ uses: github/codeql-action/analyze@v2
diff --git a/.gitignore b/.gitignore
index f792761b2b71..42d798e427ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,3 +65,4 @@ test/e2e/kratos.*.yml
# VSCode debug artifact
__debug_bin
.debug.sqlite.db
+.last-run.json
\ No newline at end of file
diff --git a/.golangci.yml b/.golangci.yml
index 079e952252ba..e83dd5a56a2e 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -19,9 +19,13 @@ linters-settings:
goimports:
local-prefixes: github.com/ory
-run:
- skip-dirs:
+issues:
+ exclude-dirs:
- sdk/
- skip-files:
+ exclude-files:
- ".+_test.go"
- "corpx/faker.go"
+ exclude:
+ - "Set is deprecated: use context-based WithConfigValue instead"
+ - "SetDefaultIdentitySchemaFromRaw is deprecated: Use context-based WithDefaultIdentitySchemaFromRaw instead"
+ - "SetDefaultIdentitySchema is deprecated: Use context-based WithDefaultIdentitySchema instead"
diff --git a/.schema/openapi/patches/schema.yaml b/.schema/openapi/patches/schema.yaml
index 206aceb2708e..ff661ce4079d 100644
--- a/.schema/openapi/patches/schema.yaml
+++ b/.schema/openapi/patches/schema.yaml
@@ -43,6 +43,7 @@
set_ory_session_token: "#/components/schemas/continueWithSetOrySessionToken"
show_settings_ui: "#/components/schemas/continueWithSettingsUi"
show_recovery_ui: "#/components/schemas/continueWithRecoveryUi"
+ redirect_browser_to: "#/components/schemas/continueWithRedirectBrowserTo"
- op: add
path: /components/schemas/continueWith/oneOf
@@ -51,3 +52,4 @@
- "$ref": "#/components/schemas/continueWithSetOrySessionToken"
- "$ref": "#/components/schemas/continueWithSettingsUi"
- "$ref": "#/components/schemas/continueWithRecoveryUi"
+ - "$ref": "#/components/schemas/continueWithRedirectBrowserTo"
diff --git a/.schema/openapi/patches/selfservice.yaml b/.schema/openapi/patches/selfservice.yaml
index 81d82247586b..7887c1c2da74 100644
--- a/.schema/openapi/patches/selfservice.yaml
+++ b/.schema/openapi/patches/selfservice.yaml
@@ -19,6 +19,7 @@
- "$ref": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod"
- "$ref": "#/components/schemas/updateRegistrationFlowWithCodeMethod"
- "$ref": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod"
+ - "$ref": "#/components/schemas/updateRegistrationFlowWithProfileMethod"
- op: add
path: /components/schemas/updateRegistrationFlowBody/discriminator
value:
@@ -28,7 +29,8 @@
oidc: "#/components/schemas/updateRegistrationFlowWithOidcMethod"
webauthn: "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod"
code: "#/components/schemas/updateRegistrationFlowWithCodeMethod"
- passKey: "#/components/schemas/updateRegistrationFlowWithPasskeyMethod"
+ passkey: "#/components/schemas/updateRegistrationFlowWithPasskeyMethod"
+ profile: "#/components/schemas/updateRegistrationFlowWithProfileMethod"
- op: add
path: /components/schemas/registrationFlowState/enum
value:
@@ -50,6 +52,7 @@
- "$ref": "#/components/schemas/updateLoginFlowWithLookupSecretMethod"
- "$ref": "#/components/schemas/updateLoginFlowWithCodeMethod"
- "$ref": "#/components/schemas/updateLoginFlowWithPasskeyMethod"
+ - "$ref": "#/components/schemas/updateLoginFlowWithIdentifierFirstMethod"
- op: add
path: /components/schemas/updateLoginFlowBody/discriminator
value:
@@ -62,6 +65,7 @@
lookup_secret: "#/components/schemas/updateLoginFlowWithLookupSecretMethod"
code: "#/components/schemas/updateLoginFlowWithCodeMethod"
passkey: "#/components/schemas/updateLoginFlowWithPasskeyMethod"
+ identifier_first: "#/components/schemas/updateLoginFlowWithIdentifierFirstMethod"
- op: add
path: /components/schemas/loginFlowState/enum
value:
diff --git a/.schemastore/config.schema.json b/.schemastore/config.schema.json
index 62e00f8baa8f..3695010d0665 100644
--- a/.schemastore/config.schema.json
+++ b/.schemastore/config.schema.json
@@ -432,7 +432,7 @@
},
"provider": {
"title": "Provider",
- "description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon, eparaksts, eparaksts-mobile.",
+ "description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, salesforce, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon, eparaksts, eparaksts-mobile.",
"type": "string",
"enum": [
"github",
@@ -442,6 +442,7 @@
"google",
"microsoft",
"discord",
+ "salesforce",
"slack",
"facebook",
"auth0",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9a081e3be7f..209e32e709f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,324 +5,624 @@
**Table of Contents**
-- [ (2024-04-26)](#2024-04-26)
+- [ (2024-07-19)](#2024-07-19)
- [Breaking Changes](#breaking-changes)
- [Bug Fixes](#bug-fixes)
+ - [Documentation](#documentation)
- [Features](#features)
- [Tests](#tests)
- [Unclassified](#unclassified)
-- [1.1.0 (2024-02-20)](#110-2024-02-20)
+- [1.2.0 (2024-06-05)](#120-2024-06-05)
- [Breaking Changes](#breaking-changes-1)
- [Bug Fixes](#bug-fixes-1)
- [Code Generation](#code-generation)
- - [Documentation](#documentation)
+ - [Documentation](#documentation-1)
- [Features](#features-1)
- - [Reverts](#reverts)
- [Tests](#tests-1)
- [Unclassified](#unclassified-1)
-- [1.0.0 (2023-07-12)](#100-2023-07-12)
- - [Bug Fixes](#bug-fixes-2)
- - [Code Generation](#code-generation-1)
- - [Documentation](#documentation-1)
- - [Features](#features-2)
- - [Tests](#tests-2)
- - [Unclassified](#unclassified-2)
-- [0.13.0 (2023-04-18)](#0130-2023-04-18)
+- [1.1.0 (2024-02-20)](#110-2024-02-20)
- [Breaking Changes](#breaking-changes-2)
- - [Bug Fixes](#bug-fixes-3)
- - [Code Generation](#code-generation-2)
- - [Code Refactoring](#code-refactoring)
+ - [Bug Fixes](#bug-fixes-2)
+ - [Code Generation](#code-generation-1)
- [Documentation](#documentation-2)
- - [Features](#features-3)
- - [Tests](#tests-3)
- - [Unclassified](#unclassified-3)
-- [0.11.1 (2023-01-14)](#0111-2023-01-14)
+ - [Features](#features-2)
+ - [Reverts](#reverts)
+ - [Tests](#tests-2)
+ - [Unclassified](#unclassified-2)
+- [1.0.0 (2023-07-12)](#100-2023-07-12)
+ - [Bug Fixes](#bug-fixes-3)
+ - [Code Generation](#code-generation-2)
+ - [Documentation](#documentation-3)
+ - [Features](#features-3)
+ - [Tests](#tests-3)
+ - [Unclassified](#unclassified-3)
+- [0.13.0 (2023-04-18)](#0130-2023-04-18)
- [Breaking Changes](#breaking-changes-3)
- [Bug Fixes](#bug-fixes-4)
- [Code Generation](#code-generation-3)
- - [Documentation](#documentation-3)
+ - [Code Refactoring](#code-refactoring)
+ - [Documentation](#documentation-4)
- [Features](#features-4)
- [Tests](#tests-4)
-- [0.11.0 (2022-12-02)](#0110-2022-12-02)
- - [Code Generation](#code-generation-4)
- - [Features](#features-5)
-- [0.11.0-alpha.0.pre.2 (2022-11-28)](#0110-alpha0pre2-2022-11-28)
+ - [Unclassified](#unclassified-4)
+- [0.11.1 (2023-01-14)](#0111-2023-01-14)
- [Breaking Changes](#breaking-changes-4)
- [Bug Fixes](#bug-fixes-5)
- - [Code Generation](#code-generation-5)
- - [Code Refactoring](#code-refactoring-1)
- - [Documentation](#documentation-4)
- - [Features](#features-6)
- - [Reverts](#reverts-1)
+ - [Code Generation](#code-generation-4)
+ - [Documentation](#documentation-5)
+ - [Features](#features-5)
- [Tests](#tests-5)
- - [Unclassified](#unclassified-4)
-- [0.10.1 (2022-06-01)](#0101-2022-06-01)
- - [Bug Fixes](#bug-fixes-6)
- - [Code Generation](#code-generation-6)
-- [0.10.0 (2022-05-30)](#0100-2022-05-30)
+- [0.11.0 (2022-12-02)](#0110-2022-12-02)
+ - [Code Generation](#code-generation-5)
+ - [Features](#features-6)
+- [0.11.0-alpha.0.pre.2 (2022-11-28)](#0110-alpha0pre2-2022-11-28)
- [Breaking Changes](#breaking-changes-5)
- - [Bug Fixes](#bug-fixes-7)
- - [Code Generation](#code-generation-7)
- - [Code Refactoring](#code-refactoring-2)
- - [Documentation](#documentation-5)
+ - [Bug Fixes](#bug-fixes-6)
+ - [Code Generation](#code-generation-6)
+ - [Code Refactoring](#code-refactoring-1)
+ - [Documentation](#documentation-6)
- [Features](#features-7)
+ - [Reverts](#reverts-1)
- [Tests](#tests-6)
- [Unclassified](#unclassified-5)
-- [0.9.0-alpha.3 (2022-03-25)](#090-alpha3-2022-03-25)
+- [0.10.1 (2022-06-01)](#0101-2022-06-01)
+ - [Bug Fixes](#bug-fixes-7)
+ - [Code Generation](#code-generation-7)
+- [0.10.0 (2022-05-30)](#0100-2022-05-30)
- [Breaking Changes](#breaking-changes-6)
- [Bug Fixes](#bug-fixes-8)
- [Code Generation](#code-generation-8)
- - [Documentation](#documentation-6)
-- [0.9.0-alpha.2 (2022-03-22)](#090-alpha2-2022-03-22)
- - [Bug Fixes](#bug-fixes-9)
- - [Code Generation](#code-generation-9)
-- [0.9.0-alpha.1 (2022-03-21)](#090-alpha1-2022-03-21)
- - [Breaking Changes](#breaking-changes-7)
- - [Bug Fixes](#bug-fixes-10)
- - [Code Generation](#code-generation-10)
- - [Code Refactoring](#code-refactoring-3)
+ - [Code Refactoring](#code-refactoring-2)
- [Documentation](#documentation-7)
- [Features](#features-8)
- [Tests](#tests-7)
- [Unclassified](#unclassified-6)
-- [0.8.3-alpha.1.pre.0 (2022-01-21)](#083-alpha1pre0-2022-01-21)
+- [0.9.0-alpha.3 (2022-03-25)](#090-alpha3-2022-03-25)
+ - [Breaking Changes](#breaking-changes-7)
+ - [Bug Fixes](#bug-fixes-9)
+ - [Code Generation](#code-generation-9)
+ - [Documentation](#documentation-8)
+- [0.9.0-alpha.2 (2022-03-22)](#090-alpha2-2022-03-22)
+ - [Bug Fixes](#bug-fixes-10)
+ - [Code Generation](#code-generation-10)
+- [0.9.0-alpha.1 (2022-03-21)](#090-alpha1-2022-03-21)
- [Breaking Changes](#breaking-changes-8)
- [Bug Fixes](#bug-fixes-11)
- [Code Generation](#code-generation-11)
- - [Code Refactoring](#code-refactoring-4)
- - [Documentation](#documentation-8)
+ - [Code Refactoring](#code-refactoring-3)
+ - [Documentation](#documentation-9)
- [Features](#features-9)
- [Tests](#tests-8)
+ - [Unclassified](#unclassified-7)
+- [0.8.3-alpha.1.pre.0 (2022-01-21)](#083-alpha1pre0-2022-01-21)
+ - [Breaking Changes](#breaking-changes-9)
+ - [Bug Fixes](#bug-fixes-12)
+ - [Code Generation](#code-generation-12)
+ - [Code Refactoring](#code-refactoring-4)
+ - [Documentation](#documentation-10)
+ - [Features](#features-10)
+ - [Tests](#tests-9)
- [0.8.2-alpha.1 (2021-12-17)](#082-alpha1-2021-12-17)
- - [Bug Fixes](#bug-fixes-12)
- - [Code Generation](#code-generation-12)
- - [Documentation](#documentation-9)
-- [0.8.1-alpha.1 (2021-12-13)](#081-alpha1-2021-12-13)
- [Bug Fixes](#bug-fixes-13)
- [Code Generation](#code-generation-13)
- - [Documentation](#documentation-10)
- - [Features](#features-10)
- - [Tests](#tests-9)
+ - [Documentation](#documentation-11)
+- [0.8.1-alpha.1 (2021-12-13)](#081-alpha1-2021-12-13)
+ - [Bug Fixes](#bug-fixes-14)
+ - [Code Generation](#code-generation-14)
+ - [Documentation](#documentation-12)
+ - [Features](#features-11)
+ - [Tests](#tests-10)
- [0.8.0-alpha.4.pre.0 (2021-11-09)](#080-alpha4pre0-2021-11-09)
- - [Breaking Changes](#breaking-changes-9)
- - [Bug Fixes](#bug-fixes-14)
- - [Code Generation](#code-generation-14)
- - [Documentation](#documentation-11)
- - [Features](#features-11)
- - [Tests](#tests-10)
+ - [Breaking Changes](#breaking-changes-10)
+ - [Bug Fixes](#bug-fixes-15)
+ - [Code Generation](#code-generation-15)
+ - [Documentation](#documentation-13)
+ - [Features](#features-12)
+ - [Tests](#tests-11)
- [0.8.0-alpha.3 (2021-10-28)](#080-alpha3-2021-10-28)
- - [Bug Fixes](#bug-fixes-15)
- - [Code Generation](#code-generation-15)
-- [0.8.0-alpha.2 (2021-10-28)](#080-alpha2-2021-10-28)
+ - [Bug Fixes](#bug-fixes-16)
- [Code Generation](#code-generation-16)
+- [0.8.0-alpha.2 (2021-10-28)](#080-alpha2-2021-10-28)
+ - [Code Generation](#code-generation-17)
- [0.8.0-alpha.1 (2021-10-27)](#080-alpha1-2021-10-27)
- - [Breaking Changes](#breaking-changes-10)
- - [Bug Fixes](#bug-fixes-16)
- - [Code Generation](#code-generation-17)
+ - [Breaking Changes](#breaking-changes-11)
+ - [Bug Fixes](#bug-fixes-17)
+ - [Code Generation](#code-generation-18)
- [Code Refactoring](#code-refactoring-5)
- - [Documentation](#documentation-12)
- - [Features](#features-12)
+ - [Documentation](#documentation-14)
+ - [Features](#features-13)
- [Reverts](#reverts-2)
- - [Tests](#tests-11)
- - [Unclassified](#unclassified-7)
+ - [Tests](#tests-12)
+ - [Unclassified](#unclassified-8)
- [0.7.6-alpha.1 (2021-09-12)](#076-alpha1-2021-09-12)
- - [Code Generation](#code-generation-18)
-- [0.7.5-alpha.1 (2021-09-11)](#075-alpha1-2021-09-11)
- [Code Generation](#code-generation-19)
-- [0.7.4-alpha.1 (2021-09-09)](#074-alpha1-2021-09-09)
- - [Bug Fixes](#bug-fixes-17)
+- [0.7.5-alpha.1 (2021-09-11)](#075-alpha1-2021-09-11)
- [Code Generation](#code-generation-20)
- - [Documentation](#documentation-13)
- - [Features](#features-13)
- - [Tests](#tests-12)
-- [0.7.3-alpha.1 (2021-08-28)](#073-alpha1-2021-08-28)
+- [0.7.4-alpha.1 (2021-09-09)](#074-alpha1-2021-09-09)
- [Bug Fixes](#bug-fixes-18)
- [Code Generation](#code-generation-21)
- - [Documentation](#documentation-14)
+ - [Documentation](#documentation-15)
- [Features](#features-14)
-- [0.7.1-alpha.1 (2021-07-22)](#071-alpha1-2021-07-22)
+ - [Tests](#tests-13)
+- [0.7.3-alpha.1 (2021-08-28)](#073-alpha1-2021-08-28)
- [Bug Fixes](#bug-fixes-19)
- [Code Generation](#code-generation-22)
- - [Documentation](#documentation-15)
- - [Tests](#tests-13)
+ - [Documentation](#documentation-16)
+ - [Features](#features-15)
+- [0.7.1-alpha.1 (2021-07-22)](#071-alpha1-2021-07-22)
+ - [Bug Fixes](#bug-fixes-20)
+ - [Code Generation](#code-generation-23)
+ - [Documentation](#documentation-17)
+ - [Tests](#tests-14)
- [0.7.0-alpha.1 (2021-07-13)](#070-alpha1-2021-07-13)
- - [Breaking Changes](#breaking-changes-11)
- - [Bug Fixes](#bug-fixes-20)
- - [Code Generation](#code-generation-23)
- - [Code Refactoring](#code-refactoring-6)
- - [Documentation](#documentation-16)
- - [Features](#features-15)
- - [Tests](#tests-14)
- - [Unclassified](#unclassified-8)
-- [0.6.3-alpha.1 (2021-05-17)](#063-alpha1-2021-05-17)
- [Breaking Changes](#breaking-changes-12)
- [Bug Fixes](#bug-fixes-21)
- [Code Generation](#code-generation-24)
+ - [Code Refactoring](#code-refactoring-6)
+ - [Documentation](#documentation-18)
+ - [Features](#features-16)
+ - [Tests](#tests-15)
+ - [Unclassified](#unclassified-9)
+- [0.6.3-alpha.1 (2021-05-17)](#063-alpha1-2021-05-17)
+ - [Breaking Changes](#breaking-changes-13)
+ - [Bug Fixes](#bug-fixes-22)
+ - [Code Generation](#code-generation-25)
- [Code Refactoring](#code-refactoring-7)
- [0.6.2-alpha.1 (2021-05-14)](#062-alpha1-2021-05-14)
- - [Code Generation](#code-generation-25)
- - [Documentation](#documentation-17)
-- [0.6.1-alpha.1 (2021-05-11)](#061-alpha1-2021-05-11)
- [Code Generation](#code-generation-26)
- - [Features](#features-16)
-- [0.6.0-alpha.2 (2021-05-07)](#060-alpha2-2021-05-07)
- - [Bug Fixes](#bug-fixes-22)
+ - [Documentation](#documentation-19)
+- [0.6.1-alpha.1 (2021-05-11)](#061-alpha1-2021-05-11)
- [Code Generation](#code-generation-27)
- [Features](#features-17)
+- [0.6.0-alpha.2 (2021-05-07)](#060-alpha2-2021-05-07)
+ - [Bug Fixes](#bug-fixes-23)
+ - [Code Generation](#code-generation-28)
+ - [Features](#features-18)
- [0.6.0-alpha.1 (2021-05-05)](#060-alpha1-2021-05-05)
- - [Breaking Changes](#breaking-changes-13)
- - [Bug Fixes](#bug-fixes-23)
- - [Code Generation](#code-generation-28)
+ - [Breaking Changes](#breaking-changes-14)
+ - [Bug Fixes](#bug-fixes-24)
+ - [Code Generation](#code-generation-29)
- [Code Refactoring](#code-refactoring-8)
- - [Documentation](#documentation-18)
- - [Features](#features-18)
- - [Tests](#tests-15)
- - [Unclassified](#unclassified-9)
+ - [Documentation](#documentation-20)
+ - [Features](#features-19)
+ - [Tests](#tests-16)
+ - [Unclassified](#unclassified-10)
- [0.5.5-alpha.1 (2020-12-09)](#055-alpha1-2020-12-09)
- - [Bug Fixes](#bug-fixes-24)
- - [Code Generation](#code-generation-29)
- - [Documentation](#documentation-19)
- - [Features](#features-19)
- - [Tests](#tests-16)
- - [Unclassified](#unclassified-10)
-- [0.5.4-alpha.1 (2020-11-11)](#054-alpha1-2020-11-11)
- [Bug Fixes](#bug-fixes-25)
- [Code Generation](#code-generation-30)
- - [Code Refactoring](#code-refactoring-9)
- - [Documentation](#documentation-20)
+ - [Documentation](#documentation-21)
- [Features](#features-20)
-- [0.5.3-alpha.1 (2020-10-27)](#053-alpha1-2020-10-27)
+ - [Tests](#tests-17)
+ - [Unclassified](#unclassified-11)
+- [0.5.4-alpha.1 (2020-11-11)](#054-alpha1-2020-11-11)
- [Bug Fixes](#bug-fixes-26)
- [Code Generation](#code-generation-31)
- - [Documentation](#documentation-21)
+ - [Code Refactoring](#code-refactoring-9)
+ - [Documentation](#documentation-22)
- [Features](#features-21)
- - [Tests](#tests-17)
-- [0.5.2-alpha.1 (2020-10-22)](#052-alpha1-2020-10-22)
+- [0.5.3-alpha.1 (2020-10-27)](#053-alpha1-2020-10-27)
- [Bug Fixes](#bug-fixes-27)
- [Code Generation](#code-generation-32)
- - [Documentation](#documentation-22)
+ - [Documentation](#documentation-23)
+ - [Features](#features-22)
- [Tests](#tests-18)
-- [0.5.1-alpha.1 (2020-10-20)](#051-alpha1-2020-10-20)
+- [0.5.2-alpha.1 (2020-10-22)](#052-alpha1-2020-10-22)
- [Bug Fixes](#bug-fixes-28)
- [Code Generation](#code-generation-33)
- - [Documentation](#documentation-23)
- - [Features](#features-22)
+ - [Documentation](#documentation-24)
- [Tests](#tests-19)
- - [Unclassified](#unclassified-11)
+- [0.5.1-alpha.1 (2020-10-20)](#051-alpha1-2020-10-20)
+ - [Bug Fixes](#bug-fixes-29)
+ - [Code Generation](#code-generation-34)
+ - [Documentation](#documentation-25)
+ - [Features](#features-23)
+ - [Tests](#tests-20)
+ - [Unclassified](#unclassified-12)
- [0.5.0-alpha.1 (2020-10-15)](#050-alpha1-2020-10-15)
- - [Breaking Changes](#breaking-changes-14)
- - [Bug Fixes](#bug-fixes-29)
- - [Code Generation](#code-generation-34)
+ - [Breaking Changes](#breaking-changes-15)
+ - [Bug Fixes](#bug-fixes-30)
+ - [Code Generation](#code-generation-35)
- [Code Refactoring](#code-refactoring-10)
- - [Documentation](#documentation-24)
- - [Features](#features-23)
- - [Tests](#tests-20)
- - [Unclassified](#unclassified-12)
+ - [Documentation](#documentation-26)
+ - [Features](#features-24)
+ - [Tests](#tests-21)
+ - [Unclassified](#unclassified-13)
- [0.4.6-alpha.1 (2020-07-13)](#046-alpha1-2020-07-13)
- - [Bug Fixes](#bug-fixes-30)
- - [Code Generation](#code-generation-35)
-- [0.4.5-alpha.1 (2020-07-13)](#045-alpha1-2020-07-13)
- [Bug Fixes](#bug-fixes-31)
- [Code Generation](#code-generation-36)
-- [0.4.4-alpha.1 (2020-07-10)](#044-alpha1-2020-07-10)
+- [0.4.5-alpha.1 (2020-07-13)](#045-alpha1-2020-07-13)
- [Bug Fixes](#bug-fixes-32)
- [Code Generation](#code-generation-37)
- - [Documentation](#documentation-25)
-- [0.4.3-alpha.1 (2020-07-08)](#043-alpha1-2020-07-08)
+- [0.4.4-alpha.1 (2020-07-10)](#044-alpha1-2020-07-10)
- [Bug Fixes](#bug-fixes-33)
- [Code Generation](#code-generation-38)
-- [0.4.2-alpha.1 (2020-07-08)](#042-alpha1-2020-07-08)
+ - [Documentation](#documentation-27)
+- [0.4.3-alpha.1 (2020-07-08)](#043-alpha1-2020-07-08)
- [Bug Fixes](#bug-fixes-34)
- [Code Generation](#code-generation-39)
+- [0.4.2-alpha.1 (2020-07-08)](#042-alpha1-2020-07-08)
+ - [Bug Fixes](#bug-fixes-35)
+ - [Code Generation](#code-generation-40)
- [0.4.0-alpha.1 (2020-07-08)](#040-alpha1-2020-07-08)
- - [Breaking Changes](#breaking-changes-15)
- - [Bug Fixes](#bug-fixes-35)
- - [Code Generation](#code-generation-40)
- - [Code Refactoring](#code-refactoring-11)
- - [Documentation](#documentation-26)
- - [Features](#features-24)
- - [Unclassified](#unclassified-13)
-- [0.3.0-alpha.1 (2020-05-15)](#030-alpha1-2020-05-15)
- [Breaking Changes](#breaking-changes-16)
- [Bug Fixes](#bug-fixes-36)
- - [Chores](#chores)
- - [Code Refactoring](#code-refactoring-12)
- - [Documentation](#documentation-27)
+ - [Code Generation](#code-generation-41)
+ - [Code Refactoring](#code-refactoring-11)
+ - [Documentation](#documentation-28)
- [Features](#features-25)
- [Unclassified](#unclassified-14)
-- [0.2.1-alpha.1 (2020-05-05)](#021-alpha1-2020-05-05)
- - [Chores](#chores-1)
- - [Documentation](#documentation-28)
-- [0.2.0-alpha.2 (2020-05-04)](#020-alpha2-2020-05-04)
+- [0.3.0-alpha.1 (2020-05-15)](#030-alpha1-2020-05-15)
- [Breaking Changes](#breaking-changes-17)
- [Bug Fixes](#bug-fixes-37)
- - [Chores](#chores-2)
- - [Code Refactoring](#code-refactoring-13)
+ - [Chores](#chores)
+ - [Code Refactoring](#code-refactoring-12)
- [Documentation](#documentation-29)
- [Features](#features-26)
- [Unclassified](#unclassified-15)
+- [0.2.1-alpha.1 (2020-05-05)](#021-alpha1-2020-05-05)
+ - [Chores](#chores-1)
+ - [Documentation](#documentation-30)
+- [0.2.0-alpha.2 (2020-05-04)](#020-alpha2-2020-05-04)
+ - [Breaking Changes](#breaking-changes-18)
+ - [Bug Fixes](#bug-fixes-38)
+ - [Chores](#chores-2)
+ - [Code Refactoring](#code-refactoring-13)
+ - [Documentation](#documentation-31)
+ - [Features](#features-27)
+ - [Unclassified](#unclassified-16)
- [0.1.1-alpha.1 (2020-02-18)](#011-alpha1-2020-02-18)
- - [Bug Fixes](#bug-fixes-38)
+ - [Bug Fixes](#bug-fixes-39)
- [Code Refactoring](#code-refactoring-14)
- - [Documentation](#documentation-30)
+ - [Documentation](#documentation-32)
- [0.1.0-alpha.6 (2020-02-16)](#010-alpha6-2020-02-16)
- - [Bug Fixes](#bug-fixes-39)
+ - [Bug Fixes](#bug-fixes-40)
- [Code Refactoring](#code-refactoring-15)
- - [Documentation](#documentation-31)
- - [Features](#features-27)
-- [0.1.0-alpha.5 (2020-02-06)](#010-alpha5-2020-02-06)
- - [Documentation](#documentation-32)
+ - [Documentation](#documentation-33)
- [Features](#features-28)
+- [0.1.0-alpha.5 (2020-02-06)](#010-alpha5-2020-02-06)
+ - [Documentation](#documentation-34)
+ - [Features](#features-29)
- [0.1.0-alpha.4 (2020-02-06)](#010-alpha4-2020-02-06)
- [Continuous Integration](#continuous-integration)
- - [Documentation](#documentation-33)
+ - [Documentation](#documentation-35)
- [0.1.0-alpha.3 (2020-02-06)](#010-alpha3-2020-02-06)
- [Continuous Integration](#continuous-integration-1)
- [0.1.0-alpha.2 (2020-02-03)](#010-alpha2-2020-02-03)
- - [Bug Fixes](#bug-fixes-40)
- - [Documentation](#documentation-34)
- - [Features](#features-29)
- - [Unclassified](#unclassified-16)
+ - [Bug Fixes](#bug-fixes-41)
+ - [Documentation](#documentation-36)
+ - [Features](#features-30)
+ - [Unclassified](#unclassified-17)
- [0.1.0-alpha.1 (2020-01-31)](#010-alpha1-2020-01-31)
- - [Documentation](#documentation-35)
+ - [Documentation](#documentation-37)
- [0.0.3-alpha.15 (2020-01-31)](#003-alpha15-2020-01-31)
- - [Unclassified](#unclassified-17)
-- [0.0.3-alpha.14 (2020-01-31)](#003-alpha14-2020-01-31)
- [Unclassified](#unclassified-18)
-- [0.0.3-alpha.13 (2020-01-31)](#003-alpha13-2020-01-31)
+- [0.0.3-alpha.14 (2020-01-31)](#003-alpha14-2020-01-31)
- [Unclassified](#unclassified-19)
-- [0.0.3-alpha.11 (2020-01-31)](#003-alpha11-2020-01-31)
+- [0.0.3-alpha.13 (2020-01-31)](#003-alpha13-2020-01-31)
- [Unclassified](#unclassified-20)
-- [0.0.3-alpha.10 (2020-01-31)](#003-alpha10-2020-01-31)
+- [0.0.3-alpha.11 (2020-01-31)](#003-alpha11-2020-01-31)
- [Unclassified](#unclassified-21)
-- [0.0.3-alpha.7 (2020-01-30)](#003-alpha7-2020-01-30)
+- [0.0.3-alpha.10 (2020-01-31)](#003-alpha10-2020-01-31)
- [Unclassified](#unclassified-22)
+- [0.0.3-alpha.7 (2020-01-30)](#003-alpha7-2020-01-30)
+ - [Unclassified](#unclassified-23)
- [0.0.3-alpha.5 (2020-01-30)](#003-alpha5-2020-01-30)
- [Continuous Integration](#continuous-integration-2)
- - [Unclassified](#unclassified-23)
-- [0.0.3-alpha.4 (2020-01-30)](#003-alpha4-2020-01-30)
- [Unclassified](#unclassified-24)
-- [0.0.3-alpha.2 (2020-01-30)](#003-alpha2-2020-01-30)
+- [0.0.3-alpha.4 (2020-01-30)](#003-alpha4-2020-01-30)
- [Unclassified](#unclassified-25)
-- [0.0.3-alpha.1 (2020-01-30)](#003-alpha1-2020-01-30)
+- [0.0.3-alpha.2 (2020-01-30)](#003-alpha2-2020-01-30)
- [Unclassified](#unclassified-26)
+- [0.0.3-alpha.1 (2020-01-30)](#003-alpha1-2020-01-30)
+ - [Unclassified](#unclassified-27)
- [0.0.1-alpha.9 (2020-01-29)](#001-alpha9-2020-01-29)
- [Continuous Integration](#continuous-integration-3)
- [0.0.2-alpha.1 (2020-01-29)](#002-alpha1-2020-01-29)
- - [Unclassified](#unclassified-27)
+ - [Unclassified](#unclassified-28)
- [0.0.1-alpha.6 (2020-01-29)](#001-alpha6-2020-01-29)
- [Continuous Integration](#continuous-integration-4)
- [0.0.1-alpha.5 (2020-01-29)](#001-alpha5-2020-01-29)
- [Continuous Integration](#continuous-integration-5)
- - [Unclassified](#unclassified-28)
+ - [Unclassified](#unclassified-29)
- [0.0.1-alpha.3 (2020-01-28)](#001-alpha3-2020-01-28)
- [Continuous Integration](#continuous-integration-6)
- - [Documentation](#documentation-36)
- - [Unclassified](#unclassified-29)
+ - [Documentation](#documentation-38)
+ - [Unclassified](#unclassified-30)
-# [](https://github.com/ory/kratos/compare/v1.1.0...v) (2024-04-26)
+# [](https://github.com/ory/kratos/compare/v1.2.0...v) (2024-07-19)
+
+## Breaking Changes
+
+Going forward, the `/admin/session/.../extend` endpoint will return 204 no
+content for new Ory Network projects. We will deprecate returning 200 + session
+body in the future.
+
+### Bug Fixes
+
+- Add continue with only for json browser requests
+ ([#4002](https://github.com/ory/kratos/issues/4002))
+ ([e0a4010](https://github.com/ory/kratos/commit/e0a4010b84b43f364be14414a380c872b166274d))
+- Add fallback to providerLabel
+ ([#3999](https://github.com/ory/kratos/issues/3999))
+ ([d26f204](https://github.com/ory/kratos/commit/d26f2042eb5325a8d639c08d95a005724e61cb8e)):
+
+ This adds a fallback to the provider label when trying to register a duplicate
+ identifier with an oidc.
+
+ Current error message:
+
+ `Signing in will link your account to "test@test.com" at provider "". If you do not wish to link that account, please start a new login flow.`
+
+ The label represents an optional label for the UI, but in my case it's always
+ empty. I suggest we fallback to the provider when the label is not present. In
+ case the label is present, the behaviour won't change.
+
+ Fallback to provider:
+
+ `Signing in will link your account to "test@test.com" at provider "google". If you do not wish to link that account, please start a new login flow.`
+
+- Add missing JS triggers
+ ([7597bc6](https://github.com/ory/kratos/commit/7597bc6345848b66161d5a9b7a42307bbc85c978))
+- Jsonnet timeouts ([#3979](https://github.com/ory/kratos/issues/3979))
+ ([7c5299f](https://github.com/ory/kratos/commit/7c5299f1f832ebbe0622d0920b7a91253d26b06c))
+- Move password migration hook config
+ ([#3986](https://github.com/ory/kratos/issues/3986))
+ ([b5a66e0](https://github.com/ory/kratos/commit/b5a66e0dde3a8fa6fdeb727482481b6302589631)):
+
+ This moves the password migration hook to
+
+ ```yaml
+ selfservice:
+ methods:
+ password:
+ config:
+ migrate_hook: ...
+ ```
+
+- Password migration hook config
+ ([#4001](https://github.com/ory/kratos/issues/4001))
+ ([50deedf](https://github.com/ory/kratos/commit/50deedfeecf7adbc948521371b181306a0c26cf1)):
+
+ This fixes the config loading for the password migration hook.
+
+- Pw migration param ([#3998](https://github.com/ory/kratos/issues/3998))
+ ([6016cc8](https://github.com/ory/kratos/commit/6016cc88a076eeea71a85d75cfb5191808b69844))
+- Remove flows from log messages
+ ([#3913](https://github.com/ory/kratos/issues/3913))
+ ([310a405](https://github.com/ory/kratos/commit/310a405202c6b44633b15ad30e1fdb8ebd153e4b))
+- Replace submit with continue button for recovery and verification and add
+ maxlength
+ ([04850f4](https://github.com/ory/kratos/commit/04850f45cfbdc89223366ffa3b540d579a3b44be))
+- Timestamp precision on mysql
+ ([9a1f171](https://github.com/ory/kratos/commit/9a1f171c1a4a8d20dc2103073bdc11ee3fdc70af))
+- Typo in login link CLI error messages
+ ([#3995](https://github.com/ory/kratos/issues/3995))
+ ([8350625](https://github.com/ory/kratos/commit/835062542077b9dd8d6a30836d0455adb015265d))
+
+### Documentation
+
+- Typo in changelog
+ ([c508980](https://github.com/ory/kratos/commit/c5089801af2a656e9c1fc371a11aeb23918ba359))
+
+### Features
+
+- Add additional messages
+ ([735fc5b](https://github.com/ory/kratos/commit/735fc5b2c5a99746d3012cc38ee2e1b7cc3a67f2))
+- Add browser return_to continue_with action
+ ([7b636d8](https://github.com/ory/kratos/commit/7b636d860c6917cb1133d6d1d7401808adb890c7))
+- Add if method to sdk
+ ([612e3bf](https://github.com/ory/kratos/commit/612e3bf09dbffd3feba08d5100bffbc39cbd240a))
+- Add redirect to continue_with for SPA flows
+ ([99c945c](https://github.com/ory/kratos/commit/99c945c92d0c2745dc8df4402d755afd53e1b9aa)):
+
+ This patch adds the new `continue_with` action `redirect_browser_to`, which
+ contains the redirect URL the app should redirect to. It is only supported for
+ SPA (not server-side browser apps, not native apps) flows at this point in
+ time.
+
+- Add social providers to credential discovery as well
+ ([5f4a2bf](https://github.com/ory/kratos/commit/5f4a2bf619d540d45e96586129c8ee1e7850e745))
+- Add support for Salesforce as identity provider
+ ([#4003](https://github.com/ory/kratos/issues/4003))
+ ([3bf1ca9](https://github.com/ory/kratos/commit/3bf1ca9030555df90ef9903c34313ae4bd1fecae))
+- Add tests for two step login
+ ([#3959](https://github.com/ory/kratos/issues/3959))
+ ([8225e40](https://github.com/ory/kratos/commit/8225e40e3d767e945006b33eebdfc47fd242ff06))
+- Allow deletion of an individual OIDC credential
+ ([#3968](https://github.com/ory/kratos/issues/3968))
+ ([a43cef2](https://github.com/ory/kratos/commit/a43cef23c177acddbf8b03afef087feeaca51981)):
+
+ This extends the existing `DELETE /admin/identities/{id}/credentials/{type}`
+ API to accept an `?identifier=foobar` query parameter for `{type}==oidc` like
+ such:
+
+ `DELETE /admin/identities/{id}/credentials/oidc?identifier=github%3A012345`
+
+ This will delete the GitHub OIDC credential with the identifier
+ `github:012345` (`012345` is the subject as returned by GitHub).
+
+ To find out which OIDC credentials exist, call
+ `GET /admin/identities/{id}?include_credential=oidc` beforehand.
+
+ This will allow you to delete individual OIDC credentials for users even if
+ they have several set up.
+
+- Better detection if credentials exist on identifier first login
+ ([#3963](https://github.com/ory/kratos/issues/3963))
+ ([42ade94](https://github.com/ory/kratos/commit/42ade94e32a9a7ad6c0bda785e86d7209c46d8bb))
+- Clarify session extend behavior
+ ([#3962](https://github.com/ory/kratos/issues/3962))
+ ([af5ea35](https://github.com/ory/kratos/commit/af5ea35759e74d7a1637823abcc21dc8e3e39a9d))
+- Identifier first auth
+ ([1bdc19a](https://github.com/ory/kratos/commit/1bdc19ae3e1a3df38234cb892f65de4a2c95f041))
+- Identifier first login for all first factor login methods
+ ([638b274](https://github.com/ory/kratos/commit/638b27431312bcd91844ac4a00733a840976aa4f))
+- Improve session extend performance
+ ([#3948](https://github.com/ory/kratos/issues/3948))
+ ([4e3fad4](https://github.com/ory/kratos/commit/4e3fad4b4739b5cf00d658155350cb599f2cd06a)):
+
+ This patch improves the performance for extending session lifespans. Lifespan
+ extension is tricky as it is often part of the middleware of Ory Kratos
+ consumers. As such, it is prone to transaction contention when we read and
+ write to the same session row at the same time (and potentially multiple
+ times).
+
+ To address this, we:
+
+ 1. Introduce a locking mechanism on the row to reduce transaction contention;
+ 2. Add a new feature flag that toggles returning 204 no content instead of
+ 200 + session.
+
+ Be aware that all reads on the session table will have to wait for the
+ transaction to commit before they return a value. This may cause long(er)
+ response times on `/session/whoami` for sessions that are being extended at
+ the same time.
+
+- Password migration hook ([#3978](https://github.com/ory/kratos/issues/3978))
+ ([c9d5573](https://github.com/ory/kratos/commit/c9d55730a10b71ac61bb5097f5f9c33f144f2a95)):
+
+ This adds a password migration hook to easily migrate passwords for which we
+ do not have the hash.
+
+ For each user that needs to be migrated to Ory Network, a new identity is
+ created with a credential of type password with a config of
+ {"use_password_migration_hook": true} . When a user logs in, the credential
+ identifier and password will be sent to the password_migration web hook if all
+ of these are true: The user’s identity’s password credential is
+ {"use_password_migration_hook": true} The password_migration hook is
+ configured After calling the password_migration hook, the HTTP status code
+ will be inspected: On 200, we parse the response as JSON and look for
+ {"status": "password_match"}. The password credential config will be replaced
+ with the hash of the actual password. On any other status code, we assume that
+ the password is not valid.
+
+- **sdk:** Add missing profile discriminator to update registration
+ ([0150795](https://github.com/ory/kratos/commit/0150795d902dcc7cfb2298c3b5a98da1c2541e46))
+- **sdk:** Avoid eval with javascript triggers
+ ([dd6e53d](https://github.com/ory/kratos/commit/dd6e53d62f343a317edf403218b20599539218c6)):
+
+ Using `OnLoadTrigger` and `OnClickTrigger` one can now map the trigger to the
+ corresponding JavaScript function.
+
+ For example, trigger `{"on_click_trigger":"oryWebAuthnRegistration"}` should
+ be translated to `window.oryWebAuthnRegistration()`:
+
+ ```
+ if (attrs.onClickTrigger) {
+ window[attrs.onClickTrigger]()
+ }
+ ```
+
+- Separate 2fa refresh from 1st factor refresh
+ ([#3961](https://github.com/ory/kratos/issues/3961))
+ ([89355d8](https://github.com/ory/kratos/commit/89355d86258ace19c03fcb38dd3861f88e28af59))
+- Set maxlength for totp input
+ ([51042d9](https://github.com/ory/kratos/commit/51042d99fab301f0bb44665e56c5a2364e7d8866))
+
+### Tests
+
+- Add form hydration tests for code login
+ ([37781a9](https://github.com/ory/kratos/commit/37781a93dda9b8f0127217a6b0ac2434dda1cc58))
+- Add form hydration tests for idfirst login
+ ([633b0ba](https://github.com/ory/kratos/commit/633b0ba7f724374f4c02128a5b0f748bd2e9413e))
+- Add form hydration tests for oidc login
+ ([df0cdcb](https://github.com/ory/kratos/commit/df0cdcb424cae6c49143ef2ef2d0b2c95f14fffb))
+- Add form hydration tests for passkey login
+ ([a777854](https://github.com/ory/kratos/commit/a777854e8d99336ab8f5755fdbc9d257e5edd1c0))
+- Add form hydration tests for password login
+ ([7186e7e](https://github.com/ory/kratos/commit/7186e7e060e04a4918e22e0b03fefbf4eb9f4a4b))
+- Add form hydration tests for webauthn login
+ ([8b68163](https://github.com/ory/kratos/commit/8b68163a3f293f7dceb58397f0ef555f1d8fd7c3))
+- Add tests for idfirst
+ ([5f76c15](https://github.com/ory/kratos/commit/5f76c1565e89bfb99f23c3f0f3a9beadbdfa270c))
+- Deflake and parallelize persister tests
+ ([#3953](https://github.com/ory/kratos/issues/3953))
+ ([61f87d9](https://github.com/ory/kratos/commit/61f87d90bd67e5bb1f00ee110d986e4f72fc4c91))
+- Deflake session extend config side-effect
+ ([#3950](https://github.com/ory/kratos/issues/3950))
+ ([b192c92](https://github.com/ory/kratos/commit/b192c92d6c969d470d6479bc33dbc351d327c1f9))
+- Enable server-side config from context
+ ([#3954](https://github.com/ory/kratos/issues/3954))
+ ([e0001b0](https://github.com/ory/kratos/commit/e0001b0db784457652581366bd7ead7cdf6b3898))
+- Resolve issues and update snapshots for all selfservice strategies
+ ([e2e81ac](https://github.com/ory/kratos/commit/e2e81ac16726b180d33c57913e3cac099daf946b))
+- Update incorrect usage of Auth0 in Salesforce tests
+ ([#4007](https://github.com/ory/kratos/issues/4007))
+ ([6ce3068](https://github.com/ory/kratos/commit/6ce306824cec81890c50dcf23c2b8a5825f20a10))
+- Verify redirect continue_with in hook executor for browser clients
+ ([7b0b94d](https://github.com/ory/kratos/commit/7b0b94d30ec9069de6978427814d55a30e62adb8))
+
+### Unclassified
+
+- Update .github/workflows/ci.yaml
+ ([2d60772](https://github.com/ory/kratos/commit/2d60772062a684c3a27f28b8836c3548f5b8cea9))
+- Update Code QL action to v2
+ ([#4008](https://github.com/ory/kratos/issues/4008))
+ ([e3f1da0](https://github.com/ory/kratos/commit/e3f1da0f4bf41a8a8733758fcd9edb9910c55cfa))
+
+# [1.2.0](https://github.com/ory/kratos/compare/v1.1.0...v1.2.0) (2024-06-05)
+
+Ory Kratos v1.2 is the most complete, scalable, and secure open-source identity
+server available. We are thrilled to announce its release!
+
+
+
+This release introduces two major features: two-step registration and full
+PassKey with resident key support.
+
+Passkeys provide a secure and convenient authentication method, eliminating the
+need for passwords while ensuring strong security. With this release, we have
+added support for resident keys, enabling offline authentication. Credential
+discovery allows users to link existing passkeys to their Ory account
+seamlessly.
+
+[Watch the PassKey demo video](https://github.com/aeneasr/web-next-deprecated/assets/3372410/e676c518-c82a-42a6-821e-28aecadb270c)
+
+Two-step registration improves the user experience by dividing the registration
+process into two steps. Users first enter their identity traits, and then choose
+a credential method for authentication, resulting in a streamlined process. This
+feature is especially useful when enabling multiple authentication strategies,
+as it eliminates the need to repeat identity traits for each strategy.
+
+
+
+The 107 commits since v1.1 include several improvements:
+
+- **Webhooks** now carry session information if available.
+- **Transient Payloads** are now available across all self-service flows.
+- **Sign in with Twitter** is now available.
+- **Sign in with LinkedIn** now includes an additional v2 provider compatible
+ with LinkedIn's new SSO API.
+- **Two-Step Registration**: An improved registration experience that separates
+ entering profile information from choosing authentication methods.
+- **User Credentials Meta-Information** can now be included on the list
+ endpoint.
+- **Social Sign-In** is now resilient to double-submit issues common with
+ Facebook and Apple mobile login.
+
+**Two-Step Registration Enabled by Default**: This is now the default setting.
+To disable, set `selfservice.flows.registration.enable_legacy_flow` to `true`.
+
+- Improved account linking and credential discovery during sign-up.
+- The `return_to` parameter is now respected in OIDC API flows.
+- Adjustments to database indices.
+- Enhanced error messages for security violations.
+- Improved SDK types.
+- The `verification` and `verification_ui` hooks are now available in the login
+ flow.
+- Webhooks now contain the correct identity state in the after-verification hook
+ chain.
+
+We are doing this survey to find out how we can support self-hosted Ory users
+better. We strive to provide you with the best product and service possible and
+your feedback will help us understand what we're doing well and where we can
+improve to better meet your needs. We truly value your opinion and thank you in
+advance for taking the time to share your thoughts with us!
+
+Fill out the
+[survey now](https://share-eu1.hsforms.com/15DiCnJpcRuijnpAdnDhxxwextgn)!
## Breaking Changes
@@ -362,8 +662,13 @@ defaults to `false`.
- Audit issues ([#3797](https://github.com/ory/kratos/issues/3797))
([7017490](https://github.com/ory/kratos/commit/7017490caa9c70e22d5c626773c0266521813ff5))
+- Change return urls in quickstarts
+ ([#3928](https://github.com/ory/kratos/issues/3928))
+ ([9730e09](https://github.com/ory/kratos/commit/9730e099a656d211389d8e993c64d8082784c929))
- Close res body ([#3870](https://github.com/ory/kratos/issues/3870))
([cc39f8d](https://github.com/ory/kratos/commit/cc39f8df7c235af0df616432bc4f88681896ad85))
+- CVEs in dependencies ([#3902](https://github.com/ory/kratos/issues/3902))
+ ([e5d3b0a](https://github.com/ory/kratos/commit/e5d3b0afde3c80c6c9cf8815c56d82e291ede663))
- Db index and duplicate credentials error
([#3896](https://github.com/ory/kratos/issues/3896))
([9f34a21](https://github.com/ory/kratos/commit/9f34a21ea2035a5d33edd96753023a3c8c6c054c)):
@@ -411,6 +716,9 @@ defaults to `false`.
- Missing indices and foreign keys
([#3800](https://github.com/ory/kratos/issues/3800))
([0b32ce1](https://github.com/ory/kratos/commit/0b32ce113be47aa724d3468062ced09f8f60c52a))
+- **oidc:** Grace period for continuity container on oidc callbacks
+ ([#3915](https://github.com/ory/kratos/issues/3915))
+ ([1a9a096](https://github.com/ory/kratos/commit/1a9a096d619925dd3718ad9dd9daf77387572ece))
- Passing transient payloads
([#3838](https://github.com/ory/kratos/issues/3838))
([d01b670](https://github.com/ory/kratos/commit/d01b6705bf36efb6e0f3d71ed22d0574ab8a98a4))
@@ -473,6 +781,17 @@ defaults to `false`.
- fix: transient payload with OIDC login
+### Code Generation
+
+- Pin v1.2.0 release commit
+ ([1a70648](https://github.com/ory/kratos/commit/1a70648c4d5b9b8d135dd7bea3842057e67b574e))
+
+### Documentation
+
+- Remove delete reference from batch patch identity
+ ([#3906](https://github.com/ory/kratos/issues/3906))
+ ([cd01cb9](https://github.com/ory/kratos/commit/cd01cb9fb23a24e52d46538a9ea63c2144c3b145))
+
### Features
- Add `include_credential` query param to `/admin/identities` list call
@@ -491,6 +810,9 @@ defaults to `false`.
- Add verification hook to login flow
([#3829](https://github.com/ory/kratos/issues/3829))
([43e4ead](https://github.com/ory/kratos/commit/43e4eadce7fa6e66bf1f9c03136d141bffd3094f))
+- Allow admin to create API code recovery flows
+ ([#3939](https://github.com/ory/kratos/issues/3939))
+ ([25d1ecd](https://github.com/ory/kratos/commit/25d1ecd90317193095e01b97ff21d92920035b02))
- Control edge cache ttl ([#3808](https://github.com/ory/kratos/issues/3808))
([c9dcce5](https://github.com/ory/kratos/commit/c9dcce5a41137937df1aad7ac81170b443740f88))
- Linkedin v2 provider ([#3804](https://github.com/ory/kratos/issues/3804))
@@ -520,6 +842,22 @@ defaults to `false`.
- Resolve failing test for empty tokens
([#3775](https://github.com/ory/kratos/issues/3775))
([7277368](https://github.com/ory/kratos/commit/7277368bc28df8f0badffc7e739cef20f05e9a02))
+- Resolve flaky e2e tests ([#3935](https://github.com/ory/kratos/issues/3935))
+ ([a14927d](https://github.com/ory/kratos/commit/a14927dfa5f8d0fbda7e5a831f0a09a42369e06c)):
+
+ - test: resolve flaky code registration tests
+
+ - chore: don't fail logout if cookie is not found
+
+ - chore: remove .only
+
+ - chore: reduce wait
+
+ - chore: u
+
+ - chore: u
+
+ - chore: u
### Unclassified
diff --git a/Makefile b/Makefile
index 61e4284d3994..0395085b9849 100644
--- a/Makefile
+++ b/Makefile
@@ -49,7 +49,7 @@ docs/swagger:
npx @redocly/openapi-cli preview-docs spec/swagger.json
.bin/golangci-lint: Makefile
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -d -b .bin v1.56.2
+ curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -d -b .bin v1.59.1
.bin/hydra: Makefile
bash <(curl https://raw.githubusercontent.com/ory/meta/master/install.sh) -d -b .bin hydra v2.2.0-rc.3
@@ -193,8 +193,8 @@ migrations-sync: .bin/ory
ory dev pop migration sync persistence/sql/migrations/templates persistence/sql/migratest/testdata
script/add-down-migrations.sh
-.PHONY: test-update-snapshots
-test-update-snapshots:
+.PHONY: test-refresh
+test-refresh:
UPDATE_SNAPSHOTS=true go test -tags sqlite,json1,refresh -short ./...
.PHONY: post-release
diff --git a/README.md b/README.md
index 792ae8c5de79..decd913076e0 100644
--- a/README.md
+++ b/README.md
@@ -3,11 +3,11 @@
diff --git a/cipher/cipher_test.go b/cipher/cipher_test.go
index 8cdb0ed0e2ac..90e02ff0de45 100644
--- a/cipher/cipher_test.go
+++ b/cipher/cipher_test.go
@@ -9,6 +9,10 @@ import (
"fmt"
"testing"
+ confighelpers "github.com/ory/kratos/driver/config/testhelpers"
+
+ "github.com/ory/x/configx"
+
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -18,10 +22,11 @@ import (
"github.com/ory/kratos/internal"
)
+var goodSecret = []string{"secret-thirty-two-character-long"}
+
func TestCipher(t *testing.T) {
ctx := context.Background()
- cfg, reg := internal.NewFastRegistryWithMocks(t)
- goodSecret := []string{"secret-thirty-two-character-long"}
+ _, reg := internal.NewFastRegistryWithMocks(t, configx.WithValue(config.ViperKeySecretsDefault, goodSecret))
ciphers := []cipher.Cipher{
cipher.NewCryptAES(reg),
@@ -30,82 +35,71 @@ func TestCipher(t *testing.T) {
for _, c := range ciphers {
t.Run(fmt.Sprintf("cipher=%T", c), func(t *testing.T) {
+ t.Parallel()
t.Run("case=all_work", func(t *testing.T) {
- cfg.MustSet(ctx, config.ViperKeySecretsCipher, goodSecret)
- testAllWork(t, c, cfg)
+ t.Parallel()
+
+ testAllWork(ctx, t, c)
})
t.Run("case=encryption_failed", func(t *testing.T) {
- // unset secret
- err := cfg.Set(ctx, config.ViperKeySecretsCipher, []string{})
- require.NoError(t, err)
+ t.Parallel()
+
+ ctx := confighelpers.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""})
// secret have to be set
- _, err = c.Encrypt(context.Background(), []byte("not-empty"))
+ _, err := c.Encrypt(ctx, []byte("not-empty"))
require.Error(t, err)
+ var hErr *herodot.DefaultError
+ require.ErrorAs(t, err, &hErr)
+ assert.Equal(t, "Unable to encrypt message because no cipher secrets were configured.", hErr.Reason())
- // unset secret
- err = cfg.Set(ctx, config.ViperKeySecretsCipher, []string{"bad-length"})
- require.NoError(t, err)
+ ctx = confighelpers.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{"bad-length"})
// bad secret length
- _, err = c.Encrypt(context.Background(), []byte("not-empty"))
- if e, ok := err.(*herodot.DefaultError); ok {
- t.Logf("reason contains: %s", e.Reason())
- }
- t.Logf("err type %T contains: %s", err, err.Error())
- require.Error(t, err)
+ _, err = c.Encrypt(ctx, []byte("not-empty"))
+ require.ErrorAs(t, err, &hErr)
+ assert.Equal(t, "Unable to encrypt message because no cipher secrets were configured.", hErr.Reason())
})
t.Run("case=decryption_failed", func(t *testing.T) {
- // set secret
- err := cfg.Set(ctx, config.ViperKeySecretsCipher, goodSecret)
- require.NoError(t, err)
+ t.Parallel()
- //
- _, err = c.Decrypt(context.Background(), hex.EncodeToString([]byte("bad-data")))
+ _, err := c.Decrypt(ctx, hex.EncodeToString([]byte("bad-data")))
require.Error(t, err)
- _, err = c.Decrypt(context.Background(), "not-empty")
+ _, err = c.Decrypt(ctx, "not-empty")
require.Error(t, err)
- // unset secret
- err = cfg.Set(ctx, config.ViperKeySecretsCipher, []string{})
- require.NoError(t, err)
-
- _, err = c.Decrypt(context.Background(), "not-empty")
+ _, err = c.Decrypt(confighelpers.WithConfigValue(ctx, config.ViperKeySecretsCipher, []string{""}), "not-empty")
require.Error(t, err)
})
})
}
+
c := cipher.NewNoop(reg)
t.Run(fmt.Sprintf("cipher=%T", c), func(t *testing.T) {
- cfg.MustSet(ctx, config.ViperKeySecretsCipher, goodSecret)
- testAllWork(t, c, cfg)
+ t.Parallel()
+ testAllWork(ctx, t, c)
})
}
-func testAllWork(t *testing.T, c cipher.Cipher, cfg *config.Config) {
- ctx := context.Background()
-
- goodSecret := []string{"secret-thirty-two-character-long"}
- cfg.MustSet(ctx, config.ViperKeySecretsCipher, goodSecret)
-
+func testAllWork(ctx context.Context, t *testing.T, c cipher.Cipher) {
message := "my secret message!"
- encryptedSecret, err := c.Encrypt(context.Background(), []byte(message))
+ encryptedSecret, err := c.Encrypt(ctx, []byte(message))
require.NoError(t, err)
- decryptedSecret, err := c.Decrypt(context.Background(), encryptedSecret)
+ decryptedSecret, err := c.Decrypt(ctx, encryptedSecret)
require.NoError(t, err, "encrypted", encryptedSecret)
assert.Equal(t, message, string(decryptedSecret))
// data to encrypt return blank result
- _, err = c.Encrypt(context.Background(), []byte(""))
+ _, err = c.Encrypt(ctx, []byte(""))
require.NoError(t, err)
// empty encrypted data return blank
- _, err = c.Decrypt(context.Background(), "")
+ _, err = c.Decrypt(ctx, "")
require.NoError(t, err)
}
diff --git a/cmd/clidoc/main.go b/cmd/clidoc/main.go
index 6ed8df8d1748..7bc0eca6b58e 100644
--- a/cmd/clidoc/main.go
+++ b/cmd/clidoc/main.go
@@ -121,7 +121,7 @@ func init() {
"NewInfoLoginLookupLabel": text.NewInfoLoginLookupLabel(),
"NewInfoLogin": text.NewInfoLogin(),
"NewInfoLoginAndLink": text.NewInfoLoginAndLink(),
- "NewInfoLoginLinkMessage": text.NewInfoLoginLinkMessage("{duplicteIdentifier}", "{provider}", "{newLoginUrl}"),
+ "NewInfoLoginLinkMessage": text.NewInfoLoginLinkMessage("{duplicateIdentifier}", "{provider}", "{newLoginUrl}"),
"NewInfoLoginTOTP": text.NewInfoLoginTOTP(),
"NewInfoLoginLookup": text.NewInfoLoginLookup(),
"NewInfoLoginVerify": text.NewInfoLoginVerify(),
@@ -177,6 +177,8 @@ func init() {
"NewErrorValidationAddressUnknown": text.NewErrorValidationAddressUnknown(),
"NewInfoSelfServiceLoginCodeMFA": text.NewInfoSelfServiceLoginCodeMFA(),
"NewInfoSelfServiceLoginCodeMFAHint": text.NewInfoSelfServiceLoginCodeMFAHint("{maskedIdentifier}"),
+ "NewInfoLoginPassword": text.NewInfoLoginPassword(),
+ "NewErrorValidationAccountNotFound": text.NewErrorValidationAccountNotFound(),
}
}
diff --git a/cmd/courier/watch_test.go b/cmd/courier/watch_test.go
index 48fd6515f53b..b521e9119a97 100644
--- a/cmd/courier/watch_test.go
+++ b/cmd/courier/watch_test.go
@@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/ory/kratos/internal"
+ "github.com/ory/x/configx"
)
func TestStartCourier(t *testing.T) {
@@ -27,10 +28,9 @@ func TestStartCourier(t *testing.T) {
t.Run("case=with metrics", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
- _, r := internal.NewFastRegistryWithMocks(t)
port, err := freeport.GetFreePort()
require.NoError(t, err)
- r.Config().Set(ctx, "expose-metrics-port", port)
+ _, r := internal.NewFastRegistryWithMocks(t, configx.WithValue("expose-metrics-port", port))
go StartCourier(ctx, r)
time.Sleep(time.Second)
res, err := http.Get("http://" + r.Config().MetricsListenOn(ctx) + "/metrics/prometheus")
diff --git a/cmd/hashers/argon2/root.go b/cmd/hashers/argon2/root.go
index c5cb76581590..2282f0404d4a 100644
--- a/cmd/hashers/argon2/root.go
+++ b/cmd/hashers/argon2/root.go
@@ -9,6 +9,8 @@ import (
"reflect"
"strings"
+ "github.com/ory/x/contextx"
+
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -70,6 +72,7 @@ func configProvider(cmd *cobra.Command, flagConf *argon2Config) (*argon2Config,
cmd.Context(),
l,
cmd.ErrOrStderr(),
+ &contextx.Default{},
configx.WithFlags(cmd.Flags()),
configx.SkipValidation(),
configx.WithContext(cmd.Context()),
diff --git a/corpx/faker.go b/corpx/faker.go
index e8fc4b0e388f..ec54a252ab6b 100644
--- a/corpx/faker.go
+++ b/corpx/faker.go
@@ -17,8 +17,8 @@ import (
"github.com/ory/kratos/session"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x"
+ "github.com/ory/x/pointerx"
"github.com/ory/x/randx"
- "github.com/ory/x/stringsx"
)
var setup sync.Once
@@ -31,13 +31,13 @@ func registerFakes() {
_ = faker.SetRandomMapAndSliceSize(4)
if err := faker.AddProvider("ptr_geo_location", func(v reflect.Value) (interface{}, error) {
- return stringsx.GetPointer("Munich, Germany"), nil
+ return pointerx.Ptr("Munich, Germany"), nil
}); err != nil {
panic(err)
}
if err := faker.AddProvider("ptr_ipv4", func(v reflect.Value) (interface{}, error) {
- return stringsx.GetPointer(faker.IPv4()), nil
+ return pointerx.Ptr(faker.IPv4()), nil
}); err != nil {
panic(err)
}
diff --git a/courier/sms_test.go b/courier/sms_test.go
index a93a7974bf71..5dc727048be6 100644
--- a/courier/sms_test.go
+++ b/courier/sms_test.go
@@ -63,6 +63,7 @@ func TestQueueSMS(t *testing.T) {
Body: body.Body,
})
}))
+ t.Cleanup(srv.Close)
requestConfig := fmt.Sprintf(`{
"url": "%s",
@@ -112,8 +113,6 @@ func TestQueueSMS(t *testing.T) {
assert.Equal(t, expected.To, message.To)
assert.Equal(t, fmt.Sprintf("stub sms body %s\n", expected.Body), message.Body)
}
-
- srv.Close()
}
func TestDisallowedInternalNetwork(t *testing.T) {
diff --git a/courier/smtp_channel.go b/courier/smtp_channel.go
index 9ed9335f8e7f..15a685bcd7ad 100644
--- a/courier/smtp_channel.go
+++ b/courier/smtp_channel.go
@@ -107,8 +107,8 @@ func (c *SMTPChannel) Dispatch(ctx context.Context, msg Message) error {
gm.AddAlternative("text/html", htmlBody)
}
- if err := c.smtpClient.DialAndSend(ctx, gm); err != nil {
- c.d.Logger().
+ if err := errors.WithStack(c.smtpClient.DialAndSend(ctx, gm)); err != nil {
+ logger.
WithError(err).
Error("Unable to send email using SMTP connection.")
diff --git a/courier/template/load_template_test.go b/courier/template/load_template_test.go
index e6b043aa0c54..1fd245497ca9 100644
--- a/courier/template/load_template_test.go
+++ b/courier/template/load_template_test.go
@@ -182,7 +182,7 @@ func TestLoadTextTemplate(t *testing.T) {
})
t.Run("case=disallowed resources", func(t *testing.T) {
- require.NoError(t, reg.Config().GetProvider(ctx).Set(config.ViperKeyClientHTTPNoPrivateIPRanges, true))
+ require.NoError(t, reg.Config().Set(ctx, config.ViperKeyClientHTTPNoPrivateIPRanges, true))
reg.HTTPClient(ctx).RetryMax = 1
reg.HTTPClient(ctx).RetryWaitMax = time.Millisecond
diff --git a/driver/config/config.go b/driver/config/config.go
index 05d7ddef52a7..3978c24668ac 100644
--- a/driver/config/config.go
+++ b/driver/config/config.go
@@ -134,6 +134,8 @@ const (
ViperKeySelfServiceRegistrationAfter = "selfservice.flows.registration.after"
ViperKeySelfServiceRegistrationBeforeHooks = "selfservice.flows.registration.before.hooks"
ViperKeySelfServiceLoginUI = "selfservice.flows.login.ui_url"
+ ViperKeySelfServiceLoginFlowStyle = "selfservice.flows.login.style"
+ ViperKeySecurityAccountEnumerationMitigate = "security.account_enumeration.mitigate"
ViperKeySelfServiceLoginRequestLifespan = "selfservice.flows.login.lifespan"
ViperKeySelfServiceLoginAfter = "selfservice.flows.login.after"
ViperKeySelfServiceLoginBeforeHooks = "selfservice.flows.login.before.hooks"
@@ -203,6 +205,7 @@ const (
ViperKeyClientHTTPPrivateIPExceptionURLs = "clients.http.private_ip_exception_urls"
ViperKeyPreviewDefaultReadConsistencyLevel = "preview.default_read_consistency_level"
ViperKeyVersion = "version"
+ ViperKeyPasswordMigrationHook = "selfservice.methods.password.config.migrate_hook"
)
const (
@@ -290,6 +293,10 @@ type (
Headers map[string]string `json:"headers" koanf:"headers"`
LocalName string `json:"local_name" koanf:"local_name"`
}
+ PasswordMigrationHook struct {
+ Enabled bool `json:"enabled" koanf:"enabled"`
+ Config json.RawMessage `json:"config" koanf:"config"`
+ }
Config struct {
l *logrusx.Logger
p *configx.Provider
@@ -367,13 +374,13 @@ func (s Schemas) FindSchemaByID(id string) (*Schema, error) {
return nil, errors.Errorf("unable to find identity schema with id: %s", id)
}
-func MustNew(t testing.TB, l *logrusx.Logger, stdOutOrErr io.Writer, opts ...configx.OptionModifier) *Config {
- p, err := New(context.TODO(), l, stdOutOrErr, opts...)
+func MustNew(t testing.TB, l *logrusx.Logger, stdOutOrErr io.Writer, ctxer contextx.Contextualizer, opts ...configx.OptionModifier) *Config {
+ p, err := New(context.TODO(), l, stdOutOrErr, ctxer, opts...)
require.NoError(t, err)
return p
}
-func New(ctx context.Context, l *logrusx.Logger, stdOutOrErr io.Writer, opts ...configx.OptionModifier) (*Config, error) {
+func New(ctx context.Context, l *logrusx.Logger, stdOutOrErr io.Writer, ctxer contextx.Contextualizer, opts ...configx.OptionModifier) (*Config, error) {
var c *Config
opts = append([]configx.OptionModifier{
@@ -402,7 +409,7 @@ func New(ctx context.Context, l *logrusx.Logger, stdOutOrErr io.Writer, opts ...
l.UseConfig(p)
- c = NewCustom(l, p, stdOutOrErr, &contextx.Default{})
+ c = NewCustom(l, p, stdOutOrErr, ctxer)
if !p.SkipValidation() {
if err := c.validateIdentitySchemas(ctx); err != nil {
@@ -518,12 +525,14 @@ func (p *Config) cors(ctx context.Context, prefix string) (cors.Options, bool) {
})
}
-func (p *Config) Set(ctx context.Context, key string, value interface{}) error {
- return p.GetProvider(ctx).Set(key, value)
+// Deprecated: use context-based WithConfigValue instead
+func (p *Config) Set(_ context.Context, key string, value interface{}) error {
+ return p.p.Set(key, value)
}
-func (p *Config) MustSet(ctx context.Context, key string, value interface{}) {
- if err := p.GetProvider(ctx).Set(key, value); err != nil {
+// Deprecated: use context-based WithConfigValue instead
+func (p *Config) MustSet(_ context.Context, key string, value interface{}) {
+ if err := p.p.Set(key, value); err != nil {
p.l.WithError(err).Fatalf("Unable to set \"%s\" to \"%s\".", key, value)
}
}
@@ -767,7 +776,7 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self
var err error
config, err = json.Marshal(pp.GetF(basePath+".config", config))
if err != nil {
- p.l.WithError(err).Warn("Unable to marshal self service strategy configuration.")
+ p.l.WithError(err).Warn("Unable to marshal self-service strategy configuration.")
config = json.RawMessage("{}")
}
@@ -775,6 +784,8 @@ func (p *Config) SelfServiceStrategy(ctx context.Context, strategy string) *Self
// we need to forcibly set these values here:
defaultEnabled := false
switch strategy {
+ case "identifier_first":
+ defaultEnabled = p.SelfServiceLoginFlowIdentifierFirstEnabled(ctx)
case "code", "password", "profile":
defaultEnabled = true
}
@@ -859,7 +870,7 @@ func (p *Config) SecretsCipher(ctx context.Context) [][32]byte {
result := make([][32]byte, len(cleanSecrets))
for n, s := range secrets {
for k, v := range []byte(s) {
- result[n][k] = byte(v)
+ result[n][k] = v
}
}
return result
@@ -1597,3 +1608,30 @@ func (p *Config) TokenizeTemplate(ctx context.Context, key string) (_ *SessionTo
func (p *Config) DefaultConsistencyLevel(ctx context.Context) crdbx.ConsistencyLevel {
return crdbx.ConsistencyLevelFromString(p.GetProvider(ctx).String(ViperKeyPreviewDefaultReadConsistencyLevel))
}
+
+func (p *Config) PasswordMigrationHook(ctx context.Context) *PasswordMigrationHook {
+
+ hook := &PasswordMigrationHook{
+ Enabled: p.GetProvider(ctx).BoolF(ViperKeyPasswordMigrationHook+".enabled", false),
+ }
+ if !hook.Enabled {
+ return hook
+ }
+
+ hook.Config, _ = json.Marshal(p.GetProvider(ctx).Get(ViperKeyPasswordMigrationHook + ".config"))
+
+ return hook
+}
+
+func (p *Config) SelfServiceLoginFlowIdentifierFirstEnabled(ctx context.Context) bool {
+ switch p.GetProvider(ctx).String(ViperKeySelfServiceLoginFlowStyle) {
+ case "identifier_first":
+ return true
+ default:
+ return false
+ }
+}
+
+func (p *Config) SecurityAccountEnumerationMitigate(ctx context.Context) bool {
+ return p.GetProvider(ctx).Bool(ViperKeySecurityAccountEnumerationMitigate)
+}
diff --git a/driver/config/config_test.go b/driver/config/config_test.go
index 6cb37f100850..8f9dfaaf20ec 100644
--- a/driver/config/config_test.go
+++ b/driver/config/config_test.go
@@ -18,6 +18,8 @@ import (
"testing"
"time"
+ "github.com/ory/x/contextx"
+
"github.com/ory/x/httpx"
"github.com/ory/x/randx"
@@ -51,6 +53,7 @@ func TestViperProvider(t *testing.T) {
t.Run("suite=loaders", func(t *testing.T) {
p := config.MustNew(t, logrusx.New("", ""), os.Stderr,
+ &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.yaml"),
configx.WithContext(ctx),
)
@@ -89,6 +92,7 @@ func TestViperProvider(t *testing.T) {
pWithFragments := config.MustNew(t, logrusx.New("", ""),
os.Stderr,
+ &contextx.Default{},
configx.WithValues(map[string]interface{}{
config.ViperKeySelfServiceLoginUI: "http://test.kratos.ory.sh/#/login",
config.ViperKeySelfServiceSettingsURL: "http://test.kratos.ory.sh/#/settings",
@@ -105,6 +109,7 @@ func TestViperProvider(t *testing.T) {
pWithRelativeFragments := config.MustNew(t, logrusx.New("", ""),
os.Stderr,
+ &contextx.Default{},
configx.WithValues(map[string]interface{}{
config.ViperKeySelfServiceLoginUI: "/login",
config.ViperKeySelfServiceSettingsURL: "/settings",
@@ -130,6 +135,7 @@ func TestViperProvider(t *testing.T) {
pWithIncorrectUrls := config.MustNew(t, logger,
os.Stderr,
+ &contextx.Default{},
configx.WithValues(map[string]interface{}{
config.ViperKeySelfServiceLoginUI: v,
}),
@@ -161,6 +167,7 @@ func TestViperProvider(t *testing.T) {
t.Run("group=identity", func(t *testing.T) {
c := config.MustNew(t, logrusx.New("", ""), os.Stderr,
+ &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.mock.identities.yaml"),
configx.SkipValidation())
@@ -198,7 +205,7 @@ func TestViperProvider(t *testing.T) {
}, p.SecretsSession(ctx))
var cipherExpected [32]byte
for k, v := range []byte("secret-thirty-two-character-long") {
- cipherExpected[k] = byte(v)
+ cipherExpected[k] = v
}
assert.Equal(t, [][32]byte{
cipherExpected,
@@ -211,7 +218,7 @@ func TestViperProvider(t *testing.T) {
config string
enabled bool
}{
- {id: "password", enabled: true, config: `{"haveibeenpwned_host":"api.pwnedpasswords.com","haveibeenpwned_enabled":true,"ignore_network_errors":true,"max_breaches":0,"min_password_length":8,"identifier_similarity_check_enabled":true}`},
+ {id: "password", enabled: true, config: `{"haveibeenpwned_host":"api.pwnedpasswords.com","haveibeenpwned_enabled":true,"ignore_network_errors":true,"max_breaches":0,"migrate_hook":{"config":{"emit_analytics_event":true,"method":"POST"},"enabled":false},"min_password_length":8,"identifier_similarity_check_enabled":true}`},
{id: "oidc", enabled: true, config: `{"providers":[{"client_id":"a","client_secret":"b","id":"github","provider":"github","mapper_url":"http://test.kratos.ory.sh/default-identity.schema.json"}]}`},
{id: "totp", enabled: true, config: `{"issuer":"issuer.ory.sh"}`},
} {
@@ -400,7 +407,7 @@ func TestViperProvider(t *testing.T) {
func TestBcrypt(t *testing.T) {
t.Parallel()
ctx := context.Background()
- p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation())
+ p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
require.NoError(t, p.Set(ctx, config.ViperKeyHasherBcryptCost, 4))
require.NoError(t, p.Set(ctx, "dev", false))
@@ -418,7 +425,7 @@ func TestProviderBaseURLs(t *testing.T) {
machineHostname = "127.0.0.1"
}
- p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation())
+ p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
assert.Equal(t, "https://"+machineHostname+":4433/", p.SelfPublicURL(ctx).String())
assert.Equal(t, "https://"+machineHostname+":4434/", p.SelfAdminURL(ctx).String())
@@ -446,7 +453,7 @@ func TestProviderSelfServiceLinkMethodBaseURL(t *testing.T) {
machineHostname = "127.0.0.1"
}
- p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation())
+ p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
assert.Equal(t, "https://"+machineHostname+":4433/", p.SelfServiceLinkMethodBaseURL(ctx).String())
p.MustSet(ctx, config.ViperKeyLinkBaseURL, "https://example.org/bar")
@@ -456,7 +463,7 @@ func TestProviderSelfServiceLinkMethodBaseURL(t *testing.T) {
func TestViperProvider_Secrets(t *testing.T) {
t.Parallel()
ctx := context.Background()
- p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation())
+ p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
def := p.SecretsDefault(ctx)
assert.NotEmpty(t, def)
@@ -479,24 +486,25 @@ func TestViperProvider_Defaults(t *testing.T) {
}{
{
init: func() *config.Config {
- return config.MustNew(t, l, os.Stderr, configx.SkipValidation())
+ return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation())
},
},
{
init: func() *config.Config {
return config.MustNew(t, l,
os.Stderr,
+ &contextx.Default{},
configx.WithConfigFiles("stub/.defaults.yml"), configx.SkipValidation())
},
},
{
init: func() *config.Config {
- return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("stub/.defaults-password.yml"), configx.SkipValidation())
+ return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.defaults-password.yml"), configx.SkipValidation())
},
},
{
init: func() *config.Config {
- return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("../../test/e2e/profiles/recovery/.kratos.yml"), configx.SkipValidation())
+ return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("../../test/e2e/profiles/recovery/.kratos.yml"), configx.SkipValidation())
},
expect: func(t *testing.T, p *config.Config) {
assert.True(t, p.SelfServiceFlowRecoveryEnabled(ctx))
@@ -512,7 +520,7 @@ func TestViperProvider_Defaults(t *testing.T) {
},
{
init: func() *config.Config {
- return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("../../test/e2e/profiles/verification/.kratos.yml"), configx.SkipValidation())
+ return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("../../test/e2e/profiles/verification/.kratos.yml"), configx.SkipValidation())
},
expect: func(t *testing.T, p *config.Config) {
assert.False(t, p.SelfServiceFlowRecoveryEnabled(ctx))
@@ -528,7 +536,7 @@ func TestViperProvider_Defaults(t *testing.T) {
},
{
init: func() *config.Config {
- return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("../../test/e2e/profiles/oidc/.kratos.yml"), configx.SkipValidation())
+ return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("../../test/e2e/profiles/oidc/.kratos.yml"), configx.SkipValidation())
},
expect: func(t *testing.T, p *config.Config) {
assert.False(t, p.SelfServiceFlowRecoveryEnabled(ctx))
@@ -543,7 +551,7 @@ func TestViperProvider_Defaults(t *testing.T) {
},
{
init: func() *config.Config {
- return config.MustNew(t, l, os.Stderr, configx.WithConfigFiles("stub/.kratos.notify-unknown-recipients.yml"), configx.SkipValidation())
+ return config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.notify-unknown-recipients.yml"), configx.SkipValidation())
},
expect: func(t *testing.T, p *config.Config) {
assert.True(t, p.SelfServiceFlowRecoveryNotifyUnknownRecipients(ctx))
@@ -572,7 +580,7 @@ func TestViperProvider_Defaults(t *testing.T) {
}
t.Run("suite=ui_url", func(t *testing.T) {
- p := config.MustNew(t, l, os.Stderr, configx.SkipValidation())
+ p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation())
assert.Equal(t, "https://www.ory.sh/kratos/docs/fallback/login", p.SelfServiceFlowLoginUI(ctx).String())
assert.Equal(t, "https://www.ory.sh/kratos/docs/fallback/settings", p.SelfServiceFlowSettingsUI(ctx).String())
assert.Equal(t, "https://www.ory.sh/kratos/docs/fallback/registration", p.SelfServiceFlowRegistrationUI(ctx).String())
@@ -585,7 +593,7 @@ func TestViperProvider_ReturnTo(t *testing.T) {
t.Parallel()
ctx := context.Background()
l := logrusx.New("", "")
- p := config.MustNew(t, l, os.Stderr, configx.SkipValidation())
+ p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation())
p.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh/")
assert.Equal(t, "https://www.ory.sh/", p.SelfServiceFlowVerificationReturnTo(ctx, urlx.ParseOrPanic("https://www.ory.sh/")).String())
@@ -602,7 +610,7 @@ func TestSession(t *testing.T) {
t.Parallel()
ctx := context.Background()
l := logrusx.New("", "")
- p := config.MustNew(t, l, os.Stderr, configx.SkipValidation())
+ p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation())
assert.Equal(t, "ory_kratos_session", p.SessionName(ctx))
p.MustSet(ctx, config.ViperKeySessionName, "ory_session")
@@ -629,7 +637,7 @@ func TestCookies(t *testing.T) {
t.Parallel()
ctx := context.Background()
l := logrusx.New("", "")
- p := config.MustNew(t, l, os.Stderr, configx.SkipValidation())
+ p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation())
t.Run("path", func(t *testing.T) {
assert.Equal(t, "/", p.CookiePath(ctx))
@@ -676,14 +684,14 @@ func TestViperProvider_DSN(t *testing.T) {
ctx := context.Background()
t.Run("case=dsn: memory", func(t *testing.T) {
- p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation())
+ p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
p.MustSet(ctx, config.ViperKeyDSN, "memory")
assert.Equal(t, config.DefaultSQLiteMemoryDSN, p.DSN(ctx))
})
t.Run("case=dsn: not memory", func(t *testing.T) {
- p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation())
+ p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
dsn := "sqlite://foo.db?_fk=true"
p.MustSet(ctx, config.ViperKeyDSN, dsn)
@@ -698,7 +706,7 @@ func TestViperProvider_DSN(t *testing.T) {
l := logrusx.New("", "", logrusx.WithExitFunc(func(i int) {
exitCode = i
}))
- p := config.MustNew(t, l, os.Stderr, configx.SkipValidation())
+ p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation())
assert.Equal(t, dsn, p.DSN(ctx))
assert.NotEqual(t, 0, exitCode)
@@ -714,7 +722,7 @@ func TestViperProvider_ParseURIOrFail(t *testing.T) {
l := logrusx.New("", "", logrusx.WithExitFunc(func(i int) {
exitCode = i
}))
- p := config.MustNew(t, l, os.Stderr, configx.SkipValidation())
+ p := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation())
require.Zero(t, exitCode)
const testKey = "testKeyNotUsedInTheRealSchema"
@@ -768,7 +776,7 @@ func TestViperProvider_HaveIBeenPwned(t *testing.T) {
t.Parallel()
ctx := context.Background()
- p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation())
+ p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
t.Run("case=hipb: host", func(t *testing.T) {
p.MustSet(ctx, config.ViperKeyPasswordHaveIBeenPwnedHost, "foo.bar")
assert.Equal(t, "foo.bar", p.PasswordPolicyConfig(ctx).HaveIBeenPwnedHost)
@@ -806,7 +814,7 @@ func newTestConfig(t *testing.T) (_ *config.Config, _ *test.Hook, exited *bool)
exited = new(bool)
l.Logger.Hooks.Add(h)
l.Logger.ExitFunc = func(code int) { *exited = true }
- config := config.MustNew(t, l, os.Stderr, configx.SkipValidation())
+ config := config.MustNew(t, l, os.Stderr, &contextx.Default{}, configx.SkipValidation())
return config, h, exited
}
@@ -972,7 +980,7 @@ func TestIdentitySchemaValidation(t *testing.T) {
l := logrusx.New("kratos-"+tmpConfig.Name(), "test")
hook := test.NewLocal(l.Logger)
- conf, err := config.New(ctx, l, os.Stderr, configx.WithConfigFiles(tmpConfig.Name()))
+ conf, err := config.New(ctx, l, os.Stderr, &contextx.Default{}, configx.WithConfigFiles(tmpConfig.Name()))
assert.NoError(t, err)
// clean the hooks since it will throw an event on first boot
@@ -986,7 +994,7 @@ func TestIdentitySchemaValidation(t *testing.T) {
t.Run("case=skip invalid schema validation", func(t *testing.T) {
ctx := ctx
- _, err := config.New(ctx, logrusx.New("", ""), os.Stderr,
+ _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.invalid.identities.yaml"),
configx.SkipValidation())
assert.NoError(t, err)
@@ -995,7 +1003,7 @@ func TestIdentitySchemaValidation(t *testing.T) {
t.Run("case=invalid schema should throw error", func(t *testing.T) {
ctx := ctx
var stdErr bytes.Buffer
- _, err := config.New(ctx, logrusx.New("", ""), &stdErr,
+ _, err := config.New(ctx, logrusx.New("", ""), &stdErr, &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.invalid.identities.yaml"))
assert.Error(t, err)
assert.Contains(t, err.Error(), "minimum 1 properties allowed, but found 0")
@@ -1013,7 +1021,7 @@ func TestIdentitySchemaValidation(t *testing.T) {
err := make(chan error, 1)
go func(err chan error) {
- _, e := config.New(ctx, logrusx.New("", ""), os.Stderr,
+ _, e := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.mock.identities.yaml"))
err <- e
}(err)
@@ -1068,7 +1076,7 @@ func TestPasswordless(t *testing.T) {
t.Parallel()
ctx := context.Background()
- conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr,
+ conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.SkipValidation(),
configx.WithValue(config.ViperKeyWebAuthnPasswordless, true))
require.NoError(t, err)
@@ -1083,7 +1091,7 @@ func TestPasswordlessCode(t *testing.T) {
ctx := context.Background()
- conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr,
+ conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.SkipValidation(),
configx.WithValue(config.ViperKeySelfServiceStrategyConfig+".code", map[string]interface{}{
"passwordless_enabled": true,
@@ -1100,7 +1108,7 @@ func TestChangeMinPasswordLength(t *testing.T) {
t.Run("case=must fail on minimum password length below enforced minimum", func(t *testing.T) {
ctx := context.Background()
- _, err := config.New(ctx, logrusx.New("", ""), os.Stderr,
+ _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.yaml"),
configx.WithValue(config.ViperKeyPasswordMinLength, 5))
@@ -1110,7 +1118,7 @@ func TestChangeMinPasswordLength(t *testing.T) {
t.Run("case=must not fail on minimum password length above enforced minimum", func(t *testing.T) {
ctx := context.Background()
- _, err := config.New(ctx, logrusx.New("", ""), os.Stderr,
+ _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.yaml"),
configx.WithValue(config.ViperKeyPasswordMinLength, 9))
@@ -1123,14 +1131,14 @@ func TestCourierEmailHTTP(t *testing.T) {
ctx := context.Background()
t.Run("case=configs set", func(t *testing.T) {
- conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr,
+ conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.courier.email.http.yaml"), configx.SkipValidation())
assert.Equal(t, "http", conf.CourierEmailStrategy(ctx))
snapshotx.SnapshotT(t, conf.CourierEmailRequestConfig(ctx))
})
t.Run("case=defaults", func(t *testing.T) {
- conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation())
+ conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
assert.Equal(t, "smtp", conf.CourierEmailStrategy(ctx))
})
@@ -1140,7 +1148,7 @@ func TestCourierChannels(t *testing.T) {
t.Parallel()
ctx := context.Background()
t.Run("case=configs set", func(t *testing.T) {
- conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.WithConfigFiles("stub/.kratos.courier.channels.yaml"), configx.SkipValidation())
+ conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithConfigFiles("stub/.kratos.courier.channels.yaml"), configx.SkipValidation())
channelConfig, err := conf.CourierChannels(ctx)
require.NoError(t, err)
@@ -1152,7 +1160,7 @@ func TestCourierChannels(t *testing.T) {
})
t.Run("case=defaults", func(t *testing.T) {
- conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation())
+ conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
channelConfig, err := conf.CourierChannels(ctx)
require.NoError(t, err)
@@ -1171,7 +1179,7 @@ func TestCourierChannels(t *testing.T) {
"smtp://username:pass%2Fword@email-smtp.eu-west-3.amazonaws.com:587/",
} {
t.Run("case="+tc, func(t *testing.T) {
- conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.WithValue(config.ViperKeyCourierSMTPURL, tc), configx.SkipValidation())
+ conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.WithValue(config.ViperKeyCourierSMTPURL, tc), configx.SkipValidation())
require.NoError(t, err)
cs, err := conf.CourierChannels(ctx)
require.NoError(t, err)
@@ -1187,13 +1195,13 @@ func TestCourierMessageTTL(t *testing.T) {
ctx := context.Background()
t.Run("case=configs set", func(t *testing.T) {
- conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr,
+ conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.courier.message_retries.yaml"), configx.SkipValidation())
assert.Equal(t, conf.CourierMessageRetries(ctx), 10)
})
t.Run("case=defaults", func(t *testing.T) {
- conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation())
+ conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
assert.Equal(t, conf.CourierMessageRetries(ctx), 5)
})
}
@@ -1203,7 +1211,7 @@ func TestOAuth2Provider(t *testing.T) {
ctx := context.Background()
t.Run("case=configs set", func(t *testing.T) {
- conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr,
+ conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.oauth2_provider.yaml"), configx.SkipValidation())
assert.Equal(t, "https://oauth2_provider/", conf.OAuth2ProviderURL(ctx).String())
assert.Equal(t, http.Header{"Authorization": {"Basic"}}, conf.OAuth2ProviderHeader(ctx))
@@ -1211,7 +1219,7 @@ func TestOAuth2Provider(t *testing.T) {
})
t.Run("case=defaults", func(t *testing.T) {
- conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, configx.SkipValidation())
+ conf, _ := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
assert.Empty(t, conf.OAuth2ProviderURL(ctx))
assert.Empty(t, conf.OAuth2ProviderHeader(ctx))
assert.False(t, conf.OAuth2ProviderOverrideReturnTo(ctx))
@@ -1223,7 +1231,7 @@ func TestWebauthn(t *testing.T) {
ctx := context.Background()
t.Run("case=multiple origins", func(t *testing.T) {
- conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr,
+ conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.webauthn.origins.yaml"))
require.NoError(t, err)
webAuthnConfig := conf.WebAuthnConfig(ctx)
@@ -1236,7 +1244,7 @@ func TestWebauthn(t *testing.T) {
})
t.Run("case=one origin", func(t *testing.T) {
- conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr,
+ conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.webauthn.origin.yaml"))
require.NoError(t, err)
webAuthnConfig := conf.WebAuthnConfig(ctx)
@@ -1247,7 +1255,7 @@ func TestWebauthn(t *testing.T) {
})
t.Run("case=id as origin", func(t *testing.T) {
- conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr,
+ conf, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.yaml"))
require.NoError(t, err)
webAuthnConfig := conf.WebAuthnConfig(ctx)
@@ -1258,7 +1266,7 @@ func TestWebauthn(t *testing.T) {
})
t.Run("case=invalid", func(t *testing.T) {
- _, err := config.New(ctx, logrusx.New("", ""), os.Stderr,
+ _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.webauthn.invalid.yaml"))
assert.Error(t, err)
})
@@ -1269,19 +1277,19 @@ func TestCourierTemplatesConfig(t *testing.T) {
ctx := context.Background()
t.Run("case=partial template update allowed", func(t *testing.T) {
- _, err := config.New(ctx, logrusx.New("", ""), os.Stderr,
+ _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.courier.remote.partial.templates.yaml"))
assert.NoError(t, err)
})
t.Run("case=load remote template with fallback template overrides path", func(t *testing.T) {
- _, err := config.New(ctx, logrusx.New("", ""), os.Stderr,
+ _, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.courier.remote.templates.yaml"))
assert.NoError(t, err)
})
t.Run("case=courier template helper", func(t *testing.T) {
- c, err := config.New(ctx, logrusx.New("", ""), os.Stderr,
+ c, err := config.New(ctx, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.courier.remote.templates.yaml"))
assert.NoError(t, err)
@@ -1323,7 +1331,7 @@ func TestCleanup(t *testing.T) {
t.Parallel()
ctx := context.Background()
- p := config.MustNew(t, logrusx.New("", ""), os.Stderr,
+ p := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{},
configx.WithConfigFiles("stub/.kratos.yaml"))
t.Run("group=cleanup config", func(t *testing.T) {
diff --git a/driver/config/handler_test.go b/driver/config/handler_test.go
index da84a5bc08d4..8c73a9319621 100644
--- a/driver/config/handler_test.go
+++ b/driver/config/handler_test.go
@@ -6,9 +6,10 @@ package config_test
import (
"context"
"io"
- "net/http/httptest"
"testing"
+ confighelpers "github.com/ory/kratos/driver/config/testhelpers"
+
"github.com/julienschmidt/httprouter"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -17,21 +18,32 @@ import (
"github.com/ory/kratos/internal"
)
+type configProvider struct {
+ cfg *config.Config
+}
+
+func (c *configProvider) Config() *config.Config {
+ return c.cfg
+}
+
func TestNewConfigHashHandler(t *testing.T) {
ctx := context.Background()
- conf, reg := internal.NewFastRegistryWithMocks(t)
+ cfg := internal.NewConfigurationWithDefaults(t)
router := httprouter.New()
- config.NewConfigHashHandler(reg, router)
- ts := httptest.NewServer(router)
+ config.NewConfigHashHandler(&configProvider{cfg: cfg}, router)
+ ts := confighelpers.NewConfigurableTestServer(router)
t.Cleanup(ts.Close)
- res, err := ts.Client().Get(ts.URL + "/health/config")
+
+ // first request, get baseline hash
+ res, err := ts.Client(ctx).Get(ts.URL + "/health/config")
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, 200, res.StatusCode)
first, err := io.ReadAll(res.Body)
require.NoError(t, err)
- res, err = ts.Client().Get(ts.URL + "/health/config")
+ // second request, no config change
+ res, err = ts.Client(ctx).Get(ts.URL + "/health/config")
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, 200, res.StatusCode)
@@ -39,13 +51,21 @@ func TestNewConfigHashHandler(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, first, second)
- require.NoError(t, conf.Set(ctx, config.ViperKeySessionDomain, "foobar"))
+ // third request, with config change
+ res, err = ts.Client(confighelpers.WithConfigValue(ctx, config.ViperKeySessionDomain, "foobar")).Get(ts.URL + "/health/config")
+ require.NoError(t, err)
+ defer res.Body.Close()
+ require.Equal(t, 200, res.StatusCode)
+ third, err := io.ReadAll(res.Body)
+ require.NoError(t, err)
+ assert.NotEqual(t, first, third)
- res, err = ts.Client().Get(ts.URL + "/health/config")
+ // fourth request, no config change
+ res, err = ts.Client(ctx).Get(ts.URL + "/health/config")
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, 200, res.StatusCode)
- second, err = io.ReadAll(res.Body)
+ fourth, err := io.ReadAll(res.Body)
require.NoError(t, err)
- assert.NotEqual(t, first, second)
+ assert.Equal(t, first, fourth)
}
diff --git a/driver/config/testhelpers/config.go b/driver/config/testhelpers/config.go
new file mode 100644
index 000000000000..6d0e4ba0b910
--- /dev/null
+++ b/driver/config/testhelpers/config.go
@@ -0,0 +1,152 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package testhelpers
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+
+ "github.com/gofrs/uuid"
+
+ "github.com/ory/kratos/embedx"
+ "github.com/ory/x/configx"
+ "github.com/ory/x/contextx"
+)
+
+type (
+ TestConfigProvider struct {
+ contextx.Contextualizer
+ Options []configx.OptionModifier
+ }
+ contextKey int
+)
+
+func (t *TestConfigProvider) NewProvider(ctx context.Context, opts ...configx.OptionModifier) (*configx.Provider, error) {
+ return configx.New(ctx, []byte(embedx.ConfigSchema), append(t.Options, opts...)...)
+}
+
+func (t *TestConfigProvider) Config(ctx context.Context, config *configx.Provider) *configx.Provider {
+ config = t.Contextualizer.Config(ctx, config)
+ values, ok := ctx.Value(contextConfigKey).([]map[string]any)
+ if !ok {
+ return config
+ }
+ opts := make([]configx.OptionModifier, 0, len(values))
+ for _, v := range values {
+ opts = append(opts, configx.WithValues(v))
+ }
+ config, err := t.NewProvider(ctx, opts...)
+ if err != nil {
+ // This is not production code. The provider is only used in tests.
+ panic(err)
+ }
+ return config
+}
+
+const contextConfigKey contextKey = 1
+
+var (
+ _ contextx.Contextualizer = (*TestConfigProvider)(nil)
+)
+
+func WithConfigValue(ctx context.Context, key string, value any) context.Context {
+ return WithConfigValues(ctx, map[string]any{key: value})
+}
+
+func WithConfigValues(ctx context.Context, setValues ...map[string]any) context.Context {
+ values, ok := ctx.Value(contextConfigKey).([]map[string]any)
+ if !ok {
+ values = make([]map[string]any, 0)
+ }
+ newValues := make([]map[string]any, len(values), len(values)+len(setValues))
+ copy(newValues, values)
+ newValues = append(newValues, setValues...)
+
+ return context.WithValue(ctx, contextConfigKey, newValues)
+}
+
+type ConfigurableTestHandler struct {
+ configs map[uuid.UUID][]map[string]any
+ handler http.Handler
+}
+
+func NewConfigurableTestHandler(h http.Handler) *ConfigurableTestHandler {
+ return &ConfigurableTestHandler{
+ configs: make(map[uuid.UUID][]map[string]any),
+ handler: h,
+ }
+}
+
+func (t *ConfigurableTestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ cID := r.Header.Get("Test-Config-Id")
+ if config, ok := t.configs[uuid.FromStringOrNil(cID)]; ok {
+ r = r.WithContext(WithConfigValues(r.Context(), config...))
+ }
+ t.handler.ServeHTTP(w, r)
+}
+
+func (t *ConfigurableTestHandler) RegisterConfig(config ...map[string]any) uuid.UUID {
+ id := uuid.Must(uuid.NewV4())
+ t.configs[id] = config
+ return id
+}
+
+func (t *ConfigurableTestHandler) UseConfig(r *http.Request, id uuid.UUID) *http.Request {
+ r.Header.Set("Test-Config-Id", id.String())
+ return r
+}
+
+func (t *ConfigurableTestHandler) UseConfigValues(r *http.Request, values ...map[string]any) *http.Request {
+ return t.UseConfig(r, t.RegisterConfig(values...))
+}
+
+type ConfigurableTestServer struct {
+ *httptest.Server
+ handler *ConfigurableTestHandler
+ transport http.RoundTripper
+}
+
+func NewConfigurableTestServer(h http.Handler) *ConfigurableTestServer {
+ handler := NewConfigurableTestHandler(h)
+ server := httptest.NewServer(handler)
+
+ t := server.Client().Transport
+ cts := &ConfigurableTestServer{
+ handler: handler,
+ Server: server,
+ transport: t,
+ }
+ server.Client().Transport = cts
+ return cts
+}
+
+func (t *ConfigurableTestServer) RoundTrip(r *http.Request) (*http.Response, error) {
+ config, ok := r.Context().Value(contextConfigKey).([]map[string]any)
+ if ok && config != nil {
+ r = t.handler.UseConfigValues(r, config...)
+ }
+ return t.transport.RoundTrip(r)
+}
+
+type AutoContextClient struct {
+ *http.Client
+ transport http.RoundTripper
+ ctx context.Context
+}
+
+func (t *ConfigurableTestServer) Client(ctx context.Context) *AutoContextClient {
+ baseClient := *t.Server.Client()
+ autoClient := &AutoContextClient{
+ Client: &baseClient,
+ transport: t,
+ ctx: ctx,
+ }
+ baseClient.Transport = autoClient
+ return autoClient
+}
+
+func (c *AutoContextClient) RoundTrip(r *http.Request) (*http.Response, error) {
+ return c.transport.RoundTrip(r.WithContext(c.ctx))
+}
diff --git a/driver/factory.go b/driver/factory.go
index e3470d3cffd9..da0dd5601e2b 100644
--- a/driver/factory.go
+++ b/driver/factory.go
@@ -38,7 +38,7 @@ func NewWithoutInit(ctx context.Context, stdOutOrErr io.Writer, sl *servicelocat
c := newOptions(dOpts).config
if c == nil {
var err error
- c, err = config.New(ctx, l, stdOutOrErr, opts...)
+ c, err = config.New(ctx, l, stdOutOrErr, sl.Contextualizer(), opts...)
if err != nil {
l.WithError(err).Error("Unable to instantiate configuration.")
return nil, err
diff --git a/driver/registry_default.go b/driver/registry_default.go
index eab63a120981..c77ab5d783f2 100644
--- a/driver/registry_default.go
+++ b/driver/registry_default.go
@@ -12,6 +12,8 @@ import (
"testing"
"time"
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+
"github.com/cenkalti/backoff"
"github.com/dgraph-io/ristretto"
"github.com/gobuffalo/pop/v6"
@@ -324,6 +326,7 @@ func (m *RegistryDefault) selfServiceStrategies() []any {
passkey.NewStrategy(m),
webauthn.NewStrategy(m),
lookup.NewStrategy(m),
+ idfirst.NewStrategy(m),
}
}
}
@@ -379,6 +382,7 @@ nextStrategy:
continue nextStrategy
}
}
+
if m.strategyLoginEnabled(ctx, s.ID().String()) {
loginStrategies = append(loginStrategies, s)
}
diff --git a/driver/registry_default_test.go b/driver/registry_default_test.go
index 009dd76173d8..a52b4fc6072c 100644
--- a/driver/registry_default_test.go
+++ b/driver/registry_default_test.go
@@ -10,6 +10,10 @@ import (
"os"
"testing"
+ confighelpers "github.com/ory/kratos/driver/config/testhelpers"
+
+ "github.com/ory/x/contextx"
+
"github.com/ory/kratos/selfservice/flow/recovery"
"github.com/ory/kratos/selfservice/flow/verification"
@@ -34,26 +38,27 @@ func TestDriverDefault_Hooks(t *testing.T) {
t.Parallel()
ctx := context.Background()
+ _, reg := internal.NewVeryFastRegistryWithoutDB(t)
+
t.Run("type=verification", func(t *testing.T) {
t.Parallel()
// BEFORE hooks
for _, tc := range []struct {
uc string
- prep func(conf *config.Config)
+ config map[string]any
expect func(reg *driver.RegistryDefault) []verification.PreHookExecutor
}{
{
uc: "No hooks configured",
- prep: func(conf *config.Config) {},
expect: func(reg *driver.RegistryDefault) []verification.PreHookExecutor { return nil },
},
{
uc: "Two web_hooks are configured",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceVerificationBeforeHooks, []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
- {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
- })
+ config: map[string]any{
+ config.ViperKeySelfServiceVerificationBeforeHooks: []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ },
},
expect: func(reg *driver.RegistryDefault) []verification.PreHookExecutor {
return []verification.PreHookExecutor{
@@ -64,8 +69,9 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
} {
t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) {
- conf, reg := internal.NewVeryFastRegistryWithoutDB(t)
- tc.prep(conf)
+ t.Parallel()
+
+ ctx := confighelpers.WithConfigValues(ctx, tc.config)
h := reg.PreVerificationHooks(ctx)
@@ -79,6 +85,7 @@ func TestDriverDefault_Hooks(t *testing.T) {
for _, tc := range []struct {
uc string
prep func(conf *config.Config)
+ config map[string]any
expect func(reg *driver.RegistryDefault) []verification.PostHookExecutor
}{
{
@@ -88,11 +95,11 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
{
uc: "Multiple web_hooks configured",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceVerificationAfter+".hooks", []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
- {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
- })
+ config: map[string]any{
+ config.ViperKeySelfServiceVerificationAfter + ".hooks": []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ },
},
expect: func(reg *driver.RegistryDefault) []verification.PostHookExecutor {
return []verification.PostHookExecutor{
@@ -103,8 +110,9 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
} {
t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) {
- conf, reg := internal.NewVeryFastRegistryWithoutDB(t)
- tc.prep(conf)
+ t.Parallel()
+
+ ctx := confighelpers.WithConfigValues(ctx, tc.config)
h := reg.PostVerificationHooks(ctx)
@@ -120,21 +128,20 @@ func TestDriverDefault_Hooks(t *testing.T) {
// BEFORE hooks
for _, tc := range []struct {
uc string
- prep func(conf *config.Config)
+ config map[string]any
expect func(reg *driver.RegistryDefault) []recovery.PreHookExecutor
}{
{
uc: "No hooks configured",
- prep: func(conf *config.Config) {},
expect: func(reg *driver.RegistryDefault) []recovery.PreHookExecutor { return nil },
},
{
uc: "Two web_hooks are configured",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryBeforeHooks, []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
- {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
- })
+ config: map[string]any{
+ config.ViperKeySelfServiceRecoveryBeforeHooks: []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ },
},
expect: func(reg *driver.RegistryDefault) []recovery.PreHookExecutor {
return []recovery.PreHookExecutor{
@@ -145,8 +152,9 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
} {
t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) {
- conf, reg := internal.NewVeryFastRegistryWithoutDB(t)
- tc.prep(conf)
+ t.Parallel()
+
+ ctx := confighelpers.WithConfigValues(ctx, tc.config)
h := reg.PreRecoveryHooks(ctx)
@@ -159,21 +167,20 @@ func TestDriverDefault_Hooks(t *testing.T) {
// AFTER hooks
for _, tc := range []struct {
uc string
- prep func(conf *config.Config)
+ config map[string]any
expect func(reg *driver.RegistryDefault) []recovery.PostHookExecutor
}{
{
uc: "No hooks configured",
- prep: func(conf *config.Config) {},
expect: func(reg *driver.RegistryDefault) []recovery.PostHookExecutor { return nil },
},
{
uc: "Multiple web_hooks configured",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryAfter+".hooks", []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
- {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
- })
+ config: map[string]any{
+ config.ViperKeySelfServiceRecoveryAfter + ".hooks": []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ },
},
expect: func(reg *driver.RegistryDefault) []recovery.PostHookExecutor {
return []recovery.PostHookExecutor{
@@ -184,8 +191,9 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
} {
t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) {
- conf, reg := internal.NewVeryFastRegistryWithoutDB(t)
- tc.prep(conf)
+ t.Parallel()
+
+ ctx := confighelpers.WithConfigValues(ctx, tc.config)
h := reg.PostRecoveryHooks(ctx)
@@ -201,12 +209,11 @@ func TestDriverDefault_Hooks(t *testing.T) {
// BEFORE hooks
for _, tc := range []struct {
uc string
- prep func(conf *config.Config)
+ config map[string]any
expect func(reg *driver.RegistryDefault) []registration.PreHookExecutor
}{
{
- uc: "No hooks configured",
- prep: func(conf *config.Config) {},
+ uc: "No hooks configured",
expect: func(reg *driver.RegistryDefault) []registration.PreHookExecutor {
return []registration.PreHookExecutor{
hook.NewTwoStepRegistration(reg),
@@ -215,11 +222,11 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
{
uc: "Two web_hooks are configured",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationBeforeHooks, []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
- {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
- })
+ config: map[string]any{
+ config.ViperKeySelfServiceRegistrationBeforeHooks: []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ },
},
expect: func(reg *driver.RegistryDefault) []registration.PreHookExecutor {
return []registration.PreHookExecutor{
@@ -231,8 +238,9 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
} {
t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) {
- conf, reg := internal.NewVeryFastRegistryWithoutDB(t)
- tc.prep(conf)
+ t.Parallel()
+
+ ctx := confighelpers.WithConfigValues(ctx, tc.config)
h := reg.PreRegistrationHooks(ctx)
@@ -245,21 +253,20 @@ func TestDriverDefault_Hooks(t *testing.T) {
// AFTER hooks
for _, tc := range []struct {
uc string
- prep func(conf *config.Config)
+ config map[string]any
expect func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor
}{
{
uc: "No hooks configured",
- prep: func(conf *config.Config) {},
expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor { return nil },
},
{
uc: "Only session hook configured for password strategy",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true)
- conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".password.hooks", []map[string]interface{}{
+ config: map[string]any{
+ config.ViperKeySelfServiceVerificationEnabled: true,
+ config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{
{"hook": "session"},
- })
+ },
},
expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor {
return []registration.PostHookPostPersistExecutor{
@@ -270,12 +277,12 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
{
uc: "A session hook and a web_hook are configured for password strategy",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true)
- conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".password.hooks", []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}},
+ config: map[string]any{
+ config.ViperKeySelfServiceVerificationEnabled: true,
+ config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}},
{"hook": "session"},
- })
+ },
},
expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor {
return []registration.PostHookPostPersistExecutor{
@@ -287,11 +294,11 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
{
uc: "Two web_hooks are configured on a global level",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
- {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
- })
+ config: map[string]any{
+ config.ViperKeySelfServiceRegistrationAfter + ".hooks": []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ },
},
expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor {
return []registration.PostHookPostPersistExecutor{
@@ -302,15 +309,15 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
{
uc: "Hooks are configured on a global level, as well as on a strategy level",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".password.hooks", []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ config: map[string]any{
+ config.ViperKeySelfServiceRegistrationAfter + ".password.hooks": []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
{"hook": "session"},
- })
- conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
- })
- conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true)
+ },
+ config.ViperKeySelfServiceRegistrationAfter + ".hooks": []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ },
+ config.ViperKeySelfServiceVerificationEnabled: true,
},
expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor {
return []registration.PostHookPostPersistExecutor{
@@ -322,10 +329,10 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
{
uc: "show_verification_ui is configured",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", []map[string]interface{}{
+ config: map[string]any{
+ config.ViperKeySelfServiceRegistrationAfter + ".hooks": []map[string]any{
{"hook": "show_verification_ui"},
- })
+ },
},
expect: func(reg *driver.RegistryDefault) []registration.PostHookPostPersistExecutor {
return []registration.PostHookPostPersistExecutor{
@@ -335,8 +342,9 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
} {
t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) {
- conf, reg := internal.NewVeryFastRegistryWithoutDB(t)
- tc.prep(conf)
+ t.Parallel()
+
+ ctx := confighelpers.WithConfigValues(ctx, tc.config)
h := reg.PostRegistrationPostPersistHooks(ctx, identity.CredentialsTypePassword)
@@ -352,21 +360,20 @@ func TestDriverDefault_Hooks(t *testing.T) {
// BEFORE hooks
for _, tc := range []struct {
uc string
- prep func(conf *config.Config)
+ config map[string]any
expect func(reg *driver.RegistryDefault) []login.PreHookExecutor
}{
{
uc: "No hooks configured",
- prep: func(conf *config.Config) {},
expect: func(reg *driver.RegistryDefault) []login.PreHookExecutor { return nil },
},
{
uc: "Two web_hooks are configured",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceLoginBeforeHooks, []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
- {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
- })
+ config: map[string]any{
+ config.ViperKeySelfServiceLoginBeforeHooks: []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ },
},
expect: func(reg *driver.RegistryDefault) []login.PreHookExecutor {
return []login.PreHookExecutor{
@@ -377,8 +384,9 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
} {
t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) {
- conf, reg := internal.NewVeryFastRegistryWithoutDB(t)
- tc.prep(conf)
+ t.Parallel()
+
+ ctx := confighelpers.WithConfigValues(ctx, tc.config)
h := reg.PreLoginHooks(ctx)
@@ -391,20 +399,19 @@ func TestDriverDefault_Hooks(t *testing.T) {
// AFTER hooks
for _, tc := range []struct {
uc string
- prep func(conf *config.Config)
+ config map[string]any
expect func(reg *driver.RegistryDefault) []login.PostHookExecutor
}{
{
uc: "No hooks configured",
- prep: func(conf *config.Config) {},
expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor { return nil },
},
{
uc: "Only revoke_active_sessions hook configured for password strategy",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{
+ config: map[string]any{
+ config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{
{"hook": "revoke_active_sessions"},
- })
+ },
},
expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor {
return []login.PostHookExecutor{
@@ -414,10 +421,10 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
{
uc: "Only require_verified_address hook configured for password strategy",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{
+ config: map[string]any{
+ config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{
{"hook": "require_verified_address"},
- })
+ },
},
expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor {
return []login.PostHookExecutor{
@@ -427,12 +434,12 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
{
uc: "A revoke_active_sessions hook, require_verified_address hook and a web_hook are configured for password strategy",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}},
+ config: map[string]any{
+ config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"headers": map[string]string{"X-Custom-Header": "test"}, "url": "foo", "method": "POST", "body": "bar"}},
{"hook": "require_verified_address"},
{"hook": "revoke_active_sessions"},
- })
+ },
},
expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor {
return []login.PostHookExecutor{
@@ -444,11 +451,11 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
{
uc: "Two web_hooks are configured on a global level",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".hooks", []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
- {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
- })
+ config: map[string]any{
+ config.ViperKeySelfServiceLoginAfter + ".hooks": []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ },
},
expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor {
return []login.PostHookExecutor{
@@ -459,15 +466,15 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
{
uc: "Hooks are configured on a global level, as well as on a strategy level",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".password.hooks", []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ config: map[string]any{
+ config.ViperKeySelfServiceLoginAfter + ".password.hooks": []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
{"hook": "revoke_active_sessions"},
{"hook": "require_verified_address"},
- })
- conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".hooks", []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
- })
+ },
+ config.ViperKeySelfServiceLoginAfter + ".hooks": []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ },
},
expect: func(reg *driver.RegistryDefault) []login.PostHookExecutor {
return []login.PostHookExecutor{
@@ -479,8 +486,9 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
} {
t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) {
- conf, reg := internal.NewVeryFastRegistryWithoutDB(t)
- tc.prep(conf)
+ t.Parallel()
+
+ ctx := confighelpers.WithConfigValues(ctx, tc.config)
h := reg.PostLoginHooks(ctx, identity.CredentialsTypePassword)
@@ -496,21 +504,20 @@ func TestDriverDefault_Hooks(t *testing.T) {
// BEFORE hooks
for _, tc := range []struct {
uc string
- prep func(conf *config.Config)
+ config map[string]any
expect func(reg *driver.RegistryDefault) []settings.PreHookExecutor
}{
{
uc: "No hooks configured",
- prep: func(conf *config.Config) {},
expect: func(reg *driver.RegistryDefault) []settings.PreHookExecutor { return nil },
},
{
uc: "Two web_hooks are configured",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceSettingsBeforeHooks, []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
- {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
- })
+ config: map[string]any{
+ config.ViperKeySelfServiceSettingsBeforeHooks: []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ },
},
expect: func(reg *driver.RegistryDefault) []settings.PreHookExecutor {
return []settings.PreHookExecutor{
@@ -521,8 +528,9 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
} {
t.Run(fmt.Sprintf("before/uc=%s", tc.uc), func(t *testing.T) {
- conf, reg := internal.NewVeryFastRegistryWithoutDB(t)
- tc.prep(conf)
+ t.Parallel()
+
+ ctx := confighelpers.WithConfigValues(ctx, tc.config)
h := reg.PreSettingsHooks(ctx)
@@ -535,18 +543,17 @@ func TestDriverDefault_Hooks(t *testing.T) {
// AFTER hooks
for _, tc := range []struct {
uc string
- prep func(conf *config.Config)
+ config map[string]any
expect func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor
}{
{
uc: "No hooks configured",
- prep: func(conf *config.Config) {},
expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor { return nil },
},
{
uc: "Only verify hook configured for the strategy",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true)
+ config: map[string]any{
+ config.ViperKeySelfServiceVerificationEnabled: true,
// I think this is a bug as there is a hook named verify defined for both profile and password
// strategies. Instead of using it, the code makes use of the property used above and which
// is defined in an entirely different flow (verification).
@@ -559,11 +566,11 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
{
uc: "A verify hook and a web_hook are configured for profile strategy",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".profile.hooks", []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"headers": []map[string]string{{"X-Custom-Header": "test"}}, "url": "foo", "method": "POST", "body": "bar"}},
- })
- conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true)
+ config: map[string]any{
+ config.ViperKeySelfServiceSettingsAfter + ".profile.hooks": []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"headers": []map[string]string{{"X-Custom-Header": "test"}}, "url": "foo", "method": "POST", "body": "bar"}},
+ },
+ config.ViperKeySelfServiceVerificationEnabled: true,
},
expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor {
return []settings.PostHookPostPersistExecutor{
@@ -574,11 +581,11 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
{
uc: "Two web_hooks are configured on a global level",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".hooks", []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
- {"hook": "web_hook", "config": map[string]interface{}{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
- })
+ config: map[string]any{
+ config.ViperKeySelfServiceSettingsAfter + ".hooks": []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ {"hook": "web_hook", "config": map[string]any{"url": "bar", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ },
},
expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor {
return []settings.PostHookPostPersistExecutor{
@@ -589,14 +596,14 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
{
uc: "Hooks are configured on a global level, as well as on a strategy level",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceVerificationEnabled, true)
- conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".profile.hooks", []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
- })
- conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".hooks", []map[string]interface{}{
- {"hook": "web_hook", "config": map[string]interface{}{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
- })
+ config: map[string]any{
+ config.ViperKeySelfServiceVerificationEnabled: true,
+ config.ViperKeySelfServiceSettingsAfter + ".profile.hooks": []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "GET", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ },
+ config.ViperKeySelfServiceSettingsAfter + ".hooks": []map[string]any{
+ {"hook": "web_hook", "config": map[string]any{"url": "foo", "method": "POST", "headers": map[string]string{"X-Custom-Header": "test"}}},
+ },
},
expect: func(reg *driver.RegistryDefault) []settings.PostHookPostPersistExecutor {
return []settings.PostHookPostPersistExecutor{
@@ -607,8 +614,9 @@ func TestDriverDefault_Hooks(t *testing.T) {
},
} {
t.Run(fmt.Sprintf("after/uc=%s", tc.uc), func(t *testing.T) {
- conf, reg := internal.NewVeryFastRegistryWithoutDB(t)
- tc.prep(conf)
+ t.Parallel()
+
+ ctx := confighelpers.WithConfigValues(ctx, tc.config)
h := reg.PostSettingsPostPersistHooks(ctx, "profile")
@@ -623,62 +631,64 @@ func TestDriverDefault_Hooks(t *testing.T) {
func TestDriverDefault_Strategies(t *testing.T) {
t.Parallel()
ctx := context.Background()
+ _, reg := internal.NewVeryFastRegistryWithoutDB(t)
+
t.Run("case=registration", func(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
- prep func(conf *config.Config)
+ config map[string]any
expect []string
}{
{
name: "no strategies",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false)
+ config: map[string]any{
+ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false,
+ config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false,
},
expect: []string{"profile"},
},
{
name: "only password",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false)
+ config: map[string]any{
+ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false,
},
expect: []string{"password", "profile"},
},
{
name: "oidc and password",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc.enabled", true)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false)
+ config: map[string]any{
+ config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false,
},
expect: []string{"password", "oidc", "profile"},
},
{
name: "oidc, password and totp",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc.enabled", true)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".totp.enabled", true)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false)
+ config: map[string]any{
+ config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false,
},
expect: []string{"password", "oidc", "profile"},
},
{
name: "password and code",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", true)
+ config: map[string]any{
+ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".code.enabled": true,
},
expect: []string{"password", "profile", "code"},
},
} {
t.Run(fmt.Sprintf("subcase=%s", tc.name), func(t *testing.T) {
- conf, reg := internal.NewVeryFastRegistryWithoutDB(t)
- tc.prep(conf)
+ t.Parallel()
- s := reg.RegistrationStrategies(context.Background())
+ ctx := confighelpers.WithConfigValues(ctx, tc.config)
+ s := reg.RegistrationStrategies(ctx)
require.Len(t, s, len(tc.expect))
for k, e := range tc.expect {
assert.Equal(t, e, s[k].ID().String())
@@ -689,68 +699,69 @@ func TestDriverDefault_Strategies(t *testing.T) {
t.Run("case=login", func(t *testing.T) {
t.Parallel()
+
for _, tc := range []struct {
name string
- prep func(conf *config.Config)
+ config map[string]any
expect []string
}{
{
name: "no strategies",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false)
+ config: map[string]any{
+ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false,
+ config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false,
},
},
{
name: "only password",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false)
+ config: map[string]any{
+ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false,
},
expect: []string{"password"},
},
{
name: "oidc and password",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc.enabled", true)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false)
+ config: map[string]any{
+ config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false,
},
expect: []string{"password", "oidc"},
},
{
name: "oidc, password and totp",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".oidc.enabled", true)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".totp.enabled", true)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false)
+ config: map[string]any{
+ config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false,
},
expect: []string{"password", "oidc", "totp"},
},
{
name: "password and code",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", true)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", true)
+ config: map[string]any{
+ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".code.enabled": true,
},
expect: []string{"password", "code"},
},
{
name: "code is enabled if passwordless_enabled is true",
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".password.enabled", false)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.passwordless_enabled", true)
+ config: map[string]any{
+ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false,
+ config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false,
+ config.ViperKeySelfServiceStrategyConfig + ".code.passwordless_enabled": true,
},
expect: []string{"code"},
},
} {
t.Run(fmt.Sprintf("run=%s", tc.name), func(t *testing.T) {
- conf, reg := internal.NewVeryFastRegistryWithoutDB(t)
- tc.prep(conf)
+ t.Parallel()
- s := reg.LoginStrategies(context.Background())
+ ctx := confighelpers.WithConfigValues(ctx, tc.config)
+ s := reg.LoginStrategies(ctx)
require.Len(t, s, len(tc.expect))
for k, e := range tc.expect {
assert.Equal(t, e, s[k].ID().String())
@@ -762,27 +773,28 @@ func TestDriverDefault_Strategies(t *testing.T) {
t.Run("case=recovery", func(t *testing.T) {
t.Parallel()
for k, tc := range []struct {
- prep func(conf *config.Config)
+ config map[string]any
expect []string
}{
{
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", false)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".link.enabled", false)
+ config: map[string]any{
+ config.ViperKeySelfServiceStrategyConfig + ".code.enabled": false,
+ config.ViperKeySelfServiceStrategyConfig + ".link.enabled": false,
},
},
{
- prep: func(conf *config.Config) {
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".code.enabled", true)
- conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+".link.enabled", true)
+ config: map[string]any{
+ config.ViperKeySelfServiceStrategyConfig + ".code.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".link.enabled": true,
}, expect: []string{"code", "link"},
},
} {
t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) {
- conf, reg := internal.NewVeryFastRegistryWithoutDB(t)
- tc.prep(conf)
+ t.Parallel()
+
+ ctx := confighelpers.WithConfigValues(ctx, tc.config)
- s := reg.RecoveryStrategies(context.Background())
+ s := reg.RecoveryStrategies(ctx)
require.Len(t, s, len(tc.expect))
for k, e := range tc.expect {
assert.Equal(t, e, s[k].RecoveryStrategyID())
@@ -796,81 +808,55 @@ func TestDriverDefault_Strategies(t *testing.T) {
l := logrusx.New("", "")
for k, tc := range []struct {
- prep func(t *testing.T) *config.Config
- expect []string
+ configOptions []configx.OptionModifier
+ expect []string
}{
{
- prep: func(t *testing.T) *config.Config {
- c := config.MustNew(t, l,
- os.Stderr,
- configx.WithValues(map[string]interface{}{
- config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN,
- config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false,
- config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": false,
- config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": false,
- }),
- configx.SkipValidation())
- return c
- },
- },
- {
- prep: func(t *testing.T) *config.Config {
- c := config.MustNew(t, l,
- os.Stderr,
- configx.WithValues(map[string]interface{}{
- config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN,
- config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true,
- config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false,
- }),
- configx.SkipValidation())
- return c
- },
+ configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{
+ config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN,
+ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false,
+ config.ViperKeySelfServiceStrategyConfig + ".oidc.enabled": false,
+ config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": false,
+ })},
+ },
+ {
+ configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{
+ config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN,
+ config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false,
+ })},
expect: []string{"profile"},
},
{
- prep: func(t *testing.T) *config.Config {
- c := config.MustNew(t, l,
- os.Stderr,
- configx.WithValues(map[string]interface{}{
- config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN,
- config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true,
- config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false,
- config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true,
- }),
- configx.SkipValidation())
- return c
- },
+ configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{
+ config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN,
+ config.ViperKeySelfServiceStrategyConfig + ".profile.enabled": true,
+ config.ViperKeySelfServiceStrategyConfig + ".password.enabled": false,
+ config.ViperKeySelfServiceStrategyConfig + ".totp.enabled": true,
+ })},
expect: []string{"profile", "totp"},
},
{
- prep: func(t *testing.T) *config.Config {
- return config.MustNew(t, l,
- os.Stderr,
- configx.WithValues(map[string]interface{}{
- config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN,
- }),
- configx.SkipValidation())
- },
+ configOptions: []configx.OptionModifier{configx.WithValues(map[string]any{
+ config.ViperKeyDSN: config.DefaultSQLiteMemoryDSN,
+ })},
expect: []string{"password", "profile"},
},
{
- prep: func(t *testing.T) *config.Config {
- return config.MustNew(t, l,
- os.Stderr,
- configx.WithConfigFiles("../test/e2e/profiles/verification/.kratos.yml"),
- configx.WithValue(config.ViperKeyDSN, config.DefaultSQLiteMemoryDSN),
- configx.SkipValidation())
+ configOptions: []configx.OptionModifier{
+ configx.WithConfigFiles("../test/e2e/profiles/verification/.kratos.yml"),
+ configx.WithValue(config.ViperKeyDSN, config.DefaultSQLiteMemoryDSN),
},
expect: []string{"password", "profile"},
},
} {
t.Run(fmt.Sprintf("run=%d", k), func(t *testing.T) {
- conf := tc.prep(t)
+ conf := config.MustNew(t, l, os.Stderr, &contextx.Default{}, append(tc.configOptions, configx.SkipValidation())...)
- reg, err := driver.NewRegistryFromDSN(ctx, conf, logrusx.New("", ""))
+ reg, err := driver.NewRegistryFromDSN(ctx, conf, l)
require.NoError(t, err)
- s := reg.SettingsStrategies(context.Background())
+ s := reg.SettingsStrategies(ctx)
require.Len(t, s, len(tc.expect))
for k, e := range tc.expect {
@@ -886,7 +872,7 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) {
_, reg := internal.NewVeryFastRegistryWithoutDB(t)
t.Run("case=all login strategies", func(t *testing.T) {
- expects := []string{"password", "oidc", "code", "totp", "passkey", "webauthn", "lookup_secret"}
+ expects := []string{"password", "oidc", "code", "totp", "passkey", "webauthn", "lookup_secret", "identifier_first"}
s := reg.AllLoginStrategies()
require.Len(t, s, len(expects))
for k, e := range expects {
@@ -924,12 +910,16 @@ func TestDefaultRegistry_AllStrategies(t *testing.T) {
func TestGetActiveRecoveryStrategy(t *testing.T) {
t.Parallel()
- conf, reg := internal.NewVeryFastRegistryWithoutDB(t)
+ ctx := context.Background()
+ _, reg := internal.NewVeryFastRegistryWithoutDB(t)
+
t.Run("returns error if active strategy is disabled", func(t *testing.T) {
- conf.Set(context.Background(), "selfservice.methods.code.enabled", false)
- conf.Set(context.Background(), config.ViperKeySelfServiceRecoveryUse, "code")
+ ctx := confighelpers.WithConfigValues(ctx, map[string]any{
+ "selfservice.methods.code.enabled": false,
+ config.ViperKeySelfServiceRecoveryUse: "code",
+ })
- _, err := reg.GetActiveRecoveryStrategy(context.Background())
+ _, err := reg.GetActiveRecoveryStrategy(ctx)
require.Error(t, err)
})
@@ -938,10 +928,12 @@ func TestGetActiveRecoveryStrategy(t *testing.T) {
"code", "link",
} {
t.Run(fmt.Sprintf("strategy=%s", sID), func(t *testing.T) {
- conf.Set(context.Background(), fmt.Sprintf("selfservice.methods.%s.enabled", sID), true)
- conf.Set(context.Background(), config.ViperKeySelfServiceRecoveryUse, sID)
+ ctx := confighelpers.WithConfigValues(ctx, map[string]any{
+ fmt.Sprintf("selfservice.methods.%s.enabled", sID): true,
+ config.ViperKeySelfServiceRecoveryUse: sID,
+ })
- s, err := reg.GetActiveRecoveryStrategy(context.Background())
+ s, err := reg.GetActiveRecoveryStrategy(ctx)
require.NoError(t, err)
require.Equal(t, sID, s.RecoveryStrategyID())
})
@@ -951,12 +943,14 @@ func TestGetActiveRecoveryStrategy(t *testing.T) {
func TestGetActiveVerificationStrategy(t *testing.T) {
t.Parallel()
- conf, reg := internal.NewVeryFastRegistryWithoutDB(t)
+ ctx := context.Background()
+ _, reg := internal.NewVeryFastRegistryWithoutDB(t)
t.Run("returns error if active strategy is disabled", func(t *testing.T) {
- conf.Set(context.Background(), "selfservice.methods.code.enabled", false)
- conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUse, "code")
-
- _, err := reg.GetActiveVerificationStrategy(context.Background())
+ ctx := confighelpers.WithConfigValues(ctx, map[string]any{
+ "selfservice.methods.code.enabled": false,
+ config.ViperKeySelfServiceVerificationUse: "code",
+ })
+ _, err := reg.GetActiveVerificationStrategy(ctx)
require.Error(t, err)
})
@@ -965,10 +959,12 @@ func TestGetActiveVerificationStrategy(t *testing.T) {
"code", "link",
} {
t.Run(fmt.Sprintf("strategy=%s", sID), func(t *testing.T) {
- conf.Set(context.Background(), fmt.Sprintf("selfservice.methods.%s.enabled", sID), true)
- conf.Set(context.Background(), config.ViperKeySelfServiceVerificationUse, sID)
+ ctx := confighelpers.WithConfigValues(ctx, map[string]any{
+ fmt.Sprintf("selfservice.methods.%s.enabled", sID): true,
+ config.ViperKeySelfServiceVerificationUse: sID,
+ })
- s, err := reg.GetActiveVerificationStrategy(context.Background())
+ s, err := reg.GetActiveVerificationStrategy(ctx)
require.NoError(t, err)
require.Equal(t, sID, s.VerificationStrategyID())
})
diff --git a/embedx/config.schema.json b/embedx/config.schema.json
index 9e6ee07d6889..43e7ce467814 100644
--- a/embedx/config.schema.json
+++ b/embedx/config.schema.json
@@ -432,7 +432,7 @@
},
"provider": {
"title": "Provider",
- "description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon, eparaksts, eparaksts-mobile.",
+ "description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, salesforce, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon, eparaksts, eparaksts-mobile.",
"type": "string",
"enum": [
"github",
@@ -442,6 +442,7 @@
"google",
"microsoft",
"discord",
+ "salesforce",
"slack",
"facebook",
"auth0",
@@ -1298,6 +1299,13 @@
"default": "1h",
"examples": ["1h", "1m", "1s"]
},
+ "style": {
+ "title": "Login Flow Style",
+ "description": "The style of the login flow. If set to `unified` the login flow will be a one-step process. If set to `identifier_first` (experimental!) the login flow will first ask for the identifier and then the credentials.",
+ "type": "string",
+ "enum": ["unified", "identifier_first"],
+ "default": "unified"
+ },
"before": {
"$ref": "#/definitions/selfServiceBeforeLogin"
},
@@ -1422,6 +1430,48 @@
"type": "object",
"additionalProperties": false,
"properties": {
+ "b2b": {
+ "title": "Single Sign-On for B2B",
+ "description": "Single Sign-On for B2B allows your customers to bring their own (workforce) identity server (e.g. OneLogin). This feature is not available in the open source licensed code.",
+ "type": "object",
+ "properties": {
+ "config": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "organizations": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "The ID of the organization.",
+ "format": "uuid",
+ "examples": ["00000000-0000-0000-0000-000000000000"]
+ },
+ "label": {
+ "type": "string",
+ "description": "The label of the organization.",
+ "examples": ["ACME SSO"]
+ },
+ "domains": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "format": "hostname",
+ "examples": ["my-app.com"],
+ "description": "If this domain matches the email's domain, this provider is shown."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "additionalProperties": false
+ },
"profile": {
"type": "object",
"additionalProperties": false,
@@ -1465,24 +1515,36 @@
},
"code": {
"type": "object",
- "additionalProperties": false,
+ "additionalProperties": true,
"anyOf": [
{
"properties": {
- "passwordless_enabled": { "const": true },
- "mfa_enabled": { "const": false }
+ "passwordless_enabled": {
+ "const": true
+ },
+ "mfa_enabled": {
+ "const": false
+ }
}
},
{
"properties": {
- "mfa_enabled": { "const": true },
- "passwordless_enabled": { "const": false }
+ "mfa_enabled": {
+ "const": true
+ },
+ "passwordless_enabled": {
+ "const": false
+ }
}
},
{
"properties": {
- "mfa_enabled": { "const": false },
- "passwordless_enabled": { "const": false }
+ "mfa_enabled": {
+ "const": false
+ },
+ "passwordless_enabled": {
+ "const": false
+ }
}
}
],
@@ -1498,12 +1560,6 @@
"title": "Enables login flows code method to fulfil MFA requests",
"default": false
},
- "passwordless_login_fallback_enabled": {
- "type": "boolean",
- "title": "Passwordless Login Fallback Enabled",
- "description": "This setting allows the code method to always login a user with code if they have registered with another authentication method such as password or social sign in.",
- "default": false
- },
"enabled": {
"type": "boolean",
"title": "Enables Code Method",
@@ -1577,6 +1633,61 @@
"description": "If set to false the password validation does not check for similarity between the password and the user identifier.",
"type": "boolean",
"default": true
+ },
+ "migrate_hook": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean",
+ "title": "Enable Password Migration",
+ "description": "If set to true will enable password migration.",
+ "default": false
+ },
+ "config": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "url": {
+ "type": "string",
+ "description": "The URL the password migration hook should call",
+ "format": "uri"
+ },
+ "method": {
+ "type": "string",
+ "description": "The HTTP method to use (GET, POST, etc).",
+ "const": "POST",
+ "default": "POST"
+ },
+ "headers": {
+ "type": "object",
+ "description": "The HTTP headers that must be applied to the password migration hook.",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "emit_analytics_event": {
+ "type": "boolean",
+ "default": true,
+ "description": "Emit tracing events for this hook on delivery or error"
+ },
+ "auth": {
+ "type": "object",
+ "title": "Auth mechanisms",
+ "description": "Define which auth mechanism the Web-Hook should use",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/webHookAuthApiKeyProperties"
+ },
+ {
+ "$ref": "#/definitions/webHookAuthBasicAuthProperties"
+ }
+ ]
+ },
+ "additionalProperties": false
+ }
+ }
+ }
}
},
"additionalProperties": false
@@ -2765,6 +2876,21 @@
}
}
},
+ "security": {
+ "type": "object",
+ "properties": {
+ "account_enumeration": {
+ "type": "object",
+ "properties": {
+ "mitigate": {
+ "type": "boolean",
+ "default": false,
+ "description": "Mitigate account enumeration by making it harder to figure out if an identifier (email, phone number) exists or not. Enabling this setting degrades user experience. This setting does not mitigate all possible attack vectors yet."
+ }
+ }
+ }
+ }
+ },
"version": {
"title": "The kratos version this config is written for.",
"description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.",
@@ -2866,7 +2992,7 @@
},
"organizations": {
"title": "Organizations",
- "description": "Secifies which organizations are available. Only effective in the Ory Network.",
+ "description": "Please use selfservice.methods.b2b instead. This key will be removed. Only effective in the Ory Network.",
"type": "array",
"default": []
},
diff --git a/examples/go/pkg/common.go b/examples/go/pkg/common.go
index edb8c17e2e19..f39725103e33 100644
--- a/examples/go/pkg/common.go
+++ b/examples/go/pkg/common.go
@@ -11,11 +11,9 @@ import (
"os"
"testing"
- "github.com/ory/kratos/x"
-
- "github.com/ory/kratos/internal/testhelpers"
-
ory "github.com/ory/client-go"
+ "github.com/ory/kratos/internal/testhelpers"
+ "github.com/ory/kratos/x"
)
func PrintJSONPretty(v interface{}) {
diff --git a/go.mod b/go.mod
index 537942ebacbd..47238d202c79 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/ory/kratos
-go 1.21
+go 1.22
replace (
github.com/go-sql-driver/mysql => github.com/go-sql-driver/mysql v1.7.2-0.20231005084435-37980127edfb
@@ -28,7 +28,7 @@ require (
github.com/davecgh/go-spew v1.1.1
github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6
github.com/dgraph-io/ristretto v0.1.1
- github.com/fatih/color v1.13.0
+ github.com/fatih/color v1.16.0
github.com/ghodss/yaml v1.0.0
github.com/go-crypt/crypt v0.2.9
github.com/go-faker/faker/v4 v4.2.0
@@ -50,7 +50,7 @@ require (
github.com/gorilla/sessions v1.2.1
github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69
github.com/hashicorp/consul/api v1.20.0
- github.com/hashicorp/go-retryablehttp v0.7.2
+ github.com/hashicorp/go-retryablehttp v0.7.7
github.com/hashicorp/golang-lru v0.5.4
github.com/imdario/mergo v0.3.13
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf
@@ -69,7 +69,7 @@ require (
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe
github.com/ory/analytics-go/v5 v5.0.1
github.com/ory/client-go v0.2.0-alpha.60
- github.com/ory/dockertest/v3 v3.9.1
+ github.com/ory/dockertest/v3 v3.10.1-0.20240619125955-3328cf9343b8
github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe
github.com/ory/graceful v0.1.4-0.20230301144740-e222150c51d0
github.com/ory/herodot v0.10.3-0.20230626083119-d7e5192f0d88
@@ -77,7 +77,7 @@ require (
github.com/ory/jsonschema/v3 v3.0.8
github.com/ory/mail/v3 v3.0.0
github.com/ory/nosurf v1.2.7
- github.com/ory/x v0.0.623
+ github.com/ory/x v0.0.639
github.com/peterhellberg/link v1.2.0
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/pkg/errors v0.9.1
@@ -85,7 +85,7 @@ require (
github.com/rakutentech/jwk-go v1.1.3
github.com/rs/cors v1.8.2
github.com/samber/lo v1.37.0
- github.com/sirupsen/logrus v1.9.0
+ github.com/sirupsen/logrus v1.9.3
github.com/slack-go/slack v0.7.4
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
@@ -110,11 +110,11 @@ require (
)
require (
- github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
- github.com/Microsoft/go-winio v0.6.0 // 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.3.0 // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
@@ -126,17 +126,17 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar v1.3.4 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
- github.com/cenkalti/backoff/v4 v4.2.1 // indirect
+ github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cockroachdb/cockroach-go/v2 v2.3.5
- github.com/containerd/continuity v0.3.0 // indirect
+ github.com/containerd/continuity v0.4.3 // indirect
github.com/cortesi/moddwatch v0.0.0-20210222043437-a6aaad86a36e // indirect
github.com/cortesi/termlog v0.0.0-20210222042314-a1eec763abec // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
- github.com/docker/cli v20.10.21+incompatible // indirect
+ github.com/docker/cli v24.0.9+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v20.10.27+incompatible // indirect
- github.com/docker/go-connections v0.4.0 // 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.4.0 // indirect
@@ -162,7 +162,7 @@ require (
github.com/go-openapi/validate v0.22.1 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
- github.com/go-sql-driver/mysql v1.7.0 // indirect
+ github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-webauthn/x v0.1.4 // indirect
github.com/gobuffalo/envy v1.10.2 // indirect
github.com/gobuffalo/flect v1.0.0 // indirect
@@ -195,7 +195,7 @@ require (
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
- github.com/hashicorp/go-hclog v1.2.0 // indirect
+ github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
@@ -233,7 +233,7 @@ require (
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
- github.com/lib/pq v1.10.7 // indirect
+ github.com/lib/pq v1.10.9 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailhog/MailHog-Server v1.0.1 // indirect
github.com/mailhog/MailHog-UI v1.0.1 // indirect
@@ -244,21 +244,21 @@ require (
github.com/mailhog/storage v1.0.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
- github.com/mattn/go-isatty v0.0.16 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
- github.com/microcosm-cc/bluemonday v1.0.21 // indirect
+ github.com/microcosm-cc/bluemonday v1.0.26 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
- github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
+ github.com/moby/term v0.5.0 // indirect
github.com/nyaruka/phonenumbers v1.3.6 // indirect
github.com/ogier/pflag v0.0.1 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
- github.com/opencontainers/runc v1.1.12 // indirect
+ github.com/opencontainers/runc v1.1.13 // indirect
github.com/openzipkin/zipkin-go v0.4.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
@@ -308,7 +308,7 @@ require (
go.opentelemetry.io/otel/metric v1.22.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
golang.org/x/mod v0.14.0 // indirect
- golang.org/x/sys v0.19.0 // indirect
+ golang.org/x/sys v0.21.0 // indirect
golang.org/x/term v0.19.0 // indirect
golang.org/x/tools v0.16.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
diff --git a/go.sum b/go.sum
index 85e85e83626c..33830713d3b9 100644
--- a/go.sum
+++ b/go.sum
@@ -38,8 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
code.dny.dev/ssrf v0.2.0 h1:wCBP990rQQ1CYfRpW+YK1+8xhwUjv189AQ3WMo1jQaI=
code.dny.dev/ssrf v0.2.0/go.mod h1:B+91l25OnyaLIeCx0WRJN5qfJ/4/ZTZxRXgm0lj/2w8=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
-github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
@@ -52,8 +52,8 @@ github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7Y
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
-github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
-github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+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/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
@@ -105,8 +105,8 @@ github.com/bwmarrin/discordgo v0.23.0 h1://ARp8qUrRZvDGMkfAjtcC20WOvsMtTgi+KrdKn
github.com/bwmarrin/discordgo v0.23.0/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
-github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
-github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -125,8 +125,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/cockroach-go/v2 v2.3.5 h1:Khtm8K6fTTz/ZCWPzU9Ne3aOW9VyAnj4qIPCJgKtwK0=
github.com/cockroachdb/cockroach-go/v2 v2.3.5/go.mod h1:1wNJ45eSXW9AnOc3skntW9ZUZz6gxrQK3cOj3rK+BC8=
-github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg=
-github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM=
+github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
+github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
@@ -140,8 +140,9 @@ github.com/cortesi/termlog v0.0.0-20210222042314-a1eec763abec/go.mod h1:10Fm2kas
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
+github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -156,14 +157,14 @@ 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/docker/cli v20.10.21+incompatible h1:qVkgyYUnOLQ98LtXBrwd/duVqPT2X4SHndOuGsfwyhU=
-github.com/docker/cli v20.10.21+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/cli v24.0.9+incompatible h1:OxbimnP/z+qVjDLpq9wbeFU3Nc30XhSe+LkwYQisD50=
+github.com/docker/cli v24.0.9+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v20.10.27+incompatible h1:Id/ZooynV4ZlD6xX20RCd3SR0Ikn7r4QZDa2ECK2TgA=
github.com/docker/docker v20.10.27+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
-github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
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=
@@ -181,8 +182,9 @@ github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2Vvl
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
-github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
+github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
@@ -480,9 +482,8 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
-github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
-github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
-github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
+github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
+github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
@@ -493,8 +494,8 @@ github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
-github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
-github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
+github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
+github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
@@ -689,8 +690,9 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/luna-duclos/instrumentedsql v1.1.3 h1:t7mvC0z1jUt5A0UQ6I/0H31ryymuQRnJcWCiqV3lSAA=
github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQLANWwZt2ULeIC8yMNYs=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
@@ -728,18 +730,19 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/goveralls v0.0.7 h1:vzy0i4a2iDzEFMdXIxcanRadkr0FBvSBKUmj0P8SPlQ=
@@ -748,8 +751,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50=
-github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
-github.com/microcosm-cc/bluemonday v1.0.21/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/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
@@ -769,8 +772,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
-github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI=
-github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
@@ -802,14 +805,14 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
-github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss=
-github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8=
+github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs=
+github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA=
github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA=
github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY=
github.com/ory/analytics-go/v5 v5.0.1 h1:LX8T5B9FN8KZXOtxgN+R3I4THRRVB6+28IKgKBpXmAM=
github.com/ory/analytics-go/v5 v5.0.1/go.mod h1:lWCiCjAaJkKfgR/BN5DCLMol8BjKS1x+4jxBxff/FF0=
-github.com/ory/dockertest/v3 v3.9.1 h1:v4dkG+dlu76goxMiTT2j8zV7s4oPPEppKT8K8p2f1kY=
-github.com/ory/dockertest/v3 v3.9.1/go.mod h1:42Ir9hmvaAPm0Mgibk6mBPi7SFvTXxEcnztDYOJ//uM=
+github.com/ory/dockertest/v3 v3.10.1-0.20240619125955-3328cf9343b8 h1:pdmvNMAN5x5kPmntdHNmfl3TDszlGeXYri+JSA4JMNM=
+github.com/ory/dockertest/v3 v3.10.1-0.20240619125955-3328cf9343b8/go.mod h1:Z3wDt3X5YzB70upzvwiBH2U3lj8q/SXHKT2dyMM7t3I=
github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe h1:rvu4obdvqR0fkSIJ8IfgzKOWwZ5kOT2UNfLq81Qk7rc=
github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe/go.mod h1:z4n3u6as84LbV4YmgjHhnwtccQqzf4cZlSk9f1FhygI=
github.com/ory/graceful v0.1.4-0.20230301144740-e222150c51d0 h1:VMUeLRfQD14fOMvhpYZIIT4vtAqxYh+f3KnSqCeJ13o=
@@ -827,8 +830,8 @@ github.com/ory/nosurf v1.2.7 h1:YrHrbSensQyU6r6HT/V5+HPdVEgrOTMJiLoJABSBOp4=
github.com/ory/nosurf v1.2.7/go.mod h1:d4L3ZBa7Amv55bqxCBtCs63wSlyaiCkWVl4vKf3OUxA=
github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2 h1:zm6sDvHy/U9XrGpixwHiuAwpp0Ock6khSVHkrv6lQQU=
github.com/ory/sessions v1.2.2-0.20220110165800-b09c17334dc2/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
-github.com/ory/x v0.0.623 h1:sFJiw2i/itTkBRJbhGXtrso9NcdscnjFlHBFitCzf8A=
-github.com/ory/x v0.0.623/go.mod h1:CUw8/O3X8lUMheyV0iH+6LQ0tePrH+FBsW39MccCHgw=
+github.com/ory/x v0.0.639 h1:6/9V6XlAwsPBFNpL/FMp83SbFD70n0Ql0dAaAlDbESA=
+github.com/ory/x v0.0.639/go.mod h1:kjXXSK3a0lC9NNSkxG1sRlnrR9GoG52mvo8z4Nsicu0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -935,8 +938,9 @@ github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
-github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slack-go/slack v0.7.4 h1:Z+7CmUDV+ym4lYLA4NNLFIpr3+nDgViHrx8xsuXgrYs=
github.com/slack-go/slack v0.7.4/go.mod h1:FGqNzJBmxIsZURAxh2a8D21AnOVvvXZvGligs4npPUM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@@ -980,6 +984,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
@@ -1270,7 +1275,6 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1314,10 +1318,12 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1328,11 +1334,12 @@ golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
-golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
+golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -1386,7 +1393,6 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -1593,9 +1599,8 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
-gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I=
-gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=
+gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
+gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/hydra/hydra_test.go b/hydra/hydra_test.go
index b2e252be5b18..d022ae6021cf 100644
--- a/hydra/hydra_test.go
+++ b/hydra/hydra_test.go
@@ -13,6 +13,7 @@ import (
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/hydra"
"github.com/ory/x/configx"
+ "github.com/ory/x/contextx"
"github.com/ory/x/logrusx"
"github.com/ory/x/sqlxx"
"github.com/ory/x/urlx"
@@ -25,11 +26,12 @@ func requestFromChallenge(s string) *http.Request {
func TestGetLoginChallengeID(t *testing.T) {
uuidChallenge := "b346a452-e8fb-4828-8ef8-a4dbc98dc23a"
blobChallenge := "1337deadbeefcafe"
- defaultConfig := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation())
+ defaultConfig := config.MustNew(t, logrusx.New("", ""), os.Stderr, &contextx.Default{}, configx.SkipValidation())
configWithHydra := config.MustNew(
t,
logrusx.New("", ""),
os.Stderr,
+ &contextx.Default{},
configx.SkipValidation(),
configx.WithValues(map[string]interface{}{
config.ViperKeyOAuth2ProviderURL: "https://hydra",
diff --git a/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_password_migration_hook_enabled.json b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_password_migration_hook_enabled.json
new file mode 100644
index 000000000000..e89fc60f2d8b
--- /dev/null
+++ b/identity/.snapshots/TestHandler-case=should_be_able_to_import_users-with_password_migration_hook_enabled.json
@@ -0,0 +1,22 @@
+{
+ "credentials": {
+ "password": {
+ "type": "password",
+ "identifiers": [
+ "pw-migration-hook@ory.sh"
+ ],
+ "config": {
+ "use_password_migration_hook": true
+ },
+ "version": 0
+ }
+ },
+ "schema_id": "default",
+ "state": "active",
+ "traits": {
+ "email": "pw-migration-hook@ory.sh"
+ },
+ "metadata_public": null,
+ "metadata_admin": null,
+ "organization_id": null
+}
diff --git a/identity/credentials_password.go b/identity/credentials_password.go
index 85f5ac1d7d0c..4a6e7ebc7144 100644
--- a/identity/credentials_password.go
+++ b/identity/credentials_password.go
@@ -9,4 +9,13 @@ package identity
type CredentialsPassword struct {
// HashedPassword is a hash-representation of the password.
HashedPassword string `json:"hashed_password"`
+
+ // UsePasswordMigrationHook is set to true if the password should be migrated
+ // using the password migration hook. If set, and the HashedPassword is empty, a
+ // webhook will be called during login to migrate the password.
+ UsePasswordMigrationHook bool `json:"use_password_migration_hook,omitempty"`
+}
+
+func (cp *CredentialsPassword) ShouldUsePasswordMigrationHook() bool {
+ return cp != nil && cp.HashedPassword == "" && cp.UsePasswordMigrationHook
}
diff --git a/identity/credentials_password_test.go b/identity/credentials_password_test.go
new file mode 100644
index 000000000000..6e62720779e6
--- /dev/null
+++ b/identity/credentials_password_test.go
@@ -0,0 +1,46 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package identity
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCredentialsPassword_ShouldUsePasswordMigrationHook(t *testing.T) {
+ tests := []struct {
+ name string
+ cp *CredentialsPassword
+ want bool
+ }{{
+ name: "pw set",
+ cp: &CredentialsPassword{
+ HashedPassword: "pw",
+ UsePasswordMigrationHook: true,
+ },
+ want: false,
+ }, {
+ name: "pw not set",
+ cp: &CredentialsPassword{
+ HashedPassword: "",
+ UsePasswordMigrationHook: true,
+ },
+ want: true,
+ }, {
+ name: "nil",
+ want: false,
+ }, {
+ name: "pw not set, hook not set",
+ cp: &CredentialsPassword{
+ HashedPassword: "",
+ },
+ want: false,
+ }}
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equalf(t, tt.want, tt.cp.ShouldUsePasswordMigrationHook(), "ShouldUsePasswordMigrationHook()")
+ })
+ }
+}
diff --git a/identity/handler.go b/identity/handler.go
index 3ac641e09377..cf85dc792c43 100644
--- a/identity/handler.go
+++ b/identity/handler.go
@@ -424,6 +424,9 @@ type AdminIdentityImportCredentialsPasswordConfig struct {
// The password in plain text if no hash is available.
Password string `json:"password"`
+
+ // If set to true, the password will be migrated using the password migration hook.
+ UsePasswordMigrationHook bool `json:"use_password_migration_hook,omitempty"`
}
// Create Identity and Import Social Sign In Credentials
@@ -910,69 +913,36 @@ func (h *Handler) patch(w http.ResponseWriter, r *http.Request, ps httprouter.Pa
h.r.Writer().Write(w, r, WithCredentialsMetadataAndAdminMetadataInJSON(updatedIdentity))
}
-func deletCredentialWebAuthFromIdentity(identity *Identity) (*Identity, error) {
- cred, ok := identity.GetCredentials(CredentialsTypeWebAuthn)
- if !ok {
- // This should never happend as it's checked earlier in the code;
- // But we never know...
- return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf("You tried to remove a CredentialsTypeWebAuthn but this user have no CredentialsTypeWebAuthn set up."))
- }
-
- var cc CredentialsWebAuthnConfig
- if err := json.Unmarshal(cred.Config, &cc); err != nil {
- // Database has been tampered or the json schema are incompatible (migration issue);
- return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error()))
- }
-
- updated := make([]CredentialWebAuthn, 0)
- for k, cred := range cc.Credentials {
- if cred.IsPasswordless {
- updated = append(updated, cc.Credentials[k])
- }
- }
-
- if len(updated) == 0 {
- identity.DeleteCredentialsType(CredentialsTypeWebAuthn)
- return identity, nil
- }
-
- cc.Credentials = updated
- message, err := json.Marshal(cc)
- if err != nil {
- return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error()))
- }
-
- cred.Config = message
- identity.SetCredentials(CredentialsTypeWebAuthn, *cred)
- return identity, nil
-}
-
// Delete Credential Parameters
//
// swagger:parameters deleteIdentityCredentials
-//
-//nolint:deadcode,unused
-//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
-type deleteIdentityCredentials struct {
+type _ struct {
// ID is the identity's ID.
//
// required: true
// in: path
ID string `json:"id"`
- // Type is the type of credentials to be deleted.
+ // Type is the type of credentials to delete.
//
// required: true
// in: path
Type CredentialsType `json:"type"`
+
+ // Identifier is the identifier of the OIDC credential to delete.
+ // Find the identifier by calling the `GET /admin/identities/{id}?include_credential=oidc` endpoint.
+ //
+ // required: false
+ // in: query
+ Identifier string `json:"identifier"`
}
// swagger:route DELETE /admin/identities/{id}/credentials/{type} identity deleteIdentityCredentials
//
// # Delete a credential for a specific identity
//
-// Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type
-// You can only delete second factor (aal2) credentials.
+// Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type.
+// You cannot delete password or code auth credentials through this API.
//
// Consumes:
// - application/json
@@ -1006,14 +976,18 @@ func (h *Handler) deleteIdentityCredentials(w http.ResponseWriter, r *http.Reque
case CredentialsTypeLookup, CredentialsTypeTOTP:
identity.DeleteCredentialsType(cred.Type)
case CredentialsTypeWebAuthn:
- identity, err = deletCredentialWebAuthFromIdentity(identity)
- if err != nil {
+ if err = identity.deleteCredentialWebAuthFromIdentity(); err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
- case CredentialsTypeOIDC, CredentialsTypePassword, CredentialsTypeCodeAuth:
- h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("You can't remove first factor credentials.")))
+ case CredentialsTypePassword, CredentialsTypeCodeAuth:
+ h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("You cannot remove first factor credentials.")))
return
+ case CredentialsTypeOIDC:
+ if err := identity.deleteCredentialOIDCFromIdentity(r.URL.Query().Get("identifier")); err != nil {
+ h.r.Writer().WriteError(w, r, err)
+ return
+ }
default:
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unknown credentials type %s.", cred.Type)))
return
diff --git a/identity/handler_import.go b/identity/handler_import.go
index a3a7ca2ab0a1..cca346e8e676 100644
--- a/identity/handler_import.go
+++ b/identity/handler_import.go
@@ -35,6 +35,10 @@ func (h *Handler) importCredentials(ctx context.Context, i *Identity, creds *Ide
}
func (h *Handler) importPasswordCredentials(ctx context.Context, i *Identity, creds *AdminIdentityImportCredentialsPassword) (err error) {
+ if creds.Config.UsePasswordMigrationHook {
+ return i.SetCredentialsWithConfig(CredentialsTypePassword, Credentials{}, CredentialsPassword{UsePasswordMigrationHook: true})
+ }
+
// In here we deliberately ignore any password policies as the point here is to import passwords, even if they
// are not matching the policy, as the user needs to able to sign in with their old password.
hashed := []byte(creds.Config.HashedPassword)
diff --git a/identity/handler_test.go b/identity/handler_test.go
index ab2ed0c0cbdc..d97c2f73fae4 100644
--- a/identity/handler_test.go
+++ b/identity/handler_test.go
@@ -32,6 +32,8 @@ import (
"github.com/ory/kratos/internal/testhelpers"
"github.com/ory/kratos/schema"
"github.com/ory/kratos/x"
+ "github.com/ory/x/ioutilx"
+ "github.com/ory/x/randx"
"github.com/ory/x/snapshotx"
"github.com/ory/x/sqlxx"
"github.com/ory/x/urlx"
@@ -81,8 +83,9 @@ func TestHandler(t *testing.T) {
res, err := base.Client().Do(req)
require.NoError(t, err)
+ defer res.Body.Close()
- require.EqualValues(t, expectCode, res.StatusCode)
+ require.EqualValues(t, expectCode, res.StatusCode, "%s", ioutilx.MustReadAll(res.Body))
}
send := func(t *testing.T, base *httptest.Server, method, href string, expectCode int, send interface{}) gjson.Result {
@@ -304,6 +307,21 @@ func TestHandler(t *testing.T) {
}
})
+ t.Run("with password migration hook enabled", func(t *testing.T) {
+ res := send(t, adminTS, "POST", "/identities", http.StatusCreated, identity.CreateIdentityBody{
+ Traits: []byte(`{"email": "pw-migration-hook@ory.sh"}`),
+ Credentials: &identity.IdentityWithCredentials{Password: &identity.AdminIdentityImportCredentialsPassword{
+ Config: identity.AdminIdentityImportCredentialsPasswordConfig{UsePasswordMigrationHook: true},
+ }},
+ })
+ actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, uuid.FromStringOrNil(res.Get("id").String()))
+ require.NoError(t, err)
+
+ snapshotx.SnapshotT(t, identity.WithCredentialsAndAdminMetadataInJSON(*actual), snapshotx.ExceptNestedKeys(ignoreDefault...), snapshotx.ExceptNestedKeys("hashed_password"))
+
+ assert.True(t, gjson.GetBytes(actual.Credentials[identity.CredentialsTypePassword].Config, "use_password_migration_hook").Bool())
+ })
+
t.Run("with not-normalized email", func(t *testing.T) {
res := send(t, adminTS, "POST", "/identities", http.StatusCreated, identity.CreateIdentityBody{
SchemaID: "customer",
@@ -1497,15 +1515,15 @@ func TestHandler(t *testing.T) {
t.Run("case=should delete credential of a specific user and no longer be able to retrieve it", func(t *testing.T) {
ignoreDefault := []string{"id", "schema_url", "state_changed_at", "created_at", "updated_at"}
- createIdentity := func(identities map[identity.CredentialsType]string) func(t *testing.T) *identity.Identity {
+ type M = map[identity.CredentialsType]identity.Credentials
+ createIdentity := func(creds M) func(*testing.T) *identity.Identity {
return func(t *testing.T) *identity.Identity {
i := identity.NewIdentity("")
- for ct, config := range identities {
- i.SetCredentials(ct, identity.Credentials{
- Type: ct,
- Config: sqlxx.JSONRawMessage(config),
- })
+ for k, v := range creds {
+ v.Type = k
+ creds[k] = v
}
+ i.Credentials = creds
i.Traits = identity.Traits("{}")
require.NoError(t, reg.Persister().CreateIdentity(context.Background(), i))
return i
@@ -1516,26 +1534,83 @@ func TestHandler(t *testing.T) {
remove(t, ts, "/identities/"+x.NewUUID().String()+"/credentials/azerty", http.StatusNotFound)
})
t.Run("type=remove unknown type/"+name, func(t *testing.T) {
- i := createIdentity(map[identity.CredentialsType]string{
- identity.CredentialsTypePassword: `{"secret":"pst"}`,
+ i := createIdentity(M{
+ identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)},
})(t)
remove(t, ts, "/identities/"+i.ID.String()+"/credentials/azerty", http.StatusNotFound)
})
t.Run("type=remove password type/"+name, func(t *testing.T) {
- i := createIdentity(map[identity.CredentialsType]string{
- identity.CredentialsTypePassword: `{"secret":"pst"}`,
+ i := createIdentity(M{
+ identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)},
})(t)
remove(t, ts, "/identities/"+i.ID.String()+"/credentials/password", http.StatusBadRequest)
})
t.Run("type=remove oidc type/"+name, func(t *testing.T) {
- i := createIdentity(map[identity.CredentialsType]string{
- identity.CredentialsTypeOIDC: `{"id":"pst"}`,
+ // force ordering among github identifiers
+ githubSubject := "0" + randx.MustString(7, randx.Numeric)
+ githubSubject2 := "1" + randx.MustString(7, randx.Numeric)
+ googleSubject := randx.MustString(8, randx.Numeric)
+ initialConfig := []byte(fmt.Sprintf(`{
+ "providers": [
+ {
+ "subject": %q,
+ "provider": "github"
+ },
+ {
+ "subject": %q,
+ "provider": "github"
+ },
+ {
+ "subject": %q,
+ "provider": "google"
+ }
+ ]
+ }`, githubSubject, githubSubject2, googleSubject))
+ identifiers := []string{
+ identity.OIDCUniqueID("github", githubSubject),
+ identity.OIDCUniqueID("github", githubSubject2),
+ identity.OIDCUniqueID("google", googleSubject),
+ }
+ i := createIdentity(M{
+ identity.CredentialsTypeOIDC: {
+ Identifiers: identifiers,
+ Config: initialConfig,
+ },
})(t)
- remove(t, ts, "/identities/"+i.ID.String()+"/credentials/oidc", http.StatusBadRequest)
+ res := get(t, ts, "/identities/"+i.ID.String()+"?include_credential=oidc", http.StatusOK)
+ assert.EqualValues(t, i.ID.String(), res.Get("id").String(), "%s", res.Raw)
+ assert.Len(t, res.Get("credentials.oidc.identifiers").Array(), 3, "%s", res.Raw)
+ assert.EqualValues(t, res.Get("credentials.oidc.identifiers.0").String(), identifiers[0], "%s", res.Raw)
+ assert.EqualValues(t, res.Get("credentials.oidc.identifiers.1").String(), identifiers[1], "%s", res.Raw)
+ assert.EqualValues(t, res.Get("credentials.oidc.identifiers.2").String(), identifiers[2], "%s", res.Raw)
+
+ oidConfig := gjson.Parse(res.Get("credentials.oidc.config").String())
+ assert.Len(t, res.Get("credentials.oidc.identifiers").Array(), 3, "%s", res.Raw)
+ assert.EqualValues(t, oidConfig.Get("providers.0.provider").String(), "github", "%s", res.Raw)
+ assert.EqualValues(t, oidConfig.Get("providers.0.subject").String(), githubSubject, "%s", res.Raw)
+ assert.EqualValues(t, oidConfig.Get("providers.1.provider").String(), "github", "%s", res.Raw)
+ assert.EqualValues(t, oidConfig.Get("providers.1.subject").String(), githubSubject2, "%s", res.Raw)
+ assert.EqualValues(t, oidConfig.Get("providers.2.provider").String(), "google", "%s", res.Raw)
+ assert.EqualValues(t, oidConfig.Get("providers.2.subject").String(), googleSubject, "%s", res.Raw)
+
+ remove(t, ts, "/identities/"+i.ID.String()+"/credentials/oidc?identifier="+identifiers[1], http.StatusNoContent)
+ res = get(t, ts, "/identities/"+i.ID.String()+"?include_credential=oidc", http.StatusOK)
+
+ assert.EqualValues(t, i.ID.String(), res.Get("id").String(), "%s", res.Raw)
+ assert.Len(t, res.Get("credentials.oidc.identifiers").Array(), 2, "%s", res.Raw)
+ assert.EqualValues(t, res.Get("credentials.oidc.identifiers.0").String(), identifiers[0], "%s", res.Raw)
+ assert.EqualValues(t, res.Get("credentials.oidc.identifiers.1").String(), identifiers[2], "%s", res.Raw)
+
+ oidConfig = gjson.Parse(res.Get("credentials.oidc.config").String())
+ assert.Len(t, res.Get("credentials.oidc.identifiers").Array(), 2, "%s", res.Raw)
+ assert.EqualValues(t, oidConfig.Get("providers.0.provider").String(), "github", "%s", res.Raw)
+ assert.EqualValues(t, oidConfig.Get("providers.0.subject").String(), githubSubject, "%s", res.Raw)
+ assert.EqualValues(t, oidConfig.Get("providers.1.provider").String(), "google", "%s", res.Raw)
+ assert.EqualValues(t, oidConfig.Get("providers.1.subject").String(), googleSubject, "%s", res.Raw)
})
t.Run("type=remove webauthn passwordless type/"+name, func(t *testing.T) {
expected := `{"credentials":[{"id":"THTndqZP5Mjvae1BFvJMaMfEMm7O7HE1ju+7PBaYA7Y=","added_at":"2022-12-16T14:11:55Z","public_key":"pQECAyYgASFYIMJLQhJxQRzhnKPTcPCUODOmxYDYo2obrm9bhp5lvSZ3IlggXjhZvJaPUqF9PXqZqTdWYPR7R+b2n/Wi+IxKKXsS4rU=","display_name":"test","authenticator":{"aaguid":"rc4AAjW8xgpkiwsl8fBVAw==","sign_count":0,"clone_warning":false},"is_passwordless":true,"attestation_type":"none"}],"user_handle":"Ef5JiMpMRwuzauWs/9J0gQ=="}`
- i := createIdentity(map[identity.CredentialsType]string{identity.CredentialsTypeWebAuthn: expected})(t)
+ i := createIdentity(M{identity.CredentialsTypeWebAuthn: {Config: []byte(expected)}})(t)
remove(t, ts, "/identities/"+i.ID.String()+"/credentials/webauthn", http.StatusNoContent)
// Check that webauthn has not been deleted
res := get(t, ts, "/identities/"+i.ID.String(), http.StatusOK)
@@ -1608,7 +1683,7 @@ func TestHandler(t *testing.T) {
message, err := json.Marshal(config)
require.NoError(t, err)
- i := createIdentity(map[identity.CredentialsType]string{identity.CredentialsTypeWebAuthn: string(message)})(t)
+ i := createIdentity(M{identity.CredentialsTypeWebAuthn: {Config: message}})(t)
remove(t, ts, "/identities/"+i.ID.String()+"/credentials/webauthn", http.StatusNoContent)
// Check that webauthn has not been deleted
res := get(t, ts, "/identities/"+i.ID.String(), http.StatusOK)
@@ -1618,10 +1693,10 @@ func TestHandler(t *testing.T) {
require.NoError(t, err)
snapshotx.SnapshotT(t, identity.WithCredentialsAndAdminMetadataInJSON(*actual), snapshotx.ExceptNestedKeys(append(ignoreDefault, "hashed_password")...), snapshotx.ExceptPaths("credentials.oidc.identifiers"))
})
- for ct, ctConf := range map[identity.CredentialsType]string{
- identity.CredentialsTypeLookup: `{"recovery_codes": [{"code": "aaa"}]}`,
- identity.CredentialsTypeTOTP: `{"totp_url":"otpauth://totp/test"}`,
- identity.CredentialsTypeWebAuthn: `{"credentials":[{"id":"THTndqZP5Mjvae1BFvJMaMfEMm7O7HE1ju+7PBaYA7Y=","added_at":"2022-12-16T14:11:55Z","public_key":"pQECAyYgASFYIMJLQhJxQRzhnKPTcPCUODOmxYDYo2obrm9bhp5lvSZ3IlggXjhZvJaPUqF9PXqZqTdWYPR7R+b2n/Wi+IxKKXsS4rU=","display_name":"test","authenticator":{"aaguid":"rc4AAjW8xgpkiwsl8fBVAw==","sign_count":0,"clone_warning":false},"is_passwordless":false,"attestation_type":"none"}],"user_handle":"Ef5JiMpMRwuzauWs/9J0gQ=="}`,
+ for ct, ctConf := range map[identity.CredentialsType][]byte{
+ identity.CredentialsTypeLookup: []byte(`{"recovery_codes": [{"code": "aaa"}]}`),
+ identity.CredentialsTypeTOTP: []byte(`{"totp_url":"otpauth://totp/test"}`),
+ identity.CredentialsTypeWebAuthn: []byte(`{"credentials":[{"id":"THTndqZP5Mjvae1BFvJMaMfEMm7O7HE1ju+7PBaYA7Y=","added_at":"2022-12-16T14:11:55Z","public_key":"pQECAyYgASFYIMJLQhJxQRzhnKPTcPCUODOmxYDYo2obrm9bhp5lvSZ3IlggXjhZvJaPUqF9PXqZqTdWYPR7R+b2n/Wi+IxKKXsS4rU=","display_name":"test","authenticator":{"aaguid":"rc4AAjW8xgpkiwsl8fBVAw==","sign_count":0,"clone_warning":false},"is_passwordless":false,"attestation_type":"none"}],"user_handle":"Ef5JiMpMRwuzauWs/9J0gQ=="}`),
} {
t.Run("type=remove "+string(ct)+"/"+name, func(t *testing.T) {
for _, tc := range []struct {
@@ -1632,25 +1707,25 @@ func TestHandler(t *testing.T) {
{
desc: "with",
exist: true,
- setup: createIdentity(map[identity.CredentialsType]string{
- identity.CredentialsTypePassword: `{"secret":"pst"}`,
- ct: ctConf,
+ setup: createIdentity(M{
+ identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)},
+ ct: {Config: ctConf},
}),
},
{
desc: "without",
exist: false,
- setup: createIdentity(map[identity.CredentialsType]string{
- identity.CredentialsTypePassword: `{"secret":"pst"}`,
+ setup: createIdentity(M{
+ identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)},
}),
},
{
desc: "multiple",
exist: true,
- setup: createIdentity(map[identity.CredentialsType]string{
- identity.CredentialsTypePassword: `{"secret":"pst"}`,
- identity.CredentialsTypeOIDC: `{"id":"pst"}`,
- ct: ctConf,
+ setup: createIdentity(M{
+ identity.CredentialsTypePassword: {Config: []byte(`{"secret":"pst"}`)},
+ identity.CredentialsTypeOIDC: {Config: []byte(`{"id":"pst"}`)},
+ ct: {Config: ctConf},
}),
},
} {
diff --git a/identity/identity.go b/identity/identity.go
index 55dd66155ea9..3f692b831f2b 100644
--- a/identity/identity.go
+++ b/identity/identity.go
@@ -507,6 +507,80 @@ func (i *Identity) WithDeclassifiedCredentials(ctx context.Context, c cipher.Pro
return &ii, nil
}
+func (i *Identity) deleteCredentialWebAuthFromIdentity() error {
+ cred, ok := i.GetCredentials(CredentialsTypeWebAuthn)
+ if !ok {
+ // This should never happend as it's checked earlier in the code;
+ // But we never know...
+ return errors.WithStack(herodot.ErrNotFound.WithReasonf("You tried to remove a WebAuthn credential but this user has no such credential set up."))
+ }
+
+ var cc CredentialsWebAuthnConfig
+ if err := json.Unmarshal(cred.Config, &cc); err != nil {
+ // Database has been tampered or the json schema are incompatible (migration issue);
+ return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error()))
+ }
+
+ updated := make([]CredentialWebAuthn, 0)
+ for k, cred := range cc.Credentials {
+ if cred.IsPasswordless {
+ updated = append(updated, cc.Credentials[k])
+ }
+ }
+
+ if len(updated) == 0 {
+ i.DeleteCredentialsType(CredentialsTypeWebAuthn)
+ return nil
+ }
+
+ cc.Credentials = updated
+ message, err := json.Marshal(cc)
+ if err != nil {
+ return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error()))
+ }
+
+ cred.Config = message
+ i.SetCredentials(CredentialsTypeWebAuthn, *cred)
+ return nil
+}
+
+func (i *Identity) deleteCredentialOIDCFromIdentity(identifierToDelete string) error {
+ if identifierToDelete == "" {
+ return errors.WithStack(herodot.ErrBadRequest.WithReasonf("You must provide an identifier to delete this credential."))
+ }
+ _, hasOIDC := i.GetCredentials(CredentialsTypeOIDC)
+ if !hasOIDC {
+ return errors.WithStack(herodot.ErrNotFound.WithReasonf("You tried to remove an OIDC credential but this user has no such credential set up."))
+ }
+ var oidcConfig CredentialsOIDC
+ creds, err := i.ParseCredentials(CredentialsTypeOIDC, &oidcConfig)
+ if err != nil {
+ return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode identity credentials.").WithDebug(err.Error()))
+ }
+
+ var updatedIdentifiers []string
+ var updatedProviders []CredentialsOIDCProvider
+ var found bool
+ for _, cfg := range oidcConfig.Providers {
+ if identifierToDelete == OIDCUniqueID(cfg.Provider, cfg.Subject) {
+ found = true
+ continue
+ }
+ updatedIdentifiers = append(updatedIdentifiers, OIDCUniqueID(cfg.Provider, cfg.Subject))
+ updatedProviders = append(updatedProviders, cfg)
+ }
+ if !found {
+ return errors.WithStack(herodot.ErrNotFound.WithReasonf("The identifier `%s` was not found among OIDC credentials.", identifierToDelete))
+ }
+ creds.Identifiers = updatedIdentifiers
+ creds.Config, err = json.Marshal(&CredentialsOIDC{Providers: updatedProviders})
+ if err != nil {
+ return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to encode identity credentials.").WithDebug(err.Error()))
+ }
+ i.Credentials[CredentialsTypeOIDC] = *creds
+ return nil
+}
+
// Patch Identities Parameters
//
// swagger:parameters batchPatchIdentities
diff --git a/identity/identity_test.go b/identity/identity_test.go
index 726011fd00eb..d14388feacd4 100644
--- a/identity/identity_test.go
+++ b/identity/identity_test.go
@@ -10,21 +10,16 @@ import (
"fmt"
"testing"
- "github.com/ory/x/snapshotx"
-
- "github.com/ory/kratos/cipher"
- "github.com/ory/kratos/x"
-
+ "github.com/gofrs/uuid"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
- "github.com/gofrs/uuid"
-
- "github.com/ory/x/sqlxx"
-
+ "github.com/ory/kratos/cipher"
"github.com/ory/kratos/driver/config"
-
- "github.com/stretchr/testify/assert"
+ "github.com/ory/kratos/x"
+ "github.com/ory/x/snapshotx"
+ "github.com/ory/x/sqlxx"
)
func TestNewIdentity(t *testing.T) {
@@ -387,3 +382,58 @@ func TestWithDeclassifiedCredentials(t *testing.T) {
}
})
}
+
+func TestDeleteCredentialOIDCFromIdentity(t *testing.T) {
+ i := NewIdentity(config.DefaultIdentityTraitsSchemaID)
+
+ err := i.deleteCredentialOIDCFromIdentity("")
+ assert.Error(t, err)
+ err = i.deleteCredentialOIDCFromIdentity("does-not-exist")
+ assert.Error(t, err)
+
+ credentials := map[CredentialsType]Credentials{
+ CredentialsTypePassword: {
+ Identifiers: []string{"zab", "bar"},
+ Type: CredentialsTypePassword,
+ Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}"),
+ },
+ CredentialsTypeOIDC: {
+ Type: CredentialsTypeOIDC,
+ Identifiers: []string{"bar:1234", "baz:5678"},
+ Config: sqlxx.JSONRawMessage(`{"providers": [{"provider": "bar", "subject": "1234"}, {"provider": "baz", "subject": "5678"}]}`),
+ },
+ CredentialsTypeWebAuthn: {
+ Type: CredentialsTypeWebAuthn,
+ Identifiers: []string{"foo", "bar"},
+ Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}"),
+ },
+ }
+ i.Credentials = credentials
+
+ err = i.deleteCredentialOIDCFromIdentity("zab")
+ assert.Error(t, err)
+ err = i.deleteCredentialOIDCFromIdentity("foo")
+ assert.Error(t, err)
+ err = i.deleteCredentialOIDCFromIdentity("bar")
+ assert.Error(t, err, "matches multiple OIDC credentials")
+
+ require.NoError(t, i.deleteCredentialOIDCFromIdentity("bar:1234"))
+
+ assert.Len(t, i.Credentials, 3)
+
+ assert.Contains(t, i.Credentials, CredentialsTypePassword)
+ assert.EqualValues(t, i.Credentials[CredentialsTypePassword].Identifiers, []string{"zab", "bar"})
+
+ assert.Contains(t, i.Credentials, CredentialsTypeWebAuthn)
+ assert.EqualValues(t, i.Credentials[CredentialsTypeWebAuthn].Identifiers, []string{"foo", "bar"})
+
+ assert.Contains(t, i.Credentials, CredentialsTypeOIDC)
+
+ oidc, ok := i.GetCredentials(CredentialsTypeOIDC)
+ require.True(t, ok)
+ assert.EqualValues(t, oidc.Identifiers, []string{"baz:5678"})
+ var cfg CredentialsOIDC
+ _, err = i.ParseCredentials(CredentialsTypeOIDC, &cfg)
+ require.NoError(t, err)
+ assert.EqualValues(t, CredentialsOIDC{Providers: []CredentialsOIDCProvider{{Provider: "baz", Subject: "5678"}}}, cfg)
+}
diff --git a/identity/manager_test.go b/identity/manager_test.go
index e0346b8ee0c0..f45a3f05e4fc 100644
--- a/identity/manager_test.go
+++ b/identity/manager_test.go
@@ -4,11 +4,11 @@
package identity_test
import (
- "context"
"fmt"
"testing"
"time"
+ "github.com/ory/x/configx"
"github.com/ory/x/pointerx"
"github.com/ory/x/sqlcon"
@@ -29,17 +29,17 @@ import (
)
func TestManager(t *testing.T) {
- conf, reg := internal.NewFastRegistryWithMocks(t)
- testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/manager.schema.json")
- extensionSchemaID := testhelpers.UseIdentitySchema(t, conf, "file://./stub/extension.schema.json")
- conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/")
- conf.MustSet(ctx, config.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/")
- conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationLoginHints, true)
+ conf, reg := internal.NewFastRegistryWithMocks(t, configx.WithValues(map[string]interface{}{
+ config.ViperKeyPublicBaseURL: "https://www.ory.sh/",
+ config.ViperKeyCourierSMTPURL: "smtp://foo@bar@dev.null/",
+ config.ViperKeySelfServiceRegistrationLoginHints: true,
+ }), configx.WithValues(testhelpers.DefaultIdentitySchemaConfig("file://./stub/manager.schema.json")))
+ ctx, extensionSchemaID := testhelpers.WithAddIdentitySchema(ctx, t, conf, "file://./stub/extension.schema.json")
t.Run("case=should fail to create because validation fails", func(t *testing.T) {
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
i.Traits = identity.Traits(`{"email":"not an email"}`)
- require.Error(t, reg.IdentityManager().Create(context.Background(), i))
+ require.Error(t, reg.IdentityManager().Create(ctx, i))
})
newTraits := func(email string, unprotected string) identity.Traits {
@@ -62,7 +62,7 @@ func TestManager(t *testing.T) {
}
checkExtensionFieldsForIdentities := func(t *testing.T, expected string, original *identity.Identity) {
- fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID)
+ fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID)
require.NoError(t, err)
identities := []identity.Identity{*original, *fromStore}
for k := range identities {
@@ -75,7 +75,7 @@ func TestManager(t *testing.T) {
email := uuid.Must(uuid.NewV4()).String() + "@ory.sh"
original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
original.Traits = newTraits(email, "")
- require.NoError(t, reg.IdentityManager().Create(context.Background(), original))
+ require.NoError(t, reg.IdentityManager().Create(ctx, original))
checkExtensionFieldsForIdentities(t, email, original)
got, ok := original.AvailableAAL.ToAAL()
require.True(t, ok)
@@ -87,7 +87,7 @@ func TestManager(t *testing.T) {
email := uuid.Must(uuid.NewV4()).String() + "@ory.sh"
original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
original.Traits = newTraits(email, "")
- require.NoError(t, reg.IdentityManager().Create(context.Background(), original))
+ require.NoError(t, reg.IdentityManager().Create(ctx, original))
got, ok := original.AvailableAAL.ToAAL()
require.True(t, ok)
assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, got)
@@ -104,7 +104,7 @@ func TestManager(t *testing.T) {
Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`),
},
}
- require.NoError(t, reg.IdentityManager().Create(context.Background(), original))
+ require.NoError(t, reg.IdentityManager().Create(ctx, original))
got, ok := original.AvailableAAL.ToAAL()
require.True(t, ok)
assert.Equal(t, identity.AuthenticatorAssuranceLevel1, got)
@@ -126,7 +126,7 @@ func TestManager(t *testing.T) {
Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`),
},
}
- require.NoError(t, reg.IdentityManager().Create(context.Background(), original))
+ require.NoError(t, reg.IdentityManager().Create(ctx, original))
got, ok := original.AvailableAAL.ToAAL()
require.True(t, ok)
assert.Equal(t, identity.AuthenticatorAssuranceLevel2, got)
@@ -143,7 +143,7 @@ func TestManager(t *testing.T) {
Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`),
},
}
- require.NoError(t, reg.IdentityManager().Create(context.Background(), original))
+ require.NoError(t, reg.IdentityManager().Create(ctx, original))
got, ok := original.AvailableAAL.ToAAL()
require.True(t, ok)
assert.Equal(t, identity.NoAuthenticatorAssuranceLevel, got)
@@ -153,7 +153,7 @@ func TestManager(t *testing.T) {
t.Run("case=should expose validation errors with option", func(t *testing.T) {
original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
original.Traits = identity.Traits(`{"email":"not an email"}`)
- err := reg.IdentityManager().Create(context.Background(), original, identity.ManagerExposeValidationErrorsForInternalTypeAssertion)
+ err := reg.IdentityManager().Create(ctx, original, identity.ManagerExposeValidationErrorsForInternalTypeAssertion)
require.Error(t, err)
assert.Contains(t, err.Error(), "\"not an email\" is not valid \"email\"")
})
@@ -161,7 +161,7 @@ func TestManager(t *testing.T) {
t.Run("case=should not expose validation errors without option", func(t *testing.T) {
original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
original.Traits = identity.Traits(`{"email":"not an email"}`)
- err := reg.IdentityManager().Create(context.Background(), original)
+ err := reg.IdentityManager().Create(ctx, original)
require.Error(t, err)
assert.NotContains(t, err.Error(), "\"not an email\" is not valid \"email\"")
})
@@ -186,10 +186,10 @@ func TestManager(t *testing.T) {
}
first := createIdentity(email, "email_creds", creds)
- require.NoError(t, reg.IdentityManager().Create(context.Background(), first))
+ require.NoError(t, reg.IdentityManager().Create(ctx, first))
second := createIdentity(email, "email_creds", creds)
- err := reg.IdentityManager().Create(context.Background(), second)
+ err := reg.IdentityManager().Create(ctx, second)
require.Error(t, err)
var verr = new(identity.ErrDuplicateCredentials)
@@ -210,10 +210,10 @@ func TestManager(t *testing.T) {
}
first := createIdentity(email, "email_webauthn", creds)
- require.NoError(t, reg.IdentityManager().Create(context.Background(), first))
+ require.NoError(t, reg.IdentityManager().Create(ctx, first))
second := createIdentity(email, "email_webauthn", nil)
- err := reg.IdentityManager().Create(context.Background(), second)
+ err := reg.IdentityManager().Create(ctx, second)
require.Error(t, err)
var verr = new(identity.ErrDuplicateCredentials)
@@ -235,10 +235,10 @@ func TestManager(t *testing.T) {
}
first := createIdentity(email, "email_creds", creds)
- require.NoError(t, reg.IdentityManager().Create(context.Background(), first))
+ require.NoError(t, reg.IdentityManager().Create(ctx, first))
second := createIdentity(email, "email_creds", creds)
- err := reg.IdentityManager().Create(context.Background(), second)
+ err := reg.IdentityManager().Create(ctx, second)
require.Error(t, err)
var verr = new(identity.ErrDuplicateCredentials)
@@ -270,10 +270,10 @@ func TestManager(t *testing.T) {
}
first := createIdentity(email, "email_creds", creds)
- require.NoError(t, reg.IdentityManager().Create(context.Background(), first))
+ require.NoError(t, reg.IdentityManager().Create(ctx, first))
second := createIdentity(email, "email_creds", creds)
- err := reg.IdentityManager().Create(context.Background(), second)
+ err := reg.IdentityManager().Create(ctx, second)
require.Error(t, err)
var verr = new(identity.ErrDuplicateCredentials)
@@ -300,10 +300,10 @@ func TestManager(t *testing.T) {
}
first := createIdentity(email, field, creds)
- require.NoError(t, reg.IdentityManager().Create(context.Background(), first))
+ require.NoError(t, reg.IdentityManager().Create(ctx, first))
second := createIdentity(email, field, nil)
- err := reg.IdentityManager().Create(context.Background(), second)
+ err := reg.IdentityManager().Create(ctx, second)
require.Error(t, err)
var verr = new(identity.ErrDuplicateCredentials)
@@ -329,10 +329,10 @@ func TestManager(t *testing.T) {
}
first := createIdentity(email, field, creds)
- require.NoError(t, reg.IdentityManager().Create(context.Background(), first))
+ require.NoError(t, reg.IdentityManager().Create(ctx, first))
second := createIdentity(email, field, nil)
- err := reg.IdentityManager().Create(context.Background(), second)
+ err := reg.IdentityManager().Create(ctx, second)
require.Error(t, err)
var verr = new(identity.ErrDuplicateCredentials)
@@ -357,10 +357,10 @@ func TestManager(t *testing.T) {
t.Run("case=should update identity and update extension fields", func(t *testing.T) {
original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
original.Traits = newTraits("baz@ory.sh", "")
- require.NoError(t, reg.IdentityManager().Create(context.Background(), original))
+ require.NoError(t, reg.IdentityManager().Create(ctx, original))
original.Traits = newTraits("bar@ory.sh", "")
- require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits))
+ require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits))
checkExtensionFieldsForIdentities(t, "bar@ory.sh", original)
})
@@ -369,7 +369,7 @@ func TestManager(t *testing.T) {
email := uuid.Must(uuid.NewV4()).String() + "@ory.sh"
original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
original.Traits = newTraits(email, "")
- require.NoError(t, reg.IdentityManager().Create(context.Background(), original))
+ require.NoError(t, reg.IdentityManager().Create(ctx, original))
original.Credentials = map[identity.CredentialsType]identity.Credentials{
identity.CredentialsTypePassword: {
Type: identity.CredentialsTypePassword,
@@ -377,7 +377,7 @@ func TestManager(t *testing.T) {
Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`),
},
}
- require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits))
+ require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits))
assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, original.AvailableAAL.String)
})
@@ -392,16 +392,16 @@ func TestManager(t *testing.T) {
Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`),
},
}
- require.NoError(t, reg.IdentityManager().Create(context.Background(), original))
+ require.NoError(t, reg.IdentityManager().Create(ctx, original))
assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, original.AvailableAAL.String)
- require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits))
+ require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits))
assert.EqualValues(t, identity.AuthenticatorAssuranceLevel1, original.AvailableAAL.String, "Updating without changes should not change AAL")
original.Credentials[identity.CredentialsTypeTOTP] = identity.Credentials{
Type: identity.CredentialsTypeTOTP,
Identifiers: []string{email},
Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`),
}
- require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits))
+ require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits))
assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, original.AvailableAAL.String)
})
@@ -409,7 +409,7 @@ func TestManager(t *testing.T) {
email := uuid.Must(uuid.NewV4()).String() + "@ory.sh"
original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
original.Traits = newTraits(email, "")
- require.NoError(t, reg.IdentityManager().Create(context.Background(), original))
+ require.NoError(t, reg.IdentityManager().Create(ctx, original))
original.Credentials = map[identity.CredentialsType]identity.Credentials{
identity.CredentialsTypeTOTP: {
Type: identity.CredentialsTypeTOTP,
@@ -417,7 +417,7 @@ func TestManager(t *testing.T) {
Config: sqlxx.JSONRawMessage(`{"totp_url":"otpauth://totp/test"}`),
},
}
- require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits))
+ require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits))
assert.True(t, original.AvailableAAL.Valid)
assert.EqualValues(t, identity.NoAuthenticatorAssuranceLevel, original.AvailableAAL.String)
})
@@ -425,14 +425,14 @@ func TestManager(t *testing.T) {
t.Run("case=should not update protected traits without option", func(t *testing.T) {
original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
original.Traits = newTraits("email-update-1@ory.sh", "")
- require.NoError(t, reg.IdentityManager().Create(context.Background(), original))
+ require.NoError(t, reg.IdentityManager().Create(ctx, original))
original.Traits = newTraits("email-update-2@ory.sh", "")
- err := reg.IdentityManager().Update(context.Background(), original)
+ err := reg.IdentityManager().Update(ctx, original)
require.Error(t, err)
assert.Equal(t, identity.ErrProtectedFieldModified, errors.Cause(err))
- fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID)
+ fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID)
require.NoError(t, err)
// As UpdateTraits takes only the ID as a parameter it cannot update the identity in place.
// That is why we only check the identity in the store.
@@ -482,21 +482,21 @@ func TestManager(t *testing.T) {
originalEmail := x.NewUUID().String() + "@ory.sh"
original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
original.Traits = newTraits(originalEmail, "")
- require.NoError(t, reg.IdentityManager().Create(context.Background(), original))
+ require.NoError(t, reg.IdentityManager().Create(ctx, original))
- fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID)
+ fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID)
require.NoError(t, err)
checkExtensionFields(fromStore, originalEmail)(t)
newEmail := x.NewUUID().String() + "@ory.sh"
original.Traits = newTraits(newEmail, "")
- require.NoError(t, reg.IdentityManager().Update(context.Background(), original, identity.ManagerAllowWriteProtectedTraits))
+ require.NoError(t, reg.IdentityManager().Update(ctx, original, identity.ManagerAllowWriteProtectedTraits))
- fromStore, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID)
+ fromStore, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID)
require.NoError(t, err)
checkExtensionFields(fromStore, newEmail)(t)
- recoveryAddresses, err := reg.PrivilegedIdentityPool().ListRecoveryAddresses(context.Background(), 0, 500)
+ recoveryAddresses, err := reg.PrivilegedIdentityPool().ListRecoveryAddresses(ctx, 0, 500)
require.NoError(t, err)
var foundRecoveryAddress bool
@@ -508,7 +508,7 @@ func TestManager(t *testing.T) {
}
require.True(t, foundRecoveryAddress)
- verifiableAddresses, err := reg.PrivilegedIdentityPool().ListVerifiableAddresses(context.Background(), 0, 500)
+ verifiableAddresses, err := reg.PrivilegedIdentityPool().ListVerifiableAddresses(ctx, 0, 500)
require.NoError(t, err)
var foundVerifiableAddress bool
for _, a := range verifiableAddresses {
@@ -569,13 +569,13 @@ func TestManager(t *testing.T) {
t.Run("case=should update protected traits with option", func(t *testing.T) {
original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
original.Traits = newTraits("email-updatetraits-1@ory.sh", "")
- require.NoError(t, reg.IdentityManager().Create(context.Background(), original))
+ require.NoError(t, reg.IdentityManager().Create(ctx, original))
require.NoError(t, reg.IdentityManager().UpdateTraits(
- context.Background(), original.ID, newTraits("email-updatetraits-2@ory.sh", ""),
+ ctx, original.ID, newTraits("email-updatetraits-2@ory.sh", ""),
identity.ManagerAllowWriteProtectedTraits))
- fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID)
+ fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID)
require.NoError(t, err)
// As UpdateTraits takes only the ID as a parameter it cannot update the identity in place.
// That is why we only check the identity in the store.
@@ -585,17 +585,17 @@ func TestManager(t *testing.T) {
t.Run("case=should update identity and update extension fields", func(t *testing.T) {
original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
original.Traits = identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`)
- require.NoError(t, reg.IdentityManager().Create(context.Background(), original))
+ require.NoError(t, reg.IdentityManager().Create(ctx, original))
// These should all fail because they modify existing keys
- require.Error(t, reg.IdentityManager().UpdateTraits(context.Background(), original.ID, identity.Traits(`{"email":"not-baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`)))
- require.Error(t, reg.IdentityManager().UpdateTraits(context.Background(), original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"not-baz@ory.sh","email_recovery":"not-baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`)))
- require.Error(t, reg.IdentityManager().UpdateTraits(context.Background(), original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"not-baz@ory.sh","unprotected": "foo"}`)))
+ require.Error(t, reg.IdentityManager().UpdateTraits(ctx, original.ID, identity.Traits(`{"email":"not-baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`)))
+ require.Error(t, reg.IdentityManager().UpdateTraits(ctx, original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"not-baz@ory.sh","email_recovery":"not-baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "foo"}`)))
+ require.Error(t, reg.IdentityManager().UpdateTraits(ctx, original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"not-baz@ory.sh","unprotected": "foo"}`)))
- require.NoError(t, reg.IdentityManager().UpdateTraits(context.Background(), original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "bar"}`)))
+ require.NoError(t, reg.IdentityManager().UpdateTraits(ctx, original.ID, identity.Traits(`{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "bar"}`)))
checkExtensionFieldsForIdentities(t, "baz@ory.sh", original)
- actual, err := reg.IdentityPool().GetIdentity(context.Background(), original.ID, identity.ExpandNothing)
+ actual, err := reg.IdentityPool().GetIdentity(ctx, original.ID, identity.ExpandNothing)
require.NoError(t, err)
assert.JSONEq(t, `{"email":"baz@ory.sh","email_verify":"baz@ory.sh","email_recovery":"baz@ory.sh","email_creds":"baz@ory.sh","unprotected": "bar"}`, string(actual.Traits))
})
@@ -603,14 +603,14 @@ func TestManager(t *testing.T) {
t.Run("case=should not update protected traits without option", func(t *testing.T) {
original := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
original.Traits = newTraits("email-updatetraits-1@ory.sh", "")
- require.NoError(t, reg.IdentityManager().Create(context.Background(), original))
+ require.NoError(t, reg.IdentityManager().Create(ctx, original))
err := reg.IdentityManager().UpdateTraits(
- context.Background(), original.ID, newTraits("email-updatetraits-2@ory.sh", ""))
+ ctx, original.ID, newTraits("email-updatetraits-2@ory.sh", ""))
require.Error(t, err)
assert.Equal(t, identity.ErrProtectedFieldModified, errors.Cause(err))
- fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), original.ID)
+ fromStore, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, original.ID)
require.NoError(t, err)
// As UpdateTraits takes only the ID as a parameter it cannot update the identity in place.
// That is why we only check the identity in the store.
@@ -619,7 +619,7 @@ func TestManager(t *testing.T) {
})
t.Run("method=ConflictingIdentity", func(t *testing.T) {
- ctx := context.Background()
+ ctx := ctx
conflicOnIdentifier := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
conflicOnIdentifier.Traits = identity.Traits(`{"email":"conflict-on-identifier@example.com"}`)
@@ -682,12 +682,13 @@ func TestManager(t *testing.T) {
}
func TestManagerNoDefaultNamedSchema(t *testing.T) {
- conf, reg := internal.NewFastRegistryWithMocks(t)
- conf.MustSet(ctx, config.ViperKeyDefaultIdentitySchemaID, "user_v0")
- conf.MustSet(ctx, config.ViperKeyIdentitySchemas, config.Schemas{
- {ID: "user_v0", URL: "file://./stub/manager.schema.json"},
- })
- conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/")
+ _, reg := internal.NewFastRegistryWithMocks(t, configx.WithValues(map[string]interface{}{
+ config.ViperKeyDefaultIdentitySchemaID: "user_v0",
+ config.ViperKeyIdentitySchemas: config.Schemas{
+ {ID: "user_v0", URL: "file://./stub/manager.schema.json"},
+ },
+ config.ViperKeyPublicBaseURL: "https://www.ory.sh/",
+ }))
t.Run("case=should create identity with default schema", func(t *testing.T) {
stateChangedAt := sqlxx.NullTime(time.Now().UTC())
@@ -697,6 +698,6 @@ func TestManagerNoDefaultNamedSchema(t *testing.T) {
State: identity.StateActive,
StateChangedAt: &stateChangedAt,
}
- require.NoError(t, reg.IdentityManager().Create(context.Background(), original))
+ require.NoError(t, reg.IdentityManager().Create(ctx, original))
})
}
diff --git a/identity/test/pool.go b/identity/test/pool.go
index 450b5c1ea881..458c057da916 100644
--- a/identity/test/pool.go
+++ b/identity/test/pool.go
@@ -13,6 +13,8 @@ import (
"testing"
"time"
+ confighelpers "github.com/ory/kratos/driver/config/testhelpers"
+
"github.com/ory/x/crdbx"
"github.com/go-faker/faker/v4"
@@ -36,12 +38,11 @@ import (
"github.com/ory/x/urlx"
)
-func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister, m *identity.Manager, dbname string) func(t *testing.T) {
+func TestPool(ctx context.Context, p persistence.Persister, m *identity.Manager, dbname string) func(t *testing.T) {
return func(t *testing.T) {
- exampleServerURL := urlx.ParseOrPanic("http://example.com")
- conf.MustSet(ctx, config.ViperKeyPublicBaseURL, exampleServerURL.String())
-
nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p)
+
+ exampleServerURL := urlx.ParseOrPanic("http://example.com")
expandSchema := schema.Schema{
ID: "expandSchema",
URL: urlx.ParseOrPanic("file://./stub/expand.schema.json"),
@@ -62,22 +63,25 @@ func TestPool(ctx context.Context, conf *config.Config, p persistence.Persister,
URL: urlx.ParseOrPanic("file://./stub/handler/multiple_emails.schema.json"),
RawURL: "file://./stub/identity-2.schema.json",
}
- conf.MustSet(ctx, config.ViperKeyIdentitySchemas, []config.Schema{
- {
- ID: altSchema.ID,
- URL: altSchema.RawURL,
- },
- {
- ID: defaultSchema.ID,
- URL: defaultSchema.RawURL,
- },
- {
- ID: expandSchema.ID,
- URL: expandSchema.RawURL,
- },
- {
- ID: multipleEmailsSchema.ID,
- URL: multipleEmailsSchema.RawURL,
+ ctx := confighelpers.WithConfigValues(ctx, map[string]any{
+ config.ViperKeyPublicBaseURL: exampleServerURL.String(),
+ config.ViperKeyIdentitySchemas: []config.Schema{
+ {
+ ID: altSchema.ID,
+ URL: altSchema.RawURL,
+ },
+ {
+ ID: defaultSchema.ID,
+ URL: defaultSchema.RawURL,
+ },
+ {
+ ID: expandSchema.ID,
+ URL: expandSchema.RawURL,
+ },
+ {
+ ID: multipleEmailsSchema.ID,
+ URL: multipleEmailsSchema.RawURL,
+ },
},
})
diff --git a/internal/client-go/.openapi-generator/FILES b/internal/client-go/.openapi-generator/FILES
index fdf34c5e1507..c573997505d8 100644
--- a/internal/client-go/.openapi-generator/FILES
+++ b/internal/client-go/.openapi-generator/FILES
@@ -15,6 +15,7 @@ docs/ConsistencyRequestParameters.md
docs/ContinueWith.md
docs/ContinueWithRecoveryUi.md
docs/ContinueWithRecoveryUiFlow.md
+docs/ContinueWithRedirectBrowserTo.md
docs/ContinueWithSetOrySessionToken.md
docs/ContinueWithSettingsUi.md
docs/ContinueWithSettingsUiFlow.md
@@ -99,6 +100,7 @@ docs/UiText.md
docs/UpdateIdentityBody.md
docs/UpdateLoginFlowBody.md
docs/UpdateLoginFlowWithCodeMethod.md
+docs/UpdateLoginFlowWithIdentifierFirstMethod.md
docs/UpdateLoginFlowWithLookupSecretMethod.md
docs/UpdateLoginFlowWithOidcMethod.md
docs/UpdateLoginFlowWithPasskeyMethod.md
@@ -139,6 +141,7 @@ model_consistency_request_parameters.go
model_continue_with.go
model_continue_with_recovery_ui.go
model_continue_with_recovery_ui_flow.go
+model_continue_with_redirect_browser_to.go
model_continue_with_set_ory_session_token.go
model_continue_with_settings_ui.go
model_continue_with_settings_ui_flow.go
@@ -219,6 +222,7 @@ model_ui_text.go
model_update_identity_body.go
model_update_login_flow_body.go
model_update_login_flow_with_code_method.go
+model_update_login_flow_with_identifier_first_method.go
model_update_login_flow_with_lookup_secret_method.go
model_update_login_flow_with_oidc_method.go
model_update_login_flow_with_passkey_method.go
diff --git a/internal/client-go/README.md b/internal/client-go/README.md
index 04dd61ab7d1e..85af88a0d079 100644
--- a/internal/client-go/README.md
+++ b/internal/client-go/README.md
@@ -142,6 +142,7 @@ Class | Method | HTTP request | Description
- [ContinueWith](docs/ContinueWith.md)
- [ContinueWithRecoveryUi](docs/ContinueWithRecoveryUi.md)
- [ContinueWithRecoveryUiFlow](docs/ContinueWithRecoveryUiFlow.md)
+ - [ContinueWithRedirectBrowserTo](docs/ContinueWithRedirectBrowserTo.md)
- [ContinueWithSetOrySessionToken](docs/ContinueWithSetOrySessionToken.md)
- [ContinueWithSettingsUi](docs/ContinueWithSettingsUi.md)
- [ContinueWithSettingsUiFlow](docs/ContinueWithSettingsUiFlow.md)
@@ -222,6 +223,7 @@ Class | Method | HTTP request | Description
- [UpdateIdentityBody](docs/UpdateIdentityBody.md)
- [UpdateLoginFlowBody](docs/UpdateLoginFlowBody.md)
- [UpdateLoginFlowWithCodeMethod](docs/UpdateLoginFlowWithCodeMethod.md)
+ - [UpdateLoginFlowWithIdentifierFirstMethod](docs/UpdateLoginFlowWithIdentifierFirstMethod.md)
- [UpdateLoginFlowWithLookupSecretMethod](docs/UpdateLoginFlowWithLookupSecretMethod.md)
- [UpdateLoginFlowWithOidcMethod](docs/UpdateLoginFlowWithOidcMethod.md)
- [UpdateLoginFlowWithPasskeyMethod](docs/UpdateLoginFlowWithPasskeyMethod.md)
diff --git a/internal/client-go/api_identity.go b/internal/client-go/api_identity.go
index 62f5f80b36c2..b48819525a13 100644
--- a/internal/client-go/api_identity.go
+++ b/internal/client-go/api_identity.go
@@ -110,11 +110,11 @@ type IdentityApi interface {
/*
* DeleteIdentityCredentials Delete a credential for a specific identity
- * Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type
- You can only delete second factor (aal2) credentials.
+ * Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type.
+ You cannot delete password or code auth credentials through this API.
* @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
* @param id ID is the identity's ID.
- * @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode
+ * @param type_ Type is the type of credentials to delete. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode
* @return IdentityApiApiDeleteIdentityCredentialsRequest
*/
DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest
@@ -161,6 +161,9 @@ type IdentityApi interface {
return a 200 OK response with the session in the body. Returning the session as part of the response
will be deprecated in the future and should not be relied upon.
+ This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those
+ scenarios. This endpoint also returns 404 errors if the session does not exist.
+
Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.
* @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
* @param id ID is the session's ID.
@@ -1068,6 +1071,12 @@ type IdentityApiApiDeleteIdentityCredentialsRequest struct {
ApiService IdentityApi
id string
type_ string
+ identifier *string
+}
+
+func (r IdentityApiApiDeleteIdentityCredentialsRequest) Identifier(identifier string) IdentityApiApiDeleteIdentityCredentialsRequest {
+ r.identifier = &identifier
+ return r
}
func (r IdentityApiApiDeleteIdentityCredentialsRequest) Execute() (*http.Response, error) {
@@ -1076,12 +1085,12 @@ func (r IdentityApiApiDeleteIdentityCredentialsRequest) Execute() (*http.Respons
/*
- DeleteIdentityCredentials Delete a credential for a specific identity
- - Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type
+ - Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type.
-You can only delete second factor (aal2) credentials.
+You cannot delete password or code auth credentials through this API.
- @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
- @param id ID is the identity's ID.
- - @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode
+ - @param type_ Type is the type of credentials to delete. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode
- @return IdentityApiApiDeleteIdentityCredentialsRequest
*/
func (a *IdentityApiService) DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest {
@@ -1118,6 +1127,9 @@ func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDe
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
+ if r.identifier != nil {
+ localVarQueryParams.Add("identifier", parameterToString(*r.identifier, ""))
+ }
// to determine the Content-Type header
localVarHTTPContentTypes := []string{}
@@ -1494,6 +1506,9 @@ This endpoint returns per default a 204 No Content response on success. Older Or
return a 200 OK response with the session in the body. Returning the session as part of the response
will be deprecated in the future and should not be relied upon.
+This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those
+scenarios. This endpoint also returns 404 errors if the session does not exist.
+
Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.
- @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
- @param id ID is the session's ID.
diff --git a/internal/client-go/model_continue_with.go b/internal/client-go/model_continue_with.go
index 9e97dbf479e7..6fb1056836e6 100644
--- a/internal/client-go/model_continue_with.go
+++ b/internal/client-go/model_continue_with.go
@@ -19,6 +19,7 @@ import (
// ContinueWith - struct for ContinueWith
type ContinueWith struct {
ContinueWithRecoveryUi *ContinueWithRecoveryUi
+ ContinueWithRedirectBrowserTo *ContinueWithRedirectBrowserTo
ContinueWithSetOrySessionToken *ContinueWithSetOrySessionToken
ContinueWithSettingsUi *ContinueWithSettingsUi
ContinueWithVerificationUi *ContinueWithVerificationUi
@@ -31,6 +32,13 @@ func ContinueWithRecoveryUiAsContinueWith(v *ContinueWithRecoveryUi) ContinueWit
}
}
+// ContinueWithRedirectBrowserToAsContinueWith is a convenience function that returns ContinueWithRedirectBrowserTo wrapped in ContinueWith
+func ContinueWithRedirectBrowserToAsContinueWith(v *ContinueWithRedirectBrowserTo) ContinueWith {
+ return ContinueWith{
+ ContinueWithRedirectBrowserTo: v,
+ }
+}
+
// ContinueWithSetOrySessionTokenAsContinueWith is a convenience function that returns ContinueWithSetOrySessionToken wrapped in ContinueWith
func ContinueWithSetOrySessionTokenAsContinueWith(v *ContinueWithSetOrySessionToken) ContinueWith {
return ContinueWith{
@@ -62,6 +70,18 @@ func (dst *ContinueWith) UnmarshalJSON(data []byte) error {
return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.")
}
+ // check if the discriminator value is 'redirect_browser_to'
+ if jsonDict["action"] == "redirect_browser_to" {
+ // try to unmarshal JSON data into ContinueWithRedirectBrowserTo
+ err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo)
+ if err == nil {
+ return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match
+ } else {
+ dst.ContinueWithRedirectBrowserTo = nil
+ return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'set_ory_session_token'
if jsonDict["action"] == "set_ory_session_token" {
// try to unmarshal JSON data into ContinueWithSetOrySessionToken
@@ -122,6 +142,18 @@ func (dst *ContinueWith) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'continueWithRedirectBrowserTo'
+ if jsonDict["action"] == "continueWithRedirectBrowserTo" {
+ // try to unmarshal JSON data into ContinueWithRedirectBrowserTo
+ err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo)
+ if err == nil {
+ return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match
+ } else {
+ dst.ContinueWithRedirectBrowserTo = nil
+ return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'continueWithSetOrySessionToken'
if jsonDict["action"] == "continueWithSetOrySessionToken" {
// try to unmarshal JSON data into ContinueWithSetOrySessionToken
@@ -167,6 +199,10 @@ func (src ContinueWith) MarshalJSON() ([]byte, error) {
return json.Marshal(&src.ContinueWithRecoveryUi)
}
+ if src.ContinueWithRedirectBrowserTo != nil {
+ return json.Marshal(&src.ContinueWithRedirectBrowserTo)
+ }
+
if src.ContinueWithSetOrySessionToken != nil {
return json.Marshal(&src.ContinueWithSetOrySessionToken)
}
@@ -191,6 +227,10 @@ func (obj *ContinueWith) GetActualInstance() interface{} {
return obj.ContinueWithRecoveryUi
}
+ if obj.ContinueWithRedirectBrowserTo != nil {
+ return obj.ContinueWithRedirectBrowserTo
+ }
+
if obj.ContinueWithSetOrySessionToken != nil {
return obj.ContinueWithSetOrySessionToken
}
diff --git a/internal/client-go/model_continue_with_recovery_ui_flow.go b/internal/client-go/model_continue_with_recovery_ui_flow.go
index 3fde7e717ef2..251725a73c3b 100644
--- a/internal/client-go/model_continue_with_recovery_ui_flow.go
+++ b/internal/client-go/model_continue_with_recovery_ui_flow.go
@@ -19,7 +19,7 @@ import (
type ContinueWithRecoveryUiFlow struct {
// The ID of the recovery flow
Id string `json:"id"`
- // The URL of the recovery flow
+ // The URL of the recovery flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
Url *string `json:"url,omitempty"`
}
diff --git a/internal/client-go/model_continue_with_redirect_browser_to.go b/internal/client-go/model_continue_with_redirect_browser_to.go
new file mode 100644
index 000000000000..20c3e4f3c562
--- /dev/null
+++ b/internal/client-go/model_continue_with_redirect_browser_to.go
@@ -0,0 +1,138 @@
+/*
+ * 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"
+)
+
+// ContinueWithRedirectBrowserTo Indicates, that the UI flow could be continued by showing a recovery ui
+type ContinueWithRedirectBrowserTo struct {
+ // Action will always be `redirect_browser_to` redirect_browser_to ContinueWithActionRedirectBrowserToString
+ Action string `json:"action"`
+ // The URL to redirect the browser to
+ RedirectBrowserTo string `json:"redirect_browser_to"`
+}
+
+// NewContinueWithRedirectBrowserTo instantiates a new ContinueWithRedirectBrowserTo 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 NewContinueWithRedirectBrowserTo(action string, redirectBrowserTo string) *ContinueWithRedirectBrowserTo {
+ this := ContinueWithRedirectBrowserTo{}
+ this.Action = action
+ this.RedirectBrowserTo = redirectBrowserTo
+ return &this
+}
+
+// NewContinueWithRedirectBrowserToWithDefaults instantiates a new ContinueWithRedirectBrowserTo 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 NewContinueWithRedirectBrowserToWithDefaults() *ContinueWithRedirectBrowserTo {
+ this := ContinueWithRedirectBrowserTo{}
+ return &this
+}
+
+// GetAction returns the Action field value
+func (o *ContinueWithRedirectBrowserTo) GetAction() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Action
+}
+
+// GetActionOk returns a tuple with the Action field value
+// and a boolean to check if the value has been set.
+func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Action, true
+}
+
+// SetAction sets field value
+func (o *ContinueWithRedirectBrowserTo) SetAction(v string) {
+ o.Action = v
+}
+
+// GetRedirectBrowserTo returns the RedirectBrowserTo field value
+func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserTo() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.RedirectBrowserTo
+}
+
+// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value
+// and a boolean to check if the value has been set.
+func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserToOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.RedirectBrowserTo, true
+}
+
+// SetRedirectBrowserTo sets field value
+func (o *ContinueWithRedirectBrowserTo) SetRedirectBrowserTo(v string) {
+ o.RedirectBrowserTo = v
+}
+
+func (o ContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) {
+ toSerialize := map[string]interface{}{}
+ if true {
+ toSerialize["action"] = o.Action
+ }
+ if true {
+ toSerialize["redirect_browser_to"] = o.RedirectBrowserTo
+ }
+ return json.Marshal(toSerialize)
+}
+
+type NullableContinueWithRedirectBrowserTo struct {
+ value *ContinueWithRedirectBrowserTo
+ isSet bool
+}
+
+func (v NullableContinueWithRedirectBrowserTo) Get() *ContinueWithRedirectBrowserTo {
+ return v.value
+}
+
+func (v *NullableContinueWithRedirectBrowserTo) Set(val *ContinueWithRedirectBrowserTo) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableContinueWithRedirectBrowserTo) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableContinueWithRedirectBrowserTo) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableContinueWithRedirectBrowserTo(val *ContinueWithRedirectBrowserTo) *NullableContinueWithRedirectBrowserTo {
+ return &NullableContinueWithRedirectBrowserTo{value: val, isSet: true}
+}
+
+func (v NullableContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableContinueWithRedirectBrowserTo) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/internal/client-go/model_continue_with_settings_ui_flow.go b/internal/client-go/model_continue_with_settings_ui_flow.go
index 4ccaf74ef1b8..d6e9b9441f99 100644
--- a/internal/client-go/model_continue_with_settings_ui_flow.go
+++ b/internal/client-go/model_continue_with_settings_ui_flow.go
@@ -19,6 +19,8 @@ import (
type ContinueWithSettingsUiFlow struct {
// The ID of the settings flow
Id string `json:"id"`
+ // The URL of the settings flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
+ Url *string `json:"url,omitempty"`
}
// NewContinueWithSettingsUiFlow instantiates a new ContinueWithSettingsUiFlow object
@@ -63,11 +65,46 @@ func (o *ContinueWithSettingsUiFlow) SetId(v string) {
o.Id = v
}
+// GetUrl returns the Url field value if set, zero value otherwise.
+func (o *ContinueWithSettingsUiFlow) GetUrl() string {
+ if o == nil || o.Url == nil {
+ var ret string
+ return ret
+ }
+ return *o.Url
+}
+
+// GetUrlOk returns a tuple with the Url field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *ContinueWithSettingsUiFlow) GetUrlOk() (*string, bool) {
+ if o == nil || o.Url == nil {
+ return nil, false
+ }
+ return o.Url, true
+}
+
+// HasUrl returns a boolean if a field has been set.
+func (o *ContinueWithSettingsUiFlow) HasUrl() bool {
+ if o != nil && o.Url != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetUrl gets a reference to the given string and assigns it to the Url field.
+func (o *ContinueWithSettingsUiFlow) SetUrl(v string) {
+ o.Url = &v
+}
+
func (o ContinueWithSettingsUiFlow) MarshalJSON() ([]byte, error) {
toSerialize := map[string]interface{}{}
if true {
toSerialize["id"] = o.Id
}
+ if o.Url != nil {
+ toSerialize["url"] = o.Url
+ }
return json.Marshal(toSerialize)
}
diff --git a/internal/client-go/model_continue_with_verification_ui_flow.go b/internal/client-go/model_continue_with_verification_ui_flow.go
index 8fdd4609cf93..3c73a0761339 100644
--- a/internal/client-go/model_continue_with_verification_ui_flow.go
+++ b/internal/client-go/model_continue_with_verification_ui_flow.go
@@ -19,7 +19,7 @@ import (
type ContinueWithVerificationUiFlow struct {
// The ID of the verification flow
Id string `json:"id"`
- // The URL of the verification flow
+ // The URL of the verification flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
Url *string `json:"url,omitempty"`
// The address that should be verified in this flow
VerifiableAddress string `json:"verifiable_address"`
diff --git a/internal/client-go/model_identity_credentials_password.go b/internal/client-go/model_identity_credentials_password.go
index 85f942fb6852..df1900568bb3 100644
--- a/internal/client-go/model_identity_credentials_password.go
+++ b/internal/client-go/model_identity_credentials_password.go
@@ -19,6 +19,8 @@ import (
type IdentityCredentialsPassword struct {
// HashedPassword is a hash-representation of the password.
HashedPassword *string `json:"hashed_password,omitempty"`
+ // UsePasswordMigrationHook is set to true if the password should be migrated using the password migration hook. If set, and the HashedPassword is empty, a webhook will be called during login to migrate the password.
+ UsePasswordMigrationHook *bool `json:"use_password_migration_hook,omitempty"`
}
// NewIdentityCredentialsPassword instantiates a new IdentityCredentialsPassword object
@@ -70,11 +72,46 @@ func (o *IdentityCredentialsPassword) SetHashedPassword(v string) {
o.HashedPassword = &v
}
+// GetUsePasswordMigrationHook returns the UsePasswordMigrationHook field value if set, zero value otherwise.
+func (o *IdentityCredentialsPassword) GetUsePasswordMigrationHook() bool {
+ if o == nil || o.UsePasswordMigrationHook == nil {
+ var ret bool
+ return ret
+ }
+ return *o.UsePasswordMigrationHook
+}
+
+// GetUsePasswordMigrationHookOk returns a tuple with the UsePasswordMigrationHook field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *IdentityCredentialsPassword) GetUsePasswordMigrationHookOk() (*bool, bool) {
+ if o == nil || o.UsePasswordMigrationHook == nil {
+ return nil, false
+ }
+ return o.UsePasswordMigrationHook, true
+}
+
+// HasUsePasswordMigrationHook returns a boolean if a field has been set.
+func (o *IdentityCredentialsPassword) HasUsePasswordMigrationHook() bool {
+ if o != nil && o.UsePasswordMigrationHook != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetUsePasswordMigrationHook gets a reference to the given bool and assigns it to the UsePasswordMigrationHook field.
+func (o *IdentityCredentialsPassword) SetUsePasswordMigrationHook(v bool) {
+ o.UsePasswordMigrationHook = &v
+}
+
func (o IdentityCredentialsPassword) MarshalJSON() ([]byte, error) {
toSerialize := map[string]interface{}{}
if o.HashedPassword != nil {
toSerialize["hashed_password"] = o.HashedPassword
}
+ if o.UsePasswordMigrationHook != nil {
+ toSerialize["use_password_migration_hook"] = o.UsePasswordMigrationHook
+ }
return json.Marshal(toSerialize)
}
diff --git a/internal/client-go/model_identity_with_credentials_password_config.go b/internal/client-go/model_identity_with_credentials_password_config.go
index 754d59460f83..34f09ae58232 100644
--- a/internal/client-go/model_identity_with_credentials_password_config.go
+++ b/internal/client-go/model_identity_with_credentials_password_config.go
@@ -21,6 +21,8 @@ type IdentityWithCredentialsPasswordConfig struct {
HashedPassword *string `json:"hashed_password,omitempty"`
// The password in plain text if no hash is available.
Password *string `json:"password,omitempty"`
+ // If set to true, the password will be migrated using the password migration hook.
+ UsePasswordMigrationHook *bool `json:"use_password_migration_hook,omitempty"`
}
// NewIdentityWithCredentialsPasswordConfig instantiates a new IdentityWithCredentialsPasswordConfig object
@@ -104,6 +106,38 @@ func (o *IdentityWithCredentialsPasswordConfig) SetPassword(v string) {
o.Password = &v
}
+// GetUsePasswordMigrationHook returns the UsePasswordMigrationHook field value if set, zero value otherwise.
+func (o *IdentityWithCredentialsPasswordConfig) GetUsePasswordMigrationHook() bool {
+ if o == nil || o.UsePasswordMigrationHook == nil {
+ var ret bool
+ return ret
+ }
+ return *o.UsePasswordMigrationHook
+}
+
+// GetUsePasswordMigrationHookOk returns a tuple with the UsePasswordMigrationHook field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *IdentityWithCredentialsPasswordConfig) GetUsePasswordMigrationHookOk() (*bool, bool) {
+ if o == nil || o.UsePasswordMigrationHook == nil {
+ return nil, false
+ }
+ return o.UsePasswordMigrationHook, true
+}
+
+// HasUsePasswordMigrationHook returns a boolean if a field has been set.
+func (o *IdentityWithCredentialsPasswordConfig) HasUsePasswordMigrationHook() bool {
+ if o != nil && o.UsePasswordMigrationHook != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetUsePasswordMigrationHook gets a reference to the given bool and assigns it to the UsePasswordMigrationHook field.
+func (o *IdentityWithCredentialsPasswordConfig) SetUsePasswordMigrationHook(v bool) {
+ o.UsePasswordMigrationHook = &v
+}
+
func (o IdentityWithCredentialsPasswordConfig) MarshalJSON() ([]byte, error) {
toSerialize := map[string]interface{}{}
if o.HashedPassword != nil {
@@ -112,6 +146,9 @@ func (o IdentityWithCredentialsPasswordConfig) MarshalJSON() ([]byte, error) {
if o.Password != nil {
toSerialize["password"] = o.Password
}
+ if o.UsePasswordMigrationHook != nil {
+ toSerialize["use_password_migration_hook"] = o.UsePasswordMigrationHook
+ }
return json.Marshal(toSerialize)
}
diff --git a/internal/client-go/model_ui_node.go b/internal/client-go/model_ui_node.go
index e73f3c5e37d8..3582d9e85f67 100644
--- a/internal/client-go/model_ui_node.go
+++ b/internal/client-go/model_ui_node.go
@@ -18,7 +18,7 @@ import (
// UiNode Nodes are represented as HTML elements or their native UI equivalents. For example, a node can be an `
` tag, or an `` but also `some plain text`.
type UiNode struct {
Attributes UiNodeAttributes `json:"attributes"`
- // Group specifies which group (e.g. password authenticator) this node belongs to. default DefaultGroup password PasswordGroup oidc OpenIDConnectGroup profile ProfileGroup link LinkGroup code CodeGroup totp TOTPGroup lookup_secret LookupGroup webauthn WebAuthnGroup passkey PasskeyGroup
+ // Group specifies which group (e.g. password authenticator) this node belongs to. default DefaultGroup password PasswordGroup oidc OpenIDConnectGroup profile ProfileGroup link LinkGroup code CodeGroup totp TOTPGroup lookup_secret LookupGroup webauthn WebAuthnGroup passkey PasskeyGroup identifier_first IdentifierFirstGroup
Group string `json:"group"`
Messages []UiText `json:"messages"`
Meta UiNodeMeta `json:"meta"`
diff --git a/internal/client-go/model_ui_node_input_attributes.go b/internal/client-go/model_ui_node_input_attributes.go
index b373dda7ccfd..f8deff5d5417 100644
--- a/internal/client-go/model_ui_node_input_attributes.go
+++ b/internal/client-go/model_ui_node_input_attributes.go
@@ -22,14 +22,20 @@ type UiNodeInputAttributes struct {
// Sets the input's disabled field to true or false.
Disabled bool `json:"disabled"`
Label *UiText `json:"label,omitempty"`
+ // MaxLength may contain the input's maximum length.
+ Maxlength *int64 `json:"maxlength,omitempty"`
// The input's element name.
Name string `json:"name"`
// NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". text Text input Input img Image a Anchor script Script
NodeType string `json:"node_type"`
- // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn.
+ // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. Deprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.
Onclick *string `json:"onclick,omitempty"`
- // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn.
+ // OnClickTrigger may contain a WebAuthn trigger which should be executed on click. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration
+ OnclickTrigger *string `json:"onclickTrigger,omitempty"`
+ // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn. Deprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.
Onload *string `json:"onload,omitempty"`
+ // OnLoadTrigger may contain a WebAuthn trigger which should be executed on load. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration
+ OnloadTrigger *string `json:"onloadTrigger,omitempty"`
// The input's pattern.
Pattern *string `json:"pattern,omitempty"`
// Mark this input field as required.
@@ -149,6 +155,38 @@ func (o *UiNodeInputAttributes) SetLabel(v UiText) {
o.Label = &v
}
+// GetMaxlength returns the Maxlength field value if set, zero value otherwise.
+func (o *UiNodeInputAttributes) GetMaxlength() int64 {
+ if o == nil || o.Maxlength == nil {
+ var ret int64
+ return ret
+ }
+ return *o.Maxlength
+}
+
+// GetMaxlengthOk returns a tuple with the Maxlength field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UiNodeInputAttributes) GetMaxlengthOk() (*int64, bool) {
+ if o == nil || o.Maxlength == nil {
+ return nil, false
+ }
+ return o.Maxlength, true
+}
+
+// HasMaxlength returns a boolean if a field has been set.
+func (o *UiNodeInputAttributes) HasMaxlength() bool {
+ if o != nil && o.Maxlength != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetMaxlength gets a reference to the given int64 and assigns it to the Maxlength field.
+func (o *UiNodeInputAttributes) SetMaxlength(v int64) {
+ o.Maxlength = &v
+}
+
// GetName returns the Name field value
func (o *UiNodeInputAttributes) GetName() string {
if o == nil {
@@ -229,6 +267,38 @@ func (o *UiNodeInputAttributes) SetOnclick(v string) {
o.Onclick = &v
}
+// GetOnclickTrigger returns the OnclickTrigger field value if set, zero value otherwise.
+func (o *UiNodeInputAttributes) GetOnclickTrigger() string {
+ if o == nil || o.OnclickTrigger == nil {
+ var ret string
+ return ret
+ }
+ return *o.OnclickTrigger
+}
+
+// GetOnclickTriggerOk returns a tuple with the OnclickTrigger field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UiNodeInputAttributes) GetOnclickTriggerOk() (*string, bool) {
+ if o == nil || o.OnclickTrigger == nil {
+ return nil, false
+ }
+ return o.OnclickTrigger, true
+}
+
+// HasOnclickTrigger returns a boolean if a field has been set.
+func (o *UiNodeInputAttributes) HasOnclickTrigger() bool {
+ if o != nil && o.OnclickTrigger != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetOnclickTrigger gets a reference to the given string and assigns it to the OnclickTrigger field.
+func (o *UiNodeInputAttributes) SetOnclickTrigger(v string) {
+ o.OnclickTrigger = &v
+}
+
// GetOnload returns the Onload field value if set, zero value otherwise.
func (o *UiNodeInputAttributes) GetOnload() string {
if o == nil || o.Onload == nil {
@@ -261,6 +331,38 @@ func (o *UiNodeInputAttributes) SetOnload(v string) {
o.Onload = &v
}
+// GetOnloadTrigger returns the OnloadTrigger field value if set, zero value otherwise.
+func (o *UiNodeInputAttributes) GetOnloadTrigger() string {
+ if o == nil || o.OnloadTrigger == nil {
+ var ret string
+ return ret
+ }
+ return *o.OnloadTrigger
+}
+
+// GetOnloadTriggerOk returns a tuple with the OnloadTrigger field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UiNodeInputAttributes) GetOnloadTriggerOk() (*string, bool) {
+ if o == nil || o.OnloadTrigger == nil {
+ return nil, false
+ }
+ return o.OnloadTrigger, true
+}
+
+// HasOnloadTrigger returns a boolean if a field has been set.
+func (o *UiNodeInputAttributes) HasOnloadTrigger() bool {
+ if o != nil && o.OnloadTrigger != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetOnloadTrigger gets a reference to the given string and assigns it to the OnloadTrigger field.
+func (o *UiNodeInputAttributes) SetOnloadTrigger(v string) {
+ o.OnloadTrigger = &v
+}
+
// GetPattern returns the Pattern field value if set, zero value otherwise.
func (o *UiNodeInputAttributes) GetPattern() string {
if o == nil || o.Pattern == nil {
@@ -393,6 +495,9 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) {
if o.Label != nil {
toSerialize["label"] = o.Label
}
+ if o.Maxlength != nil {
+ toSerialize["maxlength"] = o.Maxlength
+ }
if true {
toSerialize["name"] = o.Name
}
@@ -402,9 +507,15 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) {
if o.Onclick != nil {
toSerialize["onclick"] = o.Onclick
}
+ if o.OnclickTrigger != nil {
+ toSerialize["onclickTrigger"] = o.OnclickTrigger
+ }
if o.Onload != nil {
toSerialize["onload"] = o.Onload
}
+ if o.OnloadTrigger != nil {
+ toSerialize["onloadTrigger"] = o.OnloadTrigger
+ }
if o.Pattern != nil {
toSerialize["pattern"] = o.Pattern
}
diff --git a/internal/client-go/model_update_login_flow_body.go b/internal/client-go/model_update_login_flow_body.go
index b8bb05734e3c..f0d79322c54f 100644
--- a/internal/client-go/model_update_login_flow_body.go
+++ b/internal/client-go/model_update_login_flow_body.go
@@ -18,13 +18,14 @@ import (
// UpdateLoginFlowBody - struct for UpdateLoginFlowBody
type UpdateLoginFlowBody struct {
- UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod
- UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod
- UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod
- UpdateLoginFlowWithPasskeyMethod *UpdateLoginFlowWithPasskeyMethod
- UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod
- UpdateLoginFlowWithTotpMethod *UpdateLoginFlowWithTotpMethod
- UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod
+ UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod
+ UpdateLoginFlowWithIdentifierFirstMethod *UpdateLoginFlowWithIdentifierFirstMethod
+ UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod
+ UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod
+ UpdateLoginFlowWithPasskeyMethod *UpdateLoginFlowWithPasskeyMethod
+ UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod
+ UpdateLoginFlowWithTotpMethod *UpdateLoginFlowWithTotpMethod
+ UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod
}
// UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithCodeMethod wrapped in UpdateLoginFlowBody
@@ -34,6 +35,13 @@ func UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithCo
}
}
+// UpdateLoginFlowWithIdentifierFirstMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithIdentifierFirstMethod wrapped in UpdateLoginFlowBody
+func UpdateLoginFlowWithIdentifierFirstMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithIdentifierFirstMethod) UpdateLoginFlowBody {
+ return UpdateLoginFlowBody{
+ UpdateLoginFlowWithIdentifierFirstMethod: v,
+ }
+}
+
// UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithLookupSecretMethod wrapped in UpdateLoginFlowBody
func UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithLookupSecretMethod) UpdateLoginFlowBody {
return UpdateLoginFlowBody{
@@ -98,6 +106,18 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'identifier_first'
+ if jsonDict["method"] == "identifier_first" {
+ // try to unmarshal JSON data into UpdateLoginFlowWithIdentifierFirstMethod
+ err = json.Unmarshal(data, &dst.UpdateLoginFlowWithIdentifierFirstMethod)
+ if err == nil {
+ return nil // data stored in dst.UpdateLoginFlowWithIdentifierFirstMethod, return on the first match
+ } else {
+ dst.UpdateLoginFlowWithIdentifierFirstMethod = nil
+ return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithIdentifierFirstMethod: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'lookup_secret'
if jsonDict["method"] == "lookup_secret" {
// try to unmarshal JSON data into UpdateLoginFlowWithLookupSecretMethod
@@ -182,6 +202,18 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'updateLoginFlowWithIdentifierFirstMethod'
+ if jsonDict["method"] == "updateLoginFlowWithIdentifierFirstMethod" {
+ // try to unmarshal JSON data into UpdateLoginFlowWithIdentifierFirstMethod
+ err = json.Unmarshal(data, &dst.UpdateLoginFlowWithIdentifierFirstMethod)
+ if err == nil {
+ return nil // data stored in dst.UpdateLoginFlowWithIdentifierFirstMethod, return on the first match
+ } else {
+ dst.UpdateLoginFlowWithIdentifierFirstMethod = nil
+ return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithIdentifierFirstMethod: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'updateLoginFlowWithLookupSecretMethod'
if jsonDict["method"] == "updateLoginFlowWithLookupSecretMethod" {
// try to unmarshal JSON data into UpdateLoginFlowWithLookupSecretMethod
@@ -263,6 +295,10 @@ func (src UpdateLoginFlowBody) MarshalJSON() ([]byte, error) {
return json.Marshal(&src.UpdateLoginFlowWithCodeMethod)
}
+ if src.UpdateLoginFlowWithIdentifierFirstMethod != nil {
+ return json.Marshal(&src.UpdateLoginFlowWithIdentifierFirstMethod)
+ }
+
if src.UpdateLoginFlowWithLookupSecretMethod != nil {
return json.Marshal(&src.UpdateLoginFlowWithLookupSecretMethod)
}
@@ -299,6 +335,10 @@ func (obj *UpdateLoginFlowBody) GetActualInstance() interface{} {
return obj.UpdateLoginFlowWithCodeMethod
}
+ if obj.UpdateLoginFlowWithIdentifierFirstMethod != nil {
+ return obj.UpdateLoginFlowWithIdentifierFirstMethod
+ }
+
if obj.UpdateLoginFlowWithLookupSecretMethod != nil {
return obj.UpdateLoginFlowWithLookupSecretMethod
}
diff --git a/internal/client-go/model_update_login_flow_with_identifier_first_method.go b/internal/client-go/model_update_login_flow_with_identifier_first_method.go
new file mode 100644
index 000000000000..70cf8002990d
--- /dev/null
+++ b/internal/client-go/model_update_login_flow_with_identifier_first_method.go
@@ -0,0 +1,212 @@
+/*
+ * 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"
+)
+
+// UpdateLoginFlowWithIdentifierFirstMethod Update Login Flow with Multi-Step Method
+type UpdateLoginFlowWithIdentifierFirstMethod struct {
+ // Sending the anti-csrf token is only required for browser login flows.
+ CsrfToken *string `json:"csrf_token,omitempty"`
+ // Identifier is the email or username of the user trying to log in.
+ Identifier string `json:"identifier"`
+ // Method should be set to \"password\" when logging in using the identifier and password strategy.
+ Method string `json:"method"`
+ // Transient data to pass along to any webhooks
+ TransientPayload map[string]interface{} `json:"transient_payload,omitempty"`
+}
+
+// NewUpdateLoginFlowWithIdentifierFirstMethod instantiates a new UpdateLoginFlowWithIdentifierFirstMethod 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 NewUpdateLoginFlowWithIdentifierFirstMethod(identifier string, method string) *UpdateLoginFlowWithIdentifierFirstMethod {
+ this := UpdateLoginFlowWithIdentifierFirstMethod{}
+ this.Identifier = identifier
+ this.Method = method
+ return &this
+}
+
+// NewUpdateLoginFlowWithIdentifierFirstMethodWithDefaults instantiates a new UpdateLoginFlowWithIdentifierFirstMethod 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 NewUpdateLoginFlowWithIdentifierFirstMethodWithDefaults() *UpdateLoginFlowWithIdentifierFirstMethod {
+ this := UpdateLoginFlowWithIdentifierFirstMethod{}
+ return &this
+}
+
+// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetCsrfToken() string {
+ if o == nil || o.CsrfToken == nil {
+ var ret string
+ return ret
+ }
+ return *o.CsrfToken
+}
+
+// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetCsrfTokenOk() (*string, bool) {
+ if o == nil || o.CsrfToken == nil {
+ return nil, false
+ }
+ return o.CsrfToken, true
+}
+
+// HasCsrfToken returns a boolean if a field has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) HasCsrfToken() bool {
+ if o != nil && o.CsrfToken != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetCsrfToken(v string) {
+ o.CsrfToken = &v
+}
+
+// GetIdentifier returns the Identifier field value
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetIdentifier() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Identifier
+}
+
+// GetIdentifierOk returns a tuple with the Identifier field value
+// and a boolean to check if the value has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetIdentifierOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Identifier, true
+}
+
+// SetIdentifier sets field value
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetIdentifier(v string) {
+ o.Identifier = v
+}
+
+// GetMethod returns the Method field value
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetMethod() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Method
+}
+
+// GetMethodOk returns a tuple with the Method field value
+// and a boolean to check if the value has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetMethodOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Method, true
+}
+
+// SetMethod sets field value
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetMethod(v string) {
+ o.Method = v
+}
+
+// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetTransientPayload() map[string]interface{} {
+ if o == nil || o.TransientPayload == nil {
+ var ret map[string]interface{}
+ return ret
+ }
+ return o.TransientPayload
+}
+
+// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetTransientPayloadOk() (map[string]interface{}, bool) {
+ if o == nil || o.TransientPayload == nil {
+ return nil, false
+ }
+ return o.TransientPayload, true
+}
+
+// HasTransientPayload returns a boolean if a field has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) HasTransientPayload() bool {
+ if o != nil && o.TransientPayload != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetTransientPayload(v map[string]interface{}) {
+ o.TransientPayload = v
+}
+
+func (o UpdateLoginFlowWithIdentifierFirstMethod) MarshalJSON() ([]byte, error) {
+ toSerialize := map[string]interface{}{}
+ if o.CsrfToken != nil {
+ toSerialize["csrf_token"] = o.CsrfToken
+ }
+ if true {
+ toSerialize["identifier"] = o.Identifier
+ }
+ if true {
+ toSerialize["method"] = o.Method
+ }
+ if o.TransientPayload != nil {
+ toSerialize["transient_payload"] = o.TransientPayload
+ }
+ return json.Marshal(toSerialize)
+}
+
+type NullableUpdateLoginFlowWithIdentifierFirstMethod struct {
+ value *UpdateLoginFlowWithIdentifierFirstMethod
+ isSet bool
+}
+
+func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) Get() *UpdateLoginFlowWithIdentifierFirstMethod {
+ return v.value
+}
+
+func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) Set(val *UpdateLoginFlowWithIdentifierFirstMethod) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableUpdateLoginFlowWithIdentifierFirstMethod(val *UpdateLoginFlowWithIdentifierFirstMethod) *NullableUpdateLoginFlowWithIdentifierFirstMethod {
+ return &NullableUpdateLoginFlowWithIdentifierFirstMethod{value: val, isSet: true}
+}
+
+func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/internal/client-go/model_update_registration_flow_body.go b/internal/client-go/model_update_registration_flow_body.go
index 64374c620f8f..82a578cfc4d3 100644
--- a/internal/client-go/model_update_registration_flow_body.go
+++ b/internal/client-go/model_update_registration_flow_body.go
@@ -22,6 +22,7 @@ type UpdateRegistrationFlowBody struct {
UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod
UpdateRegistrationFlowWithPasskeyMethod *UpdateRegistrationFlowWithPasskeyMethod
UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod
+ UpdateRegistrationFlowWithProfileMethod *UpdateRegistrationFlowWithProfileMethod
UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod
}
@@ -53,6 +54,13 @@ func UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(v *Upd
}
}
+// UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithProfileMethod wrapped in UpdateRegistrationFlowBody
+func UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithProfileMethod) UpdateRegistrationFlowBody {
+ return UpdateRegistrationFlowBody{
+ UpdateRegistrationFlowWithProfileMethod: v,
+ }
+}
+
// UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithWebAuthnMethod wrapped in UpdateRegistrationFlowBody
func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithWebAuthnMethod) UpdateRegistrationFlowBody {
return UpdateRegistrationFlowBody{
@@ -94,8 +102,8 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error {
}
}
- // check if the discriminator value is 'passKey'
- if jsonDict["method"] == "passKey" {
+ // check if the discriminator value is 'passkey'
+ if jsonDict["method"] == "passkey" {
// try to unmarshal JSON data into UpdateRegistrationFlowWithPasskeyMethod
err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasskeyMethod)
if err == nil {
@@ -118,6 +126,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'profile'
+ if jsonDict["method"] == "profile" {
+ // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod
+ err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod)
+ if err == nil {
+ return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match
+ } else {
+ dst.UpdateRegistrationFlowWithProfileMethod = nil
+ return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'webauthn'
if jsonDict["method"] == "webauthn" {
// try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod
@@ -178,6 +198,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'updateRegistrationFlowWithProfileMethod'
+ if jsonDict["method"] == "updateRegistrationFlowWithProfileMethod" {
+ // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod
+ err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod)
+ if err == nil {
+ return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match
+ } else {
+ dst.UpdateRegistrationFlowWithProfileMethod = nil
+ return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'updateRegistrationFlowWithWebAuthnMethod'
if jsonDict["method"] == "updateRegistrationFlowWithWebAuthnMethod" {
// try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod
@@ -211,6 +243,10 @@ func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) {
return json.Marshal(&src.UpdateRegistrationFlowWithPasswordMethod)
}
+ if src.UpdateRegistrationFlowWithProfileMethod != nil {
+ return json.Marshal(&src.UpdateRegistrationFlowWithProfileMethod)
+ }
+
if src.UpdateRegistrationFlowWithWebAuthnMethod != nil {
return json.Marshal(&src.UpdateRegistrationFlowWithWebAuthnMethod)
}
@@ -239,6 +275,10 @@ func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} {
return obj.UpdateRegistrationFlowWithPasswordMethod
}
+ if obj.UpdateRegistrationFlowWithProfileMethod != nil {
+ return obj.UpdateRegistrationFlowWithProfileMethod
+ }
+
if obj.UpdateRegistrationFlowWithWebAuthnMethod != nil {
return obj.UpdateRegistrationFlowWithWebAuthnMethod
}
diff --git a/internal/driver.go b/internal/driver.go
index a6f1f13d7954..521be82264d1 100644
--- a/internal/driver.go
+++ b/internal/driver.go
@@ -9,9 +9,12 @@ import (
"runtime"
"testing"
- "github.com/sirupsen/logrus"
+ confighelpers "github.com/ory/kratos/driver/config/testhelpers"
"github.com/ory/x/contextx"
+
+ "github.com/sirupsen/logrus"
+
"github.com/ory/x/jsonnetsecure"
"github.com/gofrs/uuid"
@@ -36,9 +39,8 @@ func init() {
})
}
-func NewConfigurationWithDefaults(t testing.TB) *config.Config {
- c := config.MustNew(t, logrusx.New("", ""),
- os.Stderr,
+func NewConfigurationWithDefaults(t testing.TB, opts ...configx.OptionModifier) *config.Config {
+ configOpts := append([]configx.OptionModifier{
configx.WithValues(map[string]interface{}{
"log.level": "error",
config.ViperKeyDSN: dbal.NewSQLiteTestDatabase(t),
@@ -51,16 +53,22 @@ func NewConfigurationWithDefaults(t testing.TB) *config.Config {
config.ViperKeyCourierSMTPURL: "smtp://foo:bar@baz.com/",
config.ViperKeySelfServiceBrowserDefaultReturnTo: "https://www.ory.sh/redirect-not-set",
config.ViperKeySecretsCipher: []string{"secret-thirty-two-character-long"},
+ config.ViperKeySelfServiceLoginFlowStyle: "unified",
}),
configx.SkipValidation(),
+ }, opts...)
+ c := config.MustNew(t, logrusx.New("", ""),
+ os.Stderr,
+ &confighelpers.TestConfigProvider{Contextualizer: &contextx.Default{}, Options: configOpts},
+ configOpts...,
)
return c
}
// NewFastRegistryWithMocks returns a registry with several mocks and an SQLite in memory database that make testing
// easier and way faster. This suite does not work for e2e or advanced integration tests.
-func NewFastRegistryWithMocks(t *testing.T) (*config.Config, *driver.RegistryDefault) {
- conf, reg := NewRegistryDefaultWithDSN(t, "")
+func NewFastRegistryWithMocks(t *testing.T, opts ...configx.OptionModifier) (*config.Config, *driver.RegistryDefault) {
+ conf, reg := NewRegistryDefaultWithDSN(t, "", opts...)
reg.WithCSRFTokenGenerator(x.FakeCSRFTokenGenerator)
reg.WithCSRFHandler(x.NewFakeCSRFHandler(""))
reg.WithHooks(map[string]func(config.SelfServiceHook) interface{}{
@@ -76,16 +84,17 @@ func NewFastRegistryWithMocks(t *testing.T) (*config.Config, *driver.RegistryDef
}
// NewRegistryDefaultWithDSN returns a more standard registry without mocks. Good for e2e and advanced integration testing!
-func NewRegistryDefaultWithDSN(t testing.TB, dsn string) (*config.Config, *driver.RegistryDefault) {
+func NewRegistryDefaultWithDSN(t testing.TB, dsn string, opts ...configx.OptionModifier) (*config.Config, *driver.RegistryDefault) {
ctx := context.Background()
- c := NewConfigurationWithDefaults(t)
- c.MustSet(ctx, config.ViperKeyDSN, stringsx.Coalesce(dsn, dbal.NewSQLiteTestDatabase(t)))
+ 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,
+ }))...)
reg, err := driver.NewRegistryFromDSN(ctx, c, logrusx.New("", "", logrusx.ForceLevel(logrus.ErrorLevel)))
require.NoError(t, err)
- reg.Config().MustSet(ctx, "dev", true)
pool := jsonnetsecure.NewProcessPool(runtime.GOMAXPROCS(0))
t.Cleanup(pool.Close)
- require.NoError(t, reg.Init(context.Background(), &contextx.Default{}, driver.SkipNetworkInit, driver.WithDisabledMigrationLogging(), driver.WithJsonnetPool(pool)))
+ require.NoError(t, reg.Init(context.Background(), &confighelpers.TestConfigProvider{Contextualizer: &contextx.Default{}}, driver.SkipNetworkInit, driver.WithDisabledMigrationLogging(), driver.WithJsonnetPool(pool)))
require.NoError(t, reg.Persister().MigrateUp(context.Background())) // always migrate up
actual, err := reg.Persister().DetermineNetwork(context.Background())
diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES
index fdf34c5e1507..c573997505d8 100644
--- a/internal/httpclient/.openapi-generator/FILES
+++ b/internal/httpclient/.openapi-generator/FILES
@@ -15,6 +15,7 @@ docs/ConsistencyRequestParameters.md
docs/ContinueWith.md
docs/ContinueWithRecoveryUi.md
docs/ContinueWithRecoveryUiFlow.md
+docs/ContinueWithRedirectBrowserTo.md
docs/ContinueWithSetOrySessionToken.md
docs/ContinueWithSettingsUi.md
docs/ContinueWithSettingsUiFlow.md
@@ -99,6 +100,7 @@ docs/UiText.md
docs/UpdateIdentityBody.md
docs/UpdateLoginFlowBody.md
docs/UpdateLoginFlowWithCodeMethod.md
+docs/UpdateLoginFlowWithIdentifierFirstMethod.md
docs/UpdateLoginFlowWithLookupSecretMethod.md
docs/UpdateLoginFlowWithOidcMethod.md
docs/UpdateLoginFlowWithPasskeyMethod.md
@@ -139,6 +141,7 @@ model_consistency_request_parameters.go
model_continue_with.go
model_continue_with_recovery_ui.go
model_continue_with_recovery_ui_flow.go
+model_continue_with_redirect_browser_to.go
model_continue_with_set_ory_session_token.go
model_continue_with_settings_ui.go
model_continue_with_settings_ui_flow.go
@@ -219,6 +222,7 @@ model_ui_text.go
model_update_identity_body.go
model_update_login_flow_body.go
model_update_login_flow_with_code_method.go
+model_update_login_flow_with_identifier_first_method.go
model_update_login_flow_with_lookup_secret_method.go
model_update_login_flow_with_oidc_method.go
model_update_login_flow_with_passkey_method.go
diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md
index 04dd61ab7d1e..85af88a0d079 100644
--- a/internal/httpclient/README.md
+++ b/internal/httpclient/README.md
@@ -142,6 +142,7 @@ Class | Method | HTTP request | Description
- [ContinueWith](docs/ContinueWith.md)
- [ContinueWithRecoveryUi](docs/ContinueWithRecoveryUi.md)
- [ContinueWithRecoveryUiFlow](docs/ContinueWithRecoveryUiFlow.md)
+ - [ContinueWithRedirectBrowserTo](docs/ContinueWithRedirectBrowserTo.md)
- [ContinueWithSetOrySessionToken](docs/ContinueWithSetOrySessionToken.md)
- [ContinueWithSettingsUi](docs/ContinueWithSettingsUi.md)
- [ContinueWithSettingsUiFlow](docs/ContinueWithSettingsUiFlow.md)
@@ -222,6 +223,7 @@ Class | Method | HTTP request | Description
- [UpdateIdentityBody](docs/UpdateIdentityBody.md)
- [UpdateLoginFlowBody](docs/UpdateLoginFlowBody.md)
- [UpdateLoginFlowWithCodeMethod](docs/UpdateLoginFlowWithCodeMethod.md)
+ - [UpdateLoginFlowWithIdentifierFirstMethod](docs/UpdateLoginFlowWithIdentifierFirstMethod.md)
- [UpdateLoginFlowWithLookupSecretMethod](docs/UpdateLoginFlowWithLookupSecretMethod.md)
- [UpdateLoginFlowWithOidcMethod](docs/UpdateLoginFlowWithOidcMethod.md)
- [UpdateLoginFlowWithPasskeyMethod](docs/UpdateLoginFlowWithPasskeyMethod.md)
diff --git a/internal/httpclient/api_identity.go b/internal/httpclient/api_identity.go
index 62f5f80b36c2..b48819525a13 100644
--- a/internal/httpclient/api_identity.go
+++ b/internal/httpclient/api_identity.go
@@ -110,11 +110,11 @@ type IdentityApi interface {
/*
* DeleteIdentityCredentials Delete a credential for a specific identity
- * Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type
- You can only delete second factor (aal2) credentials.
+ * Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type.
+ You cannot delete password or code auth credentials through this API.
* @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
* @param id ID is the identity's ID.
- * @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode
+ * @param type_ Type is the type of credentials to delete. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode
* @return IdentityApiApiDeleteIdentityCredentialsRequest
*/
DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest
@@ -161,6 +161,9 @@ type IdentityApi interface {
return a 200 OK response with the session in the body. Returning the session as part of the response
will be deprecated in the future and should not be relied upon.
+ This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those
+ scenarios. This endpoint also returns 404 errors if the session does not exist.
+
Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.
* @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
* @param id ID is the session's ID.
@@ -1068,6 +1071,12 @@ type IdentityApiApiDeleteIdentityCredentialsRequest struct {
ApiService IdentityApi
id string
type_ string
+ identifier *string
+}
+
+func (r IdentityApiApiDeleteIdentityCredentialsRequest) Identifier(identifier string) IdentityApiApiDeleteIdentityCredentialsRequest {
+ r.identifier = &identifier
+ return r
}
func (r IdentityApiApiDeleteIdentityCredentialsRequest) Execute() (*http.Response, error) {
@@ -1076,12 +1085,12 @@ func (r IdentityApiApiDeleteIdentityCredentialsRequest) Execute() (*http.Respons
/*
- DeleteIdentityCredentials Delete a credential for a specific identity
- - Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type
+ - Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type.
-You can only delete second factor (aal2) credentials.
+You cannot delete password or code auth credentials through this API.
- @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
- @param id ID is the identity's ID.
- - @param type_ Type is the type of credentials to be deleted. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode
+ - @param type_ Type is the type of credentials to delete. password CredentialsTypePassword oidc CredentialsTypeOIDC totp CredentialsTypeTOTP lookup_secret CredentialsTypeLookup webauthn CredentialsTypeWebAuthn code CredentialsTypeCodeAuth passkey CredentialsTypePasskey profile CredentialsTypeProfile link_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself. code_recovery CredentialsTypeRecoveryCode
- @return IdentityApiApiDeleteIdentityCredentialsRequest
*/
func (a *IdentityApiService) DeleteIdentityCredentials(ctx context.Context, id string, type_ string) IdentityApiApiDeleteIdentityCredentialsRequest {
@@ -1118,6 +1127,9 @@ func (a *IdentityApiService) DeleteIdentityCredentialsExecute(r IdentityApiApiDe
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
+ if r.identifier != nil {
+ localVarQueryParams.Add("identifier", parameterToString(*r.identifier, ""))
+ }
// to determine the Content-Type header
localVarHTTPContentTypes := []string{}
@@ -1494,6 +1506,9 @@ This endpoint returns per default a 204 No Content response on success. Older Or
return a 200 OK response with the session in the body. Returning the session as part of the response
will be deprecated in the future and should not be relied upon.
+This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those
+scenarios. This endpoint also returns 404 errors if the session does not exist.
+
Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.
- @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
- @param id ID is the session's ID.
diff --git a/internal/httpclient/model_continue_with.go b/internal/httpclient/model_continue_with.go
index 9e97dbf479e7..6fb1056836e6 100644
--- a/internal/httpclient/model_continue_with.go
+++ b/internal/httpclient/model_continue_with.go
@@ -19,6 +19,7 @@ import (
// ContinueWith - struct for ContinueWith
type ContinueWith struct {
ContinueWithRecoveryUi *ContinueWithRecoveryUi
+ ContinueWithRedirectBrowserTo *ContinueWithRedirectBrowserTo
ContinueWithSetOrySessionToken *ContinueWithSetOrySessionToken
ContinueWithSettingsUi *ContinueWithSettingsUi
ContinueWithVerificationUi *ContinueWithVerificationUi
@@ -31,6 +32,13 @@ func ContinueWithRecoveryUiAsContinueWith(v *ContinueWithRecoveryUi) ContinueWit
}
}
+// ContinueWithRedirectBrowserToAsContinueWith is a convenience function that returns ContinueWithRedirectBrowserTo wrapped in ContinueWith
+func ContinueWithRedirectBrowserToAsContinueWith(v *ContinueWithRedirectBrowserTo) ContinueWith {
+ return ContinueWith{
+ ContinueWithRedirectBrowserTo: v,
+ }
+}
+
// ContinueWithSetOrySessionTokenAsContinueWith is a convenience function that returns ContinueWithSetOrySessionToken wrapped in ContinueWith
func ContinueWithSetOrySessionTokenAsContinueWith(v *ContinueWithSetOrySessionToken) ContinueWith {
return ContinueWith{
@@ -62,6 +70,18 @@ func (dst *ContinueWith) UnmarshalJSON(data []byte) error {
return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.")
}
+ // check if the discriminator value is 'redirect_browser_to'
+ if jsonDict["action"] == "redirect_browser_to" {
+ // try to unmarshal JSON data into ContinueWithRedirectBrowserTo
+ err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo)
+ if err == nil {
+ return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match
+ } else {
+ dst.ContinueWithRedirectBrowserTo = nil
+ return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'set_ory_session_token'
if jsonDict["action"] == "set_ory_session_token" {
// try to unmarshal JSON data into ContinueWithSetOrySessionToken
@@ -122,6 +142,18 @@ func (dst *ContinueWith) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'continueWithRedirectBrowserTo'
+ if jsonDict["action"] == "continueWithRedirectBrowserTo" {
+ // try to unmarshal JSON data into ContinueWithRedirectBrowserTo
+ err = json.Unmarshal(data, &dst.ContinueWithRedirectBrowserTo)
+ if err == nil {
+ return nil // data stored in dst.ContinueWithRedirectBrowserTo, return on the first match
+ } else {
+ dst.ContinueWithRedirectBrowserTo = nil
+ return fmt.Errorf("Failed to unmarshal ContinueWith as ContinueWithRedirectBrowserTo: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'continueWithSetOrySessionToken'
if jsonDict["action"] == "continueWithSetOrySessionToken" {
// try to unmarshal JSON data into ContinueWithSetOrySessionToken
@@ -167,6 +199,10 @@ func (src ContinueWith) MarshalJSON() ([]byte, error) {
return json.Marshal(&src.ContinueWithRecoveryUi)
}
+ if src.ContinueWithRedirectBrowserTo != nil {
+ return json.Marshal(&src.ContinueWithRedirectBrowserTo)
+ }
+
if src.ContinueWithSetOrySessionToken != nil {
return json.Marshal(&src.ContinueWithSetOrySessionToken)
}
@@ -191,6 +227,10 @@ func (obj *ContinueWith) GetActualInstance() interface{} {
return obj.ContinueWithRecoveryUi
}
+ if obj.ContinueWithRedirectBrowserTo != nil {
+ return obj.ContinueWithRedirectBrowserTo
+ }
+
if obj.ContinueWithSetOrySessionToken != nil {
return obj.ContinueWithSetOrySessionToken
}
diff --git a/internal/httpclient/model_continue_with_recovery_ui_flow.go b/internal/httpclient/model_continue_with_recovery_ui_flow.go
index 3fde7e717ef2..251725a73c3b 100644
--- a/internal/httpclient/model_continue_with_recovery_ui_flow.go
+++ b/internal/httpclient/model_continue_with_recovery_ui_flow.go
@@ -19,7 +19,7 @@ import (
type ContinueWithRecoveryUiFlow struct {
// The ID of the recovery flow
Id string `json:"id"`
- // The URL of the recovery flow
+ // The URL of the recovery flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
Url *string `json:"url,omitempty"`
}
diff --git a/internal/httpclient/model_continue_with_redirect_browser_to.go b/internal/httpclient/model_continue_with_redirect_browser_to.go
new file mode 100644
index 000000000000..20c3e4f3c562
--- /dev/null
+++ b/internal/httpclient/model_continue_with_redirect_browser_to.go
@@ -0,0 +1,138 @@
+/*
+ * 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"
+)
+
+// ContinueWithRedirectBrowserTo Indicates, that the UI flow could be continued by showing a recovery ui
+type ContinueWithRedirectBrowserTo struct {
+ // Action will always be `redirect_browser_to` redirect_browser_to ContinueWithActionRedirectBrowserToString
+ Action string `json:"action"`
+ // The URL to redirect the browser to
+ RedirectBrowserTo string `json:"redirect_browser_to"`
+}
+
+// NewContinueWithRedirectBrowserTo instantiates a new ContinueWithRedirectBrowserTo 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 NewContinueWithRedirectBrowserTo(action string, redirectBrowserTo string) *ContinueWithRedirectBrowserTo {
+ this := ContinueWithRedirectBrowserTo{}
+ this.Action = action
+ this.RedirectBrowserTo = redirectBrowserTo
+ return &this
+}
+
+// NewContinueWithRedirectBrowserToWithDefaults instantiates a new ContinueWithRedirectBrowserTo 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 NewContinueWithRedirectBrowserToWithDefaults() *ContinueWithRedirectBrowserTo {
+ this := ContinueWithRedirectBrowserTo{}
+ return &this
+}
+
+// GetAction returns the Action field value
+func (o *ContinueWithRedirectBrowserTo) GetAction() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Action
+}
+
+// GetActionOk returns a tuple with the Action field value
+// and a boolean to check if the value has been set.
+func (o *ContinueWithRedirectBrowserTo) GetActionOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Action, true
+}
+
+// SetAction sets field value
+func (o *ContinueWithRedirectBrowserTo) SetAction(v string) {
+ o.Action = v
+}
+
+// GetRedirectBrowserTo returns the RedirectBrowserTo field value
+func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserTo() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.RedirectBrowserTo
+}
+
+// GetRedirectBrowserToOk returns a tuple with the RedirectBrowserTo field value
+// and a boolean to check if the value has been set.
+func (o *ContinueWithRedirectBrowserTo) GetRedirectBrowserToOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.RedirectBrowserTo, true
+}
+
+// SetRedirectBrowserTo sets field value
+func (o *ContinueWithRedirectBrowserTo) SetRedirectBrowserTo(v string) {
+ o.RedirectBrowserTo = v
+}
+
+func (o ContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) {
+ toSerialize := map[string]interface{}{}
+ if true {
+ toSerialize["action"] = o.Action
+ }
+ if true {
+ toSerialize["redirect_browser_to"] = o.RedirectBrowserTo
+ }
+ return json.Marshal(toSerialize)
+}
+
+type NullableContinueWithRedirectBrowserTo struct {
+ value *ContinueWithRedirectBrowserTo
+ isSet bool
+}
+
+func (v NullableContinueWithRedirectBrowserTo) Get() *ContinueWithRedirectBrowserTo {
+ return v.value
+}
+
+func (v *NullableContinueWithRedirectBrowserTo) Set(val *ContinueWithRedirectBrowserTo) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableContinueWithRedirectBrowserTo) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableContinueWithRedirectBrowserTo) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableContinueWithRedirectBrowserTo(val *ContinueWithRedirectBrowserTo) *NullableContinueWithRedirectBrowserTo {
+ return &NullableContinueWithRedirectBrowserTo{value: val, isSet: true}
+}
+
+func (v NullableContinueWithRedirectBrowserTo) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableContinueWithRedirectBrowserTo) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/internal/httpclient/model_continue_with_settings_ui_flow.go b/internal/httpclient/model_continue_with_settings_ui_flow.go
index 4ccaf74ef1b8..d6e9b9441f99 100644
--- a/internal/httpclient/model_continue_with_settings_ui_flow.go
+++ b/internal/httpclient/model_continue_with_settings_ui_flow.go
@@ -19,6 +19,8 @@ import (
type ContinueWithSettingsUiFlow struct {
// The ID of the settings flow
Id string `json:"id"`
+ // The URL of the settings flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
+ Url *string `json:"url,omitempty"`
}
// NewContinueWithSettingsUiFlow instantiates a new ContinueWithSettingsUiFlow object
@@ -63,11 +65,46 @@ func (o *ContinueWithSettingsUiFlow) SetId(v string) {
o.Id = v
}
+// GetUrl returns the Url field value if set, zero value otherwise.
+func (o *ContinueWithSettingsUiFlow) GetUrl() string {
+ if o == nil || o.Url == nil {
+ var ret string
+ return ret
+ }
+ return *o.Url
+}
+
+// GetUrlOk returns a tuple with the Url field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *ContinueWithSettingsUiFlow) GetUrlOk() (*string, bool) {
+ if o == nil || o.Url == nil {
+ return nil, false
+ }
+ return o.Url, true
+}
+
+// HasUrl returns a boolean if a field has been set.
+func (o *ContinueWithSettingsUiFlow) HasUrl() bool {
+ if o != nil && o.Url != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetUrl gets a reference to the given string and assigns it to the Url field.
+func (o *ContinueWithSettingsUiFlow) SetUrl(v string) {
+ o.Url = &v
+}
+
func (o ContinueWithSettingsUiFlow) MarshalJSON() ([]byte, error) {
toSerialize := map[string]interface{}{}
if true {
toSerialize["id"] = o.Id
}
+ if o.Url != nil {
+ toSerialize["url"] = o.Url
+ }
return json.Marshal(toSerialize)
}
diff --git a/internal/httpclient/model_continue_with_verification_ui_flow.go b/internal/httpclient/model_continue_with_verification_ui_flow.go
index 8fdd4609cf93..3c73a0761339 100644
--- a/internal/httpclient/model_continue_with_verification_ui_flow.go
+++ b/internal/httpclient/model_continue_with_verification_ui_flow.go
@@ -19,7 +19,7 @@ import (
type ContinueWithVerificationUiFlow struct {
// The ID of the verification flow
Id string `json:"id"`
- // The URL of the verification flow
+ // The URL of the verification flow If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
Url *string `json:"url,omitempty"`
// The address that should be verified in this flow
VerifiableAddress string `json:"verifiable_address"`
diff --git a/internal/httpclient/model_identity_credentials_password.go b/internal/httpclient/model_identity_credentials_password.go
index 85f942fb6852..df1900568bb3 100644
--- a/internal/httpclient/model_identity_credentials_password.go
+++ b/internal/httpclient/model_identity_credentials_password.go
@@ -19,6 +19,8 @@ import (
type IdentityCredentialsPassword struct {
// HashedPassword is a hash-representation of the password.
HashedPassword *string `json:"hashed_password,omitempty"`
+ // UsePasswordMigrationHook is set to true if the password should be migrated using the password migration hook. If set, and the HashedPassword is empty, a webhook will be called during login to migrate the password.
+ UsePasswordMigrationHook *bool `json:"use_password_migration_hook,omitempty"`
}
// NewIdentityCredentialsPassword instantiates a new IdentityCredentialsPassword object
@@ -70,11 +72,46 @@ func (o *IdentityCredentialsPassword) SetHashedPassword(v string) {
o.HashedPassword = &v
}
+// GetUsePasswordMigrationHook returns the UsePasswordMigrationHook field value if set, zero value otherwise.
+func (o *IdentityCredentialsPassword) GetUsePasswordMigrationHook() bool {
+ if o == nil || o.UsePasswordMigrationHook == nil {
+ var ret bool
+ return ret
+ }
+ return *o.UsePasswordMigrationHook
+}
+
+// GetUsePasswordMigrationHookOk returns a tuple with the UsePasswordMigrationHook field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *IdentityCredentialsPassword) GetUsePasswordMigrationHookOk() (*bool, bool) {
+ if o == nil || o.UsePasswordMigrationHook == nil {
+ return nil, false
+ }
+ return o.UsePasswordMigrationHook, true
+}
+
+// HasUsePasswordMigrationHook returns a boolean if a field has been set.
+func (o *IdentityCredentialsPassword) HasUsePasswordMigrationHook() bool {
+ if o != nil && o.UsePasswordMigrationHook != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetUsePasswordMigrationHook gets a reference to the given bool and assigns it to the UsePasswordMigrationHook field.
+func (o *IdentityCredentialsPassword) SetUsePasswordMigrationHook(v bool) {
+ o.UsePasswordMigrationHook = &v
+}
+
func (o IdentityCredentialsPassword) MarshalJSON() ([]byte, error) {
toSerialize := map[string]interface{}{}
if o.HashedPassword != nil {
toSerialize["hashed_password"] = o.HashedPassword
}
+ if o.UsePasswordMigrationHook != nil {
+ toSerialize["use_password_migration_hook"] = o.UsePasswordMigrationHook
+ }
return json.Marshal(toSerialize)
}
diff --git a/internal/httpclient/model_identity_with_credentials_password_config.go b/internal/httpclient/model_identity_with_credentials_password_config.go
index 754d59460f83..34f09ae58232 100644
--- a/internal/httpclient/model_identity_with_credentials_password_config.go
+++ b/internal/httpclient/model_identity_with_credentials_password_config.go
@@ -21,6 +21,8 @@ type IdentityWithCredentialsPasswordConfig struct {
HashedPassword *string `json:"hashed_password,omitempty"`
// The password in plain text if no hash is available.
Password *string `json:"password,omitempty"`
+ // If set to true, the password will be migrated using the password migration hook.
+ UsePasswordMigrationHook *bool `json:"use_password_migration_hook,omitempty"`
}
// NewIdentityWithCredentialsPasswordConfig instantiates a new IdentityWithCredentialsPasswordConfig object
@@ -104,6 +106,38 @@ func (o *IdentityWithCredentialsPasswordConfig) SetPassword(v string) {
o.Password = &v
}
+// GetUsePasswordMigrationHook returns the UsePasswordMigrationHook field value if set, zero value otherwise.
+func (o *IdentityWithCredentialsPasswordConfig) GetUsePasswordMigrationHook() bool {
+ if o == nil || o.UsePasswordMigrationHook == nil {
+ var ret bool
+ return ret
+ }
+ return *o.UsePasswordMigrationHook
+}
+
+// GetUsePasswordMigrationHookOk returns a tuple with the UsePasswordMigrationHook field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *IdentityWithCredentialsPasswordConfig) GetUsePasswordMigrationHookOk() (*bool, bool) {
+ if o == nil || o.UsePasswordMigrationHook == nil {
+ return nil, false
+ }
+ return o.UsePasswordMigrationHook, true
+}
+
+// HasUsePasswordMigrationHook returns a boolean if a field has been set.
+func (o *IdentityWithCredentialsPasswordConfig) HasUsePasswordMigrationHook() bool {
+ if o != nil && o.UsePasswordMigrationHook != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetUsePasswordMigrationHook gets a reference to the given bool and assigns it to the UsePasswordMigrationHook field.
+func (o *IdentityWithCredentialsPasswordConfig) SetUsePasswordMigrationHook(v bool) {
+ o.UsePasswordMigrationHook = &v
+}
+
func (o IdentityWithCredentialsPasswordConfig) MarshalJSON() ([]byte, error) {
toSerialize := map[string]interface{}{}
if o.HashedPassword != nil {
@@ -112,6 +146,9 @@ func (o IdentityWithCredentialsPasswordConfig) MarshalJSON() ([]byte, error) {
if o.Password != nil {
toSerialize["password"] = o.Password
}
+ if o.UsePasswordMigrationHook != nil {
+ toSerialize["use_password_migration_hook"] = o.UsePasswordMigrationHook
+ }
return json.Marshal(toSerialize)
}
diff --git a/internal/httpclient/model_ui_node.go b/internal/httpclient/model_ui_node.go
index e73f3c5e37d8..3582d9e85f67 100644
--- a/internal/httpclient/model_ui_node.go
+++ b/internal/httpclient/model_ui_node.go
@@ -18,7 +18,7 @@ import (
// UiNode Nodes are represented as HTML elements or their native UI equivalents. For example, a node can be an `
` tag, or an `` but also `some plain text`.
type UiNode struct {
Attributes UiNodeAttributes `json:"attributes"`
- // Group specifies which group (e.g. password authenticator) this node belongs to. default DefaultGroup password PasswordGroup oidc OpenIDConnectGroup profile ProfileGroup link LinkGroup code CodeGroup totp TOTPGroup lookup_secret LookupGroup webauthn WebAuthnGroup passkey PasskeyGroup
+ // Group specifies which group (e.g. password authenticator) this node belongs to. default DefaultGroup password PasswordGroup oidc OpenIDConnectGroup profile ProfileGroup link LinkGroup code CodeGroup totp TOTPGroup lookup_secret LookupGroup webauthn WebAuthnGroup passkey PasskeyGroup identifier_first IdentifierFirstGroup
Group string `json:"group"`
Messages []UiText `json:"messages"`
Meta UiNodeMeta `json:"meta"`
diff --git a/internal/httpclient/model_ui_node_input_attributes.go b/internal/httpclient/model_ui_node_input_attributes.go
index b373dda7ccfd..f8deff5d5417 100644
--- a/internal/httpclient/model_ui_node_input_attributes.go
+++ b/internal/httpclient/model_ui_node_input_attributes.go
@@ -22,14 +22,20 @@ type UiNodeInputAttributes struct {
// Sets the input's disabled field to true or false.
Disabled bool `json:"disabled"`
Label *UiText `json:"label,omitempty"`
+ // MaxLength may contain the input's maximum length.
+ Maxlength *int64 `json:"maxlength,omitempty"`
// The input's element name.
Name string `json:"name"`
// NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is \"input\". text Text input Input img Image a Anchor script Script
NodeType string `json:"node_type"`
- // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn.
+ // OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. Deprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.
Onclick *string `json:"onclick,omitempty"`
- // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn.
+ // OnClickTrigger may contain a WebAuthn trigger which should be executed on click. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration
+ OnclickTrigger *string `json:"onclickTrigger,omitempty"`
+ // OnLoad may contain javascript which should be executed on load. This is primarily used for WebAuthn. Deprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.
Onload *string `json:"onload,omitempty"`
+ // OnLoadTrigger may contain a WebAuthn trigger which should be executed on load. The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login. oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration oryWebAuthnLogin WebAuthnTriggersWebAuthnLogin oryPasskeyLogin WebAuthnTriggersPasskeyLogin oryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit oryPasskeyRegistration WebAuthnTriggersPasskeyRegistration oryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration
+ OnloadTrigger *string `json:"onloadTrigger,omitempty"`
// The input's pattern.
Pattern *string `json:"pattern,omitempty"`
// Mark this input field as required.
@@ -149,6 +155,38 @@ func (o *UiNodeInputAttributes) SetLabel(v UiText) {
o.Label = &v
}
+// GetMaxlength returns the Maxlength field value if set, zero value otherwise.
+func (o *UiNodeInputAttributes) GetMaxlength() int64 {
+ if o == nil || o.Maxlength == nil {
+ var ret int64
+ return ret
+ }
+ return *o.Maxlength
+}
+
+// GetMaxlengthOk returns a tuple with the Maxlength field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UiNodeInputAttributes) GetMaxlengthOk() (*int64, bool) {
+ if o == nil || o.Maxlength == nil {
+ return nil, false
+ }
+ return o.Maxlength, true
+}
+
+// HasMaxlength returns a boolean if a field has been set.
+func (o *UiNodeInputAttributes) HasMaxlength() bool {
+ if o != nil && o.Maxlength != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetMaxlength gets a reference to the given int64 and assigns it to the Maxlength field.
+func (o *UiNodeInputAttributes) SetMaxlength(v int64) {
+ o.Maxlength = &v
+}
+
// GetName returns the Name field value
func (o *UiNodeInputAttributes) GetName() string {
if o == nil {
@@ -229,6 +267,38 @@ func (o *UiNodeInputAttributes) SetOnclick(v string) {
o.Onclick = &v
}
+// GetOnclickTrigger returns the OnclickTrigger field value if set, zero value otherwise.
+func (o *UiNodeInputAttributes) GetOnclickTrigger() string {
+ if o == nil || o.OnclickTrigger == nil {
+ var ret string
+ return ret
+ }
+ return *o.OnclickTrigger
+}
+
+// GetOnclickTriggerOk returns a tuple with the OnclickTrigger field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UiNodeInputAttributes) GetOnclickTriggerOk() (*string, bool) {
+ if o == nil || o.OnclickTrigger == nil {
+ return nil, false
+ }
+ return o.OnclickTrigger, true
+}
+
+// HasOnclickTrigger returns a boolean if a field has been set.
+func (o *UiNodeInputAttributes) HasOnclickTrigger() bool {
+ if o != nil && o.OnclickTrigger != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetOnclickTrigger gets a reference to the given string and assigns it to the OnclickTrigger field.
+func (o *UiNodeInputAttributes) SetOnclickTrigger(v string) {
+ o.OnclickTrigger = &v
+}
+
// GetOnload returns the Onload field value if set, zero value otherwise.
func (o *UiNodeInputAttributes) GetOnload() string {
if o == nil || o.Onload == nil {
@@ -261,6 +331,38 @@ func (o *UiNodeInputAttributes) SetOnload(v string) {
o.Onload = &v
}
+// GetOnloadTrigger returns the OnloadTrigger field value if set, zero value otherwise.
+func (o *UiNodeInputAttributes) GetOnloadTrigger() string {
+ if o == nil || o.OnloadTrigger == nil {
+ var ret string
+ return ret
+ }
+ return *o.OnloadTrigger
+}
+
+// GetOnloadTriggerOk returns a tuple with the OnloadTrigger field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UiNodeInputAttributes) GetOnloadTriggerOk() (*string, bool) {
+ if o == nil || o.OnloadTrigger == nil {
+ return nil, false
+ }
+ return o.OnloadTrigger, true
+}
+
+// HasOnloadTrigger returns a boolean if a field has been set.
+func (o *UiNodeInputAttributes) HasOnloadTrigger() bool {
+ if o != nil && o.OnloadTrigger != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetOnloadTrigger gets a reference to the given string and assigns it to the OnloadTrigger field.
+func (o *UiNodeInputAttributes) SetOnloadTrigger(v string) {
+ o.OnloadTrigger = &v
+}
+
// GetPattern returns the Pattern field value if set, zero value otherwise.
func (o *UiNodeInputAttributes) GetPattern() string {
if o == nil || o.Pattern == nil {
@@ -393,6 +495,9 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) {
if o.Label != nil {
toSerialize["label"] = o.Label
}
+ if o.Maxlength != nil {
+ toSerialize["maxlength"] = o.Maxlength
+ }
if true {
toSerialize["name"] = o.Name
}
@@ -402,9 +507,15 @@ func (o UiNodeInputAttributes) MarshalJSON() ([]byte, error) {
if o.Onclick != nil {
toSerialize["onclick"] = o.Onclick
}
+ if o.OnclickTrigger != nil {
+ toSerialize["onclickTrigger"] = o.OnclickTrigger
+ }
if o.Onload != nil {
toSerialize["onload"] = o.Onload
}
+ if o.OnloadTrigger != nil {
+ toSerialize["onloadTrigger"] = o.OnloadTrigger
+ }
if o.Pattern != nil {
toSerialize["pattern"] = o.Pattern
}
diff --git a/internal/httpclient/model_update_login_flow_body.go b/internal/httpclient/model_update_login_flow_body.go
index b8bb05734e3c..f0d79322c54f 100644
--- a/internal/httpclient/model_update_login_flow_body.go
+++ b/internal/httpclient/model_update_login_flow_body.go
@@ -18,13 +18,14 @@ import (
// UpdateLoginFlowBody - struct for UpdateLoginFlowBody
type UpdateLoginFlowBody struct {
- UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod
- UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod
- UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod
- UpdateLoginFlowWithPasskeyMethod *UpdateLoginFlowWithPasskeyMethod
- UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod
- UpdateLoginFlowWithTotpMethod *UpdateLoginFlowWithTotpMethod
- UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod
+ UpdateLoginFlowWithCodeMethod *UpdateLoginFlowWithCodeMethod
+ UpdateLoginFlowWithIdentifierFirstMethod *UpdateLoginFlowWithIdentifierFirstMethod
+ UpdateLoginFlowWithLookupSecretMethod *UpdateLoginFlowWithLookupSecretMethod
+ UpdateLoginFlowWithOidcMethod *UpdateLoginFlowWithOidcMethod
+ UpdateLoginFlowWithPasskeyMethod *UpdateLoginFlowWithPasskeyMethod
+ UpdateLoginFlowWithPasswordMethod *UpdateLoginFlowWithPasswordMethod
+ UpdateLoginFlowWithTotpMethod *UpdateLoginFlowWithTotpMethod
+ UpdateLoginFlowWithWebAuthnMethod *UpdateLoginFlowWithWebAuthnMethod
}
// UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithCodeMethod wrapped in UpdateLoginFlowBody
@@ -34,6 +35,13 @@ func UpdateLoginFlowWithCodeMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithCo
}
}
+// UpdateLoginFlowWithIdentifierFirstMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithIdentifierFirstMethod wrapped in UpdateLoginFlowBody
+func UpdateLoginFlowWithIdentifierFirstMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithIdentifierFirstMethod) UpdateLoginFlowBody {
+ return UpdateLoginFlowBody{
+ UpdateLoginFlowWithIdentifierFirstMethod: v,
+ }
+}
+
// UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody is a convenience function that returns UpdateLoginFlowWithLookupSecretMethod wrapped in UpdateLoginFlowBody
func UpdateLoginFlowWithLookupSecretMethodAsUpdateLoginFlowBody(v *UpdateLoginFlowWithLookupSecretMethod) UpdateLoginFlowBody {
return UpdateLoginFlowBody{
@@ -98,6 +106,18 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'identifier_first'
+ if jsonDict["method"] == "identifier_first" {
+ // try to unmarshal JSON data into UpdateLoginFlowWithIdentifierFirstMethod
+ err = json.Unmarshal(data, &dst.UpdateLoginFlowWithIdentifierFirstMethod)
+ if err == nil {
+ return nil // data stored in dst.UpdateLoginFlowWithIdentifierFirstMethod, return on the first match
+ } else {
+ dst.UpdateLoginFlowWithIdentifierFirstMethod = nil
+ return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithIdentifierFirstMethod: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'lookup_secret'
if jsonDict["method"] == "lookup_secret" {
// try to unmarshal JSON data into UpdateLoginFlowWithLookupSecretMethod
@@ -182,6 +202,18 @@ func (dst *UpdateLoginFlowBody) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'updateLoginFlowWithIdentifierFirstMethod'
+ if jsonDict["method"] == "updateLoginFlowWithIdentifierFirstMethod" {
+ // try to unmarshal JSON data into UpdateLoginFlowWithIdentifierFirstMethod
+ err = json.Unmarshal(data, &dst.UpdateLoginFlowWithIdentifierFirstMethod)
+ if err == nil {
+ return nil // data stored in dst.UpdateLoginFlowWithIdentifierFirstMethod, return on the first match
+ } else {
+ dst.UpdateLoginFlowWithIdentifierFirstMethod = nil
+ return fmt.Errorf("Failed to unmarshal UpdateLoginFlowBody as UpdateLoginFlowWithIdentifierFirstMethod: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'updateLoginFlowWithLookupSecretMethod'
if jsonDict["method"] == "updateLoginFlowWithLookupSecretMethod" {
// try to unmarshal JSON data into UpdateLoginFlowWithLookupSecretMethod
@@ -263,6 +295,10 @@ func (src UpdateLoginFlowBody) MarshalJSON() ([]byte, error) {
return json.Marshal(&src.UpdateLoginFlowWithCodeMethod)
}
+ if src.UpdateLoginFlowWithIdentifierFirstMethod != nil {
+ return json.Marshal(&src.UpdateLoginFlowWithIdentifierFirstMethod)
+ }
+
if src.UpdateLoginFlowWithLookupSecretMethod != nil {
return json.Marshal(&src.UpdateLoginFlowWithLookupSecretMethod)
}
@@ -299,6 +335,10 @@ func (obj *UpdateLoginFlowBody) GetActualInstance() interface{} {
return obj.UpdateLoginFlowWithCodeMethod
}
+ if obj.UpdateLoginFlowWithIdentifierFirstMethod != nil {
+ return obj.UpdateLoginFlowWithIdentifierFirstMethod
+ }
+
if obj.UpdateLoginFlowWithLookupSecretMethod != nil {
return obj.UpdateLoginFlowWithLookupSecretMethod
}
diff --git a/internal/httpclient/model_update_login_flow_with_identifier_first_method.go b/internal/httpclient/model_update_login_flow_with_identifier_first_method.go
new file mode 100644
index 000000000000..70cf8002990d
--- /dev/null
+++ b/internal/httpclient/model_update_login_flow_with_identifier_first_method.go
@@ -0,0 +1,212 @@
+/*
+ * 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"
+)
+
+// UpdateLoginFlowWithIdentifierFirstMethod Update Login Flow with Multi-Step Method
+type UpdateLoginFlowWithIdentifierFirstMethod struct {
+ // Sending the anti-csrf token is only required for browser login flows.
+ CsrfToken *string `json:"csrf_token,omitempty"`
+ // Identifier is the email or username of the user trying to log in.
+ Identifier string `json:"identifier"`
+ // Method should be set to \"password\" when logging in using the identifier and password strategy.
+ Method string `json:"method"`
+ // Transient data to pass along to any webhooks
+ TransientPayload map[string]interface{} `json:"transient_payload,omitempty"`
+}
+
+// NewUpdateLoginFlowWithIdentifierFirstMethod instantiates a new UpdateLoginFlowWithIdentifierFirstMethod 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 NewUpdateLoginFlowWithIdentifierFirstMethod(identifier string, method string) *UpdateLoginFlowWithIdentifierFirstMethod {
+ this := UpdateLoginFlowWithIdentifierFirstMethod{}
+ this.Identifier = identifier
+ this.Method = method
+ return &this
+}
+
+// NewUpdateLoginFlowWithIdentifierFirstMethodWithDefaults instantiates a new UpdateLoginFlowWithIdentifierFirstMethod 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 NewUpdateLoginFlowWithIdentifierFirstMethodWithDefaults() *UpdateLoginFlowWithIdentifierFirstMethod {
+ this := UpdateLoginFlowWithIdentifierFirstMethod{}
+ return &this
+}
+
+// GetCsrfToken returns the CsrfToken field value if set, zero value otherwise.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetCsrfToken() string {
+ if o == nil || o.CsrfToken == nil {
+ var ret string
+ return ret
+ }
+ return *o.CsrfToken
+}
+
+// GetCsrfTokenOk returns a tuple with the CsrfToken field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetCsrfTokenOk() (*string, bool) {
+ if o == nil || o.CsrfToken == nil {
+ return nil, false
+ }
+ return o.CsrfToken, true
+}
+
+// HasCsrfToken returns a boolean if a field has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) HasCsrfToken() bool {
+ if o != nil && o.CsrfToken != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetCsrfToken gets a reference to the given string and assigns it to the CsrfToken field.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetCsrfToken(v string) {
+ o.CsrfToken = &v
+}
+
+// GetIdentifier returns the Identifier field value
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetIdentifier() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Identifier
+}
+
+// GetIdentifierOk returns a tuple with the Identifier field value
+// and a boolean to check if the value has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetIdentifierOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Identifier, true
+}
+
+// SetIdentifier sets field value
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetIdentifier(v string) {
+ o.Identifier = v
+}
+
+// GetMethod returns the Method field value
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetMethod() string {
+ if o == nil {
+ var ret string
+ return ret
+ }
+
+ return o.Method
+}
+
+// GetMethodOk returns a tuple with the Method field value
+// and a boolean to check if the value has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetMethodOk() (*string, bool) {
+ if o == nil {
+ return nil, false
+ }
+ return &o.Method, true
+}
+
+// SetMethod sets field value
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetMethod(v string) {
+ o.Method = v
+}
+
+// GetTransientPayload returns the TransientPayload field value if set, zero value otherwise.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetTransientPayload() map[string]interface{} {
+ if o == nil || o.TransientPayload == nil {
+ var ret map[string]interface{}
+ return ret
+ }
+ return o.TransientPayload
+}
+
+// GetTransientPayloadOk returns a tuple with the TransientPayload field value if set, nil otherwise
+// and a boolean to check if the value has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) GetTransientPayloadOk() (map[string]interface{}, bool) {
+ if o == nil || o.TransientPayload == nil {
+ return nil, false
+ }
+ return o.TransientPayload, true
+}
+
+// HasTransientPayload returns a boolean if a field has been set.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) HasTransientPayload() bool {
+ if o != nil && o.TransientPayload != nil {
+ return true
+ }
+
+ return false
+}
+
+// SetTransientPayload gets a reference to the given map[string]interface{} and assigns it to the TransientPayload field.
+func (o *UpdateLoginFlowWithIdentifierFirstMethod) SetTransientPayload(v map[string]interface{}) {
+ o.TransientPayload = v
+}
+
+func (o UpdateLoginFlowWithIdentifierFirstMethod) MarshalJSON() ([]byte, error) {
+ toSerialize := map[string]interface{}{}
+ if o.CsrfToken != nil {
+ toSerialize["csrf_token"] = o.CsrfToken
+ }
+ if true {
+ toSerialize["identifier"] = o.Identifier
+ }
+ if true {
+ toSerialize["method"] = o.Method
+ }
+ if o.TransientPayload != nil {
+ toSerialize["transient_payload"] = o.TransientPayload
+ }
+ return json.Marshal(toSerialize)
+}
+
+type NullableUpdateLoginFlowWithIdentifierFirstMethod struct {
+ value *UpdateLoginFlowWithIdentifierFirstMethod
+ isSet bool
+}
+
+func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) Get() *UpdateLoginFlowWithIdentifierFirstMethod {
+ return v.value
+}
+
+func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) Set(val *UpdateLoginFlowWithIdentifierFirstMethod) {
+ v.value = val
+ v.isSet = true
+}
+
+func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) IsSet() bool {
+ return v.isSet
+}
+
+func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) Unset() {
+ v.value = nil
+ v.isSet = false
+}
+
+func NewNullableUpdateLoginFlowWithIdentifierFirstMethod(val *UpdateLoginFlowWithIdentifierFirstMethod) *NullableUpdateLoginFlowWithIdentifierFirstMethod {
+ return &NullableUpdateLoginFlowWithIdentifierFirstMethod{value: val, isSet: true}
+}
+
+func (v NullableUpdateLoginFlowWithIdentifierFirstMethod) MarshalJSON() ([]byte, error) {
+ return json.Marshal(v.value)
+}
+
+func (v *NullableUpdateLoginFlowWithIdentifierFirstMethod) UnmarshalJSON(src []byte) error {
+ v.isSet = true
+ return json.Unmarshal(src, &v.value)
+}
diff --git a/internal/httpclient/model_update_registration_flow_body.go b/internal/httpclient/model_update_registration_flow_body.go
index 64374c620f8f..82a578cfc4d3 100644
--- a/internal/httpclient/model_update_registration_flow_body.go
+++ b/internal/httpclient/model_update_registration_flow_body.go
@@ -22,6 +22,7 @@ type UpdateRegistrationFlowBody struct {
UpdateRegistrationFlowWithOidcMethod *UpdateRegistrationFlowWithOidcMethod
UpdateRegistrationFlowWithPasskeyMethod *UpdateRegistrationFlowWithPasskeyMethod
UpdateRegistrationFlowWithPasswordMethod *UpdateRegistrationFlowWithPasswordMethod
+ UpdateRegistrationFlowWithProfileMethod *UpdateRegistrationFlowWithProfileMethod
UpdateRegistrationFlowWithWebAuthnMethod *UpdateRegistrationFlowWithWebAuthnMethod
}
@@ -53,6 +54,13 @@ func UpdateRegistrationFlowWithPasswordMethodAsUpdateRegistrationFlowBody(v *Upd
}
}
+// UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithProfileMethod wrapped in UpdateRegistrationFlowBody
+func UpdateRegistrationFlowWithProfileMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithProfileMethod) UpdateRegistrationFlowBody {
+ return UpdateRegistrationFlowBody{
+ UpdateRegistrationFlowWithProfileMethod: v,
+ }
+}
+
// UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody is a convenience function that returns UpdateRegistrationFlowWithWebAuthnMethod wrapped in UpdateRegistrationFlowBody
func UpdateRegistrationFlowWithWebAuthnMethodAsUpdateRegistrationFlowBody(v *UpdateRegistrationFlowWithWebAuthnMethod) UpdateRegistrationFlowBody {
return UpdateRegistrationFlowBody{
@@ -94,8 +102,8 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error {
}
}
- // check if the discriminator value is 'passKey'
- if jsonDict["method"] == "passKey" {
+ // check if the discriminator value is 'passkey'
+ if jsonDict["method"] == "passkey" {
// try to unmarshal JSON data into UpdateRegistrationFlowWithPasskeyMethod
err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithPasskeyMethod)
if err == nil {
@@ -118,6 +126,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'profile'
+ if jsonDict["method"] == "profile" {
+ // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod
+ err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod)
+ if err == nil {
+ return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match
+ } else {
+ dst.UpdateRegistrationFlowWithProfileMethod = nil
+ return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'webauthn'
if jsonDict["method"] == "webauthn" {
// try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod
@@ -178,6 +198,18 @@ func (dst *UpdateRegistrationFlowBody) UnmarshalJSON(data []byte) error {
}
}
+ // check if the discriminator value is 'updateRegistrationFlowWithProfileMethod'
+ if jsonDict["method"] == "updateRegistrationFlowWithProfileMethod" {
+ // try to unmarshal JSON data into UpdateRegistrationFlowWithProfileMethod
+ err = json.Unmarshal(data, &dst.UpdateRegistrationFlowWithProfileMethod)
+ if err == nil {
+ return nil // data stored in dst.UpdateRegistrationFlowWithProfileMethod, return on the first match
+ } else {
+ dst.UpdateRegistrationFlowWithProfileMethod = nil
+ return fmt.Errorf("Failed to unmarshal UpdateRegistrationFlowBody as UpdateRegistrationFlowWithProfileMethod: %s", err.Error())
+ }
+ }
+
// check if the discriminator value is 'updateRegistrationFlowWithWebAuthnMethod'
if jsonDict["method"] == "updateRegistrationFlowWithWebAuthnMethod" {
// try to unmarshal JSON data into UpdateRegistrationFlowWithWebAuthnMethod
@@ -211,6 +243,10 @@ func (src UpdateRegistrationFlowBody) MarshalJSON() ([]byte, error) {
return json.Marshal(&src.UpdateRegistrationFlowWithPasswordMethod)
}
+ if src.UpdateRegistrationFlowWithProfileMethod != nil {
+ return json.Marshal(&src.UpdateRegistrationFlowWithProfileMethod)
+ }
+
if src.UpdateRegistrationFlowWithWebAuthnMethod != nil {
return json.Marshal(&src.UpdateRegistrationFlowWithWebAuthnMethod)
}
@@ -239,6 +275,10 @@ func (obj *UpdateRegistrationFlowBody) GetActualInstance() interface{} {
return obj.UpdateRegistrationFlowWithPasswordMethod
}
+ if obj.UpdateRegistrationFlowWithProfileMethod != nil {
+ return obj.UpdateRegistrationFlowWithProfileMethod
+ }
+
if obj.UpdateRegistrationFlowWithWebAuthnMethod != nil {
return obj.UpdateRegistrationFlowWithWebAuthnMethod
}
diff --git a/internal/registrationhelpers/helpers.go b/internal/registrationhelpers/helpers.go
index 9fbf7f08211d..6fd76bd6ef1a 100644
--- a/internal/registrationhelpers/helpers.go
+++ b/internal/registrationhelpers/helpers.go
@@ -288,7 +288,7 @@ func AssertCommonErrorCases(t *testing.T, flows []string) {
t.Run("description=can call endpoints only without session", func(t *testing.T) {
values := url.Values{}
t.Run("type=browser", func(t *testing.T) {
- res, err := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg).
+ res, err := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg).
Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(values.Encode()), "application/x-www-form-urlencoded"))
require.NoError(t, err)
defer res.Body.Close()
@@ -297,7 +297,7 @@ func AssertCommonErrorCases(t *testing.T, flows []string) {
})
t.Run("type=api", func(t *testing.T) {
- res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg).
+ res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg).
Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(testhelpers.EncodeFormAsJSON(t, true, values)), "application/json"))
require.NoError(t, err)
assert.Len(t, res.Cookies(), 0)
@@ -337,7 +337,7 @@ func AssertCommonErrorCases(t *testing.T, flows []string) {
values := url.Values{}
t.Run("type=browser", func(t *testing.T) {
- res, err := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg).
+ res, err := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg).
Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(values.Encode()), "application/x-www-form-urlencoded"))
require.NoError(t, err)
defer res.Body.Close()
@@ -346,7 +346,7 @@ func AssertCommonErrorCases(t *testing.T, flows []string) {
})
t.Run("type=api", func(t *testing.T) {
- res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg).
+ res, err := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg).
Do(httpx.MustNewRequest("POST", publicTS.URL+registration.RouteSubmitFlow, strings.NewReader(testhelpers.EncodeFormAsJSON(t, true, values)), "application/json"))
require.NoError(t, err)
assert.Len(t, res.Cookies(), 0)
diff --git a/internal/testhelpers/config.go b/internal/testhelpers/config.go
index 2a24709c0745..b3450bda72fd 100644
--- a/internal/testhelpers/config.go
+++ b/internal/testhelpers/config.go
@@ -8,11 +8,12 @@ import (
"encoding/base64"
"testing"
- "github.com/ory/kratos/driver/config"
+ confighelpers "github.com/ory/kratos/driver/config/testhelpers"
"github.com/spf13/pflag"
"github.com/stretchr/testify/require"
+ "github.com/ory/kratos/driver/config"
"github.com/ory/x/configx"
"github.com/ory/x/randx"
)
@@ -24,6 +25,20 @@ func UseConfigFile(t *testing.T, path string) *pflag.FlagSet {
return flags
}
+func DefaultIdentitySchemaConfig(url string) map[string]any {
+ return map[string]any{
+ config.ViperKeyDefaultIdentitySchemaID: "default",
+ config.ViperKeyIdentitySchemas: config.Schemas{
+ {ID: "default", URL: url},
+ },
+ }
+}
+
+func WithDefaultIdentitySchema(ctx context.Context, url string) context.Context {
+ return confighelpers.WithConfigValues(ctx, DefaultIdentitySchemaConfig(url))
+}
+
+// Deprecated: Use context-based WithDefaultIdentitySchema instead
func SetDefaultIdentitySchema(conf *config.Config, url string) func() {
schemaUrl, _ := conf.DefaultIdentityTraitsSchemaURL(context.Background())
conf.MustSet(context.Background(), config.ViperKeyDefaultIdentitySchemaID, "default")
@@ -37,13 +52,29 @@ func SetDefaultIdentitySchema(conf *config.Config, url string) func() {
}
}
-// UseIdentitySchema registeres an identity schema in the config with a random ID and returns the ID
+// WithAddIdentitySchema registers an identity schema in the config with a random ID and returns the ID
//
-// It also registeres a test cleanup function, to reset the schemas to the original values, after the test finishes
+// It also registers a test cleanup function, to reset the schemas to the original values, after the test finishes
+func WithAddIdentitySchema(ctx context.Context, t *testing.T, conf *config.Config, url string) (context.Context, string) {
+ id := randx.MustString(16, randx.Alpha)
+ schemas, err := conf.IdentityTraitsSchemas(ctx)
+ require.NoError(t, err)
+
+ return confighelpers.WithConfigValue(ctx, config.ViperKeyIdentitySchemas, append(schemas, config.Schema{
+ ID: id,
+ URL: url,
+ })), id
+}
+
+// UseIdentitySchema registers an identity schema in the config with a random ID and returns the ID
+//
+// It also registers a test cleanup function, to reset the schemas to the original values, after the test finishes
+// Deprecated: Use context-based WithAddIdentitySchema instead
func UseIdentitySchema(t *testing.T, conf *config.Config, url string) (id string) {
id = randx.MustString(16, randx.Alpha)
schemas, err := conf.IdentityTraitsSchemas(context.Background())
require.NoError(t, err)
+
conf.MustSet(context.Background(), config.ViperKeyIdentitySchemas, append(schemas, config.Schema{
ID: id,
URL: url,
@@ -54,7 +85,12 @@ func UseIdentitySchema(t *testing.T, conf *config.Config, url string) (id string
return id
}
-// SetDefaultIdentitySchemaFromRaw allows setting the default identity schema from a raw JSON string.
+// WithDefaultIdentitySchemaFromRaw allows setting the default identity schema from a raw JSON string.
+func WithDefaultIdentitySchemaFromRaw(ctx context.Context, schema []byte) context.Context {
+ return WithDefaultIdentitySchema(ctx, "base64://"+base64.URLEncoding.EncodeToString(schema))
+}
+
+// Deprecated: Use context-based WithDefaultIdentitySchemaFromRaw instead
func SetDefaultIdentitySchemaFromRaw(conf *config.Config, schema []byte) {
conf.MustSet(context.Background(), config.ViperKeyDefaultIdentitySchemaID, "default")
conf.MustSet(context.Background(), config.ViperKeyIdentitySchemas, config.Schemas{
diff --git a/internal/testhelpers/network.go b/internal/testhelpers/network.go
index 888f46b583f5..10978dba6b05 100644
--- a/internal/testhelpers/network.go
+++ b/internal/testhelpers/network.go
@@ -20,7 +20,7 @@ func NewNetworkUnlessExisting(t *testing.T, ctx context.Context, p persistence.P
}
n := networkx.NewNetwork()
- require.NoError(t, p.GetConnection(context.Background()).Create(n))
+ require.NoError(t, p.GetConnection(ctx).Create(n))
return n.ID, p.WithNetworkID(n.ID)
}
diff --git a/internal/testhelpers/selfservice_login.go b/internal/testhelpers/selfservice_login.go
index 2bd20f81dfd4..1ef8db2e58ba 100644
--- a/internal/testhelpers/selfservice_login.go
+++ b/internal/testhelpers/selfservice_login.go
@@ -59,6 +59,18 @@ type initFlowOptions struct {
refresh bool
oauth2LoginChallenge string
via string
+ ctx context.Context
+}
+
+func newInitFlowOptions(opts []InitFlowWithOption) *initFlowOptions {
+ return new(initFlowOptions).apply(opts)
+}
+
+func (o *initFlowOptions) Context() context.Context {
+ if o.ctx == nil {
+ return context.Background()
+ }
+ return o.ctx
}
func (o *initFlowOptions) apply(opts []InitFlowWithOption) *initFlowOptions {
@@ -116,6 +128,12 @@ func InitFlowWithRefresh() InitFlowWithOption {
}
}
+func InitFlowWithContext(ctx context.Context) InitFlowWithOption {
+ return func(o *initFlowOptions) {
+ o.ctx = ctx
+ }
+}
+
func InitFlowWithOAuth2LoginChallenge(hlc string) InitFlowWithOption {
return func(o *initFlowOptions) {
o.oauth2LoginChallenge = hlc
@@ -134,12 +152,13 @@ func InitializeLoginFlowViaBrowser(t *testing.T, client *http.Client, ts *httpte
req, err := http.NewRequest("GET", getURLFromInitOptions(ts, login.RouteInitBrowserFlow, forced, opts...), nil)
require.NoError(t, err)
+ o := newInitFlowOptions(opts)
if isSPA {
req.Header.Set("Accept", "application/json")
}
- res, err := client.Do(req)
+ res, err := client.Do(req.WithContext(o.Context()))
require.NoError(t, err)
body := x.MustReadAll(res.Body)
require.NoError(t, res.Body.Close())
@@ -167,11 +186,11 @@ func InitializeLoginFlowViaBrowser(t *testing.T, client *http.Client, ts *httpte
return rs
}
-func InitializeLoginFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server, forced bool, opts ...InitFlowWithOption) *kratos.LoginFlow {
+func InitializeLoginFlowViaAPIWithContext(t *testing.T, ctx context.Context, client *http.Client, ts *httptest.Server, forced bool, opts ...InitFlowWithOption) *kratos.LoginFlow {
publicClient := NewSDKCustomClient(ts, client)
o := new(initFlowOptions).apply(opts)
- req := publicClient.FrontendApi.CreateNativeLoginFlow(context.Background()).Refresh(forced)
+ req := publicClient.FrontendApi.CreateNativeLoginFlow(ctx).Refresh(forced)
if o.aal != "" {
req = req.Aal(string(o.aal))
}
@@ -186,6 +205,10 @@ func InitializeLoginFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.S
return rs
}
+func InitializeLoginFlowViaAPI(t *testing.T, client *http.Client, ts *httptest.Server, forced bool, opts ...InitFlowWithOption) *kratos.LoginFlow {
+ return InitializeLoginFlowViaAPIWithContext(t, context.Background(), client, ts, forced, opts...)
+}
+
func LoginMakeRequest(
t *testing.T,
isAPI bool,
@@ -193,6 +216,18 @@ func LoginMakeRequest(
f *kratos.LoginFlow,
hc *http.Client,
values string,
+) (string, *http.Response) {
+ return LoginMakeRequestWithContext(t, context.Background(), isAPI, isSPA, f, hc, values)
+}
+
+func LoginMakeRequestWithContext(
+ t *testing.T,
+ ctx context.Context,
+ isAPI bool,
+ isSPA bool,
+ f *kratos.LoginFlow,
+ hc *http.Client,
+ values string,
) (string, *http.Response) {
require.NotEmpty(t, f.Ui.Action)
@@ -201,7 +236,7 @@ func LoginMakeRequest(
req.Header.Set("Accept", "application/json")
}
- res, err := hc.Do(req)
+ res, err := hc.Do(req.WithContext(ctx))
require.NoError(t, err, "action: %s", f.Ui.Action)
defer res.Body.Close()
diff --git a/internal/testhelpers/session.go b/internal/testhelpers/session.go
index 29e8997f3438..1d2cecc824db 100644
--- a/internal/testhelpers/session.go
+++ b/internal/testhelpers/session.go
@@ -42,25 +42,25 @@ func NewSessionClient(t *testing.T, u string) *http.Client {
return c
}
-func maybePersistSession(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) {
- id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), sess.Identity.ID)
+func maybePersistSession(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) {
+ id, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, sess.Identity.ID)
if err != nil {
- require.NoError(t, sess.Identity.SetAvailableAAL(context.Background(), reg.IdentityManager()))
- require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), sess.Identity))
- id, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), sess.Identity.ID)
+ require.NoError(t, sess.Identity.SetAvailableAAL(ctx, reg.IdentityManager()))
+ require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, sess.Identity))
+ id, err = reg.PrivilegedIdentityPool().GetIdentityConfidential(ctx, sess.Identity.ID)
require.NoError(t, err)
}
sess.Identity = id
sess.IdentityID = id.ID
- require.NoError(t, err, reg.SessionPersister().UpsertSession(context.Background(), sess))
+ require.NoError(t, err, reg.SessionPersister().UpsertSession(ctx, sess))
}
-func NewHTTPClientWithSessionCookie(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
- maybePersistSession(t, reg, sess)
+func NewHTTPClientWithSessionCookie(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
+ maybePersistSession(t, ctx, reg, sess)
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- require.NoError(t, reg.SessionManager().IssueCookie(context.Background(), w, r, sess))
+ require.NoError(t, reg.SessionManager().IssueCookie(ctx, w, r, sess))
})
if _, ok := reg.CSRFHandler().(*nosurf.CSRFHandler); ok {
@@ -75,11 +75,11 @@ func NewHTTPClientWithSessionCookie(t *testing.T, reg *driver.RegistryDefault, s
return c
}
-func NewHTTPClientWithSessionCookieLocalhost(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
- maybePersistSession(t, reg, sess)
+func NewHTTPClientWithSessionCookieLocalhost(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
+ maybePersistSession(t, ctx, reg, sess)
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- require.NoError(t, reg.SessionManager().IssueCookie(context.Background(), w, r, sess))
+ require.NoError(t, reg.SessionManager().IssueCookie(ctx, w, r, sess))
})
if _, ok := reg.CSRFHandler().(*nosurf.CSRFHandler); ok {
@@ -96,11 +96,11 @@ func NewHTTPClientWithSessionCookieLocalhost(t *testing.T, reg *driver.RegistryD
return c
}
-func NewNoRedirectHTTPClientWithSessionCookie(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
- maybePersistSession(t, reg, sess)
+func NewNoRedirectHTTPClientWithSessionCookie(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
+ maybePersistSession(t, ctx, reg, sess)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- require.NoError(t, reg.SessionManager().IssueCookie(context.Background(), w, r, sess))
+ require.NoError(t, reg.SessionManager().IssueCookie(ctx, w, r, sess))
}))
defer ts.Close()
@@ -130,8 +130,8 @@ func (ct *TransportWithLogger) RoundTrip(req *http.Request) (*http.Response, err
return ct.RoundTripper.RoundTrip(req)
}
-func NewHTTPClientWithSessionToken(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
- maybePersistSession(t, reg, sess)
+func NewHTTPClientWithSessionToken(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
+ maybePersistSession(t, ctx, reg, sess)
return &http.Client{
Transport: NewTransportWithHeader(t, http.Header{
@@ -140,10 +140,14 @@ func NewHTTPClientWithSessionToken(t *testing.T, reg *driver.RegistryDefault, se
}
}
-func NewHTTPClientWithArbitrarySessionToken(t *testing.T, reg *driver.RegistryDefault) *http.Client {
+func NewHTTPClientWithArbitrarySessionToken(t *testing.T, ctx context.Context, reg *driver.RegistryDefault) *http.Client {
+ return NewHTTPClientWithArbitrarySessionTokenAndTraits(t, ctx, reg, nil)
+}
+
+func NewHTTPClientWithArbitrarySessionTokenAndTraits(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, traits identity.Traits) *http.Client {
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
s, err := session.NewActiveSession(req,
- &identity.Identity{ID: x.NewUUID(), State: identity.StateActive},
+ &identity.Identity{ID: x.NewUUID(), State: identity.StateActive, Traits: traits},
NewSessionLifespanProvider(time.Hour),
time.Now(),
identity.CredentialsTypePassword,
@@ -151,10 +155,10 @@ func NewHTTPClientWithArbitrarySessionToken(t *testing.T, reg *driver.RegistryDe
)
require.NoError(t, err, "Could not initialize session from identity.")
- return NewHTTPClientWithSessionToken(t, reg, s)
+ return NewHTTPClientWithSessionToken(t, ctx, reg, s)
}
-func NewHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver.RegistryDefault) *http.Client {
+func NewHTTPClientWithArbitrarySessionCookie(t *testing.T, ctx context.Context, reg *driver.RegistryDefault) *http.Client {
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
s, err := session.NewActiveSession(req,
&identity.Identity{ID: x.NewUUID(), State: identity.StateActive, Traits: []byte("{}")},
@@ -165,10 +169,10 @@ func NewHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver.RegistryD
)
require.NoError(t, err, "Could not initialize session from identity.")
- return NewHTTPClientWithSessionCookie(t, reg, s)
+ return NewHTTPClientWithSessionCookie(t, ctx, reg, s)
}
-func NewNoRedirectHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver.RegistryDefault) *http.Client {
+func NewNoRedirectHTTPClientWithArbitrarySessionCookie(t *testing.T, ctx context.Context, reg *driver.RegistryDefault) *http.Client {
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
s, err := session.NewActiveSession(req,
&identity.Identity{ID: x.NewUUID(), State: identity.StateActive},
@@ -179,10 +183,10 @@ func NewNoRedirectHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver
)
require.NoError(t, err, "Could not initialize session from identity.")
- return NewNoRedirectHTTPClientWithSessionCookie(t, reg, s)
+ return NewNoRedirectHTTPClientWithSessionCookie(t, ctx, reg, s)
}
-func NewHTTPClientWithIdentitySessionCookie(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
+func NewHTTPClientWithIdentitySessionCookie(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
s, err := session.NewActiveSession(req,
id,
@@ -193,10 +197,10 @@ func NewHTTPClientWithIdentitySessionCookie(t *testing.T, reg *driver.RegistryDe
)
require.NoError(t, err, "Could not initialize session from identity.")
- return NewHTTPClientWithSessionCookie(t, reg, s)
+ return NewHTTPClientWithSessionCookie(t, ctx, reg, s)
}
-func NewHTTPClientWithIdentitySessionCookieLocalhost(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
+func NewHTTPClientWithIdentitySessionCookieLocalhost(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
s, err := session.NewActiveSession(req,
id,
@@ -207,10 +211,10 @@ func NewHTTPClientWithIdentitySessionCookieLocalhost(t *testing.T, reg *driver.R
)
require.NoError(t, err, "Could not initialize session from identity.")
- return NewHTTPClientWithSessionCookieLocalhost(t, reg, s)
+ return NewHTTPClientWithSessionCookieLocalhost(t, ctx, reg, s)
}
-func NewHTTPClientWithIdentitySessionToken(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
+func NewHTTPClientWithIdentitySessionToken(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
req := NewTestHTTPRequest(t, "GET", "/sessions/whoami", nil)
s, err := session.NewActiveSession(req,
id,
@@ -221,7 +225,7 @@ func NewHTTPClientWithIdentitySessionToken(t *testing.T, reg *driver.RegistryDef
)
require.NoError(t, err, "Could not initialize session from identity.")
- return NewHTTPClientWithSessionToken(t, reg, s)
+ return NewHTTPClientWithSessionToken(t, ctx, reg, s)
}
func EnsureAAL(t *testing.T, c *http.Client, ts *httptest.Server, aal string, methods ...string) {
@@ -236,8 +240,8 @@ func EnsureAAL(t *testing.T, c *http.Client, ts *httptest.Server, aal string, me
assert.Len(t, gjson.GetBytes(sess, "authentication_methods").Array(), 1+len(methods))
}
-func NewAuthorizedTransport(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *TransportWithHeader {
- maybePersistSession(t, reg, sess)
+func NewAuthorizedTransport(t *testing.T, ctx context.Context, reg *driver.RegistryDefault, sess *session.Session) *TransportWithHeader {
+ maybePersistSession(t, ctx, reg, sess)
return NewTransportWithHeader(t, http.Header{
"Authorization": {"Bearer " + sess.Token},
@@ -259,6 +263,10 @@ type TransportWithHeader struct {
h http.Header
}
+func (ct *TransportWithHeader) GetHeader() http.Header {
+ return ct.h
+}
+
func (ct *TransportWithHeader) RoundTrip(req *http.Request) (*http.Response, error) {
for k := range ct.h {
req.Header.Set(k, ct.h.Get(k))
diff --git a/persistence/sql/persister_cleanup_test.go b/persistence/sql/persister_cleanup_test.go
index 65e95ea6ea00..efb14a05e6c9 100644
--- a/persistence/sql/persister_cleanup_test.go
+++ b/persistence/sql/persister_cleanup_test.go
@@ -14,6 +14,8 @@ import (
)
func TestPersister_Cleanup(t *testing.T) {
+ t.Parallel()
+
_, reg := internal.NewFastRegistryWithMocks(t)
p := reg.Persister()
ctx := context.Background()
@@ -29,6 +31,8 @@ func TestPersister_Cleanup(t *testing.T) {
}
func TestPersister_Continuity_Cleanup(t *testing.T) {
+ t.Parallel()
+
_, reg := internal.NewFastRegistryWithMocks(t)
p := reg.Persister()
currentTime := time.Now()
@@ -45,6 +49,8 @@ func TestPersister_Continuity_Cleanup(t *testing.T) {
}
func TestPersister_Login_Cleanup(t *testing.T) {
+ t.Parallel()
+
_, reg := internal.NewFastRegistryWithMocks(t)
p := reg.Persister()
currentTime := time.Now()
@@ -61,6 +67,8 @@ func TestPersister_Login_Cleanup(t *testing.T) {
}
func TestPersister_Recovery_Cleanup(t *testing.T) {
+ t.Parallel()
+
_, reg := internal.NewFastRegistryWithMocks(t)
p := reg.Persister()
currentTime := time.Now()
@@ -77,6 +85,8 @@ func TestPersister_Recovery_Cleanup(t *testing.T) {
}
func TestPersister_Registration_Cleanup(t *testing.T) {
+ t.Parallel()
+
_, reg := internal.NewFastRegistryWithMocks(t)
p := reg.Persister()
currentTime := time.Now()
@@ -93,6 +103,8 @@ func TestPersister_Registration_Cleanup(t *testing.T) {
}
func TestPersister_Session_Cleanup(t *testing.T) {
+ t.Parallel()
+
_, reg := internal.NewFastRegistryWithMocks(t)
p := reg.Persister()
currentTime := time.Now()
@@ -109,6 +121,8 @@ func TestPersister_Session_Cleanup(t *testing.T) {
}
func TestPersister_Settings_Cleanup(t *testing.T) {
+ t.Parallel()
+
_, reg := internal.NewFastRegistryWithMocks(t)
p := reg.Persister()
currentTime := time.Now()
@@ -125,6 +139,8 @@ func TestPersister_Settings_Cleanup(t *testing.T) {
}
func TestPersister_Verification_Cleanup(t *testing.T) {
+ t.Parallel()
+
_, reg := internal.NewFastRegistryWithMocks(t)
p := reg.Persister()
currentTime := time.Now()
@@ -141,6 +157,8 @@ func TestPersister_Verification_Cleanup(t *testing.T) {
}
func TestPersister_SessionTokenExchange_Cleanup(t *testing.T) {
+ t.Parallel()
+
_, reg := internal.NewFastRegistryWithMocks(t)
p := reg.Persister()
currentTime := time.Now()
diff --git a/persistence/sql/persister_code.go b/persistence/sql/persister_code.go
index 31e0b80dc2d2..ece7dea75ec3 100644
--- a/persistence/sql/persister_code.go
+++ b/persistence/sql/persister_code.go
@@ -94,7 +94,7 @@ func useOneTimeCode[P any, U interface {
secrets:
for _, secret := range p.r.Config().SecretsSession(ctx) {
- suppliedCode := []byte(p.hmacValueWithSecret(ctx, userProvidedCode, secret))
+ suppliedCode := []byte(hmacValueWithSecret(ctx, userProvidedCode, secret))
for i := range codes {
c := codes[i]
if subtle.ConstantTimeCompare([]byte(c.GetHMACCode()), suppliedCode) == 0 {
diff --git a/persistence/sql/persister_errorx.go b/persistence/sql/persister_errorx.go
index 15faf9fd163b..fc656074f0fc 100644
--- a/persistence/sql/persister_errorx.go
+++ b/persistence/sql/persister_errorx.go
@@ -4,18 +4,17 @@
package sql
import (
- "bytes"
"context"
"encoding/json"
"time"
+ "github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"go.opentelemetry.io/otel/attribute"
- "github.com/ory/jsonschema/v3"
-
"github.com/ory/herodot"
+ "github.com/ory/jsonschema/v3"
"github.com/ory/x/otelx"
"github.com/ory/x/sqlcon"
@@ -28,7 +27,7 @@ func (p *Persister) CreateErrorContainer(ctx context.Context, csrfToken string,
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.CreateErrorContainer")
defer otelx.End(span, &err)
- message, err := p.encodeSelfServiceErrors(ctx, errs)
+ message, err := encodeSelfServiceErrors(errs)
if err != nil {
return uuid.Nil, err
}
@@ -55,14 +54,19 @@ func (p *Persister) ReadErrorContainer(ctx context.Context, id uuid.UUID) (_ *er
defer otelx.End(span, &err)
var ec errorx.ErrorContainer
- if err := p.GetConnection(ctx).Where("id = ? AND nid = ?", id, p.NetworkID(ctx)).First(&ec); err != nil {
- return nil, sqlcon.HandleError(err)
- }
-
- if err := p.GetConnection(ctx).RawQuery(
- "UPDATE selfservice_errors SET was_seen = true, seen_at = ? WHERE id = ? AND nid = ?",
- time.Now().UTC(), id, p.NetworkID(ctx)).Exec(); err != nil {
- return nil, sqlcon.HandleError(err)
+ if err := p.Transaction(ctx, func(ctx context.Context, c *pop.Connection) error {
+ if err := c.Where("id = ? AND nid = ?", id, p.NetworkID(ctx)).First(&ec); err != nil {
+ return sqlcon.HandleError(err)
+ }
+
+ if err := c.RawQuery(
+ "UPDATE selfservice_errors SET was_seen = true, seen_at = ? WHERE id = ? AND nid = ?",
+ time.Now().UTC(), id, p.NetworkID(ctx)).Exec(); err != nil {
+ return sqlcon.HandleError(err)
+ }
+ return nil
+ }); err != nil {
+ return nil, err
}
return &ec, nil
@@ -85,7 +89,7 @@ func (p *Persister) ClearErrorContainers(ctx context.Context, olderThan time.Dur
return sqlcon.HandleError(err)
}
-func (p *Persister) encodeSelfServiceErrors(ctx context.Context, e error) ([]byte, error) {
+func encodeSelfServiceErrors(e error) ([]byte, error) {
if e == nil {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithDebug("A nil error was passed to the error manager which is most likely a code bug."))
}
@@ -98,10 +102,10 @@ func (p *Persister) encodeSelfServiceErrors(ctx context.Context, e error) ([]byt
e = herodot.ToDefaultError(e, "")
}
- var b bytes.Buffer
- if err := json.NewEncoder(&b).Encode(e); err != nil {
+ enc, err := json.Marshal(e)
+ if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to encode error messages.").WithDebug(err.Error()))
}
- return b.Bytes(), nil
+ return enc, nil
}
diff --git a/persistence/sql/persister_hmac.go b/persistence/sql/persister_hmac.go
index 9c4d6636f14c..8fdb04df3dd6 100644
--- a/persistence/sql/persister_hmac.go
+++ b/persistence/sql/persister_hmac.go
@@ -7,27 +7,19 @@ import (
"context"
"crypto/hmac"
"crypto/sha512"
- "crypto/subtle"
"fmt"
+
+ "go.opentelemetry.io/otel/trace"
)
func (p *Persister) hmacValue(ctx context.Context, value string) string {
- return p.hmacValueWithSecret(ctx, value, p.r.Config().SecretsSession(ctx)[0])
+ return hmacValueWithSecret(ctx, value, p.r.Config().SecretsSession(ctx)[0])
}
-func (p *Persister) hmacValueWithSecret(ctx context.Context, value string, secret []byte) string {
- _, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.hmacValueWithSecret")
+func hmacValueWithSecret(ctx context.Context, value string, secret []byte) string {
+ _, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, "persistence.sql.hmacValueWithSecret")
defer span.End()
h := hmac.New(sha512.New512_256, secret)
_, _ = h.Write([]byte(value))
return fmt.Sprintf("%x", h.Sum(nil))
}
-
-func (p *Persister) hmacConstantCompare(ctx context.Context, value, hash string) bool {
- for _, secret := range p.r.Config().SecretsSession(ctx) {
- if subtle.ConstantTimeCompare([]byte(p.hmacValueWithSecret(ctx, value, secret)), []byte(hash)) == 1 {
- return true
- }
- }
- return false
-}
diff --git a/persistence/sql/persister_hmac_test.go b/persistence/sql/persister_hmac_test.go
index c7adcdce3a1e..fa1d6e479308 100644
--- a/persistence/sql/persister_hmac_test.go
+++ b/persistence/sql/persister_hmac_test.go
@@ -8,9 +8,12 @@ import (
"os"
"testing"
- "github.com/ory/x/contextx"
+ confighelpers "github.com/ory/kratos/driver/config/testhelpers"
"github.com/ory/x/configx"
+
+ "github.com/ory/x/contextx"
+
"github.com/ory/x/otelx"
"github.com/gobuffalo/pop/v6"
@@ -49,10 +52,10 @@ func (l *logRegistryOnly) Audit() *logrusx.Logger {
panic("implement me")
}
-func (l *logRegistryOnly) Tracer(ctx context.Context) *otelx.Tracer {
+func (l *logRegistryOnly) Tracer(context.Context) *otelx.Tracer {
return otelx.NewNoop(l.l, new(otelx.Config))
}
-func (l *logRegistryOnly) IdentityTraitsSchemas(ctx context.Context) (schema.IdentitySchemaList, error) {
+func (l *logRegistryOnly) IdentityTraitsSchemas(context.Context) (schema.IdentitySchemaList, error) {
panic("implement me")
}
@@ -63,25 +66,36 @@ func (l *logRegistryOnly) IdentityValidator() *identity.Validator {
var _ persisterDependencies = &logRegistryOnly{}
func TestPersisterHMAC(t *testing.T) {
+ t.Parallel()
+
ctx := context.Background()
- conf := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.SkipValidation())
- conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"foobarbaz"})
+ baseSecret := "foobarbaz"
+ baseSecretBytes := []byte(baseSecret)
+ opts := []configx.OptionModifier{configx.SkipValidation(), configx.WithValue(config.ViperKeySecretsDefault, []string{baseSecret})}
+ conf := config.MustNew(t, logrusx.New("", ""), os.Stderr, &confighelpers.TestConfigProvider{Contextualizer: &contextx.Default{}, Options: opts}, opts...)
c, err := pop.NewConnection(&pop.ConnectionDetails{URL: "sqlite://foo?mode=memory"})
require.NoError(t, err)
- p, err := NewPersister(context.Background(), &logRegistryOnly{c: conf}, c)
+ p, err := NewPersister(ctx, &logRegistryOnly{c: conf}, c)
require.NoError(t, err)
- assert.True(t, p.hmacConstantCompare(context.Background(), "hashme", p.hmacValue(context.Background(), "hashme")))
- assert.False(t, p.hmacConstantCompare(context.Background(), "notme", p.hmacValue(context.Background(), "hashme")))
- assert.False(t, p.hmacConstantCompare(context.Background(), "hashme", p.hmacValue(context.Background(), "notme")))
-
- hash := p.hmacValue(context.Background(), "hashme")
- conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"notfoobarbaz"})
- assert.False(t, p.hmacConstantCompare(context.Background(), "hashme", hash))
- assert.True(t, p.hmacConstantCompare(context.Background(), "hashme", p.hmacValue(context.Background(), "hashme")))
-
- conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"notfoobarbaz", "foobarbaz"})
- assert.True(t, p.hmacConstantCompare(context.Background(), "hashme", hash))
- assert.True(t, p.hmacConstantCompare(context.Background(), "hashme", p.hmacValue(context.Background(), "hashme")))
- assert.NotEqual(t, hash, p.hmacValue(context.Background(), "hashme"))
+ t.Run("case=behaves deterministically", func(t *testing.T) {
+ assert.Equal(t, hmacValueWithSecret(ctx, "hashme", baseSecretBytes), p.hmacValue(ctx, "hashme"))
+ assert.NotEqual(t, hmacValueWithSecret(ctx, "notme", baseSecretBytes), p.hmacValue(ctx, "hashme"))
+ assert.NotEqual(t, hmacValueWithSecret(ctx, "hashme", baseSecretBytes), p.hmacValue(ctx, "notme"))
+ })
+
+ hash := p.hmacValue(ctx, "hashme")
+ newSecret := "not" + baseSecret
+
+ t.Run("case=with only new sectet", func(t *testing.T) {
+ ctx = confighelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret})
+ assert.NotEqual(t, hmacValueWithSecret(ctx, "hashme", baseSecretBytes), p.hmacValue(ctx, "hashme"))
+ assert.Equal(t, hmacValueWithSecret(ctx, "hashme", []byte(newSecret)), p.hmacValue(ctx, "hashme"))
+ })
+
+ t.Run("case=with new and old secret", func(t *testing.T) {
+ ctx = confighelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{newSecret, baseSecret})
+ assert.Equal(t, hmacValueWithSecret(ctx, "hashme", []byte(newSecret)), p.hmacValue(ctx, "hashme"))
+ assert.NotEqual(t, hash, p.hmacValue(ctx, "hashme"))
+ })
}
diff --git a/persistence/sql/persister_recovery.go b/persistence/sql/persister_recovery.go
index 468ba5a2b144..bb23d3fd319e 100644
--- a/persistence/sql/persister_recovery.go
+++ b/persistence/sql/persister_recovery.go
@@ -82,7 +82,7 @@ func (p *Persister) UseRecoveryToken(ctx context.Context, fID uuid.UUID, token s
nid := p.NetworkID(ctx)
if err := sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) {
for _, secret := range p.r.Config().SecretsSession(ctx) {
- if err = tx.Where("token = ? AND nid = ? AND NOT used AND selfservice_recovery_flow_id = ?", p.hmacValueWithSecret(ctx, token, secret), nid, fID).First(&rt); err != nil {
+ if err = tx.Where("token = ? AND nid = ? AND NOT used AND selfservice_recovery_flow_id = ?", hmacValueWithSecret(ctx, token, secret), nid, fID).First(&rt); err != nil {
if !errors.Is(sqlcon.HandleError(err), sqlcon.ErrNoRows) {
return err
}
diff --git a/persistence/sql/persister_session.go b/persistence/sql/persister_session.go
index 412ed2d8a825..37fccca0bd84 100644
--- a/persistence/sql/persister_session.go
+++ b/persistence/sql/persister_session.go
@@ -10,6 +10,7 @@ import (
"github.com/ory/herodot"
"github.com/ory/x/dbal"
+ "github.com/ory/x/pointerx"
"github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid"
@@ -286,10 +287,10 @@ func (p *Persister) UpsertSession(ctx context.Context, s *session.Session) (err
device.NID = s.NID
if device.Location != nil {
- device.Location = stringsx.GetPointer(stringsx.TruncateByteLen(*device.Location, SessionDeviceLocationMaxLength))
+ device.Location = pointerx.Ptr(stringsx.TruncateByteLen(*device.Location, SessionDeviceLocationMaxLength))
}
if device.UserAgent != nil {
- device.UserAgent = stringsx.GetPointer(stringsx.TruncateByteLen(*device.UserAgent, SessionDeviceUserAgentMaxLength))
+ device.UserAgent = pointerx.Ptr(stringsx.TruncateByteLen(*device.UserAgent, SessionDeviceUserAgentMaxLength))
}
if err := p.DevicePersister.CreateDevice(ctx, device); err != nil {
diff --git a/persistence/sql/persister_test.go b/persistence/sql/persister_test.go
index f88d7380a5c7..6a48e763d0f3 100644
--- a/persistence/sql/persister_test.go
+++ b/persistence/sql/persister_test.go
@@ -94,7 +94,7 @@ func pl(t testing.TB) func(lvl logging.Level, s string, args ...interface{}) {
func createCleanDatabases(t testing.TB) map[string]*driver.RegistryDefault {
conns := map[string]string{
- "sqlite": "sqlite://file:" + t.TempDir() + "/db.sqlite?_fk=true",
+ "sqlite": "sqlite://file:" + t.TempDir() + "/db.sqlite?_fk=true&max_conns=1&lock=false",
}
var l sync.Mutex
@@ -160,111 +160,104 @@ func createCleanDatabases(t testing.TB) map[string]*driver.RegistryDefault {
}
func TestPersister(t *testing.T) {
+ t.Parallel()
+
conns := createCleanDatabases(t)
- ctx := context.Background()
+ ctx := testhelpers.WithDefaultIdentitySchema(context.Background(), "file://./stub/identity.schema.json")
- for name := range conns {
- name := name
- reg := conns[name]
+ for name, reg := range conns {
t.Run(fmt.Sprintf("database=%s", name), func(t *testing.T) {
t.Parallel()
_, p := testhelpers.NewNetwork(t, ctx, reg.Persister())
- conf := reg.Config()
- t.Logf("DSN: %s", conf.DSN(ctx))
+ t.Logf("DSN: %s", reg.Config().DSN(ctx))
- // This test must remain the first test in the test suite!
t.Run("racy identity creation", func(t *testing.T) {
- defaultSchema := schema.Schema{
- ID: config.DefaultIdentityTraitsSchemaID,
- URL: urlx.ParseOrPanic("file://./stub/identity.schema.json"),
- RawURL: "file://./stub/identity.schema.json",
- }
+ t.Parallel()
var wg sync.WaitGroup
- testhelpers.SetDefaultIdentitySchema(reg.Config(), defaultSchema.RawURL)
+
_, ps := testhelpers.NewNetwork(t, ctx, reg.Persister())
- for i := 0; i < 10; i++ {
+ for i := range 10 {
wg.Add(1)
- // capture i
- ii := i
go func() {
defer wg.Done()
id := ri.NewIdentity("")
id.SetCredentials(ri.CredentialsTypePassword, ri.Credentials{
Type: ri.CredentialsTypePassword,
- Identifiers: []string{fmt.Sprintf("racy identity %d", ii)},
+ Identifiers: []string{fmt.Sprintf("racy identity %d", i)},
Config: sqlxx.JSONRawMessage(`{"foo":"bar"}`),
})
id.Traits = ri.Traits("{}")
- require.NoError(t, ps.CreateIdentity(context.Background(), id))
+ require.NoError(t, ps.CreateIdentity(ctx, id))
}()
}
wg.Wait()
})
- t.Run("case=credentials types", func(t *testing.T) {
+ t.Run("case=credential types exist", func(t *testing.T) {
+ t.Parallel()
for _, ct := range []ri.CredentialsType{ri.CredentialsTypeOIDC, ri.CredentialsTypePassword} {
require.NoError(t, p.(*sql.Persister).Connection(context.Background()).Where("name = ?", ct).First(&ri.CredentialsTypeTable{}))
}
})
t.Run("contract=identity.TestPool", func(t *testing.T) {
- pop.SetLogger(pl(t))
- identity.TestPool(ctx, conf, p, reg.IdentityManager(), name)(t)
+ t.Parallel()
+ identity.TestPool(ctx, p, reg.IdentityManager(), name)(t)
})
t.Run("contract=registration.TestFlowPersister", func(t *testing.T) {
- pop.SetLogger(pl(t))
+ t.Parallel()
registration.TestFlowPersister(ctx, p)(t)
})
t.Run("contract=errorx.TestPersister", func(t *testing.T) {
- pop.SetLogger(pl(t))
+ t.Parallel()
errorx.TestPersister(ctx, p)(t)
})
t.Run("contract=login.TestFlowPersister", func(t *testing.T) {
- pop.SetLogger(pl(t))
+ t.Parallel()
login.TestFlowPersister(ctx, p)(t)
})
t.Run("contract=settings.TestFlowPersister", func(t *testing.T) {
- pop.SetLogger(pl(t))
- settings.TestFlowPersister(ctx, conf, p)(t)
+ t.Parallel()
+ settings.TestFlowPersister(ctx, p)(t)
})
t.Run("contract=session.TestPersister", func(t *testing.T) {
- pop.SetLogger(pl(t))
- session.TestPersister(ctx, conf, p)(t)
+ t.Parallel()
+ session.TestPersister(ctx, reg.Config(), p)(t)
})
t.Run("contract=sessiontokenexchange.TestPersister", func(t *testing.T) {
- pop.SetLogger(pl(t))
- sessiontokenexchange.TestPersister(ctx, conf, p)(t)
+ t.Parallel()
+ sessiontokenexchange.TestPersister(ctx, p)(t)
})
t.Run("contract=courier.TestPersister", func(t *testing.T) {
- pop.SetLogger(pl(t))
+ t.Parallel()
upsert, insert := sqltesthelpers.DefaultNetworkWrapper(p)
courier.TestPersister(ctx, upsert, insert)(t)
})
t.Run("contract=verification.TestFlowPersister", func(t *testing.T) {
- pop.SetLogger(pl(t))
- verification.TestFlowPersister(ctx, conf, p)(t)
+ t.Parallel()
+ verification.TestFlowPersister(ctx, p)(t)
})
t.Run("contract=recovery.TestFlowPersister", func(t *testing.T) {
- pop.SetLogger(pl(t))
- recovery.TestFlowPersister(ctx, conf, p)(t)
+ t.Parallel()
+ recovery.TestFlowPersister(ctx, p)(t)
})
t.Run("contract=link.TestPersister", func(t *testing.T) {
- pop.SetLogger(pl(t))
- link.TestPersister(ctx, conf, p)(t)
+ t.Parallel()
+ link.TestPersister(ctx, p)(t)
})
t.Run("contract=code.TestPersister", func(t *testing.T) {
- pop.SetLogger(pl(t))
- code.TestPersister(ctx, conf, p)(t)
+ t.Parallel()
+ code.TestPersister(ctx, p)(t)
})
t.Run("contract=continuity.TestPersister", func(t *testing.T) {
- pop.SetLogger(pl(t))
+ t.Parallel()
continuity.TestPersister(ctx, p)(t)
})
})
@@ -283,6 +276,8 @@ func getErr(args ...interface{}) error {
}
func TestPersister_Transaction(t *testing.T) {
+ t.Parallel()
+
_, reg := internal.NewFastRegistryWithMocks(t)
p := reg.Persister()
diff --git a/persistence/sql/persister_verification.go b/persistence/sql/persister_verification.go
index 8d983ed1635d..7feae0592ae7 100644
--- a/persistence/sql/persister_verification.go
+++ b/persistence/sql/persister_verification.go
@@ -82,7 +82,7 @@ func (p *Persister) UseVerificationToken(ctx context.Context, fID uuid.UUID, tok
nid := p.NetworkID(ctx)
if err := sqlcon.HandleError(p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) (err error) {
for _, secret := range p.r.Config().SecretsSession(ctx) {
- if err = tx.Where("token = ? AND nid = ? AND NOT used AND selfservice_verification_flow_id = ?", p.hmacValueWithSecret(ctx, token, secret), nid, fID).First(&rt); err != nil {
+ if err = tx.Where("token = ? AND nid = ? AND NOT used AND selfservice_verification_flow_id = ?", hmacValueWithSecret(ctx, token, secret), nid, fID).First(&rt); err != nil {
if !errors.Is(sqlcon.HandleError(err), sqlcon.ErrNoRows) {
return err
}
diff --git a/schema/errors.go b/schema/errors.go
index 30c1f72976f3..6ff52a5047c2 100644
--- a/schema/errors.go
+++ b/schema/errors.go
@@ -117,12 +117,21 @@ func NewInvalidCredentialsError() error {
ValidationError: &jsonschema.ValidationError{
Message: `the provided credentials are invalid, check for spelling mistakes in your password or username, email address, or phone number`,
InstancePtr: "#/",
- Context: &ValidationErrorContextPasswordPolicyViolation{},
},
Messages: new(text.Messages).Add(text.NewErrorValidationInvalidCredentials()),
})
}
+func NewAccountNotFoundError() error {
+ return errors.WithStack(&ValidationError{
+ ValidationError: &jsonschema.ValidationError{
+ Message: "this account does not exist or has no login method configured",
+ InstancePtr: "#/identifier",
+ },
+ Messages: new(text.Messages).Add(text.NewErrorValidationAccountNotFound()),
+ })
+}
+
type ValidationErrorContextDuplicateCredentialsError struct {
AvailableCredentials []string `json:"available_credential_types"`
AvailableOIDCProviders []string `json:"available_oidc_providers"`
diff --git a/schema/handler.go b/schema/handler.go
index ff06bc8c43ef..fe2842b14a56 100644
--- a/schema/handler.go
+++ b/schema/handler.go
@@ -13,18 +13,14 @@ import (
"os"
"strings"
- "github.com/ory/x/otelx"
-
- "github.com/ory/x/pagination/migrationpagination"
-
- "github.com/ory/kratos/driver/config"
-
"github.com/julienschmidt/httprouter"
"github.com/pkg/errors"
"github.com/ory/herodot"
-
+ "github.com/ory/kratos/driver/config"
"github.com/ory/kratos/x"
+ "github.com/ory/x/otelx"
+ "github.com/ory/x/pagination/migrationpagination"
)
type (
diff --git a/schema/handler_test.go b/schema/handler_test.go
index a5bdf06a5a83..615d8092269d 100644
--- a/schema/handler_test.go
+++ b/schema/handler_test.go
@@ -15,13 +15,11 @@ import (
"strings"
"testing"
- "github.com/ory/client-go"
-
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/ory/client-go"
_ "github.com/ory/jsonschema/v3/fileloader"
-
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/internal"
"github.com/ory/kratos/schema"
diff --git a/selfservice/flow/continue_with.go b/selfservice/flow/continue_with.go
index 7a5f9ce22410..bac63d72a273 100644
--- a/selfservice/flow/continue_with.go
+++ b/selfservice/flow/continue_with.go
@@ -89,6 +89,8 @@ type ContinueWithVerificationUIFlow struct {
// The URL of the verification flow
//
+ // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
+ //
// required: false
URL string `json:"url,omitempty"`
}
@@ -134,6 +136,7 @@ type ContinueWithSettingsUI struct {
//
// required: true
Action ContinueWithActionShowSettingsUI `json:"action"`
+
// Flow contains the ID of the verification flow
//
// required: true
@@ -146,13 +149,21 @@ type ContinueWithSettingsUIFlow struct {
//
// required: true
ID uuid.UUID `json:"id"`
+
+ // The URL of the settings flow
+ //
+ // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
+ //
+ // required: false
+ URL string `json:"url,omitempty"`
}
-func NewContinueWithSettingsUI(f Flow) *ContinueWithSettingsUI {
+func NewContinueWithSettingsUI(f Flow, redirectTo string) *ContinueWithSettingsUI {
return &ContinueWithSettingsUI{
Action: ContinueWithActionShowSettingsUIString,
Flow: ContinueWithSettingsUIFlow{
- ID: f.GetID(),
+ ID: f.GetID(),
+ URL: redirectTo,
},
}
}
@@ -188,6 +199,8 @@ type ContinueWithRecoveryUIFlow struct {
// The URL of the recovery flow
//
+ // If this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.
+ //
// required: false
URL string `json:"url,omitempty"`
}
@@ -201,6 +214,36 @@ func NewContinueWithRecoveryUI(f Flow) *ContinueWithRecoveryUI {
}
}
+// swagger:enum ContinueWithActionRedirectBrowserTo
+type ContinueWithActionRedirectBrowserTo string
+
+// #nosec G101 -- only a key constant
+const (
+ ContinueWithActionRedirectBrowserToString ContinueWithActionRedirectBrowserTo = "redirect_browser_to"
+)
+
+// Indicates, that the UI flow could be continued by showing a recovery ui
+//
+// swagger:model continueWithRedirectBrowserTo
+type ContinueWithRedirectBrowserTo struct {
+ // Action will always be `redirect_browser_to`
+ //
+ // required: true
+ Action ContinueWithActionRedirectBrowserTo `json:"action"`
+
+ // The URL to redirect the browser to
+ //
+ // required: true
+ RedirectTo string `json:"redirect_browser_to"`
+}
+
+func NewContinueWithRedirectBrowserTo(redirectTo string) *ContinueWithRedirectBrowserTo {
+ return &ContinueWithRedirectBrowserTo{
+ Action: ContinueWithActionRedirectBrowserToString,
+ RedirectTo: redirectTo,
+ }
+}
+
func ErrorWithContinueWith(err *herodot.DefaultError, continueWith ...ContinueWith) *herodot.DefaultError {
if err.DetailsField == nil {
err.DetailsField = map[string]interface{}{}
diff --git a/selfservice/flow/login/error.go b/selfservice/flow/login/error.go
index 94311b92a815..e7da5da2ca81 100644
--- a/selfservice/flow/login/error.go
+++ b/selfservice/flow/login/error.go
@@ -79,10 +79,12 @@ func (s *ErrorHandler) PrepareReplacementForExpiredFlow(w http.ResponseWriter, r
}
func (s *ErrorHandler) WriteFlowError(w http.ResponseWriter, r *http.Request, f *Flow, group node.UiNodeGroup, err error) {
- s.d.Audit().
+ logger := s.d.Audit().
WithError(err).
WithRequest(r).
- WithField("login_flow", f).
+ WithField("login_flow", f.ToLoggerField())
+
+ logger.
Info("Encountered self-service login error.")
if f == nil {
diff --git a/selfservice/flow/login/error_test.go b/selfservice/flow/login/error_test.go
index 5cc78c35bda1..e53f831e3891 100644
--- a/selfservice/flow/login/error_test.go
+++ b/selfservice/flow/login/error_test.go
@@ -74,7 +74,12 @@ func TestHandleError(t *testing.T) {
require.NoError(t, err)
for _, s := range reg.LoginStrategies(context.Background()) {
- require.NoError(t, s.PopulateLoginMethod(req, identity.AuthenticatorAssuranceLevel1, f))
+ switch s.(type) {
+ case login.UnifiedFormHydrator:
+ require.NoError(t, s.(login.UnifiedFormHydrator).PopulateLoginMethod(req, identity.AuthenticatorAssuranceLevel1, f))
+ case login.FormHydrator:
+ require.NoError(t, s.(login.FormHydrator).PopulateLoginMethodFirstFactor(req, f))
+ }
}
require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), f))
diff --git a/selfservice/flow/login/flow.go b/selfservice/flow/login/flow.go
index a01d449a2751..a69732b19a9e 100644
--- a/selfservice/flow/login/flow.go
+++ b/selfservice/flow/login/flow.go
@@ -230,9 +230,9 @@ func (f Flow) GetID() uuid.UUID {
return f.ID
}
-// IsForced returns true if the login flow was triggered to re-authenticate the user.
+// IsRefresh returns true if the login flow was triggered to re-authenticate the user.
// This is the case if the refresh query parameter is set to true.
-func (f *Flow) IsForced() bool {
+func (f *Flow) IsRefresh() bool {
return f.Refresh
}
@@ -327,3 +327,20 @@ func (f *Flow) ContinueWith() []flow.ContinueWith {
func (f *Flow) SetReturnToVerification(to string) {
f.ReturnToVerification = to
}
+
+func (f *Flow) ToLoggerField() map[string]interface{} {
+ if f == nil {
+ return map[string]interface{}{}
+ }
+ return map[string]interface{}{
+ "id": f.ID.String(),
+ "return_to": f.ReturnTo,
+ "request_url": f.RequestURL,
+ "active": f.Active,
+ "type": f.Type,
+ "nid": f.NID,
+ "state": f.State,
+ "refresh": f.Refresh,
+ "requested_aal": f.RequestedAAL,
+ }
+}
diff --git a/selfservice/flow/login/handler.go b/selfservice/flow/login/handler.go
index 88b3712602a0..63c38731ef19 100644
--- a/selfservice/flow/login/handler.go
+++ b/selfservice/flow/login/handler.go
@@ -212,8 +212,39 @@ preLoginHook:
}
for _, s := range h.d.LoginStrategies(r.Context(), strategyFilters...) {
- if err := s.PopulateLoginMethod(r, f.RequestedAAL, f); err != nil {
- return nil, nil, err
+ var populateErr error
+
+ switch strategy := s.(type) {
+ case FormHydrator:
+ switch {
+ case f.RequestedAAL == identity.AuthenticatorAssuranceLevel1:
+ switch {
+ case f.IsRefresh():
+ // Refreshing takes precedence over identifier_first auth which can not be a refresh flow.
+ // Therefor this comes first.
+ populateErr = strategy.PopulateLoginMethodFirstFactorRefresh(r, f)
+ case h.d.Config().SelfServiceLoginFlowIdentifierFirstEnabled(r.Context()):
+ populateErr = strategy.PopulateLoginMethodIdentifierFirstIdentification(r, f)
+ default:
+ populateErr = strategy.PopulateLoginMethodFirstFactor(r, f)
+ }
+ case f.RequestedAAL == identity.AuthenticatorAssuranceLevel2:
+ switch {
+ case f.IsRefresh():
+ // Refresh takes precedence.
+ populateErr = strategy.PopulateLoginMethodSecondFactorRefresh(r, f)
+ default:
+ populateErr = strategy.PopulateLoginMethodSecondFactor(r, f)
+ }
+ }
+ case UnifiedFormHydrator:
+ populateErr = strategy.PopulateLoginMethod(r, f.RequestedAAL, f)
+ default:
+ populateErr = errors.WithStack(x.PseudoPanic.WithReasonf("A login strategy was expected to implement one of the interfaces UnifiedFormHydrator or FormHydrator but did not."))
+ }
+
+ if populateErr != nil {
+ return nil, nil, populateErr
}
}
diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go
index f0e06ccfc934..a94b3291f4a3 100644
--- a/selfservice/flow/login/hook.go
+++ b/selfservice/flow/login/hook.go
@@ -159,6 +159,10 @@ func (e *HookExecutor) PostLoginHook(
"redirect_reason": "login successful",
})...)
+ if f.Type == flow.TypeBrowser && x.IsJSONRequest(r) {
+ f.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String()))
+ }
+
classified := s
s = s.Declassified()
diff --git a/selfservice/flow/login/hook_test.go b/selfservice/flow/login/hook_test.go
index fe73f22d7eef..7d7f8e158174 100644
--- a/selfservice/flow/login/hook_test.go
+++ b/selfservice/flow/login/hook_test.go
@@ -93,6 +93,17 @@ func TestLoginExecutor(t *testing.T) {
assert.EqualValues(t, "https://www.ory.sh/", res.Request.URL.String())
})
+ t.Run("case=pass without hooks if client is ajax", func(t *testing.T) {
+ t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
+
+ ts := newServer(t, flow.TypeBrowser, nil)
+ res, body := makeRequestPost(t, ts, true, url.Values{})
+ assert.EqualValues(t, http.StatusOK, res.StatusCode)
+ assert.Contains(t, res.Request.URL.String(), ts.URL)
+ assert.EqualValues(t, gjson.Get(body, "continue_with").Raw, `[{"action":"redirect_browser_to","redirect_browser_to":"https://www.ory.sh/"}]`)
+ t.Logf("%s", body)
+ })
+
t.Run("case=pass if hooks pass", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
viperSetPost(t, conf, strategy.String(), []config.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}})
@@ -286,6 +297,7 @@ func TestLoginExecutor(t *testing.T) {
})
})
})
+
t.Run("case=maybe links credential", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
diff --git a/selfservice/flow/login/strategy.go b/selfservice/flow/login/strategy.go
index c70ad9cc8684..fec71d3beb1d 100644
--- a/selfservice/flow/login/strategy.go
+++ b/selfservice/flow/login/strategy.go
@@ -20,7 +20,6 @@ type Strategy interface {
ID() identity.CredentialsType
NodeGroup() node.UiNodeGroup
RegisterLoginRoutes(*x.RouterPublic)
- PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *Flow) error
Login(w http.ResponseWriter, r *http.Request, f *Flow, sess *session.Session) (i *identity.Identity, err error)
CompletedAuthenticationMethod(ctx context.Context, methods session.AuthenticationMethods) session.AuthenticationMethod
}
diff --git a/selfservice/flow/login/strategy_form_hydrator.go b/selfservice/flow/login/strategy_form_hydrator.go
new file mode 100644
index 000000000000..098c195b5df4
--- /dev/null
+++ b/selfservice/flow/login/strategy_form_hydrator.go
@@ -0,0 +1,67 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package login
+
+import (
+ "net/http"
+
+ "github.com/pkg/errors"
+
+ "github.com/ory/kratos/identity"
+)
+
+type UnifiedFormHydrator interface {
+ PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *Flow) error
+}
+
+type FormHydrator interface {
+ PopulateLoginMethodFirstFactorRefresh(r *http.Request, sr *Flow) error
+ PopulateLoginMethodFirstFactor(r *http.Request, sr *Flow) error
+ PopulateLoginMethodSecondFactor(r *http.Request, sr *Flow) error
+ PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *Flow) error
+
+ // PopulateLoginMethodIdentifierFirstCredentials populates the login form with the first factor credentials.
+ // This method is called when the login flow is set to identifier first. The method will receive information
+ // about the identity that is being used to log in and the identifier that was used to find the identity.
+ //
+ // The method should populate the login form with the credentials of the identity.
+ //
+ // If the method can not find any credentials (because the identity does not exist) idfirst.ErrNoCredentialsFound
+ // must be returned. When returning idfirst.ErrNoCredentialsFound the strategy will appropriately deal with
+ // account enumeration mitigation.
+ //
+ // This method does however need to take appropriate steps to show/hide certain fields depending on the account
+ // enumeration configuration.
+ PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, sr *Flow, options ...FormHydratorModifier) error
+ PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, sr *Flow) error
+}
+
+var ErrBreakLoginPopulate = errors.New("skip rest of login form population")
+
+type FormHydratorOptions struct {
+ IdentityHint *identity.Identity
+ Identifier string
+}
+
+type FormHydratorModifier func(o *FormHydratorOptions)
+
+func WithIdentityHint(i *identity.Identity) FormHydratorModifier {
+ return func(o *FormHydratorOptions) {
+ o.IdentityHint = i
+ }
+}
+
+func WithIdentifier(i string) FormHydratorModifier {
+ return func(o *FormHydratorOptions) {
+ o.Identifier = i
+ }
+}
+
+func NewFormHydratorOptions(modifiers []FormHydratorModifier) *FormHydratorOptions {
+ o := new(FormHydratorOptions)
+ for _, m := range modifiers {
+ m(o)
+ }
+ return o
+}
diff --git a/selfservice/flow/login/strategy_form_hydrator_test.go b/selfservice/flow/login/strategy_form_hydrator_test.go
new file mode 100644
index 000000000000..863a1031051d
--- /dev/null
+++ b/selfservice/flow/login/strategy_form_hydrator_test.go
@@ -0,0 +1,24 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package login
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/ory/kratos/identity"
+)
+
+func TestWithIdentityHint(t *testing.T) {
+ expected := new(identity.Identity)
+ opts := NewFormHydratorOptions([]FormHydratorModifier{WithIdentityHint(expected)})
+ assert.Equal(t, expected, opts.IdentityHint)
+}
+
+func TestWithIdentifier(t *testing.T) {
+ expected := "identifier"
+ opts := NewFormHydratorOptions([]FormHydratorModifier{WithIdentifier(expected)})
+ assert.Equal(t, expected, opts.Identifier)
+}
diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json
index f4c0270da2dc..17eb6e965bcb 100644
--- a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json
+++ b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=api-case=fails_if_active_strategy_is_disabled.json
@@ -50,8 +50,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json
index 56782eed4571..a9ad1e527fb4 100644
--- a/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json
+++ b/selfservice/flow/recovery/.snapshots/TestHandleError-flow=spa-case=fails_if_active_strategy_is_disabled.json
@@ -50,8 +50,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json
index f4c0270da2dc..17eb6e965bcb 100644
--- a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json
+++ b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=api-case=fails_if_active_strategy_is_disabled.json
@@ -50,8 +50,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json
index 56782eed4571..a9ad1e527fb4 100644
--- a/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json
+++ b/selfservice/flow/recovery/.snapshots/TestHandleError_WithContinueWith-flow=spa-case=fails_if_active_strategy_is_disabled.json
@@ -50,8 +50,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/flow/recovery/error.go b/selfservice/flow/recovery/error.go
index 9f93af941447..837bc6e0e4b3 100644
--- a/selfservice/flow/recovery/error.go
+++ b/selfservice/flow/recovery/error.go
@@ -64,10 +64,12 @@ func (s *ErrorHandler) WriteFlowError(
group node.UiNodeGroup,
recoveryErr error,
) {
- s.d.Audit().
+ logger := s.d.Audit().
WithError(recoveryErr).
WithRequest(r).
- WithField("recovery_flow", f).
+ WithField("recovery_flow", f.ToLoggerField())
+
+ logger.
Info("Encountered self-service recovery error.")
if f == nil {
diff --git a/selfservice/flow/recovery/flow.go b/selfservice/flow/recovery/flow.go
index 9eac423266cb..7dc1845a77b6 100644
--- a/selfservice/flow/recovery/flow.go
+++ b/selfservice/flow/recovery/flow.go
@@ -248,3 +248,18 @@ func (f *Flow) SetState(state State) {
func (t *Flow) GetTransientPayload() json.RawMessage {
return t.TransientPayload
}
+
+func (f *Flow) ToLoggerField() map[string]interface{} {
+ if f == nil {
+ return map[string]interface{}{}
+ }
+ return map[string]interface{}{
+ "id": f.ID.String(),
+ "return_to": f.ReturnTo,
+ "request_url": f.RequestURL,
+ "active": f.Active,
+ "type": f.Type,
+ "nid": f.NID,
+ "state": f.State,
+ }
+}
diff --git a/selfservice/flow/recovery/test/persistence.go b/selfservice/flow/recovery/test/persistence.go
index 8bc9efad88e0..75dd6b00c6b2 100644
--- a/selfservice/flow/recovery/test/persistence.go
+++ b/selfservice/flow/recovery/test/persistence.go
@@ -12,7 +12,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/ory/kratos/driver/config"
"github.com/ory/kratos/internal/testhelpers"
"github.com/ory/kratos/persistence"
"github.com/ory/kratos/selfservice/flow"
@@ -23,7 +22,7 @@ import (
"github.com/ory/x/sqlcon"
)
-func TestFlowPersister(ctx context.Context, conf *config.Config, p interface {
+func TestFlowPersister(ctx context.Context, p interface {
persistence.Persister
},
) func(t *testing.T) {
@@ -33,7 +32,6 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface {
return func(t *testing.T) {
nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p)
- testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
t.Run("case=should error when the recovery request does not exist", func(t *testing.T) {
_, err := p.GetRecoveryFlow(ctx, x.NewUUID())
diff --git a/selfservice/flow/registration/error.go b/selfservice/flow/registration/error.go
index b3ef3d9054df..41a15f08b2b1 100644
--- a/selfservice/flow/registration/error.go
+++ b/selfservice/flow/registration/error.go
@@ -72,6 +72,7 @@ func (s *ErrorHandler) PrepareReplacementForExpiredFlow(w http.ResponseWriter, r
return e.WithFlow(a), nil
}
+
func (s *ErrorHandler) WriteFlowError(
w http.ResponseWriter,
r *http.Request,
@@ -79,15 +80,16 @@ func (s *ErrorHandler) WriteFlowError(
group node.UiNodeGroup,
err error,
) {
-
if dup := new(identity.ErrDuplicateCredentials); errors.As(err, &dup) {
err = schema.NewDuplicateCredentialsError(dup)
}
- s.d.Audit().
+ logger := s.d.Audit().
WithError(err).
WithRequest(r).
- WithField("registration_flow", f).
+ WithField("registration_flow", f.ToLoggerField())
+
+ logger.
Info("Encountered self-service flow error.")
if f == nil {
diff --git a/selfservice/flow/registration/flow.go b/selfservice/flow/registration/flow.go
index 17ad0c6d720a..5b39dd76f750 100644
--- a/selfservice/flow/registration/flow.go
+++ b/selfservice/flow/registration/flow.go
@@ -272,10 +272,25 @@ func (f *Flow) SetState(state State) {
f.State = state
}
-func (t *Flow) GetTransientPayload() json.RawMessage {
- return t.TransientPayload
+func (f *Flow) GetTransientPayload() json.RawMessage {
+ return f.TransientPayload
}
func (f *Flow) SetReturnToVerification(to string) {
f.ReturnToVerification = to
}
+
+func (f *Flow) ToLoggerField() map[string]interface{} {
+ if f == nil {
+ return map[string]interface{}{}
+ }
+ return map[string]interface{}{
+ "id": f.ID.String(),
+ "return_to": f.ReturnTo,
+ "request_url": f.RequestURL,
+ "active": f.Active,
+ "Type": f.Type,
+ "nid": f.NID,
+ "state": f.State,
+ }
+}
diff --git a/selfservice/flow/registration/hook.go b/selfservice/flow/registration/hook.go
index 6a997009c1c5..e44be2487bbb 100644
--- a/selfservice/flow/registration/hook.go
+++ b/selfservice/flow/registration/hook.go
@@ -192,12 +192,17 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque
if err != nil {
return err
}
+
span.SetAttributes(otelx.StringAttrs(map[string]string{
"return_to": returnTo.String(),
- "flow_type": string(flow.TypeBrowser),
+ "flow_type": string(registrationFlow.Type),
"redirect_reason": "registration successful",
})...)
+ if registrationFlow.Type == flow.TypeBrowser && x.IsJSONRequest(r) {
+ registrationFlow.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String()))
+ }
+
e.d.Audit().
WithRequest(r).
WithField("identity_id", i.ID).
diff --git a/selfservice/flow/registration/hook_test.go b/selfservice/flow/registration/hook_test.go
index 3761692e3f45..9e60b33f1f52 100644
--- a/selfservice/flow/registration/hook_test.go
+++ b/selfservice/flow/registration/hook_test.go
@@ -91,6 +91,21 @@ func TestRegistrationExecutor(t *testing.T) {
assert.Equal(t, actual.Traits, i.Traits)
})
+ t.Run("case=pass without hooks if ajax client", func(t *testing.T) {
+ t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
+ i := testhelpers.SelfServiceHookFakeIdentity(t)
+
+ ts := newServer(t, i, flow.TypeBrowser)
+ res, body := makeRequestPost(t, ts, true, url.Values{})
+ assert.EqualValues(t, http.StatusOK, res.StatusCode)
+ assert.Contains(t, res.Request.URL.String(), ts.URL)
+ assert.EqualValues(t, gjson.Get(body, "continue_with").Raw, `[{"action":"redirect_browser_to","redirect_browser_to":"https://www.ory.sh/"}]`)
+
+ actual, err := reg.IdentityPool().GetIdentity(context.Background(), i.ID, identity.ExpandNothing)
+ require.NoError(t, err)
+ assert.Equal(t, actual.Traits, i.Traits)
+ })
+
t.Run("case=pass if hooks pass", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
viperSetPost(t, conf, strategy, []config.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}})
diff --git a/selfservice/flow/settings/error.go b/selfservice/flow/settings/error.go
index 2583cd9dd526..aaf6076de0be 100644
--- a/selfservice/flow/settings/error.go
+++ b/selfservice/flow/settings/error.go
@@ -140,11 +140,12 @@ func (s *ErrorHandler) WriteFlowError(
id *identity.Identity,
err error,
) {
- s.d.Audit().
+ logger := s.d.Audit().
WithError(err).
WithRequest(r).
- WithField("settings_flow", f).
- Info("Encountered self-service settings error.")
+ WithField("settings_flow", f.ToLoggerField())
+
+ logger.Info("Encountered self-service settings error.")
shouldRespondWithJSON := x.IsJSONRequest(r)
if f != nil && f.Type == flow.TypeAPI {
diff --git a/selfservice/flow/settings/error_test.go b/selfservice/flow/settings/error_test.go
index 5776cd2b6942..5cdebc400df6 100644
--- a/selfservice/flow/settings/error_test.go
+++ b/selfservice/flow/settings/error_test.go
@@ -153,7 +153,7 @@ func TestHandleError(t *testing.T) {
// This needs an authenticated client in order to call the RouteGetFlow endpoint
s, err := session.NewActiveSession(req, &id, testhelpers.NewSessionLifespanProvider(time.Hour), time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1)
require.NoError(t, err)
- c := testhelpers.NewHTTPClientWithSessionToken(t, reg, s)
+ c := testhelpers.NewHTTPClientWithSessionToken(t, ctx, reg, s)
settingsFlow = newFlow(t, time.Minute, tc.t)
flowError = flow.NewFlowExpiredError(expiredAnHourAgo)
diff --git a/selfservice/flow/settings/flow.go b/selfservice/flow/settings/flow.go
index 7b0c14bc347e..b32b4676effb 100644
--- a/selfservice/flow/settings/flow.go
+++ b/selfservice/flow/settings/flow.go
@@ -265,3 +265,18 @@ func (f *Flow) SetState(state State) {
func (t *Flow) GetTransientPayload() json.RawMessage {
return t.TransientPayload
}
+
+func (f *Flow) ToLoggerField() map[string]interface{} {
+ if f == nil {
+ return map[string]interface{}{}
+ }
+ return map[string]interface{}{
+ "id": f.ID.String(),
+ "return_to": f.ReturnTo,
+ "request_url": f.RequestURL,
+ "active": f.Active,
+ "Type": f.Type,
+ "nid": f.NID,
+ "state": f.State,
+ }
+}
diff --git a/selfservice/flow/settings/handler_test.go b/selfservice/flow/settings/handler_test.go
index 35d34fd735fc..584a7f3e5535 100644
--- a/selfservice/flow/settings/handler_test.go
+++ b/selfservice/flow/settings/handler_test.go
@@ -67,8 +67,8 @@ func TestHandler(t *testing.T) {
primaryIdentity := &identity.Identity{ID: x.NewUUID(), Traits: identity.Traits(`{}`)}
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), primaryIdentity))
- primaryUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, primaryIdentity)
- otherUser := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
+ primaryUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, primaryIdentity)
+ otherUser := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
newExpiredFlow := func() *settings.Flow {
f, err := settings.NewFlow(conf, -time.Minute,
@@ -133,7 +133,7 @@ func TestHandler(t *testing.T) {
return initAuthenticatedFlow(t, hc, false, true, opts...)
}
- aal2Identity := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, &identity.Identity{
+ aal2Identity := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, &identity.Identity{
State: identity.StateActive,
Traits: []byte(`{"email":"foo@bar"}`),
Credentials: map[identity.CredentialsType]identity.Credentials{
@@ -151,7 +151,7 @@ func TestHandler(t *testing.T) {
})
t.Run("description=success", func(t *testing.T) {
- user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg)
+ user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg)
res, body := initFlow(t, user1, true)
assert.Contains(t, res.Request.URL.String(), settings.RouteInitAPIFlow)
assertion(t, body, true)
@@ -206,7 +206,7 @@ func TestHandler(t *testing.T) {
})
t.Run("description=success", func(t *testing.T) {
- user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
+ user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
res, body := initFlow(t, user1, false)
assert.Contains(t, res.Request.URL.String(), reg.Config().SelfServiceFlowSettingsUI(ctx).String())
assertion(t, body, false)
@@ -241,7 +241,7 @@ func TestHandler(t *testing.T) {
})
t.Run("case=redirects with 303", func(t *testing.T) {
- c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
+ c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
// prevent the redirect
c.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
@@ -268,7 +268,7 @@ func TestHandler(t *testing.T) {
})
t.Run("description=success", func(t *testing.T) {
- user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg)
+ user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg)
res, body := initSPAFlow(t, user1)
assert.Contains(t, res.Request.URL.String(), settings.RouteInitBrowserFlow)
assertion(t, body, false)
@@ -277,7 +277,7 @@ func TestHandler(t *testing.T) {
t.Run("description=can not init if identity has aal2 but session has aal1", func(t *testing.T) {
email := testhelpers.RandomEmail()
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL)
- user1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, &identity.Identity{
+ user1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, &identity.Identity{
State: identity.StateActive,
Traits: []byte(`{"email":"` + email + `"}`),
Credentials: map[identity.CredentialsType]identity.Credentials{
@@ -303,7 +303,7 @@ func TestHandler(t *testing.T) {
t.Run("description=settings return_to should persist through mfa flows", func(t *testing.T) {
email := testhelpers.RandomEmail()
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsRequiredAAL, config.HighestAvailableAAL)
- user1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, &identity.Identity{
+ user1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, &identity.Identity{
State: identity.StateActive,
Traits: []byte(`{"email":"` + email + `"}`),
Credentials: map[identity.CredentialsType]identity.Credentials{
@@ -356,7 +356,7 @@ func TestHandler(t *testing.T) {
returnTo := "https://www.ory.sh"
conf.MustSet(ctx, config.ViperKeyURLsAllowedReturnToDomains, []string{returnTo})
- client := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg)
+ client := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg)
body := testhelpers.EasyGetBody(t, client, publicTS.URL+settings.RouteInitBrowserFlow+"?return_to="+returnTo)
// Expire the flow
@@ -385,8 +385,8 @@ func TestHandler(t *testing.T) {
t.Run("description=should fail to fetch request if identity changed", func(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
- user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg)
- user2 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg)
+ user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg)
+ user2 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg)
res, err := user1.Get(publicTS.URL + settings.RouteInitAPIFlow)
require.NoError(t, err)
@@ -537,8 +537,8 @@ func TestHandler(t *testing.T) {
t.Run("description=fail to submit form as another user", func(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
- user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg)
- user2 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg)
+ user1 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg)
+ user2 := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg)
_, body := initFlow(t, user1, true)
var f kratos.SettingsFlow
require.NoError(t, json.Unmarshal(body, &f))
@@ -549,8 +549,8 @@ func TestHandler(t *testing.T) {
})
t.Run("type=spa", func(t *testing.T) {
- user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
- user2 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
+ user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
+ user2 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
_, body := initFlow(t, user1, true)
var f kratos.SettingsFlow
require.NoError(t, json.Unmarshal(body, &f))
@@ -561,8 +561,8 @@ func TestHandler(t *testing.T) {
})
t.Run("type=browser", func(t *testing.T) {
- user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
- user2 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
+ user1 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
+ user2 := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
_, body := initFlow(t, user1, true)
var f kratos.SettingsFlow
require.NoError(t, json.Unmarshal(body, &f))
@@ -602,7 +602,7 @@ func TestHandler(t *testing.T) {
t.Run("case=relative redirect when self-service settings ui is a relative url", func(t *testing.T) {
reg.Config().MustSet(ctx, config.ViperKeySelfServiceSettingsURL, "/settings-ts")
- user1 := testhelpers.NewNoRedirectHTTPClientWithArbitrarySessionCookie(t, reg)
+ user1 := testhelpers.NewNoRedirectHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
res, _ := initFlow(t, user1, false)
assert.Regexp(
t,
diff --git a/selfservice/flow/settings/hook.go b/selfservice/flow/settings/hook.go
index b688fd0fc431..88741e766736 100644
--- a/selfservice/flow/settings/hook.go
+++ b/selfservice/flow/settings/hook.go
@@ -308,6 +308,7 @@ func (e *HookExecutor) PostSettingsHook(w http.ResponseWriter, r *http.Request,
}
// ContinueWith items are transient items, not stored in the database, and need to be carried over here, so
// they can be returned to the client.
+ ctxUpdate.Flow.AddContinueWith(flow.NewContinueWithRedirectBrowserTo(returnTo.String()))
updatedFlow.ContinueWithItems = ctxUpdate.Flow.ContinueWithItems
e.d.Writer().Write(w, r, updatedFlow)
diff --git a/selfservice/flow/settings/hook_test.go b/selfservice/flow/settings/hook_test.go
index 0ab28a1504f9..5253bb2886f2 100644
--- a/selfservice/flow/settings/hook_test.go
+++ b/selfservice/flow/settings/hook_test.go
@@ -101,6 +101,16 @@ func TestSettingsExecutor(t *testing.T) {
assert.Contains(t, res.Request.URL.String(), uiURL)
})
+ t.Run("case=pass without hooks if ajax client", func(t *testing.T) {
+ t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
+
+ ts := newServer(t, nil, flow.TypeBrowser)
+ res, body := makeRequestPost(t, ts, true, url.Values{})
+ assert.EqualValues(t, http.StatusOK, res.StatusCode)
+ assert.Contains(t, res.Request.URL.String(), ts.URL)
+ assert.EqualValues(t, gjson.Get(body, "continue_with.0.action").String(), "redirect_browser_to")
+ })
+
t.Run("case=pass if hooks pass", func(t *testing.T) {
t.Cleanup(testhelpers.SelfServiceHookConfigReset(t, conf))
diff --git a/selfservice/flow/settings/test/persistence.go b/selfservice/flow/settings/test/persistence.go
index 85c80e49d74e..498419a65c3a 100644
--- a/selfservice/flow/settings/test/persistence.go
+++ b/selfservice/flow/settings/test/persistence.go
@@ -27,7 +27,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/ory/kratos/driver/config"
"github.com/ory/kratos/x"
)
@@ -37,12 +36,10 @@ func clearids(r *settings.Flow) {
r.IdentityID = uuid.Nil
}
-func TestFlowPersister(ctx context.Context, conf *config.Config, p persistence.Persister) func(t *testing.T) {
+func TestFlowPersister(ctx context.Context, p persistence.Persister) func(t *testing.T) {
return func(t *testing.T) {
_, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p)
- testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
-
t.Run("case=should error when the settings request does not exist", func(t *testing.T) {
_, err := p.GetSettingsFlow(ctx, x.NewUUID())
require.Error(t, err)
diff --git a/selfservice/flow/state.go b/selfservice/flow/state.go
index 76a0683fc19d..a6b4f1a98966 100644
--- a/selfservice/flow/state.go
+++ b/selfservice/flow/state.go
@@ -33,7 +33,11 @@ const (
StateSuccess State = "success"
)
-var states = []State{StateChooseMethod, StateEmailSent, StatePassedChallenge}
+var states = []State{
+ StateChooseMethod,
+ StateEmailSent,
+ StatePassedChallenge,
+}
func indexOf(current State) int {
for k, s := range states {
diff --git a/selfservice/flow/verification/error.go b/selfservice/flow/verification/error.go
index b39875b233d0..0fcffe84869e 100644
--- a/selfservice/flow/verification/error.go
+++ b/selfservice/flow/verification/error.go
@@ -60,10 +60,12 @@ func (s *ErrorHandler) WriteFlowError(
group node.UiNodeGroup,
err error,
) {
- s.d.Audit().
+ logger := s.d.Audit().
WithError(err).
WithRequest(r).
- WithField("verification_flow", f).
+ WithField("verification_flow", f.ToLoggerField())
+
+ logger.
Info("Encountered self-service verification error.")
if f == nil {
diff --git a/selfservice/flow/verification/flow.go b/selfservice/flow/verification/flow.go
index a0de0250b54d..ca78688fc441 100644
--- a/selfservice/flow/verification/flow.go
+++ b/selfservice/flow/verification/flow.go
@@ -297,3 +297,18 @@ func (f *Flow) SetState(state State) {
func (t *Flow) GetTransientPayload() json.RawMessage {
return t.TransientPayload
}
+
+func (f *Flow) ToLoggerField() map[string]interface{} {
+ if f == nil {
+ return map[string]interface{}{}
+ }
+ return map[string]interface{}{
+ "id": f.ID.String(),
+ "return_to": f.ReturnTo,
+ "request_url": f.RequestURL,
+ "active": f.Active,
+ "Type": f.Type,
+ "nid": f.NID,
+ "state": f.State,
+ }
+}
diff --git a/selfservice/flow/verification/test/persistence.go b/selfservice/flow/verification/test/persistence.go
index 57c35cba8d2e..a021d06a152e 100644
--- a/selfservice/flow/verification/test/persistence.go
+++ b/selfservice/flow/verification/test/persistence.go
@@ -12,7 +12,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/ory/kratos/driver/config"
"github.com/ory/kratos/internal/testhelpers"
"github.com/ory/kratos/persistence"
"github.com/ory/kratos/selfservice/flow"
@@ -23,7 +22,7 @@ import (
"github.com/ory/x/sqlcon"
)
-func TestFlowPersister(ctx context.Context, conf *config.Config, p interface {
+func TestFlowPersister(ctx context.Context, p interface {
persistence.Persister
},
) func(t *testing.T) {
@@ -34,8 +33,6 @@ func TestFlowPersister(ctx context.Context, conf *config.Config, p interface {
return func(t *testing.T) {
nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p)
- testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
-
t.Run("case=should error when the verification request does not exist", func(t *testing.T) {
_, err := p.GetVerificationFlow(ctx, x.NewUUID())
require.Error(t, err)
diff --git a/selfservice/flowhelpers/login.go b/selfservice/flowhelpers/login.go
index 2e97f85ebe30..60c17176a740 100644
--- a/selfservice/flowhelpers/login.go
+++ b/selfservice/flowhelpers/login.go
@@ -15,11 +15,11 @@ func GuessForcedLoginIdentifier(r *http.Request, d interface {
session.ManagementProvider
identity.PrivilegedPoolProvider
}, f interface {
- IsForced() bool
+ IsRefresh() bool
}, ct identity.CredentialsType) (identifier string, id *identity.Identity, creds *identity.Credentials) {
var ok bool
// This block adds the identifier to the method when the request is forced - as a hint for the user.
- if !f.IsForced() {
+ if !f.IsRefresh() {
// do nothing
} else if sess, err := d.SessionManager().FetchFromRequest(r.Context(), r); err != nil {
// do nothing
diff --git a/selfservice/hook/password_migration_hook.go b/selfservice/hook/password_migration_hook.go
new file mode 100644
index 000000000000..065dc5dcddc6
--- /dev/null
+++ b/selfservice/hook/password_migration_hook.go
@@ -0,0 +1,111 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package hook
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/pkg/errors"
+ "github.com/tidwall/gjson"
+ "go.opentelemetry.io/otel/codes"
+ semconv "go.opentelemetry.io/otel/semconv/v1.11.0"
+ "go.opentelemetry.io/otel/trace"
+ grpccodes "google.golang.org/grpc/codes"
+
+ "github.com/ory/herodot"
+ "github.com/ory/kratos/request"
+ "github.com/ory/kratos/schema"
+ "github.com/ory/x/otelx"
+)
+
+type (
+ PasswordMigration struct {
+ deps webHookDependencies
+ conf json.RawMessage
+ }
+ PasswordMigrationRequest struct {
+ Identifier string `json:"identifier"`
+ Password string `json:"password"`
+ }
+ PasswordMigrationResponse struct {
+ Status string `json:"status"`
+ }
+)
+
+func NewPasswordMigrationHook(deps webHookDependencies, conf json.RawMessage) *PasswordMigration {
+ return &PasswordMigration{deps: deps, conf: conf}
+}
+
+func (p *PasswordMigration) Execute(ctx context.Context, data *PasswordMigrationRequest) (err error) {
+ var (
+ httpClient = p.deps.HTTPClient(ctx)
+ emitEvent = gjson.GetBytes(p.conf, "emit_analytics_event").Bool() || !gjson.GetBytes(p.conf, "emit_analytics_event").Exists() // default true
+ tracer = trace.SpanFromContext(ctx).TracerProvider().Tracer("kratos-webhooks")
+ )
+
+ ctx, span := tracer.Start(ctx, "selfservice.login.password_migration")
+ defer otelx.End(span, &err)
+
+ if emitEvent {
+ instrumentHTTPClientForEvents(ctx, httpClient)
+ }
+ builder, err := request.NewBuilder(ctx, p.conf, p.deps, nil)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ req, err := builder.BuildRequest(ctx, nil) // passing a nil body here skips Jsonnet
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ rawData, err := json.Marshal(data)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ if err = req.SetBody(rawData); err != nil {
+ return errors.WithStack(err)
+ }
+
+ p.deps.Logger().WithRequest(req.Request).Info("Dispatching password migration hook")
+ req = req.WithContext(ctx)
+
+ resp, err := httpClient.Do(req)
+ if err != nil {
+ return herodot.DefaultError{
+ CodeField: http.StatusBadGateway,
+ StatusField: http.StatusText(http.StatusBadGateway),
+ GRPCCodeField: grpccodes.Aborted,
+ ReasonField: "A third-party upstream service could not be reached. Please try again later.",
+ ErrorField: "calling the password migration hook failed",
+ }.WithWrap(errors.WithStack(err))
+ }
+ defer resp.Body.Close()
+ span.SetAttributes(semconv.HTTPAttributesFromHTTPStatusCode(resp.StatusCode)...)
+
+ switch resp.StatusCode {
+ case http.StatusOK:
+ // We now check if the response matches `{"status": "password_match" }`.
+ dec := json.NewDecoder(io.LimitReader(resp.Body, 1024)) // limit the response body to 1KB
+ var response PasswordMigrationResponse
+ if err := dec.Decode(&response); err != nil || response.Status != "password_match" {
+ return errors.WithStack(schema.NewInvalidCredentialsError())
+ }
+ return nil
+
+ case http.StatusForbidden:
+ return errors.WithStack(schema.NewInvalidCredentialsError())
+ default:
+ span.SetStatus(codes.Error, "Unexpected HTTP status code")
+ return herodot.DefaultError{
+ CodeField: http.StatusBadGateway,
+ StatusField: http.StatusText(http.StatusBadGateway),
+ GRPCCodeField: grpccodes.Aborted,
+ ReasonField: "A third-party upstream service responded improperly. Please try again later.",
+ ErrorField: fmt.Sprintf("password migration hook failed with status code %v", resp.StatusCode),
+ }
+ }
+}
diff --git a/selfservice/sessiontokenexchange/test/persistence.go b/selfservice/sessiontokenexchange/test/persistence.go
index 53db63db04f3..da19c3edc1a3 100644
--- a/selfservice/sessiontokenexchange/test/persistence.go
+++ b/selfservice/sessiontokenexchange/test/persistence.go
@@ -11,7 +11,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/ory/kratos/driver/config"
"github.com/ory/kratos/internal/testhelpers"
"github.com/ory/kratos/persistence"
"github.com/ory/kratos/selfservice/sessiontokenexchange"
@@ -36,11 +35,10 @@ func (t *testParams) setCodes(e *sessiontokenexchange.Exchanger) {
t.returnToCode = e.ReturnToCode
}
-func TestPersister(ctx context.Context, _ *config.Config, p interface {
+func TestPersister(ctx context.Context, p interface {
persistence.Persister
}) func(t *testing.T) {
return func(t *testing.T) {
- t.Parallel()
nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p)
t.Run("suite=create-update-get", func(t *testing.T) {
diff --git a/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json b/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json
index a9f46bedb5c4..736578d0e543 100644
--- a/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json
+++ b/selfservice/strategy/code/.snapshots/TestAdminStrategy-case=form_should_not_contain_email_field_when_creating_recovery_code.json
@@ -6,7 +6,9 @@
"name": "code",
"type": "text",
"required": true,
+ "pattern": "[0-9]+",
"disabled": false,
+ "maxlength": 6,
"node_type": "input"
},
"messages": [],
@@ -31,8 +33,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_2fa_but_request_is_1fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_2fa_but_request_is_1fa.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_2fa_but_request_is_1fa.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_passwordless_login_and_request_is_1fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_passwordless_login_and_request_is_1fa.json
new file mode 100644
index 000000000000..8e9874d00cbf
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=code_is_used_for_passwordless_login_and_request_is_1fa.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070004,
+ "text": "ID",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010015,
+ "text": "Send sign in code",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_2fa_and_request_is_1fa_with_refresh.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_2fa_and_request_is_1fa_with_refresh.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_2fa_and_request_is_1fa_with_refresh.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_1fa_with_refresh.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_1fa_with_refresh.json
new file mode 100644
index 000000000000..8e9874d00cbf
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_1fa_with_refresh.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070004,
+ "text": "ID",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010015,
+ "text": "Send sign in code",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=code_is_used_for_2fa.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=code_is_used_for_2fa.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=code_is_used_for_passwordless_login.json
new file mode 100644
index 000000000000..66b84bf1a436
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=code_is_used_for_passwordless_login.json
@@ -0,0 +1,21 @@
+[
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010015,
+ "text": "Send sign in code",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_2fa.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_2fa.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_passwordless_login.json
new file mode 100644
index 000000000000..66b84bf1a436
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_code_method-case=code_is_used_for_passwordless_login.json
@@ -0,0 +1,21 @@
+[
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010015,
+ "text": "Send sign in code",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_2fa.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_2fa.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_passwordless_login.json
new file mode 100644
index 000000000000..66b84bf1a436
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_code_method-case=code_is_used_for_passwordless_login.json
@@ -0,0 +1,21 @@
+[
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010015,
+ "text": "Send sign in code",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_2fa.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_2fa.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_passwordless_login.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=with_no_identity-case=code_is_used_for_passwordless_login.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_2fa.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_2fa.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_passwordless_login.json
new file mode 100644
index 000000000000..66b84bf1a436
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=code_is_used_for_passwordless_login.json
@@ -0,0 +1,21 @@
+[
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010015,
+ "text": "Send sign in code",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_2fa.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_2fa.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_passwordless_login.json
new file mode 100644
index 000000000000..8e9874d00cbf
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=code_is_used_for_passwordless_login.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070004,
+ "text": "ID",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010015,
+ "text": "Send sign in code",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor#01-case=code_is_used_for_2fa_and_request_is_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor#01-case=code_is_used_for_2fa_and_request_is_2fa.json
new file mode 100644
index 000000000000..60b142ed4181
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor#01-case=code_is_used_for_2fa_and_request_is_2fa.json
@@ -0,0 +1,66 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [
+ {
+ "id": 1010020,
+ "text": "We will send a code to fo****@ory.sh. To verify that this is your address please enter it here.",
+ "type": "info",
+ "context": {
+ "masked_to": "fo****@ory.sh"
+ }
+ }
+ ],
+ "meta": {
+ "label": {
+ "id": 1070002,
+ "text": "",
+ "type": "info",
+ "context": {
+ "title": ""
+ }
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010019,
+ "text": "Continue with code",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor#01-case=code_is_used_for_passwordless_login_and_request_is_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor#01-case=code_is_used_for_passwordless_login_and_request_is_2fa.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor#01-case=code_is_used_for_passwordless_login_and_request_is_2fa.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_2fa.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_2fa.json
new file mode 100644
index 000000000000..60b142ed4181
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_2fa.json
@@ -0,0 +1,66 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [
+ {
+ "id": 1010020,
+ "text": "We will send a code to fo****@ory.sh. To verify that this is your address please enter it here.",
+ "type": "info",
+ "context": {
+ "masked_to": "fo****@ory.sh"
+ }
+ }
+ ],
+ "meta": {
+ "label": {
+ "id": 1070002,
+ "text": "",
+ "type": "info",
+ "context": {
+ "title": ""
+ }
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010019,
+ "text": "Continue with code",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_passwordless_login.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_passwordless_login.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=code_is_used_for_passwordless_login.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_2fa_and_request_is_2fa_with_refresh.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_2fa_and_request_is_2fa_with_refresh.json
new file mode 100644
index 000000000000..60b142ed4181
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_2fa_and_request_is_2fa_with_refresh.json
@@ -0,0 +1,66 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [
+ {
+ "id": 1010020,
+ "text": "We will send a code to fo****@ory.sh. To verify that this is your address please enter it here.",
+ "type": "info",
+ "context": {
+ "masked_to": "fo****@ory.sh"
+ }
+ }
+ ],
+ "meta": {
+ "label": {
+ "id": 1070002,
+ "text": "",
+ "type": "info",
+ "context": {
+ "title": ""
+ }
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "code",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "code",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010019,
+ "text": "Continue with code",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_2fa_with_refresh.json b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_2fa_with_refresh.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/code/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh-case=code_is_used_for_passwordless_login_and_request_is_2fa_with_refresh.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json
index ec1092ad77a6..195ca691e981 100644
--- a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json
+++ b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json
@@ -43,8 +43,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
},
diff --git a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json
index dbf1dcd2cbb7..a5ab6784616a 100644
--- a/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json
+++ b/selfservice/strategy/code/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json
@@ -21,6 +21,7 @@
"required": true,
"pattern": "[0-9]+",
"disabled": false,
+ "maxlength": 6,
"node_type": "input"
},
"messages": [],
@@ -58,8 +59,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json
index ec1092ad77a6..195ca691e981 100644
--- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json
+++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=api.json
@@ -43,8 +43,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
},
diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json
index ec1092ad77a6..195ca691e981 100644
--- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json
+++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=browser.json
@@ -43,8 +43,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
},
diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json
index ec1092ad77a6..195ca691e981 100644
--- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json
+++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads-type=spa.json
@@ -43,8 +43,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
},
diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json
index dbf1dcd2cbb7..a5ab6784616a 100644
--- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json
+++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=api.json
@@ -21,6 +21,7 @@
"required": true,
"pattern": "[0-9]+",
"disabled": false,
+ "maxlength": 6,
"node_type": "input"
},
"messages": [],
@@ -58,8 +59,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json
index dbf1dcd2cbb7..a5ab6784616a 100644
--- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json
+++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=browser.json
@@ -21,6 +21,7 @@
"required": true,
"pattern": "[0-9]+",
"disabled": false,
+ "maxlength": 6,
"node_type": "input"
},
"messages": [],
@@ -58,8 +59,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json
index dbf1dcd2cbb7..a5ab6784616a 100644
--- a/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json
+++ b/selfservice/strategy/code/.snapshots/TestRecovery_WithContinueWith-description=should_set_all_the_correct_recovery_payloads_after_submission-type=spa.json
@@ -21,6 +21,7 @@
"required": true,
"pattern": "[0-9]+",
"disabled": false,
+ "maxlength": 6,
"node_type": "input"
},
"messages": [],
@@ -58,8 +59,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json
index 37f61ac9e827..01def57fd58f 100644
--- a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json
+++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json
@@ -30,8 +30,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
},
diff --git a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json
index 42456da54dc5..7e7096cd7358 100644
--- a/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json
+++ b/selfservice/strategy/code/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json
@@ -44,8 +44,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/strategy/code/strategy.go b/selfservice/strategy/code/strategy.go
index ee3ce353e4ae..2d275fdff85f 100644
--- a/selfservice/strategy/code/strategy.go
+++ b/selfservice/strategy/code/strategy.go
@@ -180,6 +180,7 @@ func (s *Strategy) PopulateMethod(r *http.Request, f flow.Flow) error {
if f.GetType() == flow.TypeBrowser {
f.GetUI().SetCSRF(s.deps.GenerateCSRFToken(r))
}
+
return nil
}
@@ -192,7 +193,7 @@ func (s *Strategy) populateChooseMethodFlow(r *http.Request, f flow.Flow) error
node.NewInputField("email", nil, node.CodeGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).
WithMetaLabel(text.NewInfoNodeInputEmail()),
)
- codeMetaLabel = text.NewInfoNodeLabelSubmit()
+ codeMetaLabel = text.NewInfoNodeLabelContinue()
case *login.Flow:
ds, err := s.deps.Config().DefaultIdentityTraitsSchemaURL(ctx)
if err != nil {
@@ -299,15 +300,10 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error
// preserve the login identifier that was submitted
// so we can retry the code flow with the same data
for _, n := range f.GetUI().Nodes {
- if n.Group == node.DefaultGroup {
- // we don't need the user to change the values here
- // for better UX let's make them disabled
- // when there are errors we won't hide the fields
- if len(n.Messages) == 0 {
- if input, ok := n.Attributes.(*node.InputAttributes); ok {
- input.Type = "hidden"
- n.Attributes = input
- }
+ if n.ID() == "identifier" {
+ if input, ok := n.Attributes.(*node.InputAttributes); ok {
+ input.Type = "hidden"
+ n.Attributes = input
}
freshNodes = append(freshNodes, n)
}
@@ -373,7 +369,7 @@ func (s *Strategy) populateEmailSentFlow(ctx context.Context, f flow.Flow) error
// code submit button
freshNodes.
Append(node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit).
- WithMetaLabel(text.NewInfoNodeLabelSubmit()))
+ WithMetaLabel(text.NewInfoNodeLabelContinue()))
if resendNode != nil {
freshNodes.Append(resendNode)
diff --git a/selfservice/strategy/code/strategy_login.go b/selfservice/strategy/code/strategy_login.go
index a9d7459f5c56..a6d9af4fad0f 100644
--- a/selfservice/strategy/code/strategy_login.go
+++ b/selfservice/strategy/code/strategy_login.go
@@ -10,6 +10,9 @@ import (
"net/http"
"strings"
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+ "github.com/ory/kratos/text"
+
"github.com/ory/x/sqlcon"
"github.com/pkg/errors"
@@ -29,7 +32,10 @@ import (
"github.com/ory/x/decoderx"
)
-var _ login.Strategy = new(Strategy)
+var (
+ _ login.FormHydrator = new(Strategy)
+ _ login.Strategy = new(Strategy)
+)
// Update Login flow using the code method
//
@@ -112,10 +118,6 @@ func (s *Strategy) HandleLoginError(r *http.Request, f *login.Flow, body *update
return err
}
-func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, lf *login.Flow) error {
- return s.PopulateMethod(r, lf)
-}
-
// findIdentityByIdentifier returns the identity and the code credential for the given identifier.
// If the identity does not have a code credential, it will attempt to find
// the identity through other credentials matching the identifier.
@@ -372,3 +374,41 @@ func (s *Strategy) loginVerifyCode(ctx context.Context, r *http.Request, f *logi
return i, nil
}
+
+func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, f *login.Flow) error {
+ return s.PopulateMethod(r, f)
+}
+
+func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, f *login.Flow) error {
+ return s.PopulateMethod(r, f)
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, f *login.Flow) error {
+ return s.PopulateMethod(r, f)
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, f *login.Flow) error {
+ return s.PopulateMethod(r, f)
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, f *login.Flow, opts ...login.FormHydratorModifier) error {
+ if !s.deps.Config().SelfServiceCodeStrategy(r.Context()).PasswordlessEnabled {
+ // We only return this if passwordless is disabled, because if it is enabled we can always sign in using this method.
+ return errors.WithStack(idfirst.ErrNoCredentialsFound)
+ }
+ o := login.NewFormHydratorOptions(opts)
+
+ // If the identity hint is nil and account enumeration mitigation is disabled, we return an error.
+ if o.IdentityHint == nil && !s.deps.Config().SecurityAccountEnumerationMitigate(r.Context()) {
+ return errors.WithStack(idfirst.ErrNoCredentialsFound)
+ }
+
+ f.GetUI().Nodes.Append(
+ node.NewInputField("method", s.ID(), node.CodeGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceLoginCode()),
+ )
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, f *login.Flow) error {
+ return nil
+}
diff --git a/selfservice/strategy/code/strategy_login_test.go b/selfservice/strategy/code/strategy_login_test.go
index 19cac6d38375..ea30e7256c34 100644
--- a/selfservice/strategy/code/strategy_login_test.go
+++ b/selfservice/strategy/code/strategy_login_test.go
@@ -11,6 +11,16 @@ import (
"net/http/httptest"
"net/url"
"testing"
+ "time"
+
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+
+ configtesthelpers "github.com/ory/kratos/driver/config/testhelpers"
+
+ "github.com/ory/kratos/driver"
+ "github.com/ory/kratos/selfservice/flow/login"
+
+ "github.com/ory/kratos/selfservice/flow"
"github.com/ory/x/ioutilx"
"github.com/ory/x/snapshotx"
@@ -32,6 +42,41 @@ import (
"github.com/ory/x/sqlxx"
)
+func createIdentity(ctx context.Context, t *testing.T, reg driver.Registry, withoutCodeCredential bool, moreIdentifiers ...string) *identity.Identity {
+ t.Helper()
+ i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
+ email := testhelpers.RandomEmail()
+
+ ids := fmt.Sprintf(`"email":"%s"`, email)
+ for i, identifier := range moreIdentifiers {
+ ids = fmt.Sprintf(`%s,"email_%d":"%s"`, ids, i+1, identifier)
+ }
+
+ i.Traits = identity.Traits(fmt.Sprintf(`{"tos": true, %s}`, ids))
+
+ credentials := map[identity.CredentialsType]identity.Credentials{
+ identity.CredentialsTypePassword: {Identifiers: append([]string{email}, moreIdentifiers...), Type: identity.CredentialsTypePassword, Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}")},
+ identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}")},
+ identity.CredentialsTypeWebAuthn: {Type: identity.CredentialsTypeWebAuthn, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\", \"user_handle\": \"rVIFaWRcTTuQLkXFmQWpgA==\"}")},
+ }
+ if !withoutCodeCredential {
+ credentials[identity.CredentialsTypeCodeAuth] = identity.Credentials{Type: identity.CredentialsTypeCodeAuth, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"address_type\": \"email\", \"used_at\": \"2023-07-26T16:59:06+02:00\"}")}
+ }
+ i.Credentials = credentials
+
+ var va []identity.VerifiableAddress
+ for _, identifier := range moreIdentifiers {
+ va = append(va, identity.VerifiableAddress{Value: identifier, Verified: false, Status: identity.VerifiableAddressStatusCompleted})
+ }
+
+ va = append(va, identity.VerifiableAddress{Value: email, Verified: true, Status: identity.VerifiableAddressStatusCompleted})
+
+ i.VerifiableAddresses = va
+
+ require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i))
+ return i
+}
+
func TestLoginCodeStrategy(t *testing.T) {
ctx := context.Background()
conf, reg := internal.NewFastRegistryWithMocks(t)
@@ -46,41 +91,6 @@ func TestLoginCodeStrategy(t *testing.T) {
public, _, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg)
- createIdentity := func(ctx context.Context, t *testing.T, withoutCodeCredential bool, moreIdentifiers ...string) *identity.Identity {
- t.Helper()
- i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
- email := testhelpers.RandomEmail()
-
- ids := fmt.Sprintf(`"email":"%s"`, email)
- for i, identifier := range moreIdentifiers {
- ids = fmt.Sprintf(`%s,"email_%d":"%s"`, ids, i+1, identifier)
- }
-
- i.Traits = identity.Traits(fmt.Sprintf(`{"tos": true, %s}`, ids))
-
- credentials := map[identity.CredentialsType]identity.Credentials{
- identity.CredentialsTypePassword: {Identifiers: append([]string{email}, moreIdentifiers...), Type: identity.CredentialsTypePassword, Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}")},
- identity.CredentialsTypeOIDC: {Type: identity.CredentialsTypeOIDC, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\"}")},
- identity.CredentialsTypeWebAuthn: {Type: identity.CredentialsTypeWebAuthn, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"some\" : \"secret\", \"user_handle\": \"rVIFaWRcTTuQLkXFmQWpgA==\"}")},
- }
- if !withoutCodeCredential {
- credentials[identity.CredentialsTypeCodeAuth] = identity.Credentials{Type: identity.CredentialsTypeCodeAuth, Identifiers: append([]string{email}, moreIdentifiers...), Config: sqlxx.JSONRawMessage("{\"address_type\": \"email\", \"used_at\": \"2023-07-26T16:59:06+02:00\"}")}
- }
- i.Credentials = credentials
-
- var va []identity.VerifiableAddress
- for _, identifier := range moreIdentifiers {
- va = append(va, identity.VerifiableAddress{Value: identifier, Verified: false, Status: identity.VerifiableAddressStatusCompleted})
- }
-
- va = append(va, identity.VerifiableAddress{Value: email, Verified: true, Status: identity.VerifiableAddressStatusCompleted})
-
- i.VerifiableAddresses = va
-
- require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i))
- return i
- }
-
type state struct {
flowID string
identity *identity.Identity
@@ -102,7 +112,7 @@ func TestLoginCodeStrategy(t *testing.T) {
createLoginFlow := func(ctx context.Context, t *testing.T, public *httptest.Server, apiType ApiType, withoutCodeCredential bool, moreIdentifiers ...string) *state {
t.Helper()
- identity := createIdentity(ctx, t, withoutCodeCredential, moreIdentifiers...)
+ identity := createIdentity(ctx, t, reg, withoutCodeCredential, moreIdentifiers...)
var client *http.Client
if apiType == ApiTypeNative {
@@ -247,9 +257,15 @@ func TestLoginCodeStrategy(t *testing.T) {
assert.NotEmpty(t, loginCode)
// 3. Submit OTP
- submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) {
+ state := submitLogin(ctx, t, s, tc.apiType, func(v *url.Values) {
v.Set("code", loginCode)
}, true, nil)
+ if tc.apiType == ApiTypeSPA {
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(state.body, "continue_with.0.action").String(), "%s", state.body)
+ assert.Contains(t, gjson.Get(state.body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", state.body)
+ } else {
+ assert.Empty(t, gjson.Get(state.body, "continue_with").Array(), "%s", state.body)
+ }
})
t.Run("case=new identities automatically have login with code", func(t *testing.T) {
@@ -587,14 +603,14 @@ func TestLoginCodeStrategy(t *testing.T) {
t.Run("case=should be able to get AAL2 session", func(t *testing.T) {
t.Cleanup(testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/default.schema.json")) // doesn't have the code credential
- identity := createIdentity(ctx, t, true)
+ identity := createIdentity(ctx, t, reg, true)
var cl *http.Client
var f *oryClient.LoginFlow
if tc.apiType == ApiTypeNative {
- cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, identity)
f = testhelpers.InitializeLoginFlowViaAPI(t, cl, public, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email"))
} else {
- cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, identity)
f = testhelpers.InitializeLoginFlowViaBrowser(t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email"))
}
@@ -627,14 +643,14 @@ func TestLoginCodeStrategy(t *testing.T) {
testhelpers.EnsureAAL(t, cl, public, "aal2", "code")
})
t.Run("case=cannot use different identifier", func(t *testing.T) {
- identity := createIdentity(ctx, t, false)
+ identity := createIdentity(ctx, t, reg, false)
var cl *http.Client
var f *oryClient.LoginFlow
if tc.apiType == ApiTypeNative {
- cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, identity)
f = testhelpers.InitializeLoginFlowViaAPI(t, cl, public, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email"))
} else {
- cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, identity)
f = testhelpers.InitializeLoginFlowViaBrowser(t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email"))
}
@@ -661,14 +677,14 @@ func TestLoginCodeStrategy(t *testing.T) {
t.Run("case=verify initial payload", func(t *testing.T) {
fixedEmail := fmt.Sprintf("fixed_mfa_test_%s@ory.sh", tc.apiType)
- identity := createIdentity(ctx, t, false, fixedEmail)
+ identity := createIdentity(ctx, t, reg, false, fixedEmail)
var cl *http.Client
var f *oryClient.LoginFlow
if tc.apiType == ApiTypeNative {
- cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, identity)
f = testhelpers.InitializeLoginFlowViaAPI(t, cl, public, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email_1"))
} else {
- cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, identity)
f = testhelpers.InitializeLoginFlowViaBrowser(t, cl, public, false, tc.apiType == ApiTypeSPA, false, false, testhelpers.InitFlowWithAAL("aal2"), testhelpers.InitFlowWithVia("email_1"))
}
@@ -678,15 +694,15 @@ func TestLoginCodeStrategy(t *testing.T) {
})
t.Run("case=using a non existing identity trait results in an error", func(t *testing.T) {
- identity := createIdentity(ctx, t, false)
+ identity := createIdentity(ctx, t, reg, false)
var cl *http.Client
var res *http.Response
var err error
if tc.apiType == ApiTypeNative {
- cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, identity)
res, err = cl.Get(public.URL + "/self-service/login/api?aal=aal2&via=doesnt_exist")
} else {
- cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, identity)
res, err = cl.Get(public.URL + "/self-service/login/browser?aal=aal2&via=doesnt_exist")
}
require.NoError(t, err)
@@ -699,15 +715,15 @@ func TestLoginCodeStrategy(t *testing.T) {
})
t.Run("case=missing via parameter results results in an error", func(t *testing.T) {
- identity := createIdentity(ctx, t, false)
+ identity := createIdentity(ctx, t, reg, false)
var cl *http.Client
var res *http.Response
var err error
if tc.apiType == ApiTypeNative {
- cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, identity)
res, err = cl.Get(public.URL + "/self-service/login/api?aal=aal2")
} else {
- cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, identity)
res, err = cl.Get(public.URL + "/self-service/login/browser?aal=aal2")
}
require.NoError(t, err)
@@ -718,16 +734,17 @@ func TestLoginCodeStrategy(t *testing.T) {
}
require.Equal(t, "AAL2 login via code requires the `via` query parameter", gjson.GetBytes(body, "reason").String(), "%s", body)
})
+
t.Run("case=unset trait in identity should lead to an error", func(t *testing.T) {
- identity := createIdentity(ctx, t, false)
+ identity := createIdentity(ctx, t, reg, false)
var cl *http.Client
var res *http.Response
var err error
if tc.apiType == ApiTypeNative {
- cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, identity)
res, err = cl.Get(public.URL + "/self-service/login/api?aal=aal2&via=email_1")
} else {
- cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, reg, identity)
+ cl = testhelpers.NewHTTPClientWithIdentitySessionCookieLocalhost(t, ctx, reg, identity)
res, err = cl.Get(public.URL + "/self-service/login/browser?aal=aal2&via=email_1")
}
require.NoError(t, err)
@@ -742,3 +759,236 @@ func TestLoginCodeStrategy(t *testing.T) {
})
}
}
+
+func TestFormHydration(t *testing.T) {
+ ctx := context.Background()
+ conf, reg := internal.NewFastRegistryWithMocks(t)
+ ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), map[string]interface{}{
+ "enabled": true,
+ "passwordless_enabled": true,
+ })
+ ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/default.schema.json")
+
+ s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeCodeAuth)
+ require.NoError(t, err)
+ fh, ok := s.(login.FormHydrator)
+ require.True(t, ok)
+
+ toSnapshot := func(t *testing.T, f *login.Flow) {
+ t.Helper()
+ // The CSRF token has a unique value that messes with the snapshot - ignore it.
+ f.UI.Nodes.ResetNodes("csrf_token")
+ snapshotx.SnapshotT(t, f.UI.Nodes)
+ }
+ newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) {
+ r := httptest.NewRequest("GET", "/self-service/login/browser", nil)
+ r = r.WithContext(ctx)
+ t.Helper()
+ f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser)
+ require.NoError(t, err)
+ return r, f
+ }
+
+ passwordlessEnabled := configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), map[string]interface{}{
+ "enabled": true,
+ "passwordless_enabled": true,
+ "mfa_enabled": false,
+ })
+
+ mfaEnabled := configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeCodeAuth), map[string]interface{}{
+ "enabled": true,
+ "passwordless_enabled": false,
+ "mfa_enabled": true,
+ })
+
+ toMFARequest := func(r *http.Request, f *login.Flow) {
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ r.URL = &url.URL{Path: "/", RawQuery: "via=email"}
+ // I only fear god.
+ r.Header = testhelpers.NewHTTPClientWithArbitrarySessionTokenAndTraits(t, ctx, reg, []byte(`{"email":"foo@ory.sh"}`)).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ }
+
+ t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) {
+ test := func(t *testing.T, ctx context.Context) {
+ r, f := newFlow(ctx, t)
+ toMFARequest(r, f)
+
+ r.Header = testhelpers.NewHTTPClientWithArbitrarySessionTokenAndTraits(t, ctx, reg, []byte(`{"email":"foo@ory.sh"}`)).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+
+ // We still use the legacy hydrator under the hood here and thus need to set this correctly.
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ r.URL = &url.URL{Path: "/", RawQuery: "via=email"}
+
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ }
+
+ t.Run("case=code is used for 2fa", func(t *testing.T) {
+ test(t, mfaEnabled)
+ })
+
+ t.Run("case=code is used for passwordless login", func(t *testing.T) {
+ test(t, passwordlessEnabled)
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) {
+ t.Run("case=code is used for 2fa but request is 1fa", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel1
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for passwordless login and request is 1fa", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel1
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) {
+ t.Run("case=code is used for passwordless login and request is 1fa with refresh", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel1
+ f.Refresh = true
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for 2fa and request is 1fa with refresh", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel1
+ f.Refresh = true
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) {
+ t.Run("case=code is used for 2fa and request is 2fa", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ toMFARequest(r, f)
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for passwordless login and request is 2fa", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ toMFARequest(r, f)
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodSecondFactorRefresh", func(t *testing.T) {
+ t.Run("case=code is used for 2fa and request is 2fa with refresh", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ toMFARequest(r, f)
+ f.Refresh = true
+ require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for passwordless login and request is 2fa with refresh", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ toMFARequest(r, f)
+ f.Refresh = true
+ require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) {
+ t.Run("case=no options", func(t *testing.T) {
+ t.Run("case=code is used for 2fa", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for passwordless login", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=WithIdentityHint", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ t.Run("case=code is used for 2fa", func(t *testing.T) {
+ r, f := newFlow(
+ configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true),
+ t,
+ )
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for passwordless login", func(t *testing.T) {
+ r, f := newFlow(
+ configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true),
+ t,
+ )
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ t.Run("case=with no identity", func(t *testing.T) {
+ t.Run("case=code is used for 2fa", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for passwordless login", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+ t.Run("case=identity has code method", func(t *testing.T) {
+ identifier := x.NewUUID().String()
+ id := createIdentity(ctx, t, reg, false, identifier)
+
+ t.Run("case=code is used for 2fa", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for passwordless login", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=identity does not have a code method", func(t *testing.T) {
+ id := identity.NewIdentity("default")
+
+ t.Run("case=code is used for 2fa", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=code is used for passwordless login", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)))
+ toSnapshot(t, f)
+ })
+ })
+ })
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f))
+ toSnapshot(t, f)
+ })
+}
diff --git a/selfservice/strategy/code/strategy_recovery.go b/selfservice/strategy/code/strategy_recovery.go
index 758e81d04fd9..f33356f2df31 100644
--- a/selfservice/strategy/code/strategy_recovery.go
+++ b/selfservice/strategy/code/strategy_recovery.go
@@ -43,7 +43,7 @@ func (s *Strategy) PopulateRecoveryMethod(r *http.Request, f *recovery.Flow) err
f.UI.
GetNodes().
Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit).
- WithMetaLabel(text.NewInfoNodeLabelSubmit()))
+ WithMetaLabel(text.NewInfoNodeLabelContinue()))
return nil
}
@@ -235,12 +235,13 @@ func (s *Strategy) recoveryIssueSession(w http.ResponseWriter, r *http.Request,
}
if s.deps.Config().UseContinueWithTransitions(ctx) {
+ redirectTo := sf.AppendTo(s.deps.Config().SelfServiceFlowSettingsUI(r.Context())).String()
switch {
case f.Type.IsAPI(), x.IsJSONRequest(r):
- f.ContinueWith = append(f.ContinueWith, flow.NewContinueWithSettingsUI(sf))
+ f.ContinueWith = append(f.ContinueWith, flow.NewContinueWithSettingsUI(sf, redirectTo))
s.deps.Writer().Write(w, r, f)
default:
- http.Redirect(w, r, sf.AppendTo(s.deps.Config().SelfServiceFlowSettingsUI(r.Context())).String(), http.StatusSeeOther)
+ http.Redirect(w, r, redirectTo, http.StatusSeeOther)
}
} else {
if x.IsJSONRequest(r) {
@@ -405,6 +406,7 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R
f.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithInputAttributes(func(a *node.InputAttributes) {
a.Required = true
a.Pattern = "[0-9]+"
+ a.MaxLength = CodeLength
})).
WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()),
)
@@ -413,7 +415,7 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R
f.UI.
GetNodes().
Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit).
- WithMetaLabel(text.NewInfoNodeLabelSubmit()))
+ WithMetaLabel(text.NewInfoNodeLabelContinue()))
f.UI.Nodes.Append(node.NewInputField("email", body.Email, node.CodeGroup, node.InputAttributeTypeSubmit).
WithMetaLabel(text.NewInfoNodeResendOTP()),
diff --git a/selfservice/strategy/code/strategy_recovery_admin.go b/selfservice/strategy/code/strategy_recovery_admin.go
index 028bb811bcaa..63aa36a90edd 100644
--- a/selfservice/strategy/code/strategy_recovery_admin.go
+++ b/selfservice/strategy/code/strategy_recovery_admin.go
@@ -178,13 +178,16 @@ func (s *Strategy) createRecoveryCodeForIdentity(w http.ResponseWriter, r *http.
recoveryFlow.DangerousSkipCSRFCheck = true
recoveryFlow.State = flow.StateEmailSent
recoveryFlow.UI.Nodes = node.Nodes{}
- recoveryFlow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).
+ recoveryFlow.UI.Nodes.Append(node.NewInputField("code", nil, node.CodeGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute, node.WithInputAttributes(func(a *node.InputAttributes) {
+ a.Pattern = "[0-9]+"
+ a.MaxLength = CodeLength
+ })).
WithMetaLabel(text.NewInfoNodeLabelRecoveryCode()),
)
recoveryFlow.UI.Nodes.
Append(node.NewInputField("method", s.RecoveryStrategyID(), node.CodeGroup, node.InputAttributeTypeSubmit).
- WithMetaLabel(text.NewInfoNodeLabelSubmit()))
+ WithMetaLabel(text.NewInfoNodeLabelContinue()))
if err := s.deps.RecoveryFlowPersister().CreateRecoveryFlow(ctx, recoveryFlow); err != nil {
s.deps.Writer().WriteError(w, r, err)
diff --git a/selfservice/strategy/code/strategy_recovery_test.go b/selfservice/strategy/code/strategy_recovery_test.go
index 98f3d9c7f21c..c5016b28710b 100644
--- a/selfservice/strategy/code/strategy_recovery_test.go
+++ b/selfservice/strategy/code/strategy_recovery_test.go
@@ -532,7 +532,7 @@ func TestRecovery(t *testing.T) {
require.NoError(t, err)
// Add the authentication to the request
- client.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewAuthorizedTransport(t, reg, session), t).RoundTripper
+ client.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewAuthorizedTransport(t, ctx, reg, session), t).RoundTripper
v := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v.Set("email", "some-email@example.org")
@@ -1373,7 +1373,7 @@ func TestRecovery_WithContinueWith(t *testing.T) {
require.NoError(t, err)
// Add the authentication to the request
- client.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewAuthorizedTransport(t, reg, session), t).RoundTripper
+ client.Transport = testhelpers.NewTransportWithLogger(testhelpers.NewAuthorizedTransport(t, ctx, reg, session), t).RoundTripper
v := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v.Set("email", "some-email@example.org")
diff --git a/selfservice/strategy/code/strategy_registration_test.go b/selfservice/strategy/code/strategy_registration_test.go
index 27c645a94190..0b6caaa15da1 100644
--- a/selfservice/strategy/code/strategy_registration_test.go
+++ b/selfservice/strategy/code/strategy_registration_test.go
@@ -15,6 +15,8 @@ import (
"strings"
"testing"
+ "github.com/ory/kratos/selfservice/flow"
+
"github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
@@ -37,6 +39,7 @@ type state struct {
email string
testServer *httptest.Server
resultIdentity *identity.Identity
+ body string
}
func TestRegistrationCodeStrategyDisabled(t *testing.T) {
@@ -172,6 +175,7 @@ func TestRegistrationCodeStrategy(t *testing.T) {
values.Set("method", "code")
body, resp := testhelpers.RegistrationMakeRequest(t, apiType == ApiTypeNative, apiType == ApiTypeSPA, rf, s.client, testhelpers.EncodeFormAsJSON(t, apiType == ApiTypeNative, values))
+ s.body = body
if submitAssertion != nil {
submitAssertion(ctx, t, s, body, resp)
@@ -213,6 +217,7 @@ func TestRegistrationCodeStrategy(t *testing.T) {
vals(&values)
body, resp := testhelpers.RegistrationMakeRequest(t, apiType == ApiTypeNative, apiType == ApiTypeSPA, rf, s.client, testhelpers.EncodeFormAsJSON(t, apiType == ApiTypeNative, values))
+ s.body = body
if submitAssertion != nil {
submitAssertion(ctx, t, s, body, resp)
@@ -240,7 +245,7 @@ func TestRegistrationCodeStrategy(t *testing.T) {
t.Parallel()
ctx := context.Background()
- _, reg, public := setup(ctx, t)
+ conf, reg, public := setup(ctx, t)
for _, tc := range []struct {
d string
@@ -279,6 +284,15 @@ func TestRegistrationCodeStrategy(t *testing.T) {
state = submitOTP(ctx, t, reg, state, func(v *url.Values) {
v.Set("code", registrationCode)
}, tc.apiType, nil)
+
+ if tc.apiType == ApiTypeSPA {
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(state.body, "continue_with.0.action").String(), "%s", state.body)
+ assert.Contains(t, gjson.Get(state.body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", state.body)
+ } else if tc.apiType == ApiTypeSPA {
+ assert.Empty(t, gjson.Get(state.body, "continue_with").Array(), "%s", state.body)
+ } else if tc.apiType == ApiTypeNative {
+ assert.NotContains(t, gjson.Get(state.body, "continue_with").Raw, string(flow.ContinueWithActionRedirectBrowserToString), "%s", state.body)
+ }
})
t.Run("case=should normalize email address on sign up", func(t *testing.T) {
diff --git a/selfservice/strategy/code/test/persistence.go b/selfservice/strategy/code/test/persistence.go
index f3c120402ddb..d7600e9fbd9a 100644
--- a/selfservice/strategy/code/test/persistence.go
+++ b/selfservice/strategy/code/test/persistence.go
@@ -8,6 +8,8 @@ import (
"testing"
"time"
+ confighelpers "github.com/ory/kratos/driver/config/testhelpers"
+
"github.com/ory/kratos/internal/testhelpers"
"github.com/ory/kratos/persistence"
"github.com/ory/kratos/selfservice/flow"
@@ -24,15 +26,14 @@ import (
"github.com/ory/kratos/x"
)
-func TestPersister(ctx context.Context, conf *config.Config, p interface {
+func TestPersister(ctx context.Context, p interface {
persistence.Persister
},
) func(t *testing.T) {
return func(t *testing.T) {
nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p)
- testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
- conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"})
+ ctx := confighelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"})
t.Run("code=recovery", func(t *testing.T) {
newRecoveryCodeDTO := func(t *testing.T, email string) (*code.CreateRecoveryCodeParams, *recovery.Flow, *identity.RecoveryAddress) {
@@ -51,7 +52,7 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface {
require.NoError(t, p.CreateIdentity(ctx, &i))
return &code.CreateRecoveryCodeParams{
- RawCode: string(randx.MustString(8, randx.Numeric)),
+ RawCode: randx.MustString(8, randx.Numeric),
FlowID: f.ID,
RecoveryAddress: &i.RecoveryAddresses[0],
ExpiresIn: time.Minute,
diff --git a/selfservice/strategy/idfirst/.schema/login.schema.json b/selfservice/strategy/idfirst/.schema/login.schema.json
new file mode 100644
index 000000000000..02ebd4ca9d9f
--- /dev/null
+++ b/selfservice/strategy/idfirst/.schema/login.schema.json
@@ -0,0 +1,24 @@
+{
+ "$id": "https://schemas.ory.sh/kratos/selfservice/strategy/identity_disovery/login.schema.json",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "properties": {
+ "csrf_token": {
+ "type": "string"
+ },
+ "identifier": {
+ "type": "string",
+ "minLength": 1
+ },
+ "method": {
+ "type": "string",
+ "enum": [
+ "identifier_first"
+ ]
+ },
+ "transient_payload": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+}
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
new file mode 100644
index 000000000000..086d65ade752
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "identifier_first",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070004,
+ "text": "ID",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "identifier_first",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "identifier_first",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070009,
+ "text": "Continue",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/idfirst/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/idfirst/schema.go b/selfservice/strategy/idfirst/schema.go
new file mode 100644
index 000000000000..77a0d37f54d7
--- /dev/null
+++ b/selfservice/strategy/idfirst/schema.go
@@ -0,0 +1,11 @@
+// Copyright © 2023 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package idfirst
+
+import (
+ _ "embed"
+)
+
+//go:embed .schema/login.schema.json
+var loginSchema []byte
diff --git a/selfservice/strategy/idfirst/strategy.go b/selfservice/strategy/idfirst/strategy.go
new file mode 100644
index 000000000000..b4590ce45634
--- /dev/null
+++ b/selfservice/strategy/idfirst/strategy.go
@@ -0,0 +1,68 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package idfirst
+
+import (
+ "context"
+
+ "github.com/go-playground/validator/v10"
+
+ "github.com/ory/kratos/driver/config"
+ "github.com/ory/kratos/identity"
+ "github.com/ory/kratos/selfservice/flow/login"
+ "github.com/ory/kratos/session"
+ "github.com/ory/kratos/ui/node"
+ "github.com/ory/kratos/x"
+ "github.com/ory/x/decoderx"
+)
+
+type dependencies interface {
+ x.LoggingProvider
+ x.WriterProvider
+ x.CSRFTokenGeneratorProvider
+ x.CSRFProvider
+
+ config.Provider
+
+ identity.PrivilegedPoolProvider
+ login.StrategyProvider
+ login.FlowPersistenceProvider
+}
+
+type Strategy struct {
+ d dependencies
+ v *validator.Validate
+ hd *decoderx.HTTP
+}
+
+func NewStrategy(d any) *Strategy {
+ return &Strategy{
+ d: d.(dependencies),
+ v: validator.New(),
+ hd: decoderx.NewHTTP(),
+ }
+}
+
+func (s *Strategy) CountActiveFirstFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) {
+ return 0, nil
+}
+
+func (s *Strategy) CountActiveMultiFactorCredentials(cc map[identity.CredentialsType]identity.Credentials) (count int, err error) {
+ return 0, nil
+}
+
+func (s *Strategy) ID() identity.CredentialsType {
+ return identity.CredentialsType(node.IdentifierFirstGroup)
+}
+
+func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context, _ session.AuthenticationMethods) session.AuthenticationMethod {
+ return session.AuthenticationMethod{
+ Method: s.ID(),
+ AAL: identity.AuthenticatorAssuranceLevel1,
+ }
+}
+
+func (s *Strategy) NodeGroup() node.UiNodeGroup {
+ return node.IdentifierFirstGroup
+}
diff --git a/selfservice/strategy/idfirst/strategy_login.go b/selfservice/strategy/idfirst/strategy_login.go
new file mode 100644
index 000000000000..d479ae1a5b05
--- /dev/null
+++ b/selfservice/strategy/idfirst/strategy_login.go
@@ -0,0 +1,187 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package idfirst
+
+import (
+ "net/http"
+
+ "github.com/ory/kratos/schema"
+
+ "github.com/pkg/errors"
+
+ "github.com/ory/kratos/identity"
+ "github.com/ory/kratos/selfservice/flow"
+ "github.com/ory/kratos/selfservice/flow/login"
+ "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/decoderx"
+ "github.com/ory/x/sqlcon"
+)
+
+var (
+ _ login.FormHydrator = new(Strategy)
+ _ login.Strategy = new(Strategy)
+ ErrNoCredentialsFound = errors.New("no credentials found")
+)
+
+func (s *Strategy) handleLoginError(w http.ResponseWriter, r *http.Request, f *login.Flow, payload *updateLoginFlowWithIdentifierFirstMethod, err error) error {
+ if f != nil {
+ f.UI.Nodes.SetValueAttribute("identifier", payload.Identifier)
+ if f.Type == flow.TypeBrowser {
+ f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
+ }
+ }
+
+ return err
+}
+
+func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow, _ *session.Session) (_ *identity.Identity, err error) {
+ if !s.d.Config().SelfServiceLoginFlowIdentifierFirstEnabled(r.Context()) {
+ return nil, errors.WithStack(flow.ErrStrategyNotResponsible)
+ }
+
+ if err := login.CheckAAL(f, identity.AuthenticatorAssuranceLevel1); err != nil {
+ return nil, err
+ }
+
+ var p updateLoginFlowWithIdentifierFirstMethod
+ if err := s.hd.Decode(r, &p,
+ decoderx.HTTPDecoderSetValidatePayloads(true),
+ decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema),
+ decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil {
+ return nil, s.handleLoginError(w, r, f, &p, err)
+ }
+ f.TransientPayload = p.TransientPayload
+
+ if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil {
+ return nil, s.handleLoginError(w, r, f, &p, err)
+ }
+
+ var opts []login.FormHydratorModifier
+
+ // Look up the user by the identifier.
+ identityHint, err := s.d.PrivilegedIdentityPool().FindIdentityByCredentialIdentifier(r.Context(), p.Identifier,
+ // We are dealing with user input -> lookup should be case-insensitive.
+ false,
+ )
+ if errors.Is(err, sqlcon.ErrNoRows) {
+ // If the user is not found, we still want to potentially show the UI for some method. That's why we don't exit here.
+ // We have to mitigate account enumeration. So we continue without setting the identity hint.
+ //
+ // This will later be handled by `didPopulate`.
+ } else if err != nil {
+ // An error happened during lookup
+ return nil, s.handleLoginError(w, r, f, &p, err)
+ } else if !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
+ // Hydrate credentials
+ if err := s.d.PrivilegedIdentityPool().HydrateIdentityAssociations(r.Context(), identityHint, identity.ExpandCredentials); err != nil {
+ return nil, s.handleLoginError(w, r, f, &p, err)
+ }
+ }
+
+ f.UI.ResetMessages()
+ f.UI.Nodes.SetValueAttribute("identifier", p.Identifier)
+
+ // Add identity hint
+ opts = append(opts, login.WithIdentityHint(identityHint))
+ opts = append(opts, login.WithIdentifier(p.Identifier))
+
+ didPopulate := false
+ for _, ls := range s.d.LoginStrategies(r.Context()) {
+ populator, ok := ls.(login.FormHydrator)
+ if !ok {
+ continue
+ }
+
+ if err := populator.PopulateLoginMethodIdentifierFirstCredentials(r, f, opts...); errors.Is(err, login.ErrBreakLoginPopulate) {
+ didPopulate = true
+ break
+ } else if errors.Is(err, ErrNoCredentialsFound) {
+ // This strategy is not responsible for this flow. We do not set didPopulate to true if that happens.
+ } else if err != nil {
+ return nil, s.handleLoginError(w, r, f, &p, err)
+ } else {
+ didPopulate = true
+ }
+ }
+
+ // If no strategy populated, it means that the account (very likely) does not exist. We show a user not found error,
+ // but only if account enumeration mitigation is disabled. Otherwise, we proceed to render the rest of the form.
+ if !didPopulate && !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
+ return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewAccountNotFoundError()))
+ }
+
+ // We found credentials - hide the identifier.
+ f.UI.GetNodes().RemoveMatching(node.NewInputField("method", s.ID(), s.NodeGroup(), node.InputAttributeTypeSubmit))
+
+ // We set the identifier to hidden, so it's still available in the form but not visible to the user.
+ for k, n := range f.UI.Nodes {
+ if n.ID() != "identifier" {
+ continue
+ }
+
+ attrs, ok := f.UI.Nodes[k].Attributes.(*node.InputAttributes)
+ if !ok {
+ continue
+ }
+
+ attrs.Type = node.InputAttributeTypeHidden
+ f.UI.Nodes[k].Attributes = attrs
+ }
+
+ f.Active = s.ID()
+ if err = s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil {
+ return nil, s.handleLoginError(w, r, f, &p, err)
+ }
+
+ if x.IsJSONRequest(r) {
+ s.d.Writer().WriteCode(w, r, http.StatusBadRequest, f)
+ } else {
+ http.Redirect(w, r, f.AppendTo(s.d.Config().SelfServiceFlowLoginUI(r.Context())).String(), http.StatusSeeOther)
+ }
+
+ return nil, flow.ErrCompletedByStrategy
+}
+
+func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, f *login.Flow) error {
+ f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
+
+ ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context())
+ if err != nil {
+ return err
+ }
+
+ identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String())
+ if err != nil {
+ return err
+ }
+
+ f.UI.SetNode(node.NewInputField("identifier", "", s.NodeGroup(), node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel))
+ f.UI.GetNodes().Append(node.NewInputField("method", s.ID(), s.NodeGroup(), node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue()))
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(_ *http.Request, f *login.Flow, opts ...login.FormHydratorModifier) error {
+ return ErrNoCredentialsFound
+}
+
+func (s *Strategy) RegisterLoginRoutes(_ *x.RouterPublic) {}
diff --git a/selfservice/strategy/idfirst/strategy_login_test.go b/selfservice/strategy/idfirst/strategy_login_test.go
new file mode 100644
index 000000000000..4b69a2b44fd4
--- /dev/null
+++ b/selfservice/strategy/idfirst/strategy_login_test.go
@@ -0,0 +1,594 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package idfirst_test
+
+import (
+ "bytes"
+ "context"
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+ "time"
+
+ "github.com/ory/kratos/selfservice/strategy/oidc"
+
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+
+ configtesthelpers "github.com/ory/kratos/driver/config/testhelpers"
+
+ "github.com/gofrs/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/tidwall/gjson"
+
+ kratos "github.com/ory/kratos/internal/httpclient"
+ "github.com/ory/kratos/text"
+ "github.com/ory/kratos/x"
+ "github.com/ory/x/assertx"
+ "github.com/ory/x/ioutilx"
+ "github.com/ory/x/urlx"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/ory/kratos/driver/config"
+ "github.com/ory/kratos/identity"
+ "github.com/ory/kratos/internal"
+ "github.com/ory/kratos/internal/testhelpers"
+ "github.com/ory/kratos/selfservice/flow"
+ "github.com/ory/kratos/selfservice/flow/login"
+ "github.com/ory/kratos/ui/node"
+ "github.com/ory/x/snapshotx"
+)
+
+//go:embed stub/default.schema.json
+var loginSchema []byte
+
+func TestCompleteLogin(t *testing.T) {
+ ctx := context.Background()
+ conf, reg := internal.NewFastRegistryWithMocks(t)
+
+ // We enable the password method to test the identifier first strategy
+
+ // ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true})
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true})
+
+ // ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceLoginFlowStyle, "identifier_first")
+ conf.MustSet(ctx, config.ViperKeySelfServiceLoginFlowStyle, "identifier_first")
+
+ router := x.NewRouterPublic()
+ publicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, x.NewRouterAdmin())
+
+ errTS := testhelpers.NewErrorTestServer(t, reg)
+ uiTS := testhelpers.NewLoginUIFlowEchoServer(t, reg)
+ redirTS := testhelpers.NewRedirSessionEchoTS(t, reg)
+
+ // Overwrite these two:
+ // ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceErrorUI, errTS.URL+"/error-ts")
+ conf.MustSet(ctx, config.ViperKeySelfServiceErrorUI, errTS.URL+"/error-ts")
+
+ // ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceLoginUI, uiTS.URL+"/login-ts")
+ conf.MustSet(ctx, config.ViperKeySelfServiceLoginUI, uiTS.URL+"/login-ts")
+
+ // ctx = testhelpers.WithDefaultIdentitySchemaFromRaw(ctx, loginSchema)
+ testhelpers.SetDefaultIdentitySchemaFromRaw(conf, loginSchema)
+
+ // ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"})
+ conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"})
+
+ //ensureFieldsExist := func(t *testing.T, body []byte) {
+ // registrationhelpers.CheckFormContent(t, body, "identifier",
+ // "password",
+ // "csrf_token")
+ //}
+
+ apiClient := testhelpers.NewDebugClient(t)
+
+ t.Run("case=should show the error ui because the request payload is malformed", func(t *testing.T) {
+ t.Run("type=api", func(t *testing.T) {
+ f := testhelpers.InitializeLoginFlowViaAPIWithContext(t, ctx, apiClient, publicTS, false)
+
+ body, res := testhelpers.LoginMakeRequestWithContext(t, ctx, true, false, f, apiClient, "14=)=!(%)$/ZP()GHIÖ")
+ assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
+ assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
+ assert.Contains(t, body, `Expected JSON sent in request body to be an object but got: Number`)
+ })
+
+ t.Run("type=browser", func(t *testing.T) {
+ browserClient := testhelpers.NewClientWithCookies(t)
+ f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false, testhelpers.InitFlowWithContext(ctx))
+
+ body, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, false, f, browserClient, "14=)=!(%)$/ZP()GHIÖ")
+ assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login-ts")
+ assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
+ assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "invalid URL escape", "%s", body)
+ })
+
+ t.Run("type=spa", func(t *testing.T) {
+ browserClient := testhelpers.NewClientWithCookies(t)
+ f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false, testhelpers.InitFlowWithContext(ctx))
+
+ body, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, true, f, browserClient, "14=)=!(%)$/ZP()GHIÖ")
+ assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
+ assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
+ assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "invalid URL escape", "%s", body)
+ })
+ })
+
+ t.Run("case=should fail because identifier first can not handle AAL2", func(t *testing.T) {
+ f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false)
+
+ update, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id))
+ require.NoError(t, err)
+ update.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(context.Background(), update))
+
+ req, err := http.NewRequest("POST", f.Ui.Action, bytes.NewBufferString(`{"method":"identifier_first"}`))
+ require.NoError(t, err)
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("Content-Type", "application/json")
+
+ actual, res := testhelpers.MockMakeAuthenticatedRequest(t, reg, conf, router.Router, req)
+ assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
+ assert.Equal(t, text.NewErrorValidationLoginNoStrategyFound().Text, gjson.GetBytes(actual, "ui.messages.0.text").String())
+ })
+
+ t.Run("should return an error because the request does not exist", func(t *testing.T) {
+ check := func(t *testing.T, actual string) {
+ assert.Equal(t, int64(http.StatusNotFound), gjson.Get(actual, "code").Int(), "%s", actual)
+ assert.Equal(t, "Not Found", gjson.Get(actual, "status").String(), "%s", actual)
+ assert.Contains(t, gjson.Get(actual, "message").String(), "Unable to locate the resource", "%s", actual)
+ }
+
+ fakeFlow := &kratos.LoginFlow{
+ Ui: kratos.UiContainer{
+ Action: publicTS.URL + login.RouteSubmitFlow + "?flow=" + x.NewUUID().String(),
+ },
+ }
+
+ t.Run("type=api", func(t *testing.T) {
+ actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, true, false, fakeFlow, apiClient, "{}")
+ assert.Len(t, res.Cookies(), 0)
+ assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
+ check(t, gjson.Get(actual, "error").Raw)
+ })
+
+ t.Run("type=browser", func(t *testing.T) {
+ browserClient := testhelpers.NewClientWithCookies(t)
+ actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, false, fakeFlow, browserClient, "")
+ assert.Contains(t, res.Request.URL.String(), errTS.URL)
+ check(t, actual)
+ })
+
+ t.Run("type=api", func(t *testing.T) {
+ actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, true, fakeFlow, apiClient, "{}")
+ assert.Len(t, res.Cookies(), 0)
+ assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
+ check(t, gjson.Get(actual, "error").Raw)
+ })
+ })
+
+ t.Run("case=should return an error because the request is expired", func(t *testing.T) {
+ conf.MustSet(ctx, config.ViperKeySelfServiceLoginRequestLifespan, time.Millisecond*10)
+ conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+ t.Cleanup(func() {
+ conf.MustSet(ctx, config.ViperKeySelfServiceLoginRequestLifespan, time.Hour)
+ conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, nil)
+ })
+
+ values := url.Values{
+ "csrf_token": {x.FakeCSRFToken},
+ "identifier": {"identifier"},
+ "method": {"identifier_first"},
+ }
+
+ t.Run("type=api", func(t *testing.T) {
+ f := testhelpers.InitializeLoginFlowViaAPIWithContext(t, ctx, apiClient, publicTS, false)
+
+ time.Sleep(time.Millisecond * 60)
+ actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values))
+ assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
+ assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.Get(actual, "use_flow_id").String())
+ assertx.EqualAsJSONExcept(t, flow.NewFlowExpiredError(time.Now()), json.RawMessage(actual), []string{"use_flow_id", "since", "expired_at"}, "expired", "%s", actual)
+ })
+
+ t.Run("type=browser", func(t *testing.T) {
+ browserClient := testhelpers.NewClientWithCookies(t)
+ f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false)
+
+ time.Sleep(time.Millisecond * 60)
+ actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, false, f, browserClient, values.Encode())
+ assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login-ts")
+ assert.NotEqual(t, f.Id, gjson.Get(actual, "id").String(), "%s", actual)
+ assert.Contains(t, gjson.Get(actual, "ui.messages.0.text").String(), "expired", "%s", actual)
+ })
+
+ t.Run("type=SPA", func(t *testing.T) {
+ browserClient := testhelpers.NewClientWithCookies(t)
+ f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false)
+
+ time.Sleep(time.Millisecond * 60)
+ actual, res := testhelpers.LoginMakeRequestWithContext(t, ctx, false, true, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values))
+ assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
+ assert.NotEqual(t, "00000000-0000-0000-0000-000000000000", gjson.Get(actual, "use_flow_id").String())
+ assertx.EqualAsJSONExcept(t, flow.NewFlowExpiredError(time.Now()), json.RawMessage(actual), []string{"use_flow_id", "since", "expired_at"}, "expired", "%s", actual)
+ })
+ })
+
+ t.Run("case=should have correct CSRF behavior", func(t *testing.T) {
+ conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+ t.Cleanup(func() {
+ conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, nil)
+ })
+
+ values := url.Values{
+ "method": {"identifier_first"},
+ "csrf_token": {"invalid_token"},
+ "identifier": {"login-identifier-csrf-browser"},
+ }
+
+ t.Run("case=should fail because of missing CSRF token/type=browser", func(t *testing.T) {
+ browserClient := testhelpers.NewClientWithCookies(t)
+ f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false)
+
+ actual, res := testhelpers.LoginMakeRequest(t, false, false, f, browserClient, values.Encode())
+ assert.EqualValues(t, http.StatusOK, res.StatusCode)
+ assertx.EqualAsJSON(t, x.ErrInvalidCSRFToken,
+ json.RawMessage(actual), "%s", actual)
+ })
+
+ t.Run("case=should fail because of missing CSRF token/type=spa", func(t *testing.T) {
+ browserClient := testhelpers.NewClientWithCookies(t)
+ f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false)
+
+ actual, res := testhelpers.LoginMakeRequest(t, false, true, f, browserClient, values.Encode())
+ assert.EqualValues(t, http.StatusForbidden, res.StatusCode)
+ assertx.EqualAsJSON(t, x.ErrInvalidCSRFToken,
+ json.RawMessage(gjson.Get(actual, "error").Raw), "%s", actual)
+ })
+
+ t.Run("case=should pass even without CSRF token/type=api", func(t *testing.T) {
+ f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false)
+
+ actual, res := testhelpers.LoginMakeRequest(t, true, false, f, apiClient, testhelpers.EncodeFormAsJSON(t, true, values))
+ assert.EqualValues(t, http.StatusBadRequest, res.StatusCode)
+ assert.Contains(t, actual, "1010022")
+ })
+
+ t.Run("case=should fail with correct CSRF error cause/type=api", func(t *testing.T) {
+ for k, tc := range []struct {
+ mod func(http.Header)
+ exp string
+ }{
+ {
+ mod: func(h http.Header) {
+ h.Add("Cookie", "name=bar")
+ },
+ exp: "The HTTP Request Header included the \\\"Cookie\\\" key",
+ },
+ {
+ mod: func(h http.Header) {
+ h.Add("Origin", "www.bar.com")
+ },
+ exp: "The HTTP Request Header included the \\\"Origin\\\" key",
+ },
+ } {
+ t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
+ f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false)
+
+ req := testhelpers.NewRequest(t, true, "POST", f.Ui.Action, bytes.NewBufferString(testhelpers.EncodeFormAsJSON(t, true, values)))
+ tc.mod(req.Header)
+
+ res, err := apiClient.Do(req)
+ require.NoError(t, err)
+ defer res.Body.Close()
+
+ actual := string(ioutilx.MustReadAll(res.Body))
+ assert.EqualValues(t, http.StatusBadRequest, res.StatusCode)
+ assert.Contains(t, actual, tc.exp)
+ })
+ }
+ })
+ })
+
+ expectValidationError := func(t *testing.T, isAPI, refresh, isSPA bool, values func(url.Values)) string {
+ return testhelpers.SubmitLoginForm(t, isAPI, nil, publicTS, values,
+ isSPA, refresh,
+ testhelpers.ExpectStatusCode(isAPI || isSPA, http.StatusBadRequest, http.StatusOK),
+ testhelpers.ExpectURL(isAPI || isSPA, publicTS.URL+login.RouteSubmitFlow, conf.SelfServiceFlowLoginUI(ctx).String()))
+ }
+
+ t.Run("should return an error because the user does not exist", func(t *testing.T) {
+ // In this test we check if the account mitigation behaves correctly by enabling all login strategies EXCEPT
+ // for the passwordless code strategy. That is because this strategy always shows the login button.
+
+ testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true)
+
+ testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeOIDC.String(), true)
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config", &oidc.ConfigurationCollection{Providers: []oidc.Configuration{
+ {
+ ID: "google",
+ Provider: "google",
+ Label: "Google",
+ ClientID: "a",
+ ClientSecret: "b",
+ Mapper: "file://",
+ },
+ }})
+
+ testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeWebAuthn.String(), true)
+ conf.MustSet(ctx, config.ViperKeyWebAuthnPasswordless, true)
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.display_name", "Ory Corp")
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.id", "localhost")
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config.rp.origin", "http://localhost:4455")
+
+ testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePasskey.String(), true)
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".enabled", true)
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config.rp.display_name", "Ory Corp")
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config.rp.id", "localhost")
+ conf.MustSet(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config.rp.origins", []string{"http://localhost:4455"})
+
+ t.Cleanup(func() {
+ conf.MustSet(ctx, "selfservice.methods.password", nil)
+ conf.MustSet(ctx, "selfservice.methods.oidc", nil)
+ conf.MustSet(ctx, "selfservice.methods.passkey", nil)
+ conf.MustSet(ctx, "selfservice.methods.webauthn", nil)
+ conf.MustSet(ctx, "selfservice.methods.code", nil)
+ })
+
+ t.Run("account enumeration mitigation enabled", func(t *testing.T) {
+ conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+
+ t.Cleanup(func() {
+ conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, nil)
+ })
+
+ check := func(t *testing.T, body string, isAPI bool) {
+ t.Logf("%s", body)
+ if !isAPI {
+ assert.Contains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginWebAuthn), "we do expect to see a webauthn trigger:\n%s", body)
+ assert.Contains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginPasskey), "we do expect to see a passkey trigger button:\n%s", body)
+ }
+
+ assert.Equal(t, "hidden", gjson.Get(body, "ui.nodes.#(attributes.name==identifier).attributes.type").String(), "identifier is hidden to appear that we found an identity even though we did not")
+
+ assert.NotContains(t, body, text.NewErrorValidationAccountNotFound().Text, "we do not expect to see an account not found error:\n%s", body)
+
+ assert.Contains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginPassword), "we do expect to see a password trigger:\n%s", body)
+
+ // We do expect to see the same social sign in buttons that were on the first page:
+ assert.Contains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginWith), "we do expect to see a oidc trigger:\n%s", body)
+ assert.Contains(t, body, "google", "we do expect to see a google trigger:\n%s", body)
+ }
+
+ values := func(v url.Values) {
+ v.Set("identifier", "identifier")
+ v.Set("method", "identifier_first")
+ }
+
+ t.Run("type=browser", func(t *testing.T) {
+ check(t, expectValidationError(t, false, false, false, values), false)
+ })
+
+ t.Run("type=SPA", func(t *testing.T) {
+ check(t, expectValidationError(t, false, false, true, values), false)
+ })
+
+ t.Run("type=api", func(t *testing.T) {
+ check(t, expectValidationError(t, true, false, false, values), true)
+ })
+ })
+
+ t.Run("account enumeration mitigation disabled", func(t *testing.T) {
+ conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false)
+ t.Cleanup(func() {
+ conf.MustSet(ctx, config.ViperKeySecurityAccountEnumerationMitigate, nil)
+ })
+
+ check := func(t *testing.T, body string) {
+ t.Logf("%s", body)
+
+ assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
+ assert.Contains(t, gjson.Get(body, "ui.action").String(), publicTS.URL+login.RouteSubmitFlow, "%s", body)
+ assert.Contains(t, body, text.NewErrorValidationAccountNotFound().Text, "we do expect to see an error that the account does not exist: %s", body)
+
+ assert.Equal(t, "text", gjson.Get(body, "ui.nodes.#(attributes.name==identifier).attributes.type").String(), "identifier is not hidden and we can see the input field as well")
+
+ assert.NotContains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginPasskey), "we do not expect to see a passkey trigger button: %s", body)
+ assert.NotContains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginWebAuthn), "we do not expect to see a webauthn trigger: %s", body)
+ assert.NotContains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginPassword), "we do not expect to see a password trigger: %s", body)
+
+ assert.NotContains(t, body, fmt.Sprintf("%d", text.InfoSelfServiceLoginWith), "we do not expect to see a oidc trigger: %s", body)
+ assert.NotContains(t, body, "google", "we do not expect to see a google trigger: %s", body)
+ }
+
+ values := func(v url.Values) {
+ v.Set("identifier", "identifier")
+ v.Set("method", "identifier_first")
+ }
+
+ t.Run("type=browser", func(t *testing.T) {
+ check(t, expectValidationError(t, false, false, false, values))
+ })
+
+ t.Run("type=SPA", func(t *testing.T) {
+ check(t, expectValidationError(t, false, false, true, values))
+ })
+
+ t.Run("type=api", func(t *testing.T) {
+ check(t, expectValidationError(t, true, false, false, values))
+ })
+ })
+ })
+
+ t.Run("should pass with real request", func(t *testing.T) {
+ identifier, pwd := x.NewUUID().String(), "password"
+ createIdentity(ctx, reg, t, identifier, pwd)
+
+ firstValues := func(v url.Values) {
+ v.Set("identifier", identifier)
+ v.Set("method", "identifier_first")
+ }
+
+ secondValues := func(v url.Values) {
+ v.Set("identifier", identifier)
+ v.Set("password", pwd)
+ v.Set("method", "password")
+ }
+
+ t.Run("type=browser", func(t *testing.T) {
+ browserClient := testhelpers.NewClientWithCookies(t)
+
+ secondStep := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, firstValues,
+ true, false, http.StatusBadRequest, publicTS.URL+login.RouteSubmitFlow)
+ t.Logf("secondStep: %s", secondStep)
+ assert.Contains(t, secondStep, "current-password")
+ assert.Contains(t, secondStep, `"value":"password"`)
+
+ body := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, secondValues,
+ false, false, http.StatusOK, redirTS.URL)
+
+ assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body)
+ })
+
+ t.Run("type=spa", func(t *testing.T) {
+ hc := testhelpers.NewClientWithCookies(t)
+
+ secondStep := testhelpers.SubmitLoginForm(t, false, hc, publicTS, firstValues,
+ true, false, http.StatusBadRequest, publicTS.URL+login.RouteSubmitFlow)
+ t.Logf("secondStep: %s", secondStep)
+ assert.Contains(t, secondStep, "current-password")
+ assert.Contains(t, secondStep, `"value":"password"`)
+
+ body := testhelpers.SubmitLoginForm(t, false, hc, publicTS, secondValues,
+ true, false, http.StatusOK, publicTS.URL+login.RouteSubmitFlow)
+
+ assert.Equal(t, identifier, gjson.Get(body, "session.identity.traits.subject").String(), "%s", body)
+ assert.Empty(t, gjson.Get(body, "session_token").String(), "%s", body)
+ assert.Empty(t, gjson.Get(body, "session.token").String(), "%s", body)
+
+ // Was the session cookie set?
+ require.NotEmpty(t, hc.Jar.Cookies(urlx.ParseOrPanic(publicTS.URL)), "%+v", hc.Jar)
+ })
+
+ t.Run("type=api", func(t *testing.T) {
+ secondStep := testhelpers.SubmitLoginForm(t, true, nil, publicTS, firstValues,
+ false, false, http.StatusBadRequest, publicTS.URL+login.RouteSubmitFlow)
+ t.Logf("secondStep: %s", secondStep)
+ assert.Contains(t, secondStep, "current-password")
+ assert.Contains(t, secondStep, `"value":"password"`)
+
+ body := testhelpers.SubmitLoginForm(t, true, nil, publicTS, secondValues,
+ false, false, http.StatusOK, publicTS.URL+login.RouteSubmitFlow)
+
+ assert.Equal(t, identifier, gjson.Get(body, "session.identity.traits.subject").String(), "%s", body)
+ st := gjson.Get(body, "session_token").String()
+ assert.NotEmpty(t, st, "%s", body)
+ })
+ })
+}
+
+func TestFormHydration(t *testing.T) {
+ ctx := context.Background()
+ conf, reg := internal.NewFastRegistryWithMocks(t)
+ ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceLoginFlowStyle, "identifier_first")
+
+ ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://./stub/default.schema.json")
+ s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsType(node.IdentifierFirstGroup))
+ require.NoError(t, err)
+ fh, ok := s.(login.FormHydrator)
+ require.True(t, ok)
+
+ toSnapshot := func(t *testing.T, f *login.Flow) {
+ t.Helper()
+ // The CSRF token has a unique value that messes with the snapshot - ignore it.
+ f.UI.Nodes.ResetNodes("csrf_token")
+ snapshotx.SnapshotT(t, f.UI.Nodes)
+ }
+ newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) {
+ r := httptest.NewRequest("GET", "/self-service/login/browser", nil)
+ r = r.WithContext(ctx)
+ t.Helper()
+ f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser)
+ require.NoError(t, err)
+ return r, f
+ }
+
+ t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodRefresh", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) {
+ t.Run("case=no options", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=WithIdentifier", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=WithIdentityHint", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+
+ id := identity.NewIdentity("default")
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false)
+
+ t.Run("case=identity has password", func(t *testing.T) {
+ id := identity.NewIdentity("default")
+
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=identity does not have a password", func(t *testing.T) {
+ id := identity.NewIdentity("default")
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f))
+ toSnapshot(t, f)
+ })
+}
diff --git a/selfservice/strategy/idfirst/strategy_test.go b/selfservice/strategy/idfirst/strategy_test.go
new file mode 100644
index 000000000000..f6d483090abb
--- /dev/null
+++ b/selfservice/strategy/idfirst/strategy_test.go
@@ -0,0 +1,91 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package idfirst_test
+
+import (
+ "context"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/ory/kratos/driver"
+ "github.com/ory/kratos/x"
+ "github.com/ory/x/sqlxx"
+
+ "github.com/ory/kratos/internal"
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+ "github.com/ory/kratos/ui/node"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/ory/kratos/identity"
+ "github.com/ory/kratos/session"
+)
+
+func TestCountActiveFirstFactorCredentials(t *testing.T) {
+ _, reg := internal.NewFastRegistryWithMocks(t)
+ s := idfirst.NewStrategy(reg)
+ cc := make(map[identity.CredentialsType]identity.Credentials)
+
+ count, err := s.CountActiveFirstFactorCredentials(cc)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, count)
+}
+
+func TestCountActiveMultiFactorCredentials(t *testing.T) {
+ _, reg := internal.NewFastRegistryWithMocks(t)
+ s := idfirst.NewStrategy(reg)
+ cc := make(map[identity.CredentialsType]identity.Credentials)
+
+ count, err := s.CountActiveMultiFactorCredentials(cc)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, count)
+}
+
+func TestCompletedAuthenticationMethod(t *testing.T) {
+ _, reg := internal.NewFastRegistryWithMocks(t)
+ s := idfirst.NewStrategy(reg)
+ ctx := context.Background()
+
+ method := s.CompletedAuthenticationMethod(ctx, session.AuthenticationMethods{})
+ assert.Equal(t, s.ID(), method.Method)
+ assert.Equal(t, identity.AuthenticatorAssuranceLevel1, method.AAL)
+}
+
+func TestNodeGroup(t *testing.T) {
+ _, reg := internal.NewFastRegistryWithMocks(t)
+ s := idfirst.NewStrategy(reg)
+
+ group := s.NodeGroup()
+ assert.Equal(t, node.IdentifierFirstGroup, group)
+}
+
+func createIdentity(ctx context.Context, reg *driver.RegistryDefault, t *testing.T, identifier, password string) *identity.Identity {
+ p, _ := reg.Hasher(ctx).Generate(context.Background(), []byte(password))
+ iId := x.NewUUID()
+ id := &identity.Identity{
+ ID: iId,
+ Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)),
+ Credentials: map[identity.CredentialsType]identity.Credentials{
+ identity.CredentialsTypePassword: {
+ Type: identity.CredentialsTypePassword,
+ Identifiers: []string{identifier},
+ Config: sqlxx.JSONRawMessage(`{"hashed_password":"` + string(p) + `"}`),
+ },
+ },
+ VerifiableAddresses: []identity.VerifiableAddress{
+ {
+ ID: x.NewUUID(),
+ Value: identifier,
+ Verified: false,
+ CreatedAt: time.Now(),
+ IdentityID: iId,
+ },
+ },
+ }
+ require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), id))
+ return id
+}
diff --git a/selfservice/strategy/idfirst/stub/default.schema.json b/selfservice/strategy/idfirst/stub/default.schema.json
new file mode 100644
index 000000000000..8dc923266050
--- /dev/null
+++ b/selfservice/strategy/idfirst/stub/default.schema.json
@@ -0,0 +1,29 @@
+{
+ "$id": "https://example.com/person.schema.json",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Person",
+ "type": "object",
+ "properties": {
+ "traits": {
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string",
+ "ory.sh/kratos": {
+ "credentials": {
+ "password": {
+ "identifier": true
+ }
+ },
+ "verification": {
+ "via": "email"
+ },
+ "recovery": {
+ "via": "email"
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/selfservice/strategy/idfirst/types.go b/selfservice/strategy/idfirst/types.go
new file mode 100644
index 000000000000..a8838043782a
--- /dev/null
+++ b/selfservice/strategy/idfirst/types.go
@@ -0,0 +1,29 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package idfirst
+
+import "encoding/json"
+
+// Update Login Flow with Multi-Step Method
+//
+// swagger:model updateLoginFlowWithIdentifierFirstMethod
+type updateLoginFlowWithIdentifierFirstMethod struct {
+ // Method should be set to "password" when logging in using the identifier and password strategy.
+ //
+ // required: true
+ Method string `json:"method"`
+
+ // Sending the anti-csrf token is only required for browser login flows.
+ CSRFToken string `json:"csrf_token"`
+
+ // Identifier is the email or username of the user trying to log in.
+ //
+ // required: true
+ Identifier string `json:"identifier"`
+
+ // Transient data to pass along to any webhooks
+ //
+ // required: false
+ TransientPayload json.RawMessage `json:"transient_payload,omitempty" form:"transient_payload"`
+}
diff --git a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json
index 3bb3cbbf3ef6..5ac9946936c8 100644
--- a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json
+++ b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads.json
@@ -43,8 +43,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
},
diff --git a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json
index 498575cfee1b..1a8d048fe37d 100644
--- a/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json
+++ b/selfservice/strategy/link/.snapshots/TestRecovery-description=should_set_all_the_correct_recovery_payloads_after_submission.json
@@ -45,8 +45,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json
index 3bb3cbbf3ef6..5ac9946936c8 100644
--- a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json
+++ b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads.json
@@ -43,8 +43,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
},
diff --git a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json
index 498575cfee1b..1a8d048fe37d 100644
--- a/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json
+++ b/selfservice/strategy/link/.snapshots/TestVerification-description=should_set_all_the_correct_verification_payloads_after_submission.json
@@ -45,8 +45,8 @@
"messages": [],
"meta": {
"label": {
- "id": 1070005,
- "text": "Submit",
+ "id": 1070009,
+ "text": "Continue",
"type": "info"
}
}
diff --git a/selfservice/strategy/link/strategy_recovery.go b/selfservice/strategy/link/strategy_recovery.go
index 184399ca1002..e6d91051c2c4 100644
--- a/selfservice/strategy/link/strategy_recovery.go
+++ b/selfservice/strategy/link/strategy_recovery.go
@@ -56,7 +56,7 @@ func (s *Strategy) PopulateRecoveryMethod(r *http.Request, f *recovery.Flow) err
// v0.5: form.Field{Name: "email", Type: "email", Required: true},
node.NewInputField("email", nil, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()),
)
- f.UI.GetNodes().Append(node.NewInputField("method", s.RecoveryStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSubmit()))
+ f.UI.GetNodes().Append(node.NewInputField("method", s.RecoveryStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue()))
return nil
}
diff --git a/selfservice/strategy/link/strategy_recovery_test.go b/selfservice/strategy/link/strategy_recovery_test.go
index 7b56ca5f1728..6b9240a4325b 100644
--- a/selfservice/strategy/link/strategy_recovery_test.go
+++ b/selfservice/strategy/link/strategy_recovery_test.go
@@ -369,7 +369,7 @@ func TestRecovery(t *testing.T) {
v.Set("email", "some-email@example.org")
v.Set("method", "link")
- authClient := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, reg)
+ authClient := testhelpers.NewHTTPClientWithArbitrarySessionToken(t, ctx, reg)
if isAPI {
req := httptest.NewRequest("GET", "/sessions/whoami", nil)
s, err := session.NewActiveSession(req,
@@ -380,7 +380,7 @@ func TestRecovery(t *testing.T) {
identity.AuthenticatorAssuranceLevel1,
)
require.NoError(t, err)
- authClient = testhelpers.NewHTTPClientWithSessionCookieLocalhost(t, reg, s)
+ authClient = testhelpers.NewHTTPClientWithSessionCookieLocalhost(t, ctx, reg, s)
}
body, res := testhelpers.RecoveryMakeRequest(t, isAPI || isSPA, f, authClient, testhelpers.EncodeFormAsJSON(t, isAPI || isSPA, v))
@@ -675,7 +675,7 @@ func TestRecovery(t *testing.T) {
v.Set("email", email)
}
- cl := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ cl := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
check(t, expectSuccess(t, nil, false, false, values), email, cl, func(_ *http.Client, req *http.Request) (*http.Response, error) {
_, res := testhelpers.MockMakeAuthenticatedRequestWithClientAndID(t, reg, conf, publicRouter.Router, req, cl, id)
return res, nil
diff --git a/selfservice/strategy/link/strategy_verification.go b/selfservice/strategy/link/strategy_verification.go
index a2a72ea9a277..61f95da52fef 100644
--- a/selfservice/strategy/link/strategy_verification.go
+++ b/selfservice/strategy/link/strategy_verification.go
@@ -44,7 +44,7 @@ func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.F
// v0.5: form.Field{Name: "email", Type: "email", Required: true}
node.NewInputField("email", nil, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()),
)
- f.UI.GetNodes().Append(node.NewInputField("method", s.VerificationStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSubmit()))
+ f.UI.GetNodes().Append(node.NewInputField("method", s.VerificationStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue()))
return nil
}
diff --git a/selfservice/strategy/link/test/persistence.go b/selfservice/strategy/link/test/persistence.go
index af5738eaae31..c28250836775 100644
--- a/selfservice/strategy/link/test/persistence.go
+++ b/selfservice/strategy/link/test/persistence.go
@@ -8,6 +8,8 @@ import (
"testing"
"time"
+ confighelpers "github.com/ory/kratos/driver/config/testhelpers"
+
"github.com/ory/kratos/internal/testhelpers"
"github.com/ory/kratos/persistence"
"github.com/ory/kratos/selfservice/flow"
@@ -28,15 +30,14 @@ import (
"github.com/ory/kratos/x"
)
-func TestPersister(ctx context.Context, conf *config.Config, p interface {
+func TestPersister(ctx context.Context, p interface {
persistence.Persister
},
) func(t *testing.T) {
return func(t *testing.T) {
nid, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p)
- testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
- conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"})
+ ctx := confighelpers.WithConfigValue(ctx, config.ViperKeySecretsDefault, []string{"secret-a", "secret-b"})
t.Run("token=recovery", func(t *testing.T) {
newRecoveryToken := func(t *testing.T, email string) (*link.RecoveryToken, *recovery.Flow) {
diff --git a/selfservice/strategy/lookup/login_test.go b/selfservice/strategy/lookup/login_test.go
index c4896962c660..c746d744f059 100644
--- a/selfservice/strategy/lookup/login_test.go
+++ b/selfservice/strategy/lookup/login_test.go
@@ -14,6 +14,8 @@ import (
"testing"
"time"
+ "github.com/ory/kratos/selfservice/flow"
+
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -55,7 +57,7 @@ func TestCompleteLogin(t *testing.T) {
t.Run("case=lookup payload is set when identity has lookup", func(t *testing.T) {
id, _ := createIdentity(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"})
})
@@ -63,7 +65,7 @@ func TestCompleteLogin(t *testing.T) {
t.Run("case=lookup payload is not set when identity has no lookup", func(t *testing.T) {
id := createIdentityWithoutLookup(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
assertx.EqualAsJSON(t, nil, f.Ui.Nodes)
})
@@ -71,7 +73,7 @@ func TestCompleteLogin(t *testing.T) {
t.Run("case=lookup payload is not set when identity has no lookup", func(t *testing.T) {
id := createIdentityWithoutLookup(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
assertx.EqualAsJSON(t, nil, f.Ui.Nodes)
})
@@ -86,7 +88,7 @@ func TestCompleteLogin(t *testing.T) {
}
doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
return doAPIFlowWithClient(t, v, id, apiClient, false)
}
@@ -99,7 +101,7 @@ func TestCompleteLogin(t *testing.T) {
}
doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
return doBrowserFlowWithClient(t, spa, v, id, browserClient, false)
}
@@ -235,30 +237,35 @@ func TestCompleteLogin(t *testing.T) {
}
t.Run("type=api", func(t *testing.T) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
body, res := doAPIFlowWithClient(t, payload("key-0"), id, apiClient, false)
check(t, false, body, res, "key-0", 2)
// We can still use another key
body, res = doAPIFlowWithClient(t, payload("key-2"), id, apiClient, true)
check(t, false, body, res, "key-2", 3)
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
})
t.Run("type=browser", func(t *testing.T) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
body, res := doBrowserFlowWithClient(t, false, payload("key-3"), id, browserClient, false)
check(t, true, body, res, "key-3", 2)
// We can still use another key
body, res = doBrowserFlowWithClient(t, false, payload("key-5"), id, browserClient, true)
check(t, true, body, res, "key-5", 3)
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
})
t.Run("type=spa", func(t *testing.T) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
body, res := doBrowserFlowWithClient(t, true, payload("key-6"), id, browserClient, false)
check(t, false, body, res, "key-6", 2)
// We can still use another key
body, res = doBrowserFlowWithClient(t, true, payload("key-8"), id, browserClient, true)
check(t, false, body, res, "key-8", 3)
+
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
+ assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", body)
})
})
diff --git a/selfservice/strategy/lookup/settings_test.go b/selfservice/strategy/lookup/settings_test.go
index fce2be4c0974..101f5919d04a 100644
--- a/selfservice/strategy/lookup/settings_test.go
+++ b/selfservice/strategy/lookup/settings_test.go
@@ -111,7 +111,7 @@ func TestCompleteSettings(t *testing.T) {
conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"})
doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v(values)
@@ -120,7 +120,7 @@ func TestCompleteSettings(t *testing.T) {
}
doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v(values)
@@ -129,7 +129,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("case=hide recovery codes behind reveal button and show disable button", func(t *testing.T) {
id, _ := createIdentity(t, reg)
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
t.Run("case=spa", func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, true, publicTS)
@@ -142,7 +142,7 @@ func TestCompleteSettings(t *testing.T) {
})
t.Run("case=api", func(t *testing.T) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"})
})
@@ -150,7 +150,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("case=button for regeneration is displayed when identity has no recovery codes yet", func(t *testing.T) {
id := createIdentityWithoutLookup(t, reg)
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
t.Run("case=spa", func(t *testing.T) {
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, true, publicTS)
@@ -163,7 +163,7 @@ func TestCompleteSettings(t *testing.T) {
})
t.Run("case=api", func(t *testing.T) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{"0.attributes.value"})
})
@@ -389,7 +389,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
id, _ := createIdentity(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
@@ -410,7 +410,7 @@ func TestCompleteSettings(t *testing.T) {
runBrowser := func(t *testing.T, spa bool) {
id, _ := createIdentity(t, reg)
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
@@ -423,8 +423,11 @@ func TestCompleteSettings(t *testing.T) {
if spa {
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual)
+ assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", actual)
} else {
assert.Contains(t, res.Request.URL.String(), uiTS.URL)
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
}
assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String()))
@@ -480,7 +483,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
id, _ := createIdentity(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
@@ -498,7 +501,7 @@ func TestCompleteSettings(t *testing.T) {
runBrowser := func(t *testing.T, spa bool) {
id, _ := createIdentity(t, reg)
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
@@ -508,8 +511,11 @@ func TestCompleteSettings(t *testing.T) {
if spa {
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual)
+ assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", actual)
} else {
assert.Contains(t, res.Request.URL.String(), uiTS.URL)
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
}
assert.EqualValues(t, flow.StateSuccess, json.RawMessage(gjson.Get(actual, "state").String()))
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json
new file mode 100644
index 000000000000..9ce35531c24a
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json
@@ -0,0 +1,37 @@
+[
+ {
+ "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": "test-provider",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010002,
+ "text": "Sign in with test-provider",
+ "type": "info",
+ "context": {
+ "provider": "test-provider"
+ }
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json
new file mode 100644
index 000000000000..9ce35531c24a
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json
@@ -0,0 +1,37 @@
+[
+ {
+ "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": "test-provider",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010002,
+ "text": "Sign in with test-provider",
+ "type": "info",
+ "context": {
+ "provider": "test-provider"
+ }
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_oidc.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_oidc.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_oidc.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_oidc.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_oidc.json
new file mode 100644
index 000000000000..29f5e9aa1061
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_oidc.json
@@ -0,0 +1,24 @@
+[
+ {
+ "type": "input",
+ "group": "oidc",
+ "attributes": {
+ "name": "provider",
+ "type": "submit",
+ "value": "test-provider",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010002,
+ "text": "Sign in with test-provider",
+ "type": "info",
+ "context": {
+ "provider": "test-provider"
+ }
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
new file mode 100644
index 000000000000..9ce35531c24a
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
@@ -0,0 +1,37 @@
+[
+ {
+ "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": "test-provider",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010002,
+ "text": "Sign in with test-provider",
+ "type": "info",
+ "context": {
+ "provider": "test-provider"
+ }
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json
new file mode 100644
index 000000000000..9ce35531c24a
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh.json
@@ -0,0 +1,37 @@
+[
+ {
+ "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": "test-provider",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010002,
+ "text": "Sign in with test-provider",
+ "type": "info",
+ "context": {
+ "provider": "test-provider"
+ }
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json
new file mode 100644
index 000000000000..364b8abc331c
--- /dev/null
+++ b/selfservice/strategy/oidc/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json
@@ -0,0 +1,15 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json
index 19da7fb7f971..48d0280aff04 100644
--- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json
+++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=githuber.json
@@ -153,10 +153,10 @@
"meta": {
"label": {
"context": {
- "provider": "ory"
+ "provider": "Ory"
},
"id": 1050003,
- "text": "Unlink ory",
+ "text": "Unlink Ory",
"type": "info"
}
},
diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json
index dd0dc9e5f179..3b534e240899 100644
--- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json
+++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=multiuser.json
@@ -153,10 +153,10 @@
"meta": {
"label": {
"context": {
- "provider": "ory"
+ "provider": "Ory"
},
"id": 1050003,
- "text": "Unlink ory",
+ "text": "Unlink Ory",
"type": "info"
}
},
diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json
index 55909b7380a6..94db74f27534 100644
--- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json
+++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-case=should_adjust_linkable_providers_based_on_linked_credentials-agent=password.json
@@ -153,10 +153,10 @@
"meta": {
"label": {
"context": {
- "provider": "ory"
+ "provider": "Ory"
},
"id": 1050002,
- "text": "Link ory",
+ "text": "Link Ory",
"type": "info"
}
},
diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json
index b775cb07f8b3..37108bfe985a 100644
--- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json
+++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=fetch.json
@@ -153,10 +153,10 @@
"meta": {
"label": {
"context": {
- "provider": "ory"
+ "provider": "Ory"
},
"id": 1050003,
- "text": "Unlink ory",
+ "text": "Unlink Ory",
"type": "info"
}
},
diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json
index 19da7fb7f971..48d0280aff04 100644
--- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json
+++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=original.json
@@ -153,10 +153,10 @@
"meta": {
"label": {
"context": {
- "provider": "ory"
+ "provider": "Ory"
},
"id": 1050003,
- "text": "Unlink ory",
+ "text": "Unlink Ory",
"type": "info"
}
},
diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json
index cda03ca13acb..fc364efa9d90 100644
--- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json
+++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection-flow=response.json
@@ -154,10 +154,10 @@
"meta": {
"label": {
"id": 1050003,
- "text": "Unlink ory",
+ "text": "Unlink Ory",
"type": "info",
"context": {
- "provider": "ory"
+ "provider": "Ory"
}
}
}
diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json
index 1763aae80238..cc010fb2c206 100644
--- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json
+++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_link_a_connection_even_if_user_does_not_have_oidc_credentials_yet.json
@@ -153,10 +153,10 @@
"meta": {
"label": {
"context": {
- "provider": "ory"
+ "provider": "Ory"
},
"id": 1050002,
- "text": "Link ory",
+ "text": "Link Ory",
"type": "info"
}
},
diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json
index a8b9407aab8a..ddaaf6905c12 100644
--- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json
+++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=link-case=should_not_be_able_to_link_a_connection_which_already_exists.json
@@ -154,10 +154,10 @@
"meta": {
"label": {
"id": 1050003,
- "text": "Unlink ory",
+ "text": "Unlink Ory",
"type": "info",
"context": {
- "provider": "ory"
+ "provider": "Ory"
}
}
}
diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json
index 19da7fb7f971..48d0280aff04 100644
--- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json
+++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=fetch.json
@@ -153,10 +153,10 @@
"meta": {
"label": {
"context": {
- "provider": "ory"
+ "provider": "Ory"
},
"id": 1050003,
- "text": "Unlink ory",
+ "text": "Unlink Ory",
"type": "info"
}
},
diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json
index a8b9407aab8a..ddaaf6905c12 100644
--- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json
+++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_a_connection_not_yet_linked-flow=json.json
@@ -154,10 +154,10 @@
"meta": {
"label": {
"id": 1050003,
- "text": "Unlink ory",
+ "text": "Unlink Ory",
"type": "info",
"context": {
- "provider": "ory"
+ "provider": "Ory"
}
}
}
diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json
index 19da7fb7f971..48d0280aff04 100644
--- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json
+++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=fetch.json
@@ -153,10 +153,10 @@
"meta": {
"label": {
"context": {
- "provider": "ory"
+ "provider": "Ory"
},
"id": 1050003,
- "text": "Unlink ory",
+ "text": "Unlink Ory",
"type": "info"
}
},
diff --git a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json
index a8b9407aab8a..ddaaf6905c12 100644
--- a/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json
+++ b/selfservice/strategy/oidc/.snapshots/TestSettingsStrategy-suite=unlink-case=should_not_be_able_to_unlink_an_non-existing_connection-flow=json.json
@@ -154,10 +154,10 @@
"meta": {
"label": {
"id": 1050003,
- "text": "Unlink ory",
+ "text": "Unlink Ory",
"type": "info",
"context": {
- "provider": "ory"
+ "provider": "Ory"
}
}
}
diff --git a/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateLoginMethod.json b/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateLoginMethod.json
deleted file mode 100644
index bacf802cd191..000000000000
--- a/selfservice/strategy/oidc/.snapshots/TestStrategy-method=TestPopulateLoginMethod.json
+++ /dev/null
@@ -1,106 +0,0 @@
-{
- "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": 1010002,
- "text": "Sign in with valid",
- "type": "info",
- "context": {
- "provider": "valid"
- }
- }
- }
- },
- {
- "type": "input",
- "group": "oidc",
- "attributes": {
- "name": "provider",
- "type": "submit",
- "value": "secondProvider",
- "disabled": false,
- "node_type": "input"
- },
- "messages": [],
- "meta": {
- "label": {
- "id": 1010002,
- "text": "Sign in with secondProvider",
- "type": "info",
- "context": {
- "provider": "secondProvider"
- }
- }
- }
- },
- {
- "type": "input",
- "group": "oidc",
- "attributes": {
- "name": "provider",
- "type": "submit",
- "value": "claimsViaUserInfo",
- "disabled": false,
- "node_type": "input"
- },
- "messages": [],
- "meta": {
- "label": {
- "id": 1010002,
- "text": "Sign in with claimsViaUserInfo",
- "type": "info",
- "context": {
- "provider": "claimsViaUserInfo"
- }
- }
- }
- },
- {
- "type": "input",
- "group": "oidc",
- "attributes": {
- "name": "provider",
- "type": "submit",
- "value": "invalid-issuer",
- "disabled": false,
- "node_type": "input"
- },
- "messages": [],
- "meta": {
- "label": {
- "id": 1010002,
- "text": "Sign in with invalid-issuer",
- "type": "info",
- "context": {
- "provider": "invalid-issuer"
- }
- }
- }
- }
- ]
-}
diff --git a/selfservice/strategy/oidc/nodes.go b/selfservice/strategy/oidc/nodes.go
index e60dd6324d1a..3dc725e0d967 100644
--- a/selfservice/strategy/oidc/nodes.go
+++ b/selfservice/strategy/oidc/nodes.go
@@ -8,10 +8,10 @@ import (
"github.com/ory/kratos/ui/node"
)
-func NewLinkNode(provider string) *node.Node {
- return node.NewInputField("link", provider, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateLinkOIDC(provider))
+func NewLinkNode(providerID, providerLabel string) *node.Node {
+ return node.NewInputField("link", providerID, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateLinkOIDC(providerLabel))
}
-func NewUnlinkNode(provider string) *node.Node {
- return node.NewInputField("unlink", provider, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateUnlinkOIDC(provider))
+func NewUnlinkNode(providerID, providerLabel string) *node.Node {
+ return node.NewInputField("unlink", providerID, node.OpenIDConnectGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateUnlinkOIDC(providerLabel))
}
diff --git a/selfservice/strategy/oidc/provider_config.go b/selfservice/strategy/oidc/provider_config.go
index 7ab2c716d53c..b27db69f693c 100644
--- a/selfservice/strategy/oidc/provider_config.go
+++ b/selfservice/strategy/oidc/provider_config.go
@@ -28,6 +28,7 @@ type Configuration struct {
// - gitlab
// - microsoft
// - discord
+ // - salesforce
// - slack
// - facebook
// - auth0
@@ -150,6 +151,7 @@ var supportedProviders = map[string]func(config *Configuration, reg Dependencies
"gitlab": NewProviderGitLab,
"microsoft": NewProviderMicrosoft,
"discord": NewProviderDiscord,
+ "salesforce": NewProviderSalesforce,
"slack": NewProviderSlack,
"facebook": NewProviderFacebook,
"auth0": NewProviderAuth0,
diff --git a/selfservice/strategy/oidc/provider_salesforce.go b/selfservice/strategy/oidc/provider_salesforce.go
new file mode 100644
index 000000000000..1d028a1a8de7
--- /dev/null
+++ b/selfservice/strategy/oidc/provider_salesforce.go
@@ -0,0 +1,142 @@
+// Copyright © 2023 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package oidc
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/url"
+ "path"
+ "time"
+
+ "github.com/ory/x/httpx"
+ "github.com/ory/x/stringsx"
+
+ "github.com/tidwall/sjson"
+
+ "github.com/hashicorp/go-retryablehttp"
+
+ "github.com/pkg/errors"
+ "github.com/tidwall/gjson"
+ "golang.org/x/oauth2"
+
+ "github.com/ory/herodot"
+)
+
+type ProviderSalesforce struct {
+ *ProviderGenericOIDC
+}
+
+func NewProviderSalesforce(
+ config *Configuration,
+ reg Dependencies,
+) Provider {
+ return &ProviderSalesforce{
+ ProviderGenericOIDC: &ProviderGenericOIDC{
+ config: config,
+ reg: reg,
+ },
+ }
+}
+
+func (g *ProviderSalesforce) oauth2(ctx context.Context) (*oauth2.Config, error) {
+ endpoint, err := url.Parse(g.config.IssuerURL)
+ if err != nil {
+ return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
+ }
+
+ authUrl := *endpoint
+ tokenUrl := *endpoint
+
+ authUrl.Path = path.Join(authUrl.Path, "/services/oauth2/authorize")
+ tokenUrl.Path = path.Join(tokenUrl.Path, "/services/oauth2/token")
+
+ c := &oauth2.Config{
+ ClientID: g.config.ClientID,
+ ClientSecret: g.config.ClientSecret,
+ Endpoint: oauth2.Endpoint{
+ AuthURL: authUrl.String(),
+ TokenURL: tokenUrl.String(),
+ },
+ Scopes: g.config.Scope,
+ RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)),
+ }
+
+ return c, nil
+}
+
+func (g *ProviderSalesforce) OAuth2(ctx context.Context) (*oauth2.Config, error) {
+ return g.oauth2(ctx)
+}
+
+func (g *ProviderSalesforce) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) {
+ o, err := g.OAuth2(ctx)
+ if err != nil {
+ return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
+ }
+
+ u, err := url.Parse(g.config.IssuerURL)
+ if err != nil {
+ return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
+ }
+ u.Path = path.Join(u.Path, "/services/oauth2/userinfo")
+
+ ctx, client := httpx.SetOAuth2(ctx, g.reg.HTTPClient(ctx), o, exchange)
+ req, err := retryablehttp.NewRequestWithContext(ctx, "GET", u.String(), nil)
+ if err != nil {
+ return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
+ }
+
+ req.Header.Add("Content-Type", "application/json")
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
+ }
+ defer resp.Body.Close()
+
+ if err := logUpstreamError(g.reg.Logger(), resp); err != nil {
+ return nil, err
+ }
+
+ // Once Salesforce fixes this bug, all this workaround can be removed.
+ b, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
+ if err != nil {
+ return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
+ }
+
+ b, err = salesforceUpdatedAtWorkaround(b)
+ if err != nil {
+ return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
+ }
+
+ // Once we get here, we know that if there is an updated_at field in the json, it is the correct type.
+ var claims Claims
+ if err := json.Unmarshal(b, &claims); err != nil {
+ return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
+ }
+
+ claims.Issuer = stringsx.Coalesce(claims.Issuer, g.config.IssuerURL)
+ return &claims, nil
+}
+
+// There is a bug in the response from Salesforce. The updated_at field may be a string and not an int64.
+// https://help.salesforce.com/s/articleView?id=sf.remoteaccess_using_userinfo_endpoint.htm&type=5
+// We work around this by reading the json generically (as map[string]inteface{} and looking at the updated_at field
+// if it exists. If it's the wrong type (string), we fill out the claims by hand.
+func salesforceUpdatedAtWorkaround(body []byte) ([]byte, error) {
+ // Force updatedAt to be an int if given as a string in the response.
+ if updatedAtField := gjson.GetBytes(body, "updated_at"); updatedAtField.Exists() && updatedAtField.Type == gjson.String {
+ t, err := time.Parse(time.RFC3339, updatedAtField.String())
+ if err != nil {
+ return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("bad time format in updated_at"))
+ }
+ body, err = sjson.SetBytes(body, "updated_at", t.Unix())
+ if err != nil {
+ return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
+ }
+ }
+ return body, nil
+}
diff --git a/selfservice/strategy/oidc/provider_salesforce_unit_test.go b/selfservice/strategy/oidc/provider_salesforce_unit_test.go
new file mode 100644
index 000000000000..81638fb18050
--- /dev/null
+++ b/selfservice/strategy/oidc/provider_salesforce_unit_test.go
@@ -0,0 +1,33 @@
+// Copyright © 2023 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package oidc
+
+import (
+ "encoding/json"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSalesforceUpdatedAtWorkaround(t *testing.T) {
+ actual, err := salesforceUpdatedAtWorkaround([]byte("{}"))
+ require.NoError(t, err)
+ assert.Equal(t, "{}", string(actual))
+
+ actual, err = salesforceUpdatedAtWorkaround([]byte(`{"updated_at":1234}`))
+ require.NoError(t, err)
+ assert.Equal(t, `{"updated_at":1234}`, string(actual))
+
+ timestamp := time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)
+ input, err := json.Marshal(map[string]interface{}{
+ "updated_at": timestamp,
+ })
+ require.NoError(t, err)
+ actual, err = salesforceUpdatedAtWorkaround(input)
+ require.NoError(t, err)
+ assert.Equal(t, fmt.Sprintf(`{"updated_at":%d}`, timestamp.Unix()), string(actual))
+}
diff --git a/selfservice/strategy/oidc/provider_userinfo_test.go b/selfservice/strategy/oidc/provider_userinfo_test.go
index 97456dfc404d..dde2507af319 100644
--- a/selfservice/strategy/oidc/provider_userinfo_test.go
+++ b/selfservice/strategy/oidc/provider_userinfo_test.go
@@ -89,6 +89,16 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) {
Provider: "auth0",
}, reg),
},
+ {
+ name: "salesforce",
+ userInfoHandler: defaultUserinfoHandler,
+ userInfoEndpoint: "https://www.salesforce.com/services/oauth2/userinfo",
+ provider: oidc.NewProviderSalesforce(&oidc.Configuration{
+ IssuerURL: "https://www.salesforce.com",
+ ID: "salesforce",
+ Provider: "salesforce",
+ }, reg),
+ },
{
name: "netid",
userInfoHandler: defaultUserinfoHandler,
diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go
index 6515d06367ee..901c5636424c 100644
--- a/selfservice/strategy/oidc/strategy.go
+++ b/selfservice/strategy/oidc/strategy.go
@@ -24,7 +24,6 @@ import (
"golang.org/x/oauth2"
"github.com/ory/kratos/cipher"
- "github.com/ory/kratos/selfservice/flowhelpers"
"github.com/ory/kratos/selfservice/sessiontokenexchange"
"github.com/ory/x/jsonnetsecure"
"github.com/ory/x/otelx"
@@ -119,9 +118,9 @@ type Dependencies interface {
func isForced(req interface{}) bool {
f, ok := req.(interface {
- IsForced() bool
+ IsRefresh() bool
})
- return ok && f.IsForced()
+ return ok && f.IsRefresh()
}
// Strategy implements selfservice.LoginStrategy, selfservice.RegistrationStrategy and selfservice.SettingsStrategy.
@@ -537,38 +536,8 @@ func (s *Strategy) populateMethod(r *http.Request, f flow.Flow, message func(pro
return err
}
- providers := conf.Providers
-
- if lf, ok := f.(*login.Flow); ok && lf.IsForced() {
- if _, id, c := flowhelpers.GuessForcedLoginIdentifier(r, s.d, lf, s.ID()); id != nil {
- if c == nil {
- // no OIDC credentials, don't add any providers
- providers = nil
- } else {
- var credentials identity.CredentialsOIDC
- if err := json.Unmarshal(c.Config, &credentials); err != nil {
- // failed to read OIDC credentials, don't add any providers
- providers = nil
- } else {
- // add only providers that can actually be used to log in as this identity
- providers = make([]Configuration, 0, len(conf.Providers))
- for i := range conf.Providers {
- for j := range credentials.Providers {
- if conf.Providers[i].ID == credentials.Providers[j].Provider {
- providers = append(providers, conf.Providers[i])
- break
- }
- }
- }
- }
- }
- }
- }
-
- // does not need sorting because there is only one field
- c := f.GetUI()
- c.SetCSRF(s.d.GenerateCSRFToken(r))
- AddProviders(c, providers, message)
+ f.GetUI().SetCSRF(s.d.GenerateCSRFToken(r))
+ AddProviders(f.GetUI(), conf.Providers, message)
return nil
}
@@ -673,6 +642,9 @@ func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Fl
provider, _ := s.provider(r.Context(), r, providerID)
if provider != nil && provider.Config() != nil {
providerLabel = provider.Config().Label
+ if providerLabel == "" {
+ providerLabel = provider.Config().Provider
+ }
}
lf.UI.Messages.Add(text.NewInfoLoginLinkMessage(dc.DuplicateIdentifier, providerLabel, newLoginURL))
diff --git a/selfservice/strategy/oidc/strategy_login.go b/selfservice/strategy/oidc/strategy_login.go
index 42b948ec7c11..3b5f72291704 100644
--- a/selfservice/strategy/oidc/strategy_login.go
+++ b/selfservice/strategy/oidc/strategy_login.go
@@ -10,6 +10,11 @@ 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"
@@ -34,21 +39,15 @@ import (
"github.com/ory/kratos/x"
)
-var _ login.Strategy = new(Strategy)
+var (
+ _ login.FormHydrator = new(Strategy)
+ _ login.Strategy = new(Strategy)
+)
func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) {
s.setRoutes(r)
}
-func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, l *login.Flow) error {
- // This strategy can only solve AAL1
- if requestedAAL > identity.AuthenticatorAssuranceLevel1 {
- return nil
- }
-
- return s.populateMethod(r, l, text.NewInfoLoginWith)
-}
-
// Update Login Flow with OpenID Connect Method
//
// swagger:model updateLoginFlowWithOidcMethod
@@ -290,3 +289,97 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow,
return nil, errors.WithStack(flow.ErrCompletedByStrategy)
}
+
+func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, lf *login.Flow) error {
+ conf, err := s.Config(r.Context())
+ if err != nil {
+ return err
+ }
+
+ var providers []Configuration
+ _, id, c := flowhelpers.GuessForcedLoginIdentifier(r, s.d, lf, s.ID())
+ if id == nil || c == nil {
+ providers = nil
+ } else {
+ var credentials identity.CredentialsOIDC
+ if err := json.Unmarshal(c.Config, &credentials); err != nil {
+ // failed to read OIDC credentials, don't add any providers
+ providers = nil
+ } else {
+ // add only providers that can actually be used to log in as this identity
+ providers = make([]Configuration, 0, len(conf.Providers))
+ for i := range conf.Providers {
+ for j := range credentials.Providers {
+ if conf.Providers[i].ID == credentials.Providers[j].Provider {
+ providers = append(providers, conf.Providers[i])
+ break
+ }
+ }
+ }
+ }
+ }
+
+ lf.UI.SetCSRF(s.d.GenerateCSRFToken(r))
+ AddProviders(lf.UI, providers, text.NewInfoLoginWith)
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, f *login.Flow) error {
+ return s.populateMethod(r, f, text.NewInfoLoginWith)
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, f *login.Flow, mods ...login.FormHydratorModifier) error {
+ conf, err := s.Config(r.Context())
+ if err != nil {
+ return err
+ }
+
+ o := login.NewFormHydratorOptions(mods)
+
+ var linked []Provider
+ if o.IdentityHint != nil {
+ var err error
+ // If we have an identity hint we check if the identity has any providers configured.
+ if linked, err = s.linkedProviders(r.Context(), r, conf, o.IdentityHint); err != nil {
+ return err
+ }
+ }
+
+ if len(linked) == 0 {
+ // If we found no credentials:
+ if s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
+ // We found no credentials but do not want to leak that we know that. So we return early and do not
+ // modify the initial provider list.
+ return nil
+ }
+
+ // We found no credentials. We remove all the providers and tell the strategy that we found nothing.
+ f.GetUI().UnsetNode("provider")
+ return idfirst.ErrNoCredentialsFound
+ }
+
+ if !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
+ // Account enumeration is disabled, so we show all providers that are linked to the identity.
+ // User is found and enumeration mitigation is disabled. Filter the list!
+ f.GetUI().UnsetNode("provider")
+
+ for _, l := range linked {
+ lc := l.Config()
+ AddProvider(f.UI, lc.ID, text.NewInfoLoginWith(stringsx.Coalesce(lc.Label, lc.ID)))
+ }
+ }
+
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, f *login.Flow) error {
+ return s.populateMethod(r, f, text.NewInfoLoginWith)
+}
diff --git a/selfservice/strategy/oidc/strategy_login_test.go b/selfservice/strategy/oidc/strategy_login_test.go
new file mode 100644
index 000000000000..2191d5cf59a7
--- /dev/null
+++ b/selfservice/strategy/oidc/strategy_login_test.go
@@ -0,0 +1,166 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package oidc_test
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+
+ configtesthelpers "github.com/ory/kratos/driver/config/testhelpers"
+
+ "github.com/gofrs/uuid"
+ "github.com/stretchr/testify/require"
+
+ "github.com/ory/kratos/driver"
+ "github.com/ory/kratos/driver/config"
+ "github.com/ory/kratos/identity"
+ "github.com/ory/kratos/internal"
+ "github.com/ory/kratos/internal/testhelpers"
+ "github.com/ory/kratos/selfservice/flow"
+ "github.com/ory/kratos/selfservice/flow/login"
+ "github.com/ory/kratos/x"
+ "github.com/ory/x/snapshotx"
+)
+
+func createIdentity(t *testing.T, ctx context.Context, reg driver.Registry, id uuid.UUID, provider string) *identity.Identity {
+ creds, err := identity.NewCredentialsOIDC(new(identity.CredentialsOIDCEncryptedTokens), provider, id.String(), "")
+ require.NoError(t, err)
+
+ i := identity.NewIdentity("default")
+ i.SetCredentials(identity.CredentialsTypeOIDC, *creds)
+
+ require.NoError(t, reg.IdentityManager().Create(ctx, i))
+ return i
+}
+
+func TestFormHydration(t *testing.T) {
+ ctx := context.Background()
+ conf, reg := internal.NewFastRegistryWithMocks(t)
+ providerID := "test-provider"
+
+ ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".enabled", true)
+ ctx = configtesthelpers.WithConfigValue(
+ ctx,
+ config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC)+".config",
+ map[string]interface{}{
+ "providers": []map[string]interface{}{
+ {
+ "provider": "generic",
+ "id": providerID,
+ "client_id": "invalid",
+ "client_secret": "invalid",
+ "issuer_url": "https://foobar/",
+ "mapper_url": "file://./stub/oidc.facebook.jsonnet",
+ },
+ },
+ },
+ )
+ ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/stub.schema.json")
+
+ s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeOIDC)
+ require.NoError(t, err)
+ fh, ok := s.(login.FormHydrator)
+ require.True(t, ok)
+
+ toSnapshot := func(t *testing.T, f *login.Flow) {
+ t.Helper()
+ // The CSRF token has a unique value that messes with the snapshot - ignore it.
+ f.UI.Nodes.ResetNodes("csrf_token")
+ snapshotx.SnapshotT(t, f.UI.Nodes)
+ }
+ newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) {
+ r := httptest.NewRequest("GET", "/self-service/login/browser", nil)
+ r = r.WithContext(ctx)
+ t.Helper()
+ f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser)
+ require.NoError(t, err)
+ return r, f
+ }
+
+ t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+
+ id := createIdentity(t, ctx, reg, x.NewUUID(), providerID)
+ r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ f.Refresh = true
+
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodSecondFactorRefresh", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) {
+ t.Run("case=no options", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=WithIdentifier", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=WithIdentityHint", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+
+ id := identity.NewIdentity(providerID)
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false)
+
+ t.Run("case=identity has oidc", func(t *testing.T) {
+ identifier := x.NewUUID()
+ id := createIdentity(t, ctx, reg, identifier, providerID)
+
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=identity does not have a oidc", func(t *testing.T) {
+ id := identity.NewIdentity("default")
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f))
+ toSnapshot(t, f)
+ })
+}
diff --git a/selfservice/strategy/oidc/strategy_settings.go b/selfservice/strategy/oidc/strategy_settings.go
index 4fde3a457548..d4a92056a20b 100644
--- a/selfservice/strategy/oidc/strategy_settings.go
+++ b/selfservice/strategy/oidc/strategy_settings.go
@@ -12,6 +12,7 @@ import (
"time"
"github.com/ory/x/sqlxx"
+ "github.com/ory/x/stringsx"
"github.com/tidwall/sjson"
@@ -173,7 +174,7 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity
if l.Config().OrganizationID != "" {
continue
}
- sr.UI.GetNodes().Append(NewLinkNode(l.Config().ID))
+ sr.UI.GetNodes().Append(NewLinkNode(l.Config().ID, stringsx.Coalesce(l.Config().Label, l.Config().ID)))
}
count, err := s.d.IdentityManager().CountActiveFirstFactorCredentials(r.Context(), confidential)
@@ -185,7 +186,7 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity
// This means that we're able to remove a connection because it is the last configured credential. If it is
// removed, the identity is no longer able to sign in.
for _, l := range linked {
- sr.UI.GetNodes().Append(NewUnlinkNode(l.Config().ID))
+ sr.UI.GetNodes().Append(NewUnlinkNode(l.Config().ID, stringsx.Coalesce(l.Config().Label, l.Config().ID)))
}
}
diff --git a/selfservice/strategy/oidc/strategy_settings_test.go b/selfservice/strategy/oidc/strategy_settings_test.go
index 753bae5321b8..65e5ab30600c 100644
--- a/selfservice/strategy/oidc/strategy_settings_test.go
+++ b/selfservice/strategy/oidc/strategy_settings_test.go
@@ -7,6 +7,7 @@ import (
"context"
_ "embed"
"encoding/json"
+ "fmt"
"net/http"
"net/url"
"strconv"
@@ -15,6 +16,7 @@ import (
"github.com/ory/x/snapshotx"
+ "github.com/ory/kratos/driver"
kratos "github.com/ory/kratos/internal/httpclient"
"github.com/ory/kratos/ui/container"
"github.com/ory/kratos/ui/node"
@@ -28,7 +30,6 @@ import (
"github.com/ory/x/sqlxx"
- "github.com/ory/kratos/driver"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/internal"
@@ -36,6 +37,7 @@ import (
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/settings"
+ confighelpers "github.com/ory/kratos/driver/config/testhelpers"
"github.com/ory/kratos/selfservice/strategy/oidc"
"github.com/ory/kratos/x"
)
@@ -67,7 +69,9 @@ func TestSettingsStrategy(t *testing.T) {
viperSetProviderConfig(
t,
conf,
- newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "ory"),
+ newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "ory", func(c *oidc.Configuration) {
+ c.Label = "Ory"
+ }),
newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "google"),
newOIDCProvider(t, publicTS, remotePublic, remoteAdmin, "github"),
orgSSO,
@@ -609,21 +613,27 @@ func TestSettingsStrategy(t *testing.T) {
}
func TestPopulateSettingsMethod(t *testing.T) {
- ctx := context.Background()
- nreg := func(t *testing.T, conf *oidc.ConfigurationCollection) *driver.RegistryDefault {
- c, reg := internal.NewFastRegistryWithMocks(t)
-
- testhelpers.SetDefaultIdentitySchema(c, "file://stub/registration.schema.json")
- c.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/")
+ t.Parallel()
+ nCtx := func(t *testing.T, conf *oidc.ConfigurationCollection) (*driver.RegistryDefault, context.Context) {
+ _, reg := internal.NewFastRegistryWithMocks(t)
+ ctx := context.Background()
+ ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/registration.schema.json")
+ ctx = confighelpers.WithConfigValue(ctx, config.ViperKeyPublicBaseURL, "https://www.ory.sh/")
+ baseKey := fmt.Sprintf("%s.%s", config.ViperKeySelfServiceStrategyConfig, identity.CredentialsTypeOIDC)
+
+ ctx = confighelpers.WithConfigValues(ctx, map[string]interface{}{
+ baseKey + ".enabled": true,
+ baseKey + ".config": conf,
+ })
// Enabled per default:
// conf.Set(ctx, configuration.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true})
- viperSetProviderConfig(t, c, conf.Providers...)
- return reg
+ // viperSetProviderConfig(t, c, conf.Providers...)
+ return reg, ctx
}
- ns := func(t *testing.T, reg *driver.RegistryDefault) *oidc.Strategy {
- ss, err := reg.SettingsStrategies(context.Background()).Strategy(identity.CredentialsTypeOIDC.String())
+ ns := func(t *testing.T, reg *driver.RegistryDefault, ctx context.Context) *oidc.Strategy {
+ ss, err := reg.SettingsStrategies(ctx).Strategy(identity.CredentialsTypeOIDC.String())
require.NoError(t, err)
return ss.(*oidc.Strategy)
}
@@ -632,13 +642,14 @@ func TestPopulateSettingsMethod(t *testing.T) {
return &settings.Flow{Type: flow.TypeBrowser, ID: x.NewUUID(), UI: container.New("")}
}
- populate := func(t *testing.T, reg *driver.RegistryDefault, i *identity.Identity, req *settings.Flow) *container.Container {
- require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))
- require.NoError(t, ns(t, reg).PopulateSettingsMethod(new(http.Request), i, req))
- require.NotNil(t, req.UI)
- require.NotNil(t, req.UI.Nodes)
- assert.Equal(t, "POST", req.UI.Method)
- return req.UI
+ populate := func(t *testing.T, reg *driver.RegistryDefault, ctx context.Context, i *identity.Identity, f *settings.Flow) *container.Container {
+ require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i))
+ req := new(http.Request)
+ require.NoError(t, ns(t, reg, ctx).PopulateSettingsMethod(req.WithContext(ctx), i, f))
+ require.NotNil(t, f.UI)
+ require.NotNil(t, f.UI.Nodes)
+ assert.Equal(t, "POST", f.UI.Method)
+ return f.UI
}
defaultConfig := []oidc.Configuration{
@@ -648,12 +659,14 @@ func TestPopulateSettingsMethod(t *testing.T) {
}
t.Run("case=should not populate non-browser flow", func(t *testing.T) {
- reg := nreg(t, &oidc.ConfigurationCollection{Providers: []oidc.Configuration{{Provider: "generic", ID: "github"}}})
+ t.Parallel()
+ reg, ctx := nCtx(t, &oidc.ConfigurationCollection{Providers: []oidc.Configuration{{Provider: "generic", ID: "github"}}})
i := &identity.Identity{Traits: []byte(`{"subject":"foo@bar.com"}`)}
- require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))
- req := &settings.Flow{Type: flow.TypeAPI, ID: x.NewUUID(), UI: container.New("")}
- require.NoError(t, ns(t, reg).PopulateSettingsMethod(new(http.Request), i, req))
- require.Empty(t, req.UI.Nodes)
+ require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i))
+ f := &settings.Flow{Type: flow.TypeAPI, ID: x.NewUUID(), UI: container.New("")}
+ req := new(http.Request)
+ require.NoError(t, ns(t, reg, ctx).PopulateSettingsMethod(req.WithContext(ctx), i, f))
+ require.Empty(t, f.UI.Nodes)
})
for k, tc := range []struct {
@@ -674,25 +687,25 @@ func TestPopulateSettingsMethod(t *testing.T) {
},
e: node.Nodes{
node.NewCSRFNode(x.FakeCSRFToken),
- oidc.NewLinkNode("github"),
+ oidc.NewLinkNode("github", "github"),
},
},
{
c: defaultConfig,
e: node.Nodes{
node.NewCSRFNode(x.FakeCSRFToken),
- oidc.NewLinkNode("facebook"),
- oidc.NewLinkNode("google"),
- oidc.NewLinkNode("github"),
+ oidc.NewLinkNode("facebook", "facebook"),
+ oidc.NewLinkNode("google", "google"),
+ oidc.NewLinkNode("github", "github"),
},
},
{
c: defaultConfig,
e: node.Nodes{
node.NewCSRFNode(x.FakeCSRFToken),
- oidc.NewLinkNode("facebook"),
- oidc.NewLinkNode("google"),
- oidc.NewLinkNode("github"),
+ oidc.NewLinkNode("facebook", "facebook"),
+ oidc.NewLinkNode("google", "google"),
+ oidc.NewLinkNode("github", "github"),
},
i: &identity.Credentials{Type: identity.CredentialsTypeOIDC, Identifiers: []string{}, Config: []byte(`{}`)},
},
@@ -700,8 +713,8 @@ func TestPopulateSettingsMethod(t *testing.T) {
c: defaultConfig,
e: node.Nodes{
node.NewCSRFNode(x.FakeCSRFToken),
- oidc.NewLinkNode("facebook"),
- oidc.NewLinkNode("github"),
+ oidc.NewLinkNode("facebook", "facebook"),
+ oidc.NewLinkNode("github", "github"),
},
i: &identity.Credentials{Type: identity.CredentialsTypeOIDC, Identifiers: []string{
"google:1234",
@@ -711,9 +724,9 @@ func TestPopulateSettingsMethod(t *testing.T) {
c: defaultConfig,
e: node.Nodes{
node.NewCSRFNode(x.FakeCSRFToken),
- oidc.NewLinkNode("facebook"),
- oidc.NewLinkNode("github"),
- oidc.NewUnlinkNode("google"),
+ oidc.NewLinkNode("facebook", "facebook"),
+ oidc.NewLinkNode("github", "github"),
+ oidc.NewUnlinkNode("google", "google"),
},
withpw: true,
i: &identity.Credentials{
@@ -727,9 +740,9 @@ func TestPopulateSettingsMethod(t *testing.T) {
c: defaultConfig,
e: node.Nodes{
node.NewCSRFNode(x.FakeCSRFToken),
- oidc.NewLinkNode("github"),
- oidc.NewUnlinkNode("google"),
- oidc.NewUnlinkNode("facebook"),
+ oidc.NewLinkNode("github", "github"),
+ oidc.NewUnlinkNode("google", "google"),
+ oidc.NewUnlinkNode("facebook", "facebook"),
},
i: &identity.Credentials{
Type: identity.CredentialsTypeOIDC, Identifiers: []string{
@@ -739,9 +752,37 @@ func TestPopulateSettingsMethod(t *testing.T) {
Config: []byte(`{"providers":[{"provider":"google","subject":"1234"},{"provider":"facebook","subject":"1234"}]}`),
},
},
+ {
+ c: []oidc.Configuration{
+ {Provider: "generic", ID: "labeled", Label: "Labeled"},
+ },
+ e: node.Nodes{
+ node.NewCSRFNode(x.FakeCSRFToken),
+ oidc.NewLinkNode("labeled", "Labeled"),
+ },
+ },
+ {
+ c: []oidc.Configuration{
+ {Provider: "generic", ID: "labeled", Label: "Labeled"},
+ {Provider: "generic", ID: "facebook"},
+ },
+ e: node.Nodes{
+ node.NewCSRFNode(x.FakeCSRFToken),
+ oidc.NewUnlinkNode("labeled", "Labeled"),
+ oidc.NewUnlinkNode("facebook", "facebook"),
+ },
+ i: &identity.Credentials{
+ Type: identity.CredentialsTypeOIDC, Identifiers: []string{
+ "labeled:1234",
+ "facebook:1234",
+ },
+ Config: []byte(`{"providers":[{"provider":"labeled","subject":"1234"},{"provider":"facebook","subject":"1234"}]}`),
+ },
+ },
} {
t.Run("iteration="+strconv.Itoa(k), func(t *testing.T) {
- reg := nreg(t, &oidc.ConfigurationCollection{Providers: tc.c})
+ t.Parallel()
+ reg, ctx := nCtx(t, &oidc.ConfigurationCollection{Providers: tc.c})
i := &identity.Identity{
Traits: []byte(`{"subject":"foo@bar.com"}`),
Credentials: make(map[identity.CredentialsType]identity.Credentials, 2),
@@ -756,7 +797,7 @@ func TestPopulateSettingsMethod(t *testing.T) {
Config: []byte(`{"hashed_password":"$argon2id$..."}`),
}
}
- actual := populate(t, reg, i, nr())
+ actual := populate(t, reg, ctx, i, nr())
assert.EqualValues(t, tc.e, actual.Nodes)
})
}
diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go
index 65c8f09b2e06..0f41fd622d0e 100644
--- a/selfservice/strategy/oidc/strategy_test.go
+++ b/selfservice/strategy/oidc/strategy_test.go
@@ -1415,16 +1415,6 @@ func TestStrategy(t *testing.T) {
snapshotx.SnapshotTExcept(t, sr.UI, []string{"action", "nodes.0.attributes.value"})
})
-
- t.Run("method=TestPopulateLoginMethod", func(t *testing.T) {
- conf.MustSet(ctx, config.ViperKeyPublicBaseURL, "https://foo/")
-
- sr, err := login.NewFlow(conf, time.Minute, "nosurf", &http.Request{URL: urlx.ParseOrPanic("/")}, flow.TypeBrowser)
- require.NoError(t, err)
- require.NoError(t, reg.LoginStrategies(context.Background()).MustStrategy(identity.CredentialsTypeOIDC).(*oidc.Strategy).PopulateLoginMethod(&http.Request{}, identity.AuthenticatorAssuranceLevel1, sr))
-
- snapshotx.SnapshotTExcept(t, sr.UI, []string{"action", "nodes.0.attributes.value"})
- })
}
func prettyJSON(t *testing.T, body []byte) string {
@@ -1533,7 +1523,7 @@ func TestCountActiveFirstFactorCredentials(t *testing.T) {
func TestDisabledEndpoint(t *testing.T) {
conf, reg := internal.NewFastRegistryWithMocks(t)
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypeOIDC.String(), false)
-
+ ctx := context.Background()
publicTS, _ := testhelpers.NewKratosServer(t, reg)
t.Run("case=should not callback when oidc method is disabled", func(t *testing.T) {
@@ -1551,7 +1541,7 @@ func TestDisabledEndpoint(t *testing.T) {
t.Run("flow=settings", func(t *testing.T) {
testhelpers.SetDefaultIdentitySchema(conf, "file://stub/stub.schema.json")
- c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
+ c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, ctx, reg)
f := testhelpers.InitializeSettingsFlowViaAPI(t, c, publicTS)
res, err := c.PostForm(f.Ui.Action, url.Values{"link": {"oidc"}})
diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json
index d2dd6567d240..ffb5ec222642 100644
--- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json
+++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=passwordless-case=passkey_button_exists.json
@@ -38,7 +38,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -53,8 +53,10 @@
"disabled": false,
"name": "passkey_login_trigger",
"node_type": "input",
- "onclick": "window.__oryPasskeyLogin()",
- "onload": "window.__oryPasskeyLoginAutocompleteInit()",
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
+ "onload": "window.oryPasskeyLoginAutocompleteInit()",
+ "onloadTrigger": "oryPasskeyLoginAutocompleteInit",
"type": "button",
"value": ""
},
@@ -74,6 +76,8 @@
"disabled": false,
"name": "passkey_login",
"node_type": "input",
+ "onload": "window.oryPasskeyLoginAutocompleteInit()",
+ "onloadTrigger": "oryPasskeyLoginAutocompleteInit",
"type": "hidden"
},
"group": "passkey",
diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json
index c331d4f4280f..1e026fb9979a 100644
--- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json
+++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-browser.json
@@ -30,7 +30,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -45,7 +45,8 @@
"disabled": false,
"name": "passkey_login_trigger",
"node_type": "input",
- "onclick": "window.__oryPasskeyLogin()",
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
"type": "button",
"value": ""
},
diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json
index c331d4f4280f..1e026fb9979a 100644
--- a/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json
+++ b/selfservice/strategy/passkey/.snapshots/TestCompleteLogin-flow=refresh-case=refresh_passwordless_credentials-spa.json
@@ -30,7 +30,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -45,7 +45,8 @@
"disabled": false,
"name": "passkey_login_trigger",
"node_type": "input",
- "onclick": "window.__oryPasskeyLogin()",
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
"type": "button",
"value": ""
},
diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json
index f9032e39049d..a0383567eda4 100644
--- a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json
+++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json
@@ -4,7 +4,8 @@
"disabled": false,
"name": "passkey_register_trigger",
"node_type": "input",
- "onclick": "window.__oryPasskeySettingsRegistration()",
+ "onclick": "window.oryPasskeySettingsRegistration()",
+ "onclickTrigger": "oryPasskeySettingsRegistration",
"type": "button",
"value": ""
},
@@ -109,7 +110,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
diff --git a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json
index 7e5c5b3d082b..8d91edf04ce5 100644
--- a/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json
+++ b/selfservice/strategy/passkey/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json
@@ -4,7 +4,8 @@
"disabled": false,
"name": "passkey_register_trigger",
"node_type": "input",
- "onclick": "window.__oryPasskeySettingsRegistration()",
+ "onclick": "window.oryPasskeySettingsRegistration()",
+ "onclickTrigger": "oryPasskeySettingsRegistration",
"type": "button",
"value": ""
},
@@ -61,7 +62,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json
new file mode 100644
index 000000000000..3e9aa5b5199e
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json
@@ -0,0 +1,100 @@
+[
+ {
+ "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": "text",
+ "value": "",
+ "required": true,
+ "autocomplete": "username webauthn",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070004,
+ "text": "ID",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_challenge",
+ "type": "hidden",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "script",
+ "group": "webauthn",
+ "attributes": {
+ "async": true,
+ "referrerpolicy": "no-referrer",
+ "crossorigin": "anonymous",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
+ "type": "text/javascript",
+ "id": "webauthn_script",
+ "node_type": "script"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login",
+ "type": "hidden",
+ "disabled": false,
+ "onload": "window.oryPasskeyLoginAutocompleteInit()",
+ "onloadTrigger": "oryPasskeyLoginAutocompleteInit",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login_trigger",
+ "type": "button",
+ "value": "",
+ "disabled": false,
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
+ "onload": "window.oryPasskeyLoginAutocompleteInit()",
+ "onloadTrigger": "oryPasskeyLoginAutocompleteInit",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010021,
+ "text": "Sign in with passkey",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json
new file mode 100644
index 000000000000..33d9f8afd952
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json
@@ -0,0 +1,88 @@
+[
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_challenge",
+ "type": "hidden",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "script",
+ "group": "webauthn",
+ "attributes": {
+ "async": true,
+ "referrerpolicy": "no-referrer",
+ "crossorigin": "anonymous",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
+ "type": "text/javascript",
+ "id": "webauthn_script",
+ "node_type": "script"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login",
+ "type": "hidden",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login_trigger",
+ "type": "button",
+ "value": "",
+ "disabled": false,
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010021,
+ "text": "Sign in with passkey",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "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": "",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..94263a4da9d1
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1,23 @@
+[
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login_trigger",
+ "type": "button",
+ "value": "",
+ "disabled": false,
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010021,
+ "text": "Sign in with passkey",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_passkey.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_passkey.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_passkey.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_passkey.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_passkey.json
new file mode 100644
index 000000000000..94263a4da9d1
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_passkey.json
@@ -0,0 +1,23 @@
+[
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login_trigger",
+ "type": "button",
+ "value": "",
+ "disabled": false,
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010021,
+ "text": "Sign in with passkey",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..94263a4da9d1
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1,23 @@
+[
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login_trigger",
+ "type": "button",
+ "value": "",
+ "disabled": false,
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010021,
+ "text": "Sign in with passkey",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..94263a4da9d1
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1,23 @@
+[
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login_trigger",
+ "type": "button",
+ "value": "",
+ "disabled": false,
+ "onclick": "window.oryPasskeyLogin()",
+ "onclickTrigger": "oryPasskeyLogin",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010021,
+ "text": "Sign in with passkey",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
new file mode 100644
index 000000000000..222443d4988b
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
@@ -0,0 +1,77 @@
+[
+ {
+ "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": "text",
+ "value": "",
+ "required": true,
+ "autocomplete": "username webauthn",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070004,
+ "text": "ID",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_challenge",
+ "type": "hidden",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "script",
+ "group": "webauthn",
+ "attributes": {
+ "async": true,
+ "referrerpolicy": "no-referrer",
+ "crossorigin": "anonymous",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
+ "type": "text/javascript",
+ "id": "webauthn_script",
+ "node_type": "script"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "passkey",
+ "attributes": {
+ "name": "passkey_login",
+ "type": "hidden",
+ "disabled": false,
+ "onload": "window.oryPasskeyLoginAutocompleteInit()",
+ "onloadTrigger": "oryPasskeyLoginAutocompleteInit",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/passkey/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json
index 18e0cda77811..e4c5160c9697 100644
--- a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json
+++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-browser.json
@@ -43,7 +43,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -70,7 +70,8 @@
"disabled": false,
"name": "passkey_register_trigger",
"node_type": "input",
- "onclick": "window.__oryPasskeyRegistration()",
+ "onclick": "window.oryPasskeyRegistration()",
+ "onclickTrigger": "oryPasskeyRegistration",
"type": "button"
},
"group": "passkey",
diff --git a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json
index 18e0cda77811..e4c5160c9697 100644
--- a/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json
+++ b/selfservice/strategy/passkey/.snapshots/TestRegistration-case=passkey_button_exists-spa.json
@@ -43,7 +43,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -70,7 +70,8 @@
"disabled": false,
"name": "passkey_register_trigger",
"node_type": "input",
- "onclick": "window.__oryPasskeyRegistration()",
+ "onclick": "window.oryPasskeyRegistration()",
+ "onclickTrigger": "oryPasskeyRegistration",
"type": "button"
},
"group": "passkey",
diff --git a/selfservice/strategy/passkey/passkey_login.go b/selfservice/strategy/passkey/passkey_login.go
index 63d5ec66f2f0..857d6e824d32 100644
--- a/selfservice/strategy/passkey/passkey_login.go
+++ b/selfservice/strategy/passkey/passkey_login.go
@@ -9,6 +9,10 @@ import (
"net/http"
"strings"
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+
+ "github.com/ory/kratos/x/webauthnx/js"
+
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/pkg/errors"
@@ -29,23 +33,13 @@ import (
"github.com/ory/x/decoderx"
)
+var _ login.FormHydrator = new(Strategy)
+
func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) {
webauthnx.RegisterWebauthnRoute(r)
}
-func (s *Strategy) PopulateLoginMethod(r *http.Request, aal identity.AuthenticatorAssuranceLevel, sr *login.Flow) error {
- if sr.Type != flow.TypeBrowser || aal != identity.AuthenticatorAssuranceLevel1 {
- return nil
- }
-
- return s.populateLoginMethodForPasskeys(r, sr)
-}
-
func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *login.Flow) error {
- if loginFlow.IsForced() {
- return s.populateLoginMethodForRefresh(r, loginFlow)
- }
-
ctx := r.Context()
loginFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r))
@@ -100,7 +94,8 @@ func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *lo
Name: node.PasskeyChallenge,
Type: node.InputAttributeTypeHidden,
FieldValue: string(injectWebAuthnOptions),
- }})
+ },
+ })
loginFlow.UI.Nodes.Upsert(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx)))
@@ -109,122 +104,12 @@ func (s *Strategy) populateLoginMethodForPasskeys(r *http.Request, loginFlow *lo
Group: node.PasskeyGroup,
Meta: &node.Meta{},
Attributes: &node.InputAttributes{
- Name: node.PasskeyLogin,
- Type: node.InputAttributeTypeHidden,
- }})
-
- loginFlow.UI.Nodes.Append(node.NewInputField(
- node.PasskeyLoginTrigger,
- "",
- node.PasskeyGroup,
- node.InputAttributeTypeButton,
- node.WithInputAttributes(func(attr *node.InputAttributes) {
- attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js
- attr.OnLoad = "window.__oryPasskeyLoginAutocompleteInit()" // same here
- }),
- ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey()))
-
- return nil
-}
-
-func (s *Strategy) populateLoginMethodForRefresh(r *http.Request, loginFlow *login.Flow) error {
- ctx := r.Context()
-
- identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, loginFlow, s.ID())
- if identifier == "" {
- return nil
- }
-
- id, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), id.ID)
- if err != nil {
- return err
- }
-
- cred, ok := id.GetCredentials(s.ID())
- if !ok {
- // Identity has no passkey
- return nil
- }
-
- var conf identity.CredentialsWebAuthnConfig
- if err := json.Unmarshal(cred.Config, &conf); err != nil {
- return errors.WithStack(err)
- }
-
- webAuthCreds := conf.Credentials.ToWebAuthn()
- if len(webAuthCreds) == 0 {
- // Identity has no webauthn
- return nil
- }
-
- passkeyIdentifier := s.PasskeyDisplayNameFromIdentity(ctx, id)
-
- webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx))
- if err != nil {
- return errors.WithStack(err)
- }
- option, sessionData, err := webAuthn.BeginLogin(&webauthnx.User{
- Name: passkeyIdentifier,
- ID: conf.UserHandle,
- Credentials: webAuthCreds,
- Config: webAuthn.Config,
+ Name: node.PasskeyLogin,
+ Type: node.InputAttributeTypeHidden,
+ OnLoad: js.WebAuthnTriggersPasskeyLoginAutocompleteInit.String() + "()",
+ OnLoadTrigger: js.WebAuthnTriggersPasskeyLoginAutocompleteInit,
+ },
})
- if err != nil {
- return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate passkey login.").WithDebug(err.Error()))
- }
-
- loginFlow.InternalContext, err = sjson.SetBytes(
- loginFlow.InternalContext,
- flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData),
- sessionData,
- )
- if err != nil {
- return errors.WithStack(err)
- }
-
- injectWebAuthnOptions, err := json.Marshal(option)
- if err != nil {
- return errors.WithStack(err)
- }
-
- loginFlow.UI.Nodes.Upsert(&node.Node{
- Type: node.Input,
- Group: node.PasskeyGroup,
- Meta: &node.Meta{},
- Attributes: &node.InputAttributes{
- Name: node.PasskeyChallenge,
- Type: node.InputAttributeTypeHidden,
- FieldValue: string(injectWebAuthnOptions),
- }})
-
- loginFlow.UI.Nodes.Append(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx)))
-
- loginFlow.UI.Nodes.Upsert(&node.Node{
- Type: node.Input,
- Group: node.PasskeyGroup,
- Meta: &node.Meta{},
- Attributes: &node.InputAttributes{
- Name: node.PasskeyLogin,
- Type: node.InputAttributeTypeHidden,
- }})
-
- loginFlow.UI.Nodes.Append(node.NewInputField(
- node.PasskeyLoginTrigger,
- "",
- node.PasskeyGroup,
- node.InputAttributeTypeButton,
- node.WithInputAttributes(func(attr *node.InputAttributes) {
- attr.OnClick = "window.__oryPasskeyLogin()" // this function is defined in webauthn.js
- }),
- ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey()))
-
- loginFlow.UI.SetCSRF(s.d.GenerateCSRFToken(r))
- loginFlow.UI.SetNode(node.NewInputField(
- "identifier",
- passkeyIdentifier,
- node.DefaultGroup,
- node.InputAttributeTypeHidden,
- ))
return nil
}
@@ -393,3 +278,200 @@ func (s *Strategy) loginAuthenticate(_ http.ResponseWriter, r *http.Request, f *
return i, nil
}
+
+func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, f *login.Flow) error {
+ if f.Type != flow.TypeBrowser {
+ return nil
+ }
+
+ ctx := r.Context()
+
+ identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, f, s.ID())
+ if identifier == "" {
+ return nil
+ }
+
+ id, err := s.d.PrivilegedIdentityPool().GetIdentityConfidential(r.Context(), id.ID)
+ if err != nil {
+ return err
+ }
+
+ cred, ok := id.GetCredentials(s.ID())
+ if !ok {
+ // Identity has no passkey
+ return nil
+ }
+
+ var conf identity.CredentialsWebAuthnConfig
+ if err := json.Unmarshal(cred.Config, &conf); err != nil {
+ return errors.WithStack(err)
+ }
+
+ webAuthCreds := conf.Credentials.ToWebAuthn()
+ if len(webAuthCreds) == 0 {
+ // Identity has no webauthn
+ return nil
+ }
+
+ passkeyIdentifier := s.PasskeyDisplayNameFromIdentity(ctx, id)
+
+ webAuthn, err := webauthn.New(s.d.Config().PasskeyConfig(ctx))
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ option, sessionData, err := webAuthn.BeginLogin(&webauthnx.User{
+ Name: passkeyIdentifier,
+ ID: conf.UserHandle,
+ Credentials: webAuthCreds,
+ Config: webAuthn.Config,
+ })
+ if err != nil {
+ return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to initiate passkey login.").WithDebug(err.Error()))
+ }
+
+ f.InternalContext, err = sjson.SetBytes(
+ f.InternalContext,
+ flow.PrefixInternalContextKey(s.ID(), InternalContextKeySessionData),
+ sessionData,
+ )
+ if err != nil {
+ return errors.WithStack(err)
+ }
+
+ injectWebAuthnOptions, err := json.Marshal(option)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+
+ f.UI.Nodes.Upsert(&node.Node{
+ Type: node.Input,
+ Group: node.PasskeyGroup,
+ Meta: &node.Meta{},
+ Attributes: &node.InputAttributes{
+ Name: node.PasskeyChallenge,
+ Type: node.InputAttributeTypeHidden,
+ FieldValue: string(injectWebAuthnOptions),
+ },
+ })
+
+ f.UI.Nodes.Append(webauthnx.NewWebAuthnScript(s.d.Config().SelfPublicURL(ctx)))
+
+ f.UI.Nodes.Upsert(&node.Node{
+ Type: node.Input,
+ Group: node.PasskeyGroup,
+ Meta: &node.Meta{},
+ Attributes: &node.InputAttributes{
+ Name: node.PasskeyLogin,
+ Type: node.InputAttributeTypeHidden,
+ },
+ })
+
+ f.UI.Nodes.Append(node.NewInputField(
+ node.PasskeyLoginTrigger,
+ "",
+ node.PasskeyGroup,
+ node.InputAttributeTypeButton,
+ node.WithInputAttributes(func(attr *node.InputAttributes) {
+ //nolint:staticcheck
+ attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js
+ attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin
+ }),
+ ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey()))
+
+ f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
+ f.UI.SetNode(node.NewInputField(
+ "identifier",
+ passkeyIdentifier,
+ node.DefaultGroup,
+ node.InputAttributeTypeHidden,
+ ))
+
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, f *login.Flow) error {
+ if f.Type != flow.TypeBrowser {
+ return nil
+ }
+
+ if err := s.populateLoginMethodForPasskeys(r, f); err != nil {
+ return err
+ }
+
+ f.UI.Nodes.Append(node.NewInputField(
+ node.PasskeyLoginTrigger,
+ "",
+ node.PasskeyGroup,
+ node.InputAttributeTypeButton,
+ node.WithInputAttributes(func(attr *node.InputAttributes) {
+ //nolint:staticcheck
+ attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js
+ attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin
+
+ //nolint:staticcheck
+ attr.OnLoad = js.WebAuthnTriggersPasskeyLoginAutocompleteInit.String() + "()" // same here
+ attr.OnLoadTrigger = js.WebAuthnTriggersPasskeyLoginAutocompleteInit
+ }),
+ ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey()))
+
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) error {
+ if sr.Type != flow.TypeBrowser {
+ return errors.WithStack(idfirst.ErrNoCredentialsFound)
+ }
+
+ o := login.NewFormHydratorOptions(opts)
+
+ var count int
+ if o.IdentityHint != nil {
+ var err error
+ // If we have an identity hint we can perform identity credentials discovery and
+ // hide this credential if it should not be included.
+ count, err = s.CountActiveFirstFactorCredentials(o.IdentityHint.Credentials)
+ if err != nil {
+ return err
+ }
+ }
+
+ if count > 0 || s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
+ sr.UI.Nodes.Append(node.NewInputField(
+ node.PasskeyLoginTrigger,
+ "",
+ node.PasskeyGroup,
+ node.InputAttributeTypeButton,
+ node.WithInputAttributes(func(attr *node.InputAttributes) {
+ //nolint:staticcheck
+ attr.OnClick = js.WebAuthnTriggersPasskeyLogin.String() + "()" // this function is defined in webauthn.js
+ attr.OnClickTrigger = js.WebAuthnTriggersPasskeyLogin
+ }),
+ ).WithMetaLabel(text.NewInfoSelfServiceLoginPasskey()))
+ }
+
+ if count == 0 {
+ return errors.WithStack(idfirst.ErrNoCredentialsFound)
+ }
+
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, sr *login.Flow) error {
+ if sr.Type != flow.TypeBrowser {
+ return nil
+ }
+
+ if err := s.populateLoginMethodForPasskeys(r, sr); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/selfservice/strategy/passkey/passkey_login_test.go b/selfservice/strategy/passkey/passkey_login_test.go
index cae6aa0ee4dd..028d7281c5e6 100644
--- a/selfservice/strategy/passkey/passkey_login_test.go
+++ b/selfservice/strategy/passkey/passkey_login_test.go
@@ -8,22 +8,31 @@ import (
_ "embed"
"encoding/json"
"net/http"
+ "net/http/httptest"
"net/url"
"testing"
+ "time"
+
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+
+ configtesthelpers "github.com/ory/kratos/driver/config/testhelpers"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
+ "github.com/ory/kratos/driver"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
+ "github.com/ory/kratos/internal"
"github.com/ory/kratos/internal/testhelpers"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/kratos/selfservice/strategy/passkey"
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
+ "github.com/ory/kratos/x"
"github.com/ory/x/snapshotx"
)
@@ -45,12 +54,12 @@ func TestPopulateLoginMethod(t *testing.T) {
t.Run("case=should not handle AAL2", func(t *testing.T) {
loginFlow := &login.Flow{Type: flow.TypeBrowser}
- assert.Nil(t, s.PopulateLoginMethod(nil, identity.AuthenticatorAssuranceLevel2, loginFlow))
+ assert.Nil(t, s.PopulateLoginMethodSecondFactor(nil, loginFlow))
})
t.Run("case=should not handle API flows", func(t *testing.T) {
loginFlow := &login.Flow{Type: flow.TypeAPI}
- assert.Nil(t, s.PopulateLoginMethod(nil, identity.AuthenticatorAssuranceLevel1, loginFlow))
+ assert.Nil(t, s.PopulateLoginMethodFirstFactor(nil, loginFlow))
})
}
@@ -209,7 +218,14 @@ func TestCompleteLogin(t *testing.T) {
actualFlow, err := fix.reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id))
require.NoError(t, err)
+
assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypePasskey, passkey.InternalContextKeySessionData)))
+ if spa {
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
+ assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), fix.conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", body)
+ } else {
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
+ }
}
// We test here that login works even if the identity schema contains
@@ -233,8 +249,8 @@ func TestCompleteLogin(t *testing.T) {
fix.conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, "aal1")
loginFixtureSuccessEmail := gjson.GetBytes(loginSuccessIdentity, "traits.email").String()
- run := func(t *testing.T, id *identity.Identity, context, response []byte, isSPA bool, expectedAAL identity.AuthenticatorAssuranceLevel) {
- body, res, f := fix.submitWebAuthnLogin(t, isSPA, id, context, func(values url.Values) {
+ run := func(t *testing.T, ctx context.Context, id *identity.Identity, context, response []byte, isSPA bool, expectedAAL identity.AuthenticatorAssuranceLevel) {
+ body, res, f := fix.submitWebAuthnLogin(t, ctx, isSPA, id, context, func(values url.Values) {
values.Set("identifier", loginFixtureSuccessEmail)
values.Set(node.PasskeyLogin, string(response))
}, testhelpers.InitFlowWithRefresh())
@@ -290,10 +306,166 @@ func TestCompleteLogin(t *testing.T) {
"spa",
} {
t.Run(f, func(t *testing.T) {
- run(t, id, tc.context, tc.response, f == "spa", expectedAAL)
+ run(t, ctx, id, tc.context, tc.response, f == "spa", expectedAAL)
})
}
})
}
})
}
+
+func createIdentity(t *testing.T, ctx context.Context, reg driver.Registry, id uuid.UUID) *identity.Identity {
+ i := identity.NewIdentity("default")
+ i.SetCredentials(identity.CredentialsTypePasskey, identity.Credentials{
+ Identifiers: []string{id.String()},
+ Config: loginPasswordlessCredentials,
+ Type: identity.CredentialsTypePasskey,
+ Version: 1,
+ })
+
+ require.NoError(t, reg.IdentityManager().Create(ctx, i))
+ return i
+}
+
+func TestFormHydration(t *testing.T) {
+ ctx := context.Background()
+ conf, reg := internal.NewFastRegistryWithMocks(t)
+
+ ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".enabled", true)
+ ctx = configtesthelpers.WithConfigValue(
+ ctx,
+ config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePasskey)+".config",
+ map[string]interface{}{
+ "rp": map[string]interface{}{
+ "display_name": "foo",
+ "id": "localhost",
+ "origins": []string{"http://localhost"},
+ },
+ },
+ )
+ ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/login.schema.json")
+
+ s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypePasskey)
+ require.NoError(t, err)
+ fh, ok := s.(login.FormHydrator)
+ require.True(t, ok)
+
+ toSnapshot := func(t *testing.T, f *login.Flow) {
+ t.Helper()
+ // The CSRF token has a unique value that messes with the snapshot - ignore it.
+ f.UI.Nodes.ResetNodes("csrf_token")
+ f.UI.Nodes.ResetNodes("passkey_challenge")
+ snapshotx.SnapshotT(t, f.UI.Nodes, snapshotx.ExceptNestedKeys("nonce", "src"))
+ }
+
+ newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) {
+ r := httptest.NewRequest("GET", "/self-service/login/browser", nil)
+ r = r.WithContext(ctx)
+ t.Helper()
+ f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser)
+ f.UI.Nodes = make(node.Nodes, 0)
+ require.NoError(t, err)
+ return r, f
+ }
+
+ t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+
+ id := createIdentity(t, ctx, reg, x.NewUUID())
+ r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ f.Refresh = true
+
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodSecondFactorRefresh", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) {
+ t.Run("case=no options", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false)
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=WithIdentifier", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false)
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=WithIdentityHint", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+
+ id := identity.NewIdentity("test-provider")
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false)
+
+ t.Run("case=identity has passkey", func(t *testing.T) {
+ identifier := x.NewUUID()
+ id := createIdentity(t, ctx, reg, identifier)
+
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=identity does not have a passkey", func(t *testing.T) {
+ id := identity.NewIdentity("default")
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f))
+ toSnapshot(t, f)
+ })
+}
diff --git a/selfservice/strategy/passkey/passkey_registration.go b/selfservice/strategy/passkey/passkey_registration.go
index 88efd420d725..9be753f70c40 100644
--- a/selfservice/strategy/passkey/passkey_registration.go
+++ b/selfservice/strategy/passkey/passkey_registration.go
@@ -11,6 +11,8 @@ import (
"net/url"
"strings"
+ "github.com/ory/kratos/x/webauthnx/js"
+
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/pkg/errors"
@@ -280,9 +282,10 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, regFlow *registra
Group: node.PasskeyGroup,
Meta: &node.Meta{Label: text.NewInfoSelfServiceRegistrationRegisterPasskey()},
Attributes: &node.InputAttributes{
- Name: node.PasskeyRegisterTrigger,
- Type: node.InputAttributeTypeButton,
- OnClick: "window.__oryPasskeyRegistration()", // defined in webauthn.js
+ Name: node.PasskeyRegisterTrigger,
+ Type: node.InputAttributeTypeButton,
+ OnClick: js.WebAuthnTriggersPasskeyRegistration.String() + "()", // defined in webauthn.js
+ OnClickTrigger: js.WebAuthnTriggersPasskeyRegistration,
}})
// Passkey nodes end
diff --git a/selfservice/strategy/passkey/passkey_registration_test.go b/selfservice/strategy/passkey/passkey_registration_test.go
index d495e8c4dfe4..d7191207cedb 100644
--- a/selfservice/strategy/passkey/passkey_registration_test.go
+++ b/selfservice/strategy/passkey/passkey_registration_test.go
@@ -8,6 +8,8 @@ import (
"net/url"
"testing"
+ "github.com/ory/kratos/selfservice/flow"
+
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
@@ -327,6 +329,13 @@ func TestRegistration(t *testing.T) {
i, _, err := fix.reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(fix.ctx, identity.CredentialsTypePasskey, userID)
require.NoError(t, err)
assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual)
+
+ if f == "spa" {
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual)
+ assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), fix.redirNoSessionTS.URL+"/registration-return-ts", "%s", actual)
+ } else {
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
+ }
})
}
})
diff --git a/selfservice/strategy/passkey/passkey_settings.go b/selfservice/strategy/passkey/passkey_settings.go
index 548a261e442b..89423bf8adeb 100644
--- a/selfservice/strategy/passkey/passkey_settings.go
+++ b/selfservice/strategy/passkey/passkey_settings.go
@@ -11,6 +11,8 @@ import (
"strings"
"time"
+ "github.com/ory/kratos/x/webauthnx/js"
+
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/gofrs/uuid"
@@ -114,7 +116,9 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity
node.PasskeyGroup,
node.InputAttributeTypeButton,
node.WithInputAttributes(func(a *node.InputAttributes) {
- a.OnClick = "window.__oryPasskeySettingsRegistration()"
+ //nolint:staticcheck
+ a.OnClick = js.WebAuthnTriggersPasskeySettingsRegistration.String() + "()"
+ a.OnClickTrigger = js.WebAuthnTriggersPasskeySettingsRegistration
}),
).WithMetaLabel(text.NewInfoSelfServiceSettingsRegisterPasskey()))
diff --git a/selfservice/strategy/passkey/passkey_settings_test.go b/selfservice/strategy/passkey/passkey_settings_test.go
index ced111071711..a37fe39a38f1 100644
--- a/selfservice/strategy/passkey/passkey_settings_test.go
+++ b/selfservice/strategy/passkey/passkey_settings_test.go
@@ -54,7 +54,7 @@ func TestCompleteSettings(t *testing.T) {
fix := newSettingsFixture(t)
fix.conf.MustSet(ctx, config.ViperKeyPasskeyRPID, "")
id := fix.createIdentity(t)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, id)
req, err := http.NewRequest("GET", fix.publicTS.URL+settings.RouteInitBrowserFlow, nil)
require.NoError(t, err)
@@ -67,7 +67,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("case=a device is shown which can be unlinked", func(t *testing.T) {
id := fix.createIdentity(t)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, fix.publicTS)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
@@ -81,7 +81,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("case=invalid credentials", func(t *testing.T) {
id, _ := fix.createIdentityAndReturnIdentifier(t, []byte(`{invalid}`))
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, id)
req, err := http.NewRequest("GET", fix.publicTS.URL+settings.RouteInitBrowserFlow, nil)
require.NoError(t, err)
@@ -95,7 +95,7 @@ func TestCompleteSettings(t *testing.T) {
id := fix.createIdentityWithoutPasskey(t)
require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(fix.ctx, id))
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, fix.publicTS)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
@@ -110,7 +110,7 @@ func TestCompleteSettings(t *testing.T) {
id := fix.createIdentityWithoutPasskey(t)
require.NoError(t, fix.reg.PrivilegedIdentityPool().UpdateIdentity(fix.ctx, id))
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, fix.reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, fix.reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, fix.publicTS)
for _, n := range f.Ui.Nodes {
assert.NotEqual(t, n.Group, "passkey", "unexpected group: %s", n.Group)
@@ -118,7 +118,7 @@ func TestCompleteSettings(t *testing.T) {
})
doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, fix.reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, fix.reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, fix.publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v(values)
@@ -127,7 +127,7 @@ func TestCompleteSettings(t *testing.T) {
}
doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, fix.publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v(values)
@@ -234,7 +234,7 @@ func TestCompleteSettings(t *testing.T) {
var id identity.Identity
require.NoError(t, json.Unmarshal(settingsFixtureSuccessIdentity, &id))
_ = fix.reg.PrivilegedIdentityPool().DeleteIdentity(fix.ctx, id.ID)
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, &id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, &id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, fix.publicTS)
// We inject the session to replay
@@ -271,6 +271,13 @@ func TestCompleteSettings(t *testing.T) {
flow.PrefixInternalContextKey(identity.CredentialsTypePasskey, passkey.InternalContextKeySessionData)))
testhelpers.EnsureAAL(t, browserClient, fix.publicTS, "aal1", string(identity.CredentialsTypePasskey))
+
+ if spa {
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
+ assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), fix.uiTS.URL, "%s", body)
+ } else {
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
+ }
}
t.Run("type=browser", func(t *testing.T) {
@@ -431,7 +438,7 @@ func TestCompleteSettings(t *testing.T) {
var id identity.Identity
require.NoError(t, json.Unmarshal(settingsFixtureSuccessIdentity, &id))
_ = fix.reg.PrivilegedIdentityPool().DeleteIdentity(fix.ctx, id.ID)
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, &id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, &id)
req, err := http.NewRequest("GET", fix.publicTS.URL+settings.RouteInitBrowserFlow, nil)
require.NoError(t, err)
diff --git a/selfservice/strategy/passkey/testfixture_test.go b/selfservice/strategy/passkey/testfixture_test.go
index d7abf8459fb6..1f3090177341 100644
--- a/selfservice/strategy/passkey/testfixture_test.go
+++ b/selfservice/strategy/passkey/testfixture_test.go
@@ -207,8 +207,8 @@ func (fix *fixture) submitWebAuthnLoginWithClient(t *testing.T, isSPA bool, cont
return fix.submitWebAuthnLoginFlowWithClient(t, isSPA, f, contextFixture, client, cb)
}
-func (fix *fixture) submitWebAuthnLogin(t *testing.T, isSPA bool, id *identity.Identity, contextFixture []byte, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.LoginFlow) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, fix.reg, id)
+func (fix *fixture) submitWebAuthnLogin(t *testing.T, ctx context.Context, isSPA bool, id *identity.Identity, contextFixture []byte, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.LoginFlow) {
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, fix.reg, id)
return fix.submitWebAuthnLoginWithClient(t, isSPA, contextFixture, browserClient, cb, opts...)
}
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json
new file mode 100644
index 000000000000..4b13f4012f6f
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor.json
@@ -0,0 +1,74 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "value": "",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070004,
+ "text": "ID",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "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"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json
new file mode 100644
index 000000000000..eb9d0e213786
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactorRefresh.json
@@ -0,0 +1,67 @@
+[
+ {
+ "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": "some@user.com",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "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": 1010001,
+ "text": "Sign in",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_disabled.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..831d9f07ba25
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "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"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_password.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json
new file mode 100644
index 000000000000..831d9f07ba25
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_password.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "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"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled_and_identity_has_no_password.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled_and_identity_has_no_password.json
new file mode 100644
index 000000000000..831d9f07ba25
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled_and_identity_has_no_password.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "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"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_disabled.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..831d9f07ba25
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "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"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/selfservice/strategy/password/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactorRefresh.json
@@ -0,0 +1 @@
+null
diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go
index 8c91d7e6c4f9..89730d299463 100644
--- a/selfservice/strategy/password/login.go
+++ b/selfservice/strategy/password/login.go
@@ -10,32 +10,32 @@ import (
"net/http"
"time"
- "github.com/ory/kratos/selfservice/flowhelpers"
- "github.com/ory/kratos/session"
-
- "github.com/ory/x/stringsx"
-
"github.com/gofrs/uuid"
-
"github.com/pkg/errors"
"github.com/ory/herodot"
- "github.com/ory/x/decoderx"
-
"github.com/ory/kratos/hash"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/schema"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/selfservice/flow/login"
+ "github.com/ory/kratos/selfservice/flowhelpers"
+ "github.com/ory/kratos/selfservice/hook"
+ "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/decoderx"
+ "github.com/ory/x/stringsx"
)
+var _ login.FormHydrator = new(Strategy)
+
func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) {
}
-func (s *Strategy) handleLoginError(w http.ResponseWriter, r *http.Request, f *login.Flow, payload *updateLoginFlowWithPasswordMethod, err error) error {
+func (s *Strategy) handleLoginError(r *http.Request, f *login.Flow, payload *updateLoginFlowWithPasswordMethod, err error) error {
if f != nil {
f.UI.Nodes.ResetNodes("password")
f.UI.Nodes.SetValueAttribute("identifier", stringsx.Coalesce(payload.Identifier, payload.LegacyIdentifier))
@@ -61,18 +61,19 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow,
decoderx.HTTPDecoderSetValidatePayloads(true),
decoderx.MustHTTPRawJSONSchemaCompiler(loginSchema),
decoderx.HTTPDecoderJSONFollowsFormFormat()); err != nil {
- return nil, s.handleLoginError(w, r, f, &p, err)
+ return nil, s.handleLoginError(r, f, &p, err)
}
f.TransientPayload = p.TransientPayload
if err := flow.EnsureCSRF(s.d, r, f.Type, s.d.Config().DisableAPIFlowEnforcement(r.Context()), s.d.GenerateCSRFToken, p.CSRFToken); err != nil {
- return nil, s.handleLoginError(w, r, f, &p, err)
+ return nil, s.handleLoginError(r, f, &p, err)
}
- i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), stringsx.Coalesce(p.Identifier, p.LegacyIdentifier))
+ identifier := stringsx.Coalesce(p.Identifier, p.LegacyIdentifier)
+ i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), identifier)
if err != nil {
time.Sleep(x.RandomDelay(s.d.Config().HasherArgon2(r.Context()).ExpectedDuration, s.d.Config().HasherArgon2(r.Context()).ExpectedDeviation))
- return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError()))
+ return nil, s.handleLoginError(r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError()))
}
var o identity.CredentialsPassword
@@ -81,20 +82,36 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow,
return nil, herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error()).WithWrap(err)
}
- if err := hash.Compare(r.Context(), []byte(p.Password), []byte(o.HashedPassword)); err != nil {
- return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError()))
- }
+ if o.ShouldUsePasswordMigrationHook() {
+ pwHook := s.d.Config().PasswordMigrationHook(r.Context())
+ if !pwHook.Enabled {
+ return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Password migration hook is not enabled but password migration is requested."))
+ }
+
+ migrationHook := hook.NewPasswordMigrationHook(s.d, pwHook.Config)
+ err = migrationHook.Execute(r.Context(), &hook.PasswordMigrationRequest{Identifier: identifier, Password: p.Password})
+ if err != nil {
+ return nil, s.handleLoginError(r, f, &p, err)
+ }
- if !s.d.Hasher(r.Context()).Understands([]byte(o.HashedPassword)) {
if err := s.migratePasswordHash(r.Context(), i.ID, []byte(p.Password)); err != nil {
- return nil, s.handleLoginError(w, r, f, &p, err)
+ return nil, s.handleLoginError(r, f, &p, err)
+ }
+ } else {
+ if err := hash.Compare(r.Context(), []byte(p.Password), []byte(o.HashedPassword)); err != nil {
+ return nil, s.handleLoginError(r, f, &p, errors.WithStack(schema.NewInvalidCredentialsError()))
+ }
+
+ if !s.d.Hasher(r.Context()).Understands([]byte(o.HashedPassword)) {
+ if err := s.migratePasswordHash(r.Context(), i.ID, []byte(p.Password)); err != nil {
+ return nil, s.handleLoginError(r, f, &p, err)
+ }
}
}
- f.Active = identity.CredentialsTypePassword
f.Active = s.ID()
if err = s.d.LoginFlowPersister().UpdateLoginFlow(r.Context(), f); err != nil {
- return nil, s.handleLoginError(w, r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error())))
+ return nil, s.handleLoginError(r, f, &p, errors.WithStack(herodot.ErrInternalServerError.WithReason("Could not update flow").WithDebug(err.Error())))
}
return i, nil
@@ -126,43 +143,87 @@ func (s *Strategy) migratePasswordHash(ctx context.Context, identifier uuid.UUID
return s.d.PrivilegedIdentityPool().UpdateIdentity(ctx, i)
}
-func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *login.Flow) error {
- // This strategy can only solve AAL1
- if requestedAAL > identity.AuthenticatorAssuranceLevel1 {
+func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, sr *login.Flow) error {
+ identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID())
+ if identifier == "" {
return nil
}
- if sr.IsForced() {
- // We only show this method on a refresh request if the user has indeed a password set.
- identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID())
- if identifier == "" {
- return nil
- }
+ // If we don't have a password set, do not show the password field.
+ count, err := s.CountActiveFirstFactorCredentials(id.Credentials)
+ if err != nil {
+ return err
+ } else if count == 0 {
+ return nil
+ }
- count, err := s.CountActiveFirstFactorCredentials(id.Credentials)
- if err != nil {
+ sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
+ sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden))
+ sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword))
+ sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLogin()))
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *login.Flow) error {
+ return nil
+}
+
+func (s *Strategy) addIdentifierNode(r *http.Request, sr *login.Flow) error {
+ ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context())
+ if err != nil {
+ return err
+ }
+
+ identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String())
+ if err != nil {
+ return err
+ }
+
+ sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel))
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error {
+ if err := s.addIdentifierNode(r, sr); err != nil {
+ return err
+ }
+
+ sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
+ sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword))
+ sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginPassword()))
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) error {
+ o := login.NewFormHydratorOptions(opts)
+
+ var count int
+ if o.IdentityHint != nil {
+ var err error
+ // If we have an identity hint we can perform identity credentials discovery and
+ // hide this credential if it should not be included.
+ if count, err = s.CountActiveFirstFactorCredentials(o.IdentityHint.Credentials); err != nil {
return err
- } else if count == 0 {
- return nil
}
+ }
+ if count > 0 || s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
- sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden))
- } else {
- ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context())
- if err != nil {
- return err
- }
- identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String())
- if err != nil {
- return err
- }
- sr.UI.SetNode(node.NewInputField("identifier", "", node.DefaultGroup, node.InputAttributeTypeText, node.WithRequiredInputAttribute).WithMetaLabel(identifierLabel))
+ sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword))
+ sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLoginPassword()))
}
- sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
- sr.UI.SetNode(NewPasswordNode("password", node.InputAttributeAutocompleteCurrentPassword))
- sr.UI.GetNodes().Append(node.NewInputField("method", "password", node.PasswordGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoLogin()))
+ if count == 0 {
+ return errors.WithStack(idfirst.ErrNoCredentialsFound)
+ }
+
+ return nil
+}
+func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, sr *login.Flow) error {
return nil
}
diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go
index 8d8879cce91c..036aa684169b 100644
--- a/selfservice/strategy/password/login_test.go
+++ b/selfservice/strategy/password/login_test.go
@@ -11,48 +11,52 @@ import (
"fmt"
"io"
"net/http"
+ "net/http/httptest"
"net/url"
"strings"
"testing"
"time"
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+
+ configtesthelpers "github.com/ory/kratos/driver/config/testhelpers"
+
+ "github.com/ory/x/snapshotx"
+
"github.com/ory/kratos/driver"
"github.com/ory/kratos/internal/registrationhelpers"
"github.com/ory/kratos/selfservice/flow"
"github.com/gofrs/uuid"
-
- "github.com/ory/x/urlx"
-
- "github.com/ory/kratos/hash"
- kratos "github.com/ory/kratos/internal/httpclient"
- "github.com/ory/x/assertx"
- "github.com/ory/x/errorsx"
- "github.com/ory/x/ioutilx"
- "github.com/ory/x/sqlxx"
-
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/ory/kratos/driver/config"
+ "github.com/ory/kratos/hash"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/internal"
+ kratos "github.com/ory/kratos/internal/httpclient"
"github.com/ory/kratos/internal/testhelpers"
"github.com/ory/kratos/schema"
"github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/kratos/text"
"github.com/ory/kratos/x"
+ "github.com/ory/x/assertx"
+ "github.com/ory/x/errorsx"
+ "github.com/ory/x/ioutilx"
+ "github.com/ory/x/sqlxx"
+ "github.com/ory/x/urlx"
)
//go:embed stub/login.schema.json
var loginSchema []byte
-func createIdentity(ctx context.Context, reg *driver.RegistryDefault, t *testing.T, identifier, password string) {
+func createIdentity(ctx context.Context, reg *driver.RegistryDefault, t *testing.T, identifier, password string) *identity.Identity {
p, _ := reg.Hasher(ctx).Generate(context.Background(), []byte(password))
iId := x.NewUUID()
- require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &identity.Identity{
+ id := &identity.Identity{
ID: iId,
Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)),
Credentials: map[identity.CredentialsType]identity.Credentials{
@@ -71,7 +75,9 @@ func createIdentity(ctx context.Context, reg *driver.RegistryDefault, t *testing
IdentityID: iId,
},
},
- }))
+ }
+ require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, id))
+ return id
}
func TestCompleteLogin(t *testing.T) {
@@ -514,7 +520,7 @@ func TestCompleteLogin(t *testing.T) {
t.Run("do not show password method if identity has no password set", func(t *testing.T) {
id := identity.NewIdentity("")
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
res, err := browserClient.Get(publicTS.URL + login.RouteInitBrowserFlow + "?refresh=true")
require.NoError(t, err)
@@ -574,7 +580,7 @@ func TestCompleteLogin(t *testing.T) {
t.Run("do not show password method if identity has no password set", func(t *testing.T) {
id := identity.NewIdentity("")
- hc := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ hc := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
res, err := hc.Do(testhelpers.NewHTTPGetAJAXRequest(t, publicTS.URL+login.RouteInitBrowserFlow+"?refresh=true"))
require.NoError(t, err)
@@ -633,7 +639,7 @@ func TestCompleteLogin(t *testing.T) {
t.Run("do not show password method if identity has no password set", func(t *testing.T) {
id := identity.NewIdentity("")
- hc := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ hc := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
res, err := hc.Do(testhelpers.NewHTTPGetAJAXRequest(t, publicTS.URL+login.RouteInitAPIFlow+"?refresh=true"))
require.NoError(t, err)
@@ -741,6 +747,32 @@ func TestCompleteLogin(t *testing.T) {
assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body)
})
+ t.Run("should succeed and include redirect continue_with in SPA flow", func(t *testing.T) {
+ identifier, pwd := x.NewUUID().String(), "password"
+ createIdentity(ctx, reg, t, identifier, pwd)
+
+ browserClient := testhelpers.NewClientWithCookies(t)
+ f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false)
+ values := url.Values{"method": {"password"}, "identifier": {strings.ToUpper(identifier)}, "password": {pwd}, "csrf_token": {x.FakeCSRFToken}}.Encode()
+ body, res := testhelpers.LoginMakeRequest(t, false, true, f, browserClient, values)
+
+ assert.EqualValues(t, http.StatusOK, res.StatusCode)
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
+ assert.EqualValues(t, conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), gjson.Get(body, "continue_with.0.redirect_browser_to").String(), "%s", body)
+ })
+
+ t.Run("should succeed and not have redirect continue_with in api flow", func(t *testing.T) {
+ identifier, pwd := x.NewUUID().String(), "password"
+ createIdentity(ctx, reg, t, identifier, pwd)
+ browserClient := testhelpers.NewClientWithCookies(t)
+ f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false)
+
+ body, res := testhelpers.LoginMakeRequest(t, true, true, f, browserClient, fmt.Sprintf(`{"method":"password","identifier":"%s","password":"%s"}`, strings.ToUpper(identifier), pwd))
+
+ assert.EqualValues(t, http.StatusOK, res.StatusCode, body)
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
+ })
+
t.Run("should login even if old form field name is used", func(t *testing.T) {
identifier, pwd := x.NewUUID().String(), "password"
createIdentity(ctx, reg, t, identifier, pwd)
@@ -864,4 +896,331 @@ func TestCompleteLogin(t *testing.T) {
false, true, http.StatusOK, redirTS.URL)
assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body)
})
+
+ t.Run("suite=password migration hook", func(t *testing.T) {
+ ctx := context.Background()
+
+ type (
+ hookPayload = struct {
+ Identifier string `json:"identifier"`
+ Password string `json:"password"`
+ }
+ tsRequestHandler = func(hookPayload) (status int, body string)
+ )
+ returnStatus := func(status int) func(string, string) tsRequestHandler {
+ return func(string, string) tsRequestHandler {
+ return func(hookPayload) (int, string) { return status, "" }
+ }
+ }
+ returnStatic := func(status int, body string) func(string, string) tsRequestHandler {
+ return func(string, string) tsRequestHandler {
+ return func(hookPayload) (int, string) { return status, body }
+ }
+ }
+
+ // each test case sends (number of expected calls) handlers to the channel, at a max of 3
+ tsChan := make(chan tsRequestHandler, 3)
+
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ b, err := io.ReadAll(r.Body)
+ require.NoError(t, err)
+ _ = r.Body.Close()
+ var payload hookPayload
+ require.NoError(t, json.Unmarshal(b, &payload))
+
+ select {
+ case handlerFn := <-tsChan:
+ status, body := handlerFn(payload)
+ w.WriteHeader(status)
+ _, _ = io.WriteString(w, body)
+
+ default:
+ t.Fatal("unexpected call to the password migration hook")
+ }
+ }))
+ t.Cleanup(ts.Close)
+
+ require.NoError(t, reg.Config().Set(ctx, config.ViperKeyPasswordMigrationHook, map[string]any{
+ "config": map[string]any{"url": ts.URL},
+ "enabled": true}))
+
+ for _, tc := range []struct {
+ name string
+ hookHandler func(identifier, password string) tsRequestHandler
+ expectHookCalls int
+ setupFn func() func()
+ credentialsConfig string
+ expectSuccess bool
+ }{{
+ name: "should call migration hook",
+ credentialsConfig: `{"use_password_migration_hook": true}`,
+ hookHandler: func(identifier, password string) tsRequestHandler {
+ return func(payload hookPayload) (status int, body string) {
+ if payload.Identifier == identifier && payload.Password == password {
+ return http.StatusOK, `{"status":"password_match"}`
+ } else {
+ return http.StatusOK, `{"status":"no_match"}`
+ }
+ }
+ },
+ expectHookCalls: 1,
+ expectSuccess: true,
+ }, {
+ name: "should not update identity when the password is wrong",
+ credentialsConfig: `{"use_password_migration_hook": true}`,
+ hookHandler: returnStatus(http.StatusForbidden),
+ expectHookCalls: 1,
+ expectSuccess: false,
+ }, {
+ name: "should inspect response",
+ credentialsConfig: `{"use_password_migration_hook": true}`,
+ hookHandler: returnStatic(http.StatusOK, `{"status":"password_no_match"}`),
+ expectHookCalls: 1,
+ expectSuccess: false,
+ }, {
+ name: "should not update identity when the migration hook returns 200 without JSON",
+ credentialsConfig: `{"use_password_migration_hook": true}`,
+ hookHandler: returnStatus(http.StatusOK),
+ expectHookCalls: 1,
+ expectSuccess: false,
+ }, {
+ name: "should not update identity when the migration hook returns 500",
+ credentialsConfig: `{"use_password_migration_hook": true}`,
+ hookHandler: returnStatus(http.StatusInternalServerError),
+ expectHookCalls: 3, // expect retries on 500
+ expectSuccess: false,
+ }, {
+ name: "should not update identity when the migration hook returns 201",
+ credentialsConfig: `{"use_password_migration_hook": true}`,
+ hookHandler: returnStatic(http.StatusCreated, `{"status":"password_match"}`),
+ expectHookCalls: 1,
+ expectSuccess: false,
+ }, {
+ name: "should not update identity and not call hook when hash is set",
+ credentialsConfig: `{"use_password_migration_hook": true, "hashed_password":"hash"}`,
+ expectSuccess: false,
+ }, {
+ name: "should not update identity and not call hook when use_password_migration_hook is not set",
+ credentialsConfig: `{"hashed_password":"hash"}`,
+ expectSuccess: false,
+ }, {
+ name: "should not update identity and not call hook when credential is empty",
+ credentialsConfig: `{}`,
+ expectSuccess: false,
+ }, {
+ name: "should not call migration hook if disabled",
+ credentialsConfig: `{"use_password_migration_hook": true}`,
+ setupFn: func() func() {
+ require.NoError(t, reg.Config().Set(ctx, config.ViperKeyPasswordMigrationHook+".enabled", false))
+ return func() {
+ require.NoError(t, reg.Config().Set(ctx, config.ViperKeyPasswordMigrationHook+".enabled", true))
+ }
+ },
+ expectSuccess: false,
+ }} {
+ t.Run("case="+tc.name, func(t *testing.T) {
+ if tc.setupFn != nil {
+ cleanup := tc.setupFn()
+ t.Cleanup(cleanup)
+ }
+
+ identifier := x.NewUUID().String()
+ password := x.NewUUID().String()
+ iId := x.NewUUID()
+ require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, &identity.Identity{
+ ID: iId,
+ Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)),
+ Credentials: map[identity.CredentialsType]identity.Credentials{
+ identity.CredentialsTypePassword: {
+ Type: identity.CredentialsTypePassword,
+ Identifiers: []string{identifier},
+ Config: sqlxx.JSONRawMessage(tc.credentialsConfig),
+ },
+ },
+ VerifiableAddresses: []identity.VerifiableAddress{
+ {
+ ID: x.NewUUID(),
+ Value: identifier,
+ Verified: true,
+ CreatedAt: time.Now(),
+ IdentityID: iId,
+ },
+ },
+ }))
+
+ values := func(v url.Values) {
+ v.Set("identifier", identifier)
+ v.Set("method", identity.CredentialsTypePassword.String())
+ v.Set("password", password)
+ }
+
+ for range tc.expectHookCalls {
+ tsChan <- tc.hookHandler(identifier, password)
+ }
+
+ browserClient := testhelpers.NewClientWithCookies(t)
+
+ if tc.expectSuccess {
+ body := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, values,
+ false, false, http.StatusOK, redirTS.URL)
+ assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body)
+
+ // check if password hash algorithm is upgraded
+ _, c, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(ctx, identity.CredentialsTypePassword, identifier)
+ require.NoError(t, err)
+ var o identity.CredentialsPassword
+ require.NoError(t, json.NewDecoder(bytes.NewBuffer(c.Config)).Decode(&o))
+ assert.True(t, reg.Hasher(ctx).Understands([]byte(o.HashedPassword)), "%s", o.HashedPassword)
+ assert.True(t, hash.IsBcryptHash([]byte(o.HashedPassword)), "%s", o.HashedPassword)
+
+ // retry after upgraded
+ body = testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, values,
+ false, true, http.StatusOK, redirTS.URL)
+ assert.Equal(t, identifier, gjson.Get(body, "identity.traits.subject").String(), "%s", body)
+ } else {
+ body := testhelpers.SubmitLoginForm(t, false, browserClient, publicTS, values,
+ false, false, http.StatusOK, "")
+ assert.Empty(t, gjson.Get(body, "identity.traits.subject").String(), "%s", body)
+ // Check that the config did not change
+ _, c, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypePassword, identifier)
+ require.NoError(t, err)
+ assert.JSONEq(t, tc.credentialsConfig, string(c.Config))
+ }
+
+ // expect all hook calls to be done
+ select {
+ case <-tsChan:
+ t.Fatal("the test unexpectedly did too few calls to the password hook")
+ default:
+ // pass
+ }
+ })
+ }
+ })
+}
+
+func TestFormHydration(t *testing.T) {
+ ctx := context.Background()
+ conf, reg := internal.NewFastRegistryWithMocks(t)
+ ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword), map[string]interface{}{"enabled": true})
+ ctx = testhelpers.WithDefaultIdentitySchemaFromRaw(ctx, loginSchema)
+
+ s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypePassword)
+ require.NoError(t, err)
+ fh, ok := s.(login.FormHydrator)
+ require.True(t, ok)
+
+ toSnapshot := func(t *testing.T, f *login.Flow) {
+ t.Helper()
+ // The CSRF token has a unique value that messes with the snapshot - ignore it.
+ f.UI.Nodes.ResetNodes("csrf_token")
+ snapshotx.SnapshotT(t, f.UI.Nodes)
+ }
+ newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) {
+ r := httptest.NewRequest("GET", "/self-service/login/browser", nil)
+ r = r.WithContext(ctx)
+ t.Helper()
+ f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser)
+ require.NoError(t, err)
+ return r, f
+ }
+
+ t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactorRefresh", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ id := createIdentity(ctx, reg, t, "some@user.com", "password")
+ r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ f.Refresh = true
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodSecondFactorRefresh", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodSecondFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) {
+ t.Run("case=no options", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false)
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=WithIdentifier", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false)
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=WithIdentityHint", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation enabled and identity has no password", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, true)
+
+ id := identity.NewIdentity("default")
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ ctx := configtesthelpers.WithConfigValue(ctx, config.ViperKeySecurityAccountEnumerationMitigate, false)
+
+ t.Run("case=identity has password", func(t *testing.T) {
+ identifier, pwd := x.NewUUID().String(), "password"
+ id := createIdentity(ctx, reg, t, identifier, pwd)
+
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=identity does not have a password", func(t *testing.T) {
+ id := identity.NewIdentity("default")
+ r, f := newFlow(ctx, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) {
+ r, f := newFlow(ctx, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f))
+ toSnapshot(t, f)
+ })
}
diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go
index 14bf2382b212..d52ca2d77707 100644
--- a/selfservice/strategy/password/registration_test.go
+++ b/selfservice/strategy/password/registration_test.go
@@ -14,6 +14,8 @@ import (
"testing"
"time"
+ "github.com/ory/kratos/selfservice/flow"
+
"github.com/ory/kratos/driver"
"github.com/ory/kratos/internal/registrationhelpers"
@@ -106,7 +108,7 @@ func TestRegistration(t *testing.T) {
})
})
- var expectLoginBody = func(t *testing.T, browserRedirTS *httptest.Server, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string {
+ var expectRegistrationBody = func(t *testing.T, browserRedirTS *httptest.Server, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string {
if isAPI {
return testhelpers.SubmitRegistrationForm(t, isAPI, hc, publicTS, values,
isSPA, http.StatusOK,
@@ -126,17 +128,17 @@ func TestRegistration(t *testing.T) {
isSPA, http.StatusOK, expectReturnTo)
}
- var expectSuccessfulLogin = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string {
+ var expectSuccessfulRegistration = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string {
useReturnToFromTS(redirTS)
- return expectLoginBody(t, redirTS, isAPI, isSPA, hc, values)
+ return expectRegistrationBody(t, redirTS, isAPI, isSPA, hc, values)
}
- var expectNoLogin = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string {
+ var expectNoRegistration = func(t *testing.T, isAPI, isSPA bool, hc *http.Client, values func(url.Values)) string {
useReturnToFromTS(redirNoSessionTS)
t.Cleanup(func() {
useReturnToFromTS(redirTS)
})
- return expectLoginBody(t, redirNoSessionTS, isAPI, isSPA, hc, values)
+ return expectRegistrationBody(t, redirNoSessionTS, isAPI, isSPA, hc, values)
}
t.Run("case=should reject invalid transient payload", func(t *testing.T) {
@@ -178,7 +180,7 @@ func TestRegistration(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
username := x.NewUUID().String()
- body := expectSuccessfulLogin(t, true, false, nil, func(v url.Values) {
+ body := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) {
setValues(username, v)
})
assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body)
@@ -188,7 +190,7 @@ func TestRegistration(t *testing.T) {
t.Run("type=spa", func(t *testing.T) {
username := x.NewUUID().String()
- body := expectSuccessfulLogin(t, false, true, nil, func(v url.Values) {
+ body := expectSuccessfulRegistration(t, false, true, nil, func(v url.Values) {
setValues(username, v)
})
assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body)
@@ -198,7 +200,7 @@ func TestRegistration(t *testing.T) {
t.Run("type=browser", func(t *testing.T) {
username := x.NewUUID().String()
- body := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) {
+ body := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) {
setValues(username, v)
})
assert.Equal(t, username, gjson.Get(body, "identity.traits.username").String(), "%s", body)
@@ -213,7 +215,7 @@ func TestRegistration(t *testing.T) {
})
t.Run("type=api", func(t *testing.T) {
- body := expectSuccessfulLogin(t, true, false, nil, func(v url.Values) {
+ body := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-8-api")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
@@ -221,10 +223,11 @@ func TestRegistration(t *testing.T) {
assert.Equal(t, `registration-identifier-8-api`, gjson.Get(body, "identity.traits.username").String(), "%s", body)
assert.NotEmpty(t, gjson.Get(body, "session_token").String(), "%s", body)
assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body)
+ assert.NotContains(t, gjson.Get(body, "continue_with").Raw, string(flow.ContinueWithActionRedirectBrowserToString), "%s", body)
})
t.Run("type=spa", func(t *testing.T) {
- body := expectSuccessfulLogin(t, false, true, nil, func(v url.Values) {
+ body := expectSuccessfulRegistration(t, false, true, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-8-spa")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
@@ -232,15 +235,17 @@ func TestRegistration(t *testing.T) {
assert.Equal(t, `registration-identifier-8-spa`, gjson.Get(body, "identity.traits.username").String(), "%s", body)
assert.Empty(t, gjson.Get(body, "session_token").String(), "%s", body)
assert.NotEmpty(t, gjson.Get(body, "session.id").String(), "%s", body)
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
})
t.Run("type=browser", func(t *testing.T) {
- body := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) {
+ body := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-8-browser")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
})
assert.Equal(t, `registration-identifier-8-browser`, gjson.Get(body, "identity.traits.username").String(), "%s", body)
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
})
})
@@ -249,7 +254,7 @@ func TestRegistration(t *testing.T) {
conf.MustSet(ctx, config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil)
t.Run("type=api", func(t *testing.T) {
- body := expectNoLogin(t, true, false, nil, func(v url.Values) {
+ body := expectNoRegistration(t, true, false, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-8-api-nosession")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
@@ -260,7 +265,7 @@ func TestRegistration(t *testing.T) {
})
t.Run("type=spa", func(t *testing.T) {
- expectNoLogin(t, false, true, nil, func(v url.Values) {
+ expectNoRegistration(t, false, true, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-8-spa-nosession")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
@@ -268,7 +273,7 @@ func TestRegistration(t *testing.T) {
})
t.Run("type=browser", func(t *testing.T) {
- expectNoLogin(t, false, false, nil, func(v url.Values) {
+ expectNoRegistration(t, false, false, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-8-browser-nosession")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
@@ -300,7 +305,7 @@ func TestRegistration(t *testing.T) {
v.Set("traits.foobar", "bar")
}
- _ = expectSuccessfulLogin(t, true, false, apiClient, values)
+ _ = expectSuccessfulRegistration(t, true, false, apiClient, values)
body := testhelpers.SubmitRegistrationForm(t, true, apiClient, publicTS,
applyTransform(values, transform), false, http.StatusBadRequest,
publicTS.URL+registration.RouteSubmitFlow)
@@ -314,7 +319,7 @@ func TestRegistration(t *testing.T) {
v.Set("traits.foobar", "bar")
}
- _ = expectSuccessfulLogin(t, false, true, nil, values)
+ _ = expectSuccessfulRegistration(t, false, true, nil, values)
body := registrationhelpers.ExpectValidationError(t, publicTS, conf, "spa", applyTransform(values, transform))
assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "You tried signing in with registration-identifier-8-spa-duplicate-"+suffix+" which is already in use by another account. You can sign in using your password.", "%s", body)
})
@@ -326,7 +331,7 @@ func TestRegistration(t *testing.T) {
v.Set("traits.foobar", "bar")
}
- _ = expectSuccessfulLogin(t, false, false, nil, values)
+ _ = expectSuccessfulRegistration(t, false, false, nil, values)
body := registrationhelpers.ExpectValidationError(t, publicTS, conf, "browser", applyTransform(values, transform))
assert.Contains(t, gjson.Get(body, "ui.messages.0.text").String(), "You tried signing in with registration-identifier-8-browser-duplicate-"+suffix+" which is already in use by another account. You can sign in using your password.", "%s", body)
})
@@ -541,7 +546,7 @@ func TestRegistration(t *testing.T) {
})
t.Run("type=api", func(t *testing.T) {
- actual := expectSuccessfulLogin(t, true, false, nil, func(v url.Values) {
+ actual := expectSuccessfulRegistration(t, true, false, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-10-api")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
@@ -550,7 +555,7 @@ func TestRegistration(t *testing.T) {
})
t.Run("type=spa", func(t *testing.T) {
- actual := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) {
+ actual := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-10-spa")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
@@ -559,7 +564,7 @@ func TestRegistration(t *testing.T) {
})
t.Run("type=browser", func(t *testing.T) {
- actual := expectSuccessfulLogin(t, false, false, nil, func(v url.Values) {
+ actual := expectSuccessfulRegistration(t, false, false, nil, func(v url.Values) {
v.Set("traits.username", "registration-identifier-10-browser")
v.Set("password", x.NewUUID().String())
v.Set("traits.foobar", "bar")
@@ -620,7 +625,7 @@ func TestRegistration(t *testing.T) {
username := "registration-custom-schema"
t.Run("type=api", func(t *testing.T) {
- body := expectNoLogin(t, true, false, nil, func(v url.Values) {
+ body := expectNoRegistration(t, true, false, nil, func(v url.Values) {
v.Set("traits.username", username+"-api")
v.Set("password", x.NewUUID().String())
v.Set("traits.baz", "bar")
@@ -631,7 +636,7 @@ func TestRegistration(t *testing.T) {
})
t.Run("type=spa", func(t *testing.T) {
- expectNoLogin(t, false, true, nil, func(v url.Values) {
+ expectNoRegistration(t, false, true, nil, func(v url.Values) {
v.Set("traits.username", username+"-spa")
v.Set("password", x.NewUUID().String())
v.Set("traits.baz", "bar")
@@ -639,7 +644,7 @@ func TestRegistration(t *testing.T) {
})
t.Run("type=browser", func(t *testing.T) {
- expectNoLogin(t, false, false, nil, func(v url.Values) {
+ expectNoRegistration(t, false, false, nil, func(v url.Values) {
v.Set("traits.username", username+"-browser")
v.Set("password", x.NewUUID().String())
v.Set("traits.baz", "bar")
diff --git a/selfservice/strategy/password/settings_test.go b/selfservice/strategy/password/settings_test.go
index a4ee7e6c7fa0..e5ea909856b0 100644
--- a/selfservice/strategy/password/settings_test.go
+++ b/selfservice/strategy/password/settings_test.go
@@ -13,6 +13,8 @@ import (
"strings"
"testing"
+ "github.com/ory/kratos/selfservice/flow"
+
"github.com/ory/kratos/internal/settingshelpers"
"github.com/ory/kratos/text"
@@ -82,7 +84,7 @@ func TestSettings(t *testing.T) {
testhelpers.StrategyEnable(t, conf, identity.CredentialsTypePassword.String(), true)
testhelpers.StrategyEnable(t, conf, settings.StrategyProfile, true)
- _ = testhelpers.NewSettingsUIFlowEchoServer(t, reg)
+ settingsUI := testhelpers.NewSettingsUIFlowEchoServer(t, reg)
_ = testhelpers.NewErrorTestServer(t, reg)
_ = testhelpers.NewLoginUIWith401Response(t, conf)
conf.MustSet(ctx, config.ViperKeySelfServiceSettingsPrivilegedAuthenticationAfter, "1m")
@@ -94,10 +96,10 @@ func TestSettings(t *testing.T) {
publicTS, _ := testhelpers.NewKratosServer(t, reg)
- browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity1)
- browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity2)
- apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, apiIdentity1)
- apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, apiIdentity2)
+ browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, browserIdentity1)
+ browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, browserIdentity2)
+ apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, apiIdentity1)
+ apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, apiIdentity2)
t.Run("description=not authorized to call endpoints without a session", func(t *testing.T) {
c := testhelpers.NewDebugClient(t)
@@ -242,15 +244,20 @@ func TestSettings(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
actual := testhelpers.SubmitSettingsForm(t, true, false, apiUser1, publicTS, payload, http.StatusOK, publicTS.URL+settings.RouteSubmitFlow)
check(t, actual)
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
t.Run("type=spa", func(t *testing.T) {
actual := testhelpers.SubmitSettingsForm(t, false, true, browserUser1, publicTS, payload, http.StatusOK, publicTS.URL+settings.RouteSubmitFlow)
check(t, actual)
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual)
+ assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), settingsUI.URL, "%s", actual)
})
t.Run("type=browser", func(t *testing.T) {
- check(t, testhelpers.SubmitSettingsForm(t, false, false, browserUser1, publicTS, payload, http.StatusOK, conf.SelfServiceFlowSettingsUI(ctx).String()))
+ actual := testhelpers.SubmitSettingsForm(t, false, false, browserUser1, publicTS, payload, http.StatusOK, conf.SelfServiceFlowSettingsUI(ctx).String())
+ check(t, actual)
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
})
@@ -340,9 +347,9 @@ func TestSettings(t *testing.T) {
bi := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh")
si := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh")
ai := newIdentityWithoutCredentials(x.NewUUID().String() + "@ory.sh")
- browserUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, bi)
- spaUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, si)
- apiUser := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, ai)
+ browserUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, bi)
+ spaUser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, si)
+ apiUser := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, ai)
var check = func(t *testing.T, actual string, id *identity.Identity) {
assert.Equal(t, "success", gjson.Get(actual, "state").String(), "%s", actual)
@@ -433,12 +440,12 @@ func TestSettings(t *testing.T) {
var initClients = func(isAPI, isSPA bool, id *identity.Identity) (client1, client2 *http.Client) {
if isAPI {
- client1 = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
- client2 = testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ client1 = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
+ client2 = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
return client1, client2
}
- client1 = testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
- client2 = testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ client1 = testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
+ client2 = testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
return client1, client2
}
@@ -485,8 +492,8 @@ func TestSettings(t *testing.T) {
testhelpers.SetDefaultIdentitySchema(conf, "file://stub/missing-identifier.schema.json")
id := newIdentityWithoutCredentials(testhelpers.RandomEmail())
- browser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
- api := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ browser := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
+ api := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
for _, f := range []string{"spa", "api", "browser"} {
t.Run("type="+f, func(t *testing.T) {
diff --git a/selfservice/strategy/password/strategy.go b/selfservice/strategy/password/strategy.go
index 911ad619cd15..ae57982dd89f 100644
--- a/selfservice/strategy/password/strategy.go
+++ b/selfservice/strategy/password/strategy.go
@@ -7,11 +7,12 @@ import (
"context"
"encoding/json"
- "github.com/ory/kratos/ui/node"
-
"github.com/go-playground/validator/v10"
"github.com/pkg/errors"
+ "github.com/ory/kratos/ui/node"
+ "github.com/ory/x/jsonnetsecure"
+
"github.com/ory/x/decoderx"
"github.com/ory/kratos/continuity"
@@ -37,9 +38,10 @@ type registrationStrategyDependencies interface {
x.WriterProvider
x.CSRFTokenGeneratorProvider
x.CSRFProvider
-
+ x.HTTPClientProvider
+ x.TracingProvider
+ jsonnetsecure.VMProvider
config.Provider
-
continuity.ManagementProvider
errorx.ManagementProvider
diff --git a/selfservice/strategy/password/strategy_disabled_test.go b/selfservice/strategy/password/strategy_disabled_test.go
index 6cf92147c3a4..e95e98c5913e 100644
--- a/selfservice/strategy/password/strategy_disabled_test.go
+++ b/selfservice/strategy/password/strategy_disabled_test.go
@@ -4,6 +4,7 @@
package password_test
import (
+ "context"
"io"
"net/http"
"net/url"
@@ -54,7 +55,7 @@ func TestDisabledEndpoint(t *testing.T) {
t.Run("case=should not settings when password method is disabled", func(t *testing.T) {
testhelpers.SetDefaultIdentitySchema(conf, "file://stub/login.schema.json")
- c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, reg)
+ c := testhelpers.NewHTTPClientWithArbitrarySessionCookie(t, context.Background(), reg)
t.Run("method=GET", func(t *testing.T) {
t.Skip("GET is currently not supported for this endpoint.")
diff --git a/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=api#01.json b/selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json
similarity index 100%
rename from selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=api#01.json
rename to selfservice/strategy/profile/.snapshots/TestStrategyTraits-description=hydrate_the_proper_fields-type=spa.json
diff --git a/selfservice/strategy/profile/strategy_test.go b/selfservice/strategy/profile/strategy_test.go
index 7d0c831711c3..fa5b1652a130 100644
--- a/selfservice/strategy/profile/strategy_test.go
+++ b/selfservice/strategy/profile/strategy_test.go
@@ -92,10 +92,10 @@ func TestStrategyTraits(t *testing.T) {
browserIdentity2 := &identity.Identity{ID: x.NewUUID(), Traits: identity.Traits(`{}`), State: identity.StateActive}
apiIdentity2 := &identity.Identity{ID: x.NewUUID(), Traits: identity.Traits(`{}`), State: identity.StateActive}
- browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity1)
- browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity2)
- apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, apiIdentity1)
- apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, apiIdentity2)
+ browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, browserIdentity1)
+ browserUser2 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, browserIdentity2)
+ apiUser1 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, apiIdentity1)
+ apiUser2 := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, apiIdentity2)
t.Run("description=not authorized to call endpoints without a session", func(t *testing.T) {
setUnprivileged(t)
@@ -210,7 +210,7 @@ func TestStrategyTraits(t *testing.T) {
run(t, apiIdentity1, pr, settings.RouteInitAPIFlow)
})
- t.Run("type=api", func(t *testing.T) {
+ t.Run("type=spa", func(t *testing.T) {
pr, _, err := testhelpers.NewSDKCustomClient(publicTS, browserUser1).FrontendApi.CreateBrowserSettingsFlow(context.Background()).Execute()
require.NoError(t, err)
run(t, browserIdentity1, pr, settings.RouteInitBrowserFlow)
@@ -449,15 +449,20 @@ func TestStrategyTraits(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
actual := expectSuccess(t, true, false, apiUser1, payload("not-john-doe-api@mail.com"))
check(t, actual)
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
t.Run("type=sqa", func(t *testing.T) {
actual := expectSuccess(t, false, true, browserUser1, payload("not-john-doe-browser@mail.com"))
check(t, actual)
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual)
+ assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), ui.URL, "%s", actual)
})
t.Run("type=browser", func(t *testing.T) {
- check(t, expectSuccess(t, false, false, browserUser1, payload("not-john-doe-browser@mail.com")))
+ actual := expectSuccess(t, false, false, browserUser1, payload("not-john-doe-browser@mail.com"))
+ check(t, actual)
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
})
@@ -612,7 +617,7 @@ func TestDisabledEndpoint(t *testing.T) {
publicTS, _ := testhelpers.NewKratosServer(t, reg)
browserIdentity1 := newIdentityWithPassword("john-browser@doe.com")
- browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, browserIdentity1)
+ browserUser1 := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, context.Background(), reg, browserIdentity1)
t.Run("case=should not submit when profile method is disabled", func(t *testing.T) {
t.Run("method=GET", func(t *testing.T) {
diff --git a/selfservice/strategy/totp/generator.go b/selfservice/strategy/totp/generator.go
index fe79d8991d0d..9846506f1671 100644
--- a/selfservice/strategy/totp/generator.go
+++ b/selfservice/strategy/totp/generator.go
@@ -25,6 +25,7 @@ import (
// So we need 160/8 = 20 key length. stdtotp.Generate uses the key
// length for reading from crypto.Rand.
const secretSize = 160 / 8
+const digits = otp.DigitsSix
func NewKey(ctx context.Context, accountName string, d interface {
config.Provider
@@ -33,7 +34,7 @@ func NewKey(ctx context.Context, accountName string, d interface {
Issuer: d.Config().TOTPIssuer(ctx),
AccountName: accountName,
SecretSize: secretSize,
- Digits: otp.DigitsSix,
+ Digits: digits,
Period: 30,
})
if err != nil {
diff --git a/selfservice/strategy/totp/login_test.go b/selfservice/strategy/totp/login_test.go
index 6456ea7cc599..7a48424ab412 100644
--- a/selfservice/strategy/totp/login_test.go
+++ b/selfservice/strategy/totp/login_test.go
@@ -13,6 +13,8 @@ import (
"testing"
"time"
+ "github.com/ory/kratos/selfservice/flow"
+
"github.com/ory/x/assertx"
"github.com/gofrs/uuid"
@@ -107,7 +109,7 @@ func TestCompleteLogin(t *testing.T) {
t.Run("case=totp payload is set when identity has totp", func(t *testing.T) {
id, _, _ := createIdentity(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
@@ -117,7 +119,7 @@ func TestCompleteLogin(t *testing.T) {
t.Run("case=totp payload is not set when identity has no totp", func(t *testing.T) {
id := createIdentityWithoutTOTP(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
assertx.EqualAsJSON(t, nil, f.Ui.Nodes)
})
@@ -126,7 +128,7 @@ func TestCompleteLogin(t *testing.T) {
id, _, _ := createIdentity(t, reg)
t.Run("type=api", func(t *testing.T) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
body, res := testhelpers.LoginMakeRequest(t, true, false, f, apiClient, "14=)=!(%)$/ZP()GHIÖ")
@@ -136,7 +138,7 @@ func TestCompleteLogin(t *testing.T) {
})
t.Run("type=browser", func(t *testing.T) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, false, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
body, res := testhelpers.LoginMakeRequest(t, false, false, f, browserClient, "14=)=!(%)$/ZP()GHIÖ")
@@ -146,7 +148,7 @@ func TestCompleteLogin(t *testing.T) {
})
t.Run("type=spa", func(t *testing.T) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, true, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
body, res := testhelpers.LoginMakeRequest(t, false, true, f, browserClient, "14=)=!(%)$/ZP()GHIÖ")
@@ -157,7 +159,7 @@ func TestCompleteLogin(t *testing.T) {
})
doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", "totp")
@@ -167,7 +169,7 @@ func TestCompleteLogin(t *testing.T) {
}
doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity, returnTo string) (string, *http.Response) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
opts := []testhelpers.InitFlowWithOption{testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2)}
if len(returnTo) > 0 {
@@ -333,23 +335,36 @@ func TestCompleteLogin(t *testing.T) {
t.Run("type=api", func(t *testing.T) {
body, res := doAPIFlow(t, payload, id)
check(t, false, body, res)
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
})
t.Run("type=browser", func(t *testing.T) {
body, res := doBrowserFlow(t, false, payload, id, "")
check(t, true, body, res)
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
})
t.Run("type=browser set return_to", func(t *testing.T) {
returnTo := "https://www.ory.sh"
- _, res := doBrowserFlow(t, false, payload, id, returnTo)
+ body, res := doBrowserFlow(t, false, payload, id, returnTo)
t.Log(res.Request.URL.String())
assert.Contains(t, res.Request.URL.String(), returnTo)
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
})
t.Run("type=spa", func(t *testing.T) {
body, res := doBrowserFlow(t, true, payload, id, "")
check(t, false, body, res)
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
+ assert.EqualValues(t, conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), gjson.Get(body, "continue_with.0.redirect_browser_to").String(), "%s", body)
+ })
+
+ t.Run("type=spa set return_to", func(t *testing.T) {
+ returnTo := "https://www.ory.sh"
+ body, res := doBrowserFlow(t, true, payload, id, returnTo)
+ check(t, false, body, res)
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
+ assert.EqualValues(t, returnTo, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), "%s", body)
})
})
diff --git a/selfservice/strategy/totp/settings_test.go b/selfservice/strategy/totp/settings_test.go
index 0fd479f1b220..b44cc736a560 100644
--- a/selfservice/strategy/totp/settings_test.go
+++ b/selfservice/strategy/totp/settings_test.go
@@ -64,7 +64,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("case=device unlinking is available when identity has totp", func(t *testing.T) {
id, _, _ := createIdentity(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
@@ -76,7 +76,7 @@ func TestCompleteSettings(t *testing.T) {
id.Credentials = nil
require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id))
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
@@ -87,7 +87,7 @@ func TestCompleteSettings(t *testing.T) {
})
doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", "totp")
@@ -97,7 +97,7 @@ func TestCompleteSettings(t *testing.T) {
}
doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", "totp")
@@ -241,6 +241,7 @@ func TestCompleteSettings(t *testing.T) {
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual)
checkIdentity(t, id)
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
t.Run("type=spa", func(t *testing.T) {
@@ -250,6 +251,9 @@ func TestCompleteSettings(t *testing.T) {
assert.Contains(t, res.Request.URL.String(), publicTS.URL+settings.RouteSubmitFlow)
assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual)
checkIdentity(t, id)
+
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual)
+ assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", actual)
})
t.Run("type=browser", func(t *testing.T) {
@@ -259,6 +263,7 @@ func TestCompleteSettings(t *testing.T) {
assert.Contains(t, res.Request.URL.String(), uiTS.URL)
assert.EqualValues(t, flow.StateSuccess, gjson.Get(actual, "state").String(), actual)
checkIdentity(t, id)
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
})
})
@@ -344,12 +349,19 @@ func TestCompleteSettings(t *testing.T) {
checkIdentity(t, id, key)
testhelpers.EnsureAAL(t, hc, publicTS, "aal2", string(identity.CredentialsTypeTOTP))
+
+ if isSPA {
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual)
+ assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", actual)
+ } else {
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
+ }
}
t.Run("type=api", func(t *testing.T) {
id := createIdentityWithoutTOTP(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
run(t, true, false, id, apiClient, f)
@@ -358,7 +370,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("type=spa", func(t *testing.T) {
id := createIdentityWithoutTOTP(t, reg)
- user := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ user := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, user, true, publicTS)
run(t, false, true, id, user, f)
@@ -367,7 +379,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("type=browser", func(t *testing.T) {
id := createIdentityWithoutTOTP(t, reg)
- user := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ user := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, user, false, publicTS)
run(t, false, false, id, user, f)
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json
index ca960c98d683..b180cf04a403 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=mfa-case=webauthn_payload_is_set_when_identity_has_webauthn.json
@@ -24,25 +24,6 @@
"meta": {},
"type": "input"
},
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
{
"attributes": {
"disabled": false,
@@ -61,7 +42,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -70,5 +51,24 @@
"messages": [],
"meta": {},
"type": "script"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login_trigger",
+ "node_type": "input",
+ "onclickTrigger": "oryWebAuthnLogin",
+ "type": "button"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ },
+ "type": "input"
}
]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json
index f4be195cdecf..399562e7015d 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=browser.json
@@ -37,7 +37,7 @@
"async": true,
"referrerpolicy": "no-referrer",
"crossorigin": "anonymous",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"type": "text/javascript",
"node_type": "script"
},
@@ -51,6 +51,7 @@
"name": "webauthn_login_trigger",
"type": "button",
"disabled": false,
+ "onclickTrigger": "oryWebAuthnLogin",
"node_type": "input"
},
"messages": [],
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json
index f4be195cdecf..399562e7015d 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=should_fail_if_webauthn_login_is_invalid-type=spa.json
@@ -37,7 +37,7 @@
"async": true,
"referrerpolicy": "no-referrer",
"crossorigin": "anonymous",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"type": "text/javascript",
"node_type": "script"
},
@@ -51,6 +51,7 @@
"name": "webauthn_login_trigger",
"type": "button",
"disabled": false,
+ "onclickTrigger": "oryWebAuthnLogin",
"node_type": "input"
},
"messages": [],
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json
index 6668b171ed43..052fc466dc5b 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=passwordless-case=webauthn_button_exists.json
@@ -46,7 +46,7 @@
"meta": {
"label": {
"id": 1010008,
- "text": "Use security key",
+ "text": "Sign in with hardware key",
"type": "info"
}
},
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json
deleted file mode 100644
index 581bff275b17..000000000000
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-browser.json
+++ /dev/null
@@ -1,75 +0,0 @@
-[
- {
- "attributes": {
- "disabled": false,
- "name": "csrf_token",
- "node_type": "input",
- "required": true,
- "type": "hidden"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "identifier",
- "node_type": "input",
- "type": "hidden",
- "value": "foo@bar.com"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login",
- "node_type": "input",
- "type": "hidden",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "async": true,
- "crossorigin": "anonymous",
- "id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
- "node_type": "script",
- "referrerpolicy": "no-referrer",
- "type": "text/javascript"
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "script"
- }
-]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json
deleted file mode 100644
index 581bff275b17..000000000000
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false-spa.json
+++ /dev/null
@@ -1,75 +0,0 @@
-[
- {
- "attributes": {
- "disabled": false,
- "name": "csrf_token",
- "node_type": "input",
- "required": true,
- "type": "hidden"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "identifier",
- "node_type": "input",
- "type": "hidden",
- "value": "foo@bar.com"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login",
- "node_type": "input",
- "type": "hidden",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "async": true,
- "crossorigin": "anonymous",
- "id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
- "node_type": "script",
- "referrerpolicy": "no-referrer",
- "type": "text/javascript"
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "script"
- }
-]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json
deleted file mode 100644
index 581bff275b17..000000000000
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-browser.json
+++ /dev/null
@@ -1,75 +0,0 @@
-[
- {
- "attributes": {
- "disabled": false,
- "name": "csrf_token",
- "node_type": "input",
- "required": true,
- "type": "hidden"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "identifier",
- "node_type": "input",
- "type": "hidden",
- "value": "foo@bar.com"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login",
- "node_type": "input",
- "type": "hidden",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "async": true,
- "crossorigin": "anonymous",
- "id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
- "node_type": "script",
- "referrerpolicy": "no-referrer",
- "type": "text/javascript"
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "script"
- }
-]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json
deleted file mode 100644
index 581bff275b17..000000000000
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#01-spa.json
+++ /dev/null
@@ -1,75 +0,0 @@
-[
- {
- "attributes": {
- "disabled": false,
- "name": "csrf_token",
- "node_type": "input",
- "required": true,
- "type": "hidden"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "identifier",
- "node_type": "input",
- "type": "hidden",
- "value": "foo@bar.com"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login",
- "node_type": "input",
- "type": "hidden",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "async": true,
- "crossorigin": "anonymous",
- "id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
- "node_type": "script",
- "referrerpolicy": "no-referrer",
- "type": "text/javascript"
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "script"
- }
-]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json
deleted file mode 100644
index 581bff275b17..000000000000
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-browser.json
+++ /dev/null
@@ -1,75 +0,0 @@
-[
- {
- "attributes": {
- "disabled": false,
- "name": "csrf_token",
- "node_type": "input",
- "required": true,
- "type": "hidden"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "identifier",
- "node_type": "input",
- "type": "hidden",
- "value": "foo@bar.com"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login",
- "node_type": "input",
- "type": "hidden",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "async": true,
- "crossorigin": "anonymous",
- "id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
- "node_type": "script",
- "referrerpolicy": "no-referrer",
- "type": "text/javascript"
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "script"
- }
-]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json
deleted file mode 100644
index 581bff275b17..000000000000
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true#02-spa.json
+++ /dev/null
@@ -1,75 +0,0 @@
-[
- {
- "attributes": {
- "disabled": false,
- "name": "csrf_token",
- "node_type": "input",
- "required": true,
- "type": "hidden"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "identifier",
- "node_type": "input",
- "type": "hidden",
- "value": "foo@bar.com"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login",
- "node_type": "input",
- "type": "hidden",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "async": true,
- "crossorigin": "anonymous",
- "id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
- "node_type": "script",
- "referrerpolicy": "no-referrer",
- "type": "text/javascript"
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "script"
- }
-]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json
deleted file mode 100644
index 581bff275b17..000000000000
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-browser.json
+++ /dev/null
@@ -1,75 +0,0 @@
-[
- {
- "attributes": {
- "disabled": false,
- "name": "csrf_token",
- "node_type": "input",
- "required": true,
- "type": "hidden"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "identifier",
- "node_type": "input",
- "type": "hidden",
- "value": "foo@bar.com"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login",
- "node_type": "input",
- "type": "hidden",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "async": true,
- "crossorigin": "anonymous",
- "id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
- "node_type": "script",
- "referrerpolicy": "no-referrer",
- "type": "text/javascript"
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "script"
- }
-]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json
deleted file mode 100644
index 581bff275b17..000000000000
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=true-spa.json
+++ /dev/null
@@ -1,75 +0,0 @@
-[
- {
- "attributes": {
- "disabled": false,
- "name": "csrf_token",
- "node_type": "input",
- "required": true,
- "type": "hidden"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "identifier",
- "node_type": "input",
- "type": "hidden",
- "value": "foo@bar.com"
- },
- "group": "default",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login",
- "node_type": "input",
- "type": "hidden",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "input"
- },
- {
- "attributes": {
- "async": true,
- "crossorigin": "anonymous",
- "id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
- "node_type": "script",
- "referrerpolicy": "no-referrer",
- "type": "text/javascript"
- },
- "group": "webauthn",
- "messages": [],
- "meta": {},
- "type": "script"
- }
-]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-browser.json
similarity index 85%
rename from selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json
rename to selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-browser.json
index 581bff275b17..5021f44d2f94 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-spa.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-browser.json
@@ -25,25 +25,6 @@
"meta": {},
"type": "input"
},
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
{
"attributes": {
"disabled": false,
@@ -62,7 +43,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -71,5 +52,24 @@
"messages": [],
"meta": {},
"type": "script"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login_trigger",
+ "node_type": "input",
+ "onclickTrigger": "oryWebAuthnLogin",
+ "type": "button"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ },
+ "type": "input"
}
]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-spa.json
similarity index 85%
rename from selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json
rename to selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-spa.json
index 581bff275b17..5021f44d2f94 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-browser.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v0_credentials-spa.json
@@ -25,25 +25,6 @@
"meta": {},
"type": "input"
},
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
{
"attributes": {
"disabled": false,
@@ -62,7 +43,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -71,5 +52,24 @@
"messages": [],
"meta": {},
"type": "script"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login_trigger",
+ "node_type": "input",
+ "onclickTrigger": "oryWebAuthnLogin",
+ "type": "button"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ },
+ "type": "input"
}
]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-browser.json
similarity index 85%
rename from selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json
rename to selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-browser.json
index 581bff275b17..5021f44d2f94 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#01-spa.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-browser.json
@@ -25,25 +25,6 @@
"meta": {},
"type": "input"
},
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
{
"attributes": {
"disabled": false,
@@ -62,7 +43,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -71,5 +52,24 @@
"messages": [],
"meta": {},
"type": "script"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login_trigger",
+ "node_type": "input",
+ "onclickTrigger": "oryWebAuthnLogin",
+ "type": "button"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ },
+ "type": "input"
}
]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-spa.json
similarity index 85%
rename from selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json
rename to selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-spa.json
index 581bff275b17..5021f44d2f94 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-case=mfa_v0_credentials-passwordless_enabled=false#02-browser.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=mfa_v1_credentials-spa.json
@@ -25,25 +25,6 @@
"meta": {},
"type": "input"
},
- {
- "attributes": {
- "disabled": false,
- "name": "webauthn_login_trigger",
- "node_type": "input",
- "type": "button",
- "value": ""
- },
- "group": "webauthn",
- "messages": [],
- "meta": {
- "label": {
- "id": 1010008,
- "text": "Use security key",
- "type": "info"
- }
- },
- "type": "input"
- },
{
"attributes": {
"disabled": false,
@@ -62,7 +43,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
@@ -71,5 +52,24 @@
"messages": [],
"meta": {},
"type": "script"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login_trigger",
+ "node_type": "input",
+ "onclickTrigger": "oryWebAuthnLogin",
+ "type": "button"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ },
+ "type": "input"
}
]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-browser.json
new file mode 100644
index 000000000000..d601ae14020f
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-browser.json
@@ -0,0 +1,15 @@
+[
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "csrf_token",
+ "node_type": "input",
+ "required": true,
+ "type": "hidden"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-spa.json
new file mode 100644
index 000000000000..d601ae14020f
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=false-case=passwordless_credentials-spa.json
@@ -0,0 +1,15 @@
+[
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "csrf_token",
+ "node_type": "input",
+ "required": true,
+ "type": "hidden"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-browser.json
new file mode 100644
index 000000000000..d601ae14020f
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-browser.json
@@ -0,0 +1,15 @@
+[
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "csrf_token",
+ "node_type": "input",
+ "required": true,
+ "type": "hidden"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-spa.json
new file mode 100644
index 000000000000..d601ae14020f
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v0_credentials-spa.json
@@ -0,0 +1,15 @@
+[
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "csrf_token",
+ "node_type": "input",
+ "required": true,
+ "type": "hidden"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-browser.json
new file mode 100644
index 000000000000..d601ae14020f
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-browser.json
@@ -0,0 +1,15 @@
+[
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "csrf_token",
+ "node_type": "input",
+ "required": true,
+ "type": "hidden"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-spa.json
new file mode 100644
index 000000000000..d601ae14020f
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=mfa_v1_credentials-spa.json
@@ -0,0 +1,15 @@
+[
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "csrf_token",
+ "node_type": "input",
+ "required": true,
+ "type": "hidden"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-browser.json
new file mode 100644
index 000000000000..5021f44d2f94
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-browser.json
@@ -0,0 +1,75 @@
+[
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "csrf_token",
+ "node_type": "input",
+ "required": true,
+ "type": "hidden"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "identifier",
+ "node_type": "input",
+ "type": "hidden",
+ "value": "foo@bar.com"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login",
+ "node_type": "input",
+ "type": "hidden",
+ "value": ""
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ },
+ {
+ "attributes": {
+ "async": true,
+ "crossorigin": "anonymous",
+ "id": "webauthn_script",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
+ "node_type": "script",
+ "referrerpolicy": "no-referrer",
+ "type": "text/javascript"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {},
+ "type": "script"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login_trigger",
+ "node_type": "input",
+ "onclickTrigger": "oryWebAuthnLogin",
+ "type": "button"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ },
+ "type": "input"
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-spa.json
new file mode 100644
index 000000000000..5021f44d2f94
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteLogin-flow=refresh-case=passwordless-passwordless_enabled=true-case=passwordless_credentials-spa.json
@@ -0,0 +1,75 @@
+[
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "csrf_token",
+ "node_type": "input",
+ "required": true,
+ "type": "hidden"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "identifier",
+ "node_type": "input",
+ "type": "hidden",
+ "value": "foo@bar.com"
+ },
+ "group": "default",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login",
+ "node_type": "input",
+ "type": "hidden",
+ "value": ""
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {},
+ "type": "input"
+ },
+ {
+ "attributes": {
+ "async": true,
+ "crossorigin": "anonymous",
+ "id": "webauthn_script",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
+ "node_type": "script",
+ "referrerpolicy": "no-referrer",
+ "type": "text/javascript"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {},
+ "type": "script"
+ },
+ {
+ "attributes": {
+ "disabled": false,
+ "name": "webauthn_login_trigger",
+ "node_type": "input",
+ "onclickTrigger": "oryWebAuthnLogin",
+ "type": "button"
+ },
+ "group": "webauthn",
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ },
+ "type": "input"
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json
index 0b1702c09413..f0edfe3c5966 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=a_device_is_shown_which_can_be_unlinked.json
@@ -82,33 +82,33 @@
{
"attributes": {
"disabled": false,
- "name": "webauthn_register_trigger",
+ "name": "webauthn_register",
"node_type": "input",
- "type": "button",
+ "type": "hidden",
"value": ""
},
"group": "webauthn",
"messages": [],
- "meta": {
- "label": {
- "id": 1050012,
- "text": "Add security key",
- "type": "info"
- }
- },
+ "meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
- "name": "webauthn_register",
+ "name": "webauthn_register_trigger",
"node_type": "input",
- "type": "hidden",
- "value": ""
+ "onclickTrigger": "oryWebAuthnRegistration",
+ "type": "button"
},
"group": "webauthn",
"messages": [],
- "meta": {},
+ "meta": {
+ "label": {
+ "id": 1050012,
+ "text": "Add security key",
+ "type": "info"
+ }
+ },
"type": "input"
},
{
@@ -116,7 +116,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json
index 515658a3d64f..9bd36e752fd0 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=browser.json
@@ -5,9 +5,6 @@
"webauthn_register_displayname": [
""
],
- "webauthn_register_trigger": [
- ""
- ],
"webauthn_remove": [
"666f6f666f6f"
]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json
index 515658a3d64f..9bd36e752fd0 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=fails_to_remove_security_key_if_it_is_passwordless_and_the_last_credential_available-type=spa.json
@@ -5,9 +5,6 @@
"webauthn_register_displayname": [
""
],
- "webauthn_register_trigger": [
- ""
- ],
"webauthn_remove": [
"666f6f666f6f"
]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json
index b21fa4833028..c15a847d4703 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=one_activation_element_is_shown.json
@@ -34,33 +34,33 @@
{
"attributes": {
"disabled": false,
- "name": "webauthn_register_trigger",
+ "name": "webauthn_register",
"node_type": "input",
- "type": "button",
+ "type": "hidden",
"value": ""
},
"group": "webauthn",
"messages": [],
- "meta": {
- "label": {
- "id": 1050012,
- "text": "Add security key",
- "type": "info"
- }
- },
+ "meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
- "name": "webauthn_register",
+ "name": "webauthn_register_trigger",
"node_type": "input",
- "type": "hidden",
- "value": ""
+ "onclickTrigger": "oryWebAuthnRegistration",
+ "type": "button"
},
"group": "webauthn",
"messages": [],
- "meta": {},
+ "meta": {
+ "label": {
+ "id": 1050012,
+ "text": "Add security key",
+ "type": "info"
+ }
+ },
"type": "input"
},
{
@@ -68,7 +68,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json
index 515658a3d64f..9bd36e752fd0 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=browser.json
@@ -5,9 +5,6 @@
"webauthn_register_displayname": [
""
],
- "webauthn_register_trigger": [
- ""
- ],
"webauthn_remove": [
"666f6f666f6f"
]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json
index 515658a3d64f..9bd36e752fd0 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestCompleteSettings-case=possible_to_remove_webauthn_credential_if_it_is_MFA_at_all_times-type=spa.json
@@ -5,9 +5,6 @@
"webauthn_register_displayname": [
""
],
- "webauthn_register_trigger": [
- ""
- ],
"webauthn_remove": [
"666f6f666f6f"
]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=mfa_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=mfa_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=passwordless_enabled.json
new file mode 100644
index 000000000000..ddd8316aa00f
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodFirstFactor-case=passwordless_enabled.json
@@ -0,0 +1,54 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "text",
+ "required": true,
+ "autocomplete": "username webauthn",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1070004,
+ "text": "ID",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "webauthn",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=mfa_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..63ce82315a77
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentifier-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1,34 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "webauthn",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=mfa_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=mfa_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=passwordless_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_does_not_have_a_webauthn-case=passwordless_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=mfa_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=mfa_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=passwordless_enabled.json
new file mode 100644
index 000000000000..63ce82315a77
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_disabled-case=identity_has_webauthn-case=passwordless_enabled.json
@@ -0,0 +1,34 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "webauthn",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=mfa_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=mfa_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=passwordless_enabled.json
new file mode 100644
index 000000000000..63ce82315a77
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=WithIdentityHint-case=account_enumeration_mitigation_enabled-case=passwordless_enabled.json
@@ -0,0 +1,34 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "webauthn",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_disabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=mfa_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_disabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json
new file mode 100644
index 000000000000..63ce82315a77
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstCredentials-case=no_options-case=passwordless_enabled-case=account_enumeration_mitigation_enabled.json
@@ -0,0 +1,34 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "method",
+ "type": "submit",
+ "value": "webauthn",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ }
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=mfa_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=mfa_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=passwordless_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodIdentifierFirstIdentification-case=passwordless_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_and_user_has_mfa_credentials.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_and_user_has_mfa_credentials.json
new file mode 100644
index 000000000000..1be62bb13f42
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_and_user_has_mfa_credentials.json
@@ -0,0 +1,74 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "hidden",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "script",
+ "group": "webauthn",
+ "attributes": {
+ "async": true,
+ "referrerpolicy": "no-referrer",
+ "crossorigin": "anonymous",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
+ "type": "text/javascript",
+ "id": "webauthn_script",
+ "node_type": "script"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "webauthn_login_trigger",
+ "type": "button",
+ "disabled": false,
+ "onclickTrigger": "oryWebAuthnLogin",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "webauthn_login",
+ "type": "hidden",
+ "value": "",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_but_user_has_passwordless_credentials.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_but_user_has_passwordless_credentials.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=mfa_enabled_but_user_has_passwordless_credentials.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_and_user_has_passwordless_credentials.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_and_user_has_passwordless_credentials.json
new file mode 100644
index 000000000000..1be62bb13f42
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_and_user_has_passwordless_credentials.json
@@ -0,0 +1,74 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "hidden",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "script",
+ "group": "webauthn",
+ "attributes": {
+ "async": true,
+ "referrerpolicy": "no-referrer",
+ "crossorigin": "anonymous",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
+ "type": "text/javascript",
+ "id": "webauthn_script",
+ "node_type": "script"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "webauthn_login_trigger",
+ "type": "button",
+ "disabled": false,
+ "onclickTrigger": "oryWebAuthnLogin",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "webauthn_login",
+ "type": "hidden",
+ "value": "",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_but_user_has_no_passwordless_credentials.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_but_user_has_no_passwordless_credentials.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodRefresh-case=passwordless_enabled_but_user_has_no_passwordless_credentials.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=mfa_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=mfa_enabled.json
new file mode 100644
index 000000000000..1be62bb13f42
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=mfa_enabled.json
@@ -0,0 +1,74 @@
+[
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "identifier",
+ "type": "hidden",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "default",
+ "attributes": {
+ "name": "csrf_token",
+ "type": "hidden",
+ "required": true,
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "script",
+ "group": "webauthn",
+ "attributes": {
+ "async": true,
+ "referrerpolicy": "no-referrer",
+ "crossorigin": "anonymous",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
+ "type": "text/javascript",
+ "id": "webauthn_script",
+ "node_type": "script"
+ },
+ "messages": [],
+ "meta": {}
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "webauthn_login_trigger",
+ "type": "button",
+ "disabled": false,
+ "onclickTrigger": "oryWebAuthnLogin",
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {
+ "label": {
+ "id": 1010008,
+ "text": "Sign in with hardware key",
+ "type": "info"
+ }
+ }
+ },
+ {
+ "type": "input",
+ "group": "webauthn",
+ "attributes": {
+ "name": "webauthn_login",
+ "type": "hidden",
+ "value": "",
+ "disabled": false,
+ "node_type": "input"
+ },
+ "messages": [],
+ "meta": {}
+ }
+]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=passwordless_enabled.json b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=passwordless_enabled.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/selfservice/strategy/webauthn/.snapshots/TestFormHydration-method=PopulateLoginMethodSecondFactor-case=passwordless_enabled.json
@@ -0,0 +1 @@
+[]
diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json
index 14a920d0a18d..20e3d3566fb0 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-browser.json
@@ -75,8 +75,8 @@
"disabled": false,
"name": "webauthn_register_trigger",
"node_type": "input",
- "type": "button",
- "value": ""
+ "onclickTrigger": "oryWebAuthnRegistration",
+ "type": "button"
},
"group": "webauthn",
"messages": [],
@@ -94,7 +94,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
diff --git a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json
index 14a920d0a18d..20e3d3566fb0 100644
--- a/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json
+++ b/selfservice/strategy/webauthn/.snapshots/TestRegistration-case=webauthn_button_exists-spa.json
@@ -75,8 +75,8 @@
"disabled": false,
"name": "webauthn_register_trigger",
"node_type": "input",
- "type": "button",
- "value": ""
+ "onclickTrigger": "oryWebAuthnRegistration",
+ "type": "button"
},
"group": "webauthn",
"messages": [],
@@ -94,7 +94,7 @@
"async": true,
"crossorigin": "anonymous",
"id": "webauthn_script",
- "integrity": "sha512-SSVrbpK6KOwN4xsH+nSyinjp4BOw8dsCU5dgegdpYUk0k7idTnQTd2JGVd+EJZV/TkdRaSKFJHRetpt5vydIZA==",
+ "integrity": "sha512-MDzBlwh32rr+eus2Yf1BetIj94m+ULLbewYDulbZjczycs81klNed+qQWG2yi2N03KV5uZlRJJtWdV2x9JNHzQ==",
"node_type": "script",
"referrerpolicy": "no-referrer",
"type": "text/javascript"
diff --git a/selfservice/strategy/webauthn/login.go b/selfservice/strategy/webauthn/login.go
index 4c7dd23f09ea..fe98d1d88c55 100644
--- a/selfservice/strategy/webauthn/login.go
+++ b/selfservice/strategy/webauthn/login.go
@@ -9,6 +9,8 @@ import (
"strings"
"time"
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
+
"github.com/ory/kratos/selfservice/flowhelpers"
"github.com/ory/kratos/session"
"github.com/ory/kratos/x/webauthnx"
@@ -34,84 +36,14 @@ import (
"github.com/ory/x/decoderx"
)
+var _ login.FormHydrator = new(Strategy)
+
func (s *Strategy) RegisterLoginRoutes(r *x.RouterPublic) {
webauthnx.RegisterWebauthnRoute(r)
}
-func (s *Strategy) PopulateLoginMethod(r *http.Request, requestedAAL identity.AuthenticatorAssuranceLevel, sr *login.Flow) error {
- if sr.Type != flow.TypeBrowser {
- return nil
- }
-
- if s.d.Config().WebAuthnForPasswordless(r.Context()) && (requestedAAL == identity.AuthenticatorAssuranceLevel1) {
- if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) {
- return nil
- } else if err != nil {
- return err
- }
- return nil
- } else if sr.IsForced() {
- if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) {
- return nil
- } else if err != nil {
- return err
- }
- return nil
- } else if !s.d.Config().WebAuthnForPasswordless(r.Context()) && (requestedAAL == identity.AuthenticatorAssuranceLevel2) {
- // We have done proper validation before so this should never error
- sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r)
- if err != nil {
- return err
- }
-
- if err := s.populateLoginMethod(r, sr, sess.Identity, text.NewInfoSelfServiceLoginWebAuthn(), identity.AuthenticatorAssuranceLevel2); errors.Is(err, webauthnx.ErrNoCredentials) {
- return nil
- } else if err != nil {
- return err
- }
-
- return nil
- }
-
- return nil
-}
-
func (s *Strategy) populateLoginMethodForPasswordless(r *http.Request, sr *login.Flow) error {
- if sr.IsForced() {
- identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID())
- if identifier == "" {
- return nil
- }
-
- if err := s.populateLoginMethod(r, sr, id, text.NewInfoSelfServiceLoginWebAuthn(), ""); errors.Is(err, webauthnx.ErrNoCredentials) {
- return nil
- } else if err != nil {
- return err
- }
-
- sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
- sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden))
- return nil
- }
-
- ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context())
- if err != nil {
- return err
- }
- identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String())
- if err != nil {
- return err
- }
-
sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
- sr.UI.SetNode(node.NewInputField(
- "identifier",
- "",
- node.DefaultGroup,
- node.InputAttributeTypeText,
- node.WithRequiredInputAttribute,
- func(attributes *node.InputAttributes) { attributes.Autocomplete = "username webauthn" },
- ).WithMetaLabel(identifierLabel))
sr.UI.GetNodes().Append(node.NewInputField("method", "webauthn", node.WebAuthnGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoSelfServiceLoginWebAuthn()))
return nil
}
@@ -133,11 +65,7 @@ func (s *Strategy) populateLoginMethod(r *http.Request, sr *login.Flow, i *ident
return errors.WithStack(err)
}
- webAuthCreds := conf.Credentials.ToWebAuthn()
- if !sr.IsForced() {
- webAuthCreds = conf.Credentials.ToWebAuthnFiltered(aal)
- }
-
+ webAuthCreds := conf.Credentials.ToWebAuthnFiltered(aal)
if len(webAuthCreds) == 0 {
// Identity has no webauthn
return webauthnx.ErrNoCredentials
@@ -245,7 +173,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow,
return nil, s.handleLoginError(r, f, err)
}
- if s.d.Config().WebAuthnForPasswordless(r.Context()) || f.IsForced() && f.RequestedAAL == identity.AuthenticatorAssuranceLevel1 {
+ if s.d.Config().WebAuthnForPasswordless(r.Context()) || f.IsRefresh() && f.RequestedAAL == identity.AuthenticatorAssuranceLevel1 {
return s.loginPasswordless(w, r, f, &p)
}
@@ -337,7 +265,7 @@ func (s *Strategy) loginAuthenticate(_ http.ResponseWriter, r *http.Request, f *
}
webAuthCreds := o.Credentials.ToWebAuthnFiltered(aal)
- if f.IsForced() {
+ if f.IsRefresh() {
webAuthCreds = o.Credentials.ToWebAuthn()
}
@@ -365,3 +293,124 @@ func (s *Strategy) loginMultiFactor(w http.ResponseWriter, r *http.Request, f *l
}
return s.loginAuthenticate(w, r, f, identityID, p, identity.AuthenticatorAssuranceLevel2)
}
+
+func (s *Strategy) populateLoginMethodRefresh(r *http.Request, sr *login.Flow) error {
+ if sr.Type != flow.TypeBrowser {
+ return nil
+ }
+
+ identifier, id, _ := flowhelpers.GuessForcedLoginIdentifier(r, s.d, sr, s.ID())
+ if identifier == "" {
+ return nil
+ }
+
+ if err := s.populateLoginMethod(r, sr, id, text.NewInfoSelfServiceLoginWebAuthn(), sr.RequestedAAL); errors.Is(err, webauthnx.ErrNoCredentials) {
+ return nil
+ } else if err != nil {
+ return err
+ }
+
+ sr.UI.SetCSRF(s.d.GenerateCSRFToken(r))
+ sr.UI.SetNode(node.NewInputField("identifier", identifier, node.DefaultGroup, node.InputAttributeTypeHidden))
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodFirstFactorRefresh(r *http.Request, sr *login.Flow) error {
+ return s.populateLoginMethodRefresh(r, sr)
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactorRefresh(r *http.Request, sr *login.Flow) error {
+ return s.populateLoginMethodRefresh(r, sr)
+}
+
+func (s *Strategy) PopulateLoginMethodFirstFactor(r *http.Request, sr *login.Flow) error {
+ if sr.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(r.Context()) {
+ return nil
+ }
+
+ ds, err := s.d.Config().DefaultIdentityTraitsSchemaURL(r.Context())
+ if err != nil {
+ return err
+ }
+
+ identifierLabel, err := login.GetIdentifierLabelFromSchema(r.Context(), ds.String())
+ if err != nil {
+ return err
+ }
+
+ sr.UI.SetNode(node.NewInputField(
+ "identifier",
+ "",
+ node.DefaultGroup,
+ node.InputAttributeTypeText,
+ node.WithRequiredInputAttribute,
+ func(attributes *node.InputAttributes) { attributes.Autocomplete = "username webauthn" },
+ ).WithMetaLabel(identifierLabel))
+
+ if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) {
+ return nil
+ } else if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodSecondFactor(r *http.Request, sr *login.Flow) error {
+ if sr.Type != flow.TypeBrowser || s.d.Config().WebAuthnForPasswordless(r.Context()) {
+ return nil
+ }
+
+ // We have done proper validation before so this should never error
+ sess, err := s.d.SessionManager().FetchFromRequest(r.Context(), r)
+ if err != nil {
+ return err
+ }
+
+ if err := s.populateLoginMethod(r, sr, sess.Identity, text.NewInfoSelfServiceLoginWebAuthn(), identity.AuthenticatorAssuranceLevel2); errors.Is(err, webauthnx.ErrNoCredentials) {
+ return nil
+ } else if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstCredentials(r *http.Request, sr *login.Flow, opts ...login.FormHydratorModifier) error {
+ if sr.Type != flow.TypeBrowser || !s.d.Config().WebAuthnForPasswordless(r.Context()) {
+ return errors.WithStack(idfirst.ErrNoCredentialsFound)
+ }
+
+ o := login.NewFormHydratorOptions(opts)
+
+ var count int
+ if o.IdentityHint != nil {
+ var err error
+ // If we have an identity hint we can perform identity credentials discovery and
+ // hide this credential if it should not be included.
+ if count, err = s.CountActiveFirstFactorCredentials(o.IdentityHint.Credentials); err != nil {
+ return err
+ }
+ }
+
+ if count > 0 || s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
+ if err := s.populateLoginMethodForPasswordless(r, sr); errors.Is(err, webauthnx.ErrNoCredentials) {
+ if !s.d.Config().SecurityAccountEnumerationMitigate(r.Context()) {
+ return errors.WithStack(idfirst.ErrNoCredentialsFound)
+ }
+ return nil
+ } else if err != nil {
+ return err
+ }
+ }
+
+ if count == 0 {
+ return errors.WithStack(idfirst.ErrNoCredentialsFound)
+ }
+
+ return nil
+}
+
+func (s *Strategy) PopulateLoginMethodIdentifierFirstIdentification(r *http.Request, sr *login.Flow) error {
+ return nil
+}
diff --git a/selfservice/strategy/webauthn/login_test.go b/selfservice/strategy/webauthn/login_test.go
index f5d332182163..f1323b9b6789 100644
--- a/selfservice/strategy/webauthn/login_test.go
+++ b/selfservice/strategy/webauthn/login_test.go
@@ -10,8 +10,12 @@ import (
"fmt"
"io"
"net/http"
+ "net/http/httptest"
"net/url"
"testing"
+ "time"
+
+ "github.com/ory/kratos/selfservice/strategy/idfirst"
"github.com/ory/x/jsonx"
@@ -30,6 +34,7 @@ import (
"github.com/tidwall/gjson"
"github.com/ory/kratos/driver/config"
+ configtesthelpers "github.com/ory/kratos/driver/config/testhelpers"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/internal"
"github.com/ory/kratos/internal/testhelpers"
@@ -153,7 +158,7 @@ func TestCompleteLogin(t *testing.T) {
}
submitWebAuthnLogin := func(t *testing.T, isSPA bool, id *identity.Identity, contextFixture []byte, cb func(values url.Values), opts ...testhelpers.InitFlowWithOption) (string, *http.Response, *kratos.LoginFlow) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
return submitWebAuthnLoginWithClient(t, isSPA, id, contextFixture, browserClient, cb, opts...)
}
@@ -163,19 +168,30 @@ func TestCompleteLogin(t *testing.T) {
conf.MustSet(ctx, config.ViperKeySessionWhoAmIAAL, nil)
})
- run := func(t *testing.T, id *identity.Identity, context, response []byte, isSPA bool, expectedAAL identity.AuthenticatorAssuranceLevel) {
+ run := func(t *testing.T, id *identity.Identity, context, response []byte, isSPA bool, expectedAAL identity.AuthenticatorAssuranceLevel, expectTriggers bool) {
body, res, f := submitWebAuthnLogin(t, isSPA, id, context, func(values url.Values) {
values.Set("identifier", loginFixtureSuccessEmail)
values.Set(node.WebAuthnLogin, string(response))
- }, testhelpers.InitFlowWithRefresh())
+ },
+ testhelpers.InitFlowWithRefresh(),
+ testhelpers.InitFlowWithAAL(expectedAAL),
+ )
snapshotx.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
- "2.attributes.onclick",
- "4.attributes.nonce",
- "4.attributes.src",
+ "3.attributes.nonce",
+ "3.attributes.src",
+ "4.attributes.value",
+ "4.attributes.onclick",
})
+
nodes, err := json.Marshal(f.Ui.Nodes)
require.NoError(t, err)
+
+ if !expectTriggers {
+ assert.Falsef(t, gjson.GetBytes(nodes, "#(attributes.name==identifier)").Exists(), "%s", nodes)
+ return
+ }
+
assert.Equal(t, loginFixtureSuccessEmail, gjson.GetBytes(nodes, "#(attributes.name==identifier).attributes.value").String(), "%s", nodes)
prefix := ""
@@ -208,40 +224,44 @@ func TestCompleteLogin(t *testing.T) {
}
for _, tc := range []struct {
- creds identity.Credentials
- response []byte
- context []byte
- descript string
+ creds identity.Credentials
+ response []byte
+ context []byte
+ descript string
+ expectTriggers bool
}{
{
creds: identity.Credentials{
Config: loginFixtureSuccessV0Credentials,
Version: 0,
},
- context: loginFixtureSuccessV0Context,
- response: loginFixtureSuccessV0Response,
- descript: "mfa v0 credentials",
+ context: loginFixtureSuccessV0Context,
+ response: loginFixtureSuccessV0Response,
+ descript: "mfa v0 credentials",
+ expectTriggers: !e,
},
{
creds: identity.Credentials{
Config: loginFixtureSuccessV1Credentials,
Version: 1,
},
- context: loginFixtureSuccessV1Context,
- response: loginFixtureSuccessV1Response,
- descript: "mfa v1 credentials",
+ context: loginFixtureSuccessV1Context,
+ response: loginFixtureSuccessV1Response,
+ descript: "mfa v1 credentials",
+ expectTriggers: !e,
},
{
creds: identity.Credentials{
Config: loginFixtureSuccessV1PasswordlessCredentials,
Version: 1,
},
- context: loginFixtureSuccessV1PasswordlessContext,
- response: loginFixtureSuccessV1PasswordlessResponse,
- descript: "passwordless credentials",
+ context: loginFixtureSuccessV1PasswordlessContext,
+ response: loginFixtureSuccessV1PasswordlessResponse,
+ descript: "passwordless credentials",
+ expectTriggers: e,
},
} {
- t.Run(fmt.Sprintf("case=mfa v0 credentials/passwordless enabled=%v", e), func(t *testing.T) {
+ t.Run(fmt.Sprintf("passwordless enabled=%v/case=%s", e, tc.descript), func(t *testing.T) {
id := createIdentityWithWebAuthn(t, tc.creds)
for _, f := range []string{
@@ -249,7 +269,7 @@ func TestCompleteLogin(t *testing.T) {
"spa",
} {
t.Run(f, func(t *testing.T) {
- run(t, id, tc.context, tc.response, f == "spa", expectedAAL)
+ run(t, id, tc.context, tc.response, f == "spa", expectedAAL, tc.expectTriggers)
})
}
})
@@ -264,7 +284,7 @@ func TestCompleteLogin(t *testing.T) {
for _, f := range []string{"browser", "spa"} {
t.Run(f, func(t *testing.T) {
id := identity.NewIdentity("")
- client := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ client := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaBrowser(t, client, publicTS, true, f == "spa", false, false)
snapshotx.SnapshotTExcept(t, f.Ui.Nodes, []string{
@@ -317,7 +337,7 @@ func TestCompleteLogin(t *testing.T) {
})
t.Run("case=webauthn shows error if user tries to sign in but user has no webauth credentials set up", func(t *testing.T) {
- id, subject := createIdentityAndReturnIdentifier(t, reg, nil)
+ id, subject := createIdentityAndReturnIdentifier(t, ctx, reg, nil)
id.DeleteCredentialsType(identity.CredentialsTypeWebAuthn)
require.NoError(t, reg.IdentityManager().Update(ctx, id, identity.ManagerAllowWriteProtectedTraits))
@@ -344,7 +364,7 @@ func TestCompleteLogin(t *testing.T) {
})
t.Run("case=webauthn MFA credentials can not be used for passwordless login", func(t *testing.T) {
- _, subject := createIdentityAndReturnIdentifier(t, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","is_passwordless":false}]}`))
+ _, subject := createIdentityAndReturnIdentifier(t, ctx, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","is_passwordless":false}]}`))
payload := func(v url.Values) {
v.Set("method", identity.CredentialsTypeWebAuthn.String())
@@ -369,7 +389,7 @@ func TestCompleteLogin(t *testing.T) {
})
t.Run("case=should fail if webauthn login is invalid", func(t *testing.T) {
- _, subject := createIdentityAndReturnIdentifier(t, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`))
+ _, subject := createIdentityAndReturnIdentifier(t, ctx, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`))
doBrowserFlow := func(t *testing.T, spa bool, browserClient *http.Client, opts ...testhelpers.InitFlowWithOption) {
f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, spa, false, false, opts...)
@@ -446,6 +466,13 @@ func TestCompleteLogin(t *testing.T) {
actualFlow, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id))
require.NoError(t, err)
assert.Empty(t, gjson.GetBytes(actualFlow.InternalContext, flow.PrefixInternalContextKey(identity.CredentialsTypeWebAuthn, webauthn.InternalContextKeySessionData)))
+
+ if spa {
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
+ assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), conf.SelfServiceBrowserDefaultReturnTo(ctx).String(), "%s", body)
+ } else {
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
+ }
}
t.Run("type=browser", func(t *testing.T) {
@@ -460,25 +487,26 @@ func TestCompleteLogin(t *testing.T) {
t.Run("flow=mfa", func(t *testing.T) {
t.Run("case=webauthn payload is set when identity has webauthn", func(t *testing.T) {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaBrowser(t, apiClient, publicTS, false, true, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
assert.Equal(t, gjson.GetBytes(id.Traits, "subject").String(), f.Ui.Nodes[1].Attributes.UiNodeInputAttributes.Value, jsonx.TestMarshalJSONString(t, f.Ui))
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
"1.attributes.value",
- "2.attributes.onclick",
- "2.attributes.onload",
- "4.attributes.src",
- "4.attributes.nonce",
+ "3.attributes.src",
+ "3.attributes.nonce",
+ "4.attributes.onclick",
+ "4.attributes.onload",
+ "4.attributes.value",
})
- ensureReplacement(t, "2", f.Ui, "allowCredentials")
+ ensureReplacement(t, "4", f.Ui, "allowCredentials")
})
t.Run("case=webauthn payload is not set when identity has no webauthn", func(t *testing.T) {
id := createIdentityWithoutWebAuthn(t, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaBrowser(t, apiClient, publicTS, false, true, false, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
@@ -487,23 +515,23 @@ func TestCompleteLogin(t *testing.T) {
})
t.Run("case=webauthn payload is not set for API clients", func(t *testing.T) {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
assertx.EqualAsJSON(t, nil, f.Ui.Nodes)
})
doAPIFlowSignedIn := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- return doAPIFlow(t, v, testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id), testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
+ return doAPIFlow(t, v, testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id), testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
}
doBrowserFlowSignIn := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- return doBrowserFlow(t, spa, v, testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id), testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
+ return doBrowserFlow(t, spa, v, testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id), testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
}
t.Run("case=should refuse to execute api flow", func(t *testing.T) {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
payload := func(v url.Values) {
v.Set(node.WebAuthnLogin, "{}")
}
@@ -515,7 +543,7 @@ func TestCompleteLogin(t *testing.T) {
})
t.Run("case=should fail if webauthn login is invalid", func(t *testing.T) {
- id, sub := createIdentityAndReturnIdentifier(t, reg, nil)
+ id, sub := createIdentityAndReturnIdentifier(t, ctx, reg, nil)
payload := func(v url.Values) {
v.Set("identifier", sub)
v.Set(node.WebAuthnLogin, string(loginFixtureSuccessResponseInvalid))
@@ -615,3 +643,284 @@ func TestCompleteLogin(t *testing.T) {
})
})
}
+
+func TestFormHydration(t *testing.T) {
+ ctx := context.Background()
+ conf, reg := internal.NewFastRegistryWithMocks(t)
+
+ ctx = configtesthelpers.WithConfigValue(ctx, config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".enabled", true)
+ ctx = configtesthelpers.WithConfigValue(
+ ctx,
+ config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeWebAuthn)+".config",
+ map[string]interface{}{
+ "rp": map[string]interface{}{
+ "display_name": "foo",
+ "id": "localhost",
+ "origins": []string{"http://localhost"},
+ },
+ },
+ )
+ ctx = testhelpers.WithDefaultIdentitySchema(ctx, "file://stub/login.schema.json")
+
+ s, err := reg.AllLoginStrategies().Strategy(identity.CredentialsTypeWebAuthn)
+ require.NoError(t, err)
+ fh, ok := s.(login.FormHydrator)
+ require.True(t, ok)
+
+ toSnapshot := func(t *testing.T, f *login.Flow) {
+ t.Helper()
+ // The CSRF token has a unique value that messes with the snapshot - ignore it.
+ f.UI.Nodes.ResetNodes("csrf_token")
+ f.UI.Nodes.ResetNodes("identifier")
+ f.UI.Nodes.ResetNodes("webauthn_login_trigger")
+ snapshotx.SnapshotT(t, f.UI.Nodes, snapshotx.ExceptNestedKeys("onclick", "nonce", "src"))
+ }
+
+ newFlow := func(ctx context.Context, t *testing.T) (*http.Request, *login.Flow) {
+ r := httptest.NewRequest("GET", "/self-service/login/browser", nil)
+ r = r.WithContext(ctx)
+ t.Helper()
+ f, err := login.NewFlow(conf, time.Minute, "csrf_token", r, flow.TypeBrowser)
+ f.UI.Nodes = make(node.Nodes, 0)
+ require.NoError(t, err)
+ return r, f
+ }
+
+ passwordlessEnabled := configtesthelpers.WithConfigValue(ctx, config.ViperKeyWebAuthnPasswordless, true)
+ mfaEnabled := configtesthelpers.WithConfigValue(ctx, config.ViperKeyWebAuthnPasswordless, false)
+
+ t.Run("method=PopulateLoginMethodSecondFactor", func(t *testing.T) {
+ id := createIdentity(t, ctx, reg)
+ headers := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ t.Run("case=passwordless enabled", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+
+ r.Header = headers
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=mfa enabled", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+
+ r.Header = headers
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+
+ require.NoError(t, fh.PopulateLoginMethodSecondFactor(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodFirstFactor", func(t *testing.T) {
+ t.Run("case=passwordless enabled", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=mfa enabled", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodFirstFactor(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodRefresh", func(t *testing.T) {
+ t.Run("case=passwordless enabled but user has no passwordless credentials", func(t *testing.T) {
+ id := createIdentity(t, ctx, reg)
+ r, f := newFlow(passwordlessEnabled, t)
+ r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ f.Refresh = true
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=passwordless enabled and user has passwordless credentials", func(t *testing.T) {
+ id, _ := createIdentityAndReturnIdentifier(t, ctx, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`))
+ r, f := newFlow(passwordlessEnabled, t)
+ r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ f.Refresh = true
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=mfa enabled and user has mfa credentials", func(t *testing.T) {
+ id := createIdentity(t, ctx, reg)
+ r, f := newFlow(mfaEnabled, t)
+ r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ f.Refresh = true
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=mfa enabled but user has passwordless credentials", func(t *testing.T) {
+ id, _ := createIdentityAndReturnIdentifier(t, ctx, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`))
+ r, f := newFlow(mfaEnabled, t)
+ r.Header = testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id).Transport.(*testhelpers.TransportWithHeader).GetHeader()
+ f.Refresh = true
+ f.RequestedAAL = identity.AuthenticatorAssuranceLevel2
+ require.NoError(t, fh.PopulateLoginMethodFirstFactorRefresh(r, f))
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstCredentials", func(t *testing.T) {
+ t.Run("case=no options", func(t *testing.T) {
+ t.Run("case=passwordless enabled", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ r, f := newFlow(
+ configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false),
+ t,
+ )
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ r, f := newFlow(
+ configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true),
+ t,
+ )
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=mfa enabled", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ r, f := newFlow(
+ configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false),
+ t,
+ )
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ r, f := newFlow(
+ configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true),
+ t,
+ )
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+ })
+
+ t.Run("case=WithIdentifier", func(t *testing.T) {
+ t.Run("case=passwordless enabled", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ r, f := newFlow(
+ configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false),
+ t,
+ )
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ r, f := newFlow(
+ configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true),
+ t,
+ )
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=mfa enabled", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ r, f := newFlow(
+ configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false),
+ t,
+ )
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ r, f := newFlow(
+ configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true),
+ t,
+ )
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentifier("foo@bar.com")), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+ })
+
+ t.Run("case=WithIdentityHint", func(t *testing.T) {
+ t.Run("case=account enumeration mitigation enabled", func(t *testing.T) {
+ mfaEnabled := configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true)
+ passwordlessEnabled := configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, true)
+
+ id := identity.NewIdentity("test-provider")
+ t.Run("case=passwordless enabled", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=mfa enabled", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=account enumeration mitigation disabled", func(t *testing.T) {
+ mfaEnabled := configtesthelpers.WithConfigValue(mfaEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false)
+ passwordlessEnabled := configtesthelpers.WithConfigValue(passwordlessEnabled, config.ViperKeySecurityAccountEnumerationMitigate, false)
+
+ id, _ := createIdentityAndReturnIdentifier(t, ctx, reg, []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`))
+
+ t.Run("case=identity has webauthn", func(t *testing.T) {
+ t.Run("case=passwordless enabled", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=mfa enabled", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+
+ t.Run("case=identity does not have a webauthn", func(t *testing.T) {
+ t.Run("case=passwordless enabled", func(t *testing.T) {
+ id := identity.NewIdentity("default")
+ r, f := newFlow(passwordlessEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=mfa enabled", func(t *testing.T) {
+ id := identity.NewIdentity("default")
+ r, f := newFlow(mfaEnabled, t)
+ require.ErrorIs(t, fh.PopulateLoginMethodIdentifierFirstCredentials(r, f, login.WithIdentityHint(id)), idfirst.ErrNoCredentialsFound)
+ toSnapshot(t, f)
+ })
+ })
+ })
+ })
+ })
+
+ t.Run("method=PopulateLoginMethodIdentifierFirstIdentification", func(t *testing.T) {
+ t.Run("case=passwordless enabled", func(t *testing.T) {
+ r, f := newFlow(passwordlessEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f))
+ toSnapshot(t, f)
+ })
+
+ t.Run("case=mfa enabled", func(t *testing.T) {
+ r, f := newFlow(mfaEnabled, t)
+ require.NoError(t, fh.PopulateLoginMethodIdentifierFirstIdentification(r, f))
+ toSnapshot(t, f)
+ })
+ })
+}
diff --git a/selfservice/strategy/webauthn/registration.go b/selfservice/strategy/webauthn/registration.go
index 85cb3628e59d..e3ca6c9e5fd7 100644
--- a/selfservice/strategy/webauthn/registration.go
+++ b/selfservice/strategy/webauthn/registration.go
@@ -76,9 +76,10 @@ func (s *Strategy) handleRegistrationError(_ http.ResponseWriter, r *http.Reques
// we only set the value and not the whole field because we want to keep types from the initial form generation
f.UI.Nodes.SetValueAttribute(n.ID(), n.Attributes.GetValue())
}
+
+ f.UI.Nodes.SetValueAttribute(node.WebAuthnRegisterDisplayName, p.RegisterDisplayName)
}
- f.UI.Nodes.SetValueAttribute(node.WebAuthnRegisterDisplayName, p.RegisterDisplayName)
if f.Type == flow.TypeBrowser {
f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
}
diff --git a/selfservice/strategy/webauthn/registration_test.go b/selfservice/strategy/webauthn/registration_test.go
index c0503b151ed8..8dd3e38bd036 100644
--- a/selfservice/strategy/webauthn/registration_test.go
+++ b/selfservice/strategy/webauthn/registration_test.go
@@ -145,6 +145,7 @@ func TestRegistration(t *testing.T) {
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"2.attributes.value",
"5.attributes.onclick",
+ "5.attributes.value",
"6.attributes.nonce",
"6.attributes.src",
})
@@ -367,6 +368,13 @@ func TestRegistration(t *testing.T) {
i, _, err := reg.PrivilegedIdentityPool().FindByCredentialsIdentifier(context.Background(), identity.CredentialsTypeWebAuthn, email)
require.NoError(t, err)
assert.Equal(t, email, gjson.GetBytes(i.Traits, "username").String(), "%s", actual)
+
+ if f == "spa" {
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(actual, "continue_with.0.action").String(), "%s", actual)
+ assert.Contains(t, gjson.Get(actual, "continue_with.0.redirect_browser_to").String(), redirNoSessionTS.URL+"/registration-return-ts", "%s", actual)
+ } else {
+ assert.Empty(t, gjson.Get(actual, "continue_with").Array(), "%s", actual)
+ }
})
}
})
diff --git a/selfservice/strategy/webauthn/settings_test.go b/selfservice/strategy/webauthn/settings_test.go
index acf4fd357b1d..01ccdbf48578 100644
--- a/selfservice/strategy/webauthn/settings_test.go
+++ b/selfservice/strategy/webauthn/settings_test.go
@@ -53,19 +53,20 @@ var settingsFixtureSuccessInternalContext []byte
const registerDisplayNameGJSONQuery = "ui.nodes.#(attributes.name==" + node.WebAuthnRegisterDisplayName + ")"
func createIdentityWithoutWebAuthn(t *testing.T, reg driver.Registry) *identity.Identity {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
delete(id.Credentials, identity.CredentialsTypeWebAuthn)
require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id))
return id
}
-func createIdentityAndReturnIdentifier(t *testing.T, reg driver.Registry, conf []byte) (*identity.Identity, string) {
+func createIdentityAndReturnIdentifier(t *testing.T, ctx context.Context, reg driver.Registry, conf []byte) (*identity.Identity, string) {
identifier := x.NewUUID().String() + "@ory.sh"
password := x.NewUUID().String()
- p, err := reg.Hasher(ctx).Generate(context.Background(), []byte(password))
+ p, err := reg.Hasher(ctx).Generate(ctx, []byte(password))
require.NoError(t, err)
i := &identity.Identity{
- Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)),
+ SchemaID: "default",
+ Traits: identity.Traits(fmt.Sprintf(`{"subject":"%s"}`, identifier)),
VerifiableAddresses: []identity.VerifiableAddress{
{
Value: identifier,
@@ -77,7 +78,7 @@ func createIdentityAndReturnIdentifier(t *testing.T, reg driver.Registry, conf [
if conf == nil {
conf = []byte(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo"},{"id":"YmFyYmFy","display_name":"bar"}]}`)
}
- require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))
+ require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i))
i.Credentials = map[identity.CredentialsType]identity.Credentials{
identity.CredentialsTypePassword: {
Type: identity.CredentialsTypePassword,
@@ -90,12 +91,12 @@ func createIdentityAndReturnIdentifier(t *testing.T, reg driver.Registry, conf [
Config: conf,
},
}
- require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), i))
+ require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(ctx, i))
return i, identifier
}
-func createIdentity(t *testing.T, reg driver.Registry) *identity.Identity {
- id, _ := createIdentityAndReturnIdentifier(t, reg, nil)
+func createIdentity(t *testing.T, ctx context.Context, reg driver.Registry) *identity.Identity {
+ id, _ := createIdentityAndReturnIdentifier(t, ctx, reg, nil)
return id
}
@@ -136,48 +137,50 @@ func TestCompleteSettings(t *testing.T) {
conf.MustSet(ctx, config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"})
t.Run("case=a device is shown which can be unlinked", func(t *testing.T) {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, publicTS)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
- "4.attributes.onclick",
+ "5.attributes.onclick",
+ "5.attributes.value",
"6.attributes.src",
"6.attributes.nonce",
})
- ensureReplacement(t, "4", f.Ui, "Ory Corp")
+ ensureReplacement(t, "5", f.Ui, "Ory Corp")
})
t.Run("case=one activation element is shown", func(t *testing.T) {
id := createIdentityWithoutWebAuthn(t, reg)
require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id))
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, apiClient, true, publicTS)
testhelpers.SnapshotTExcept(t, f.Ui.Nodes, []string{
"0.attributes.value",
- "2.attributes.onload",
- "2.attributes.onclick",
+ "3.attributes.onload",
+ "3.attributes.onclick",
+ "3.attributes.value",
"4.attributes.src",
"4.attributes.nonce",
})
- ensureReplacement(t, "2", f.Ui, "Ory Corp")
+ ensureReplacement(t, "3", f.Ui, "Ory Corp")
})
t.Run("case=webauthn only works for browsers", func(t *testing.T) {
id := createIdentityWithoutWebAuthn(t, reg)
require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id))
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
assert.Empty(t, f.Ui.Nodes)
})
doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
+ apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaAPI(t, apiClient, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v(values)
@@ -186,7 +189,7 @@ func TestCompleteSettings(t *testing.T) {
}
doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) {
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS)
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
v(values)
@@ -314,7 +317,7 @@ func TestCompleteSettings(t *testing.T) {
var id identity.Identity
require.NoError(t, json.Unmarshal(settingsFixtureSuccessIdentity, &id))
_ = reg.PrivilegedIdentityPool().DeleteIdentity(context.Background(), id.ID)
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, &id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, &id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, spa, publicTS)
// We inject the session to replay
@@ -367,7 +370,7 @@ func TestCompleteSettings(t *testing.T) {
})
run := func(t *testing.T, spa bool) {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
id.DeleteCredentialsType(identity.CredentialsTypePassword)
conf := sqlxx.JSONRawMessage(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":true}]}`)
id.UpsertCredentialsConfig(identity.CredentialsTypeWebAuthn, conf, 0)
@@ -375,7 +378,7 @@ func TestCompleteSettings(t *testing.T) {
body, res := doBrowserFlow(t, spa, func(v url.Values) {
// The remove key should be empty
- snapshotx.SnapshotTExcept(t, v, []string{"csrf_token"})
+ snapshotx.SnapshotTExcept(t, v, []string{"csrf_token", "webauthn_register_trigger"})
v.Set(node.WebAuthnRemove, "666f6f666f6f")
}, id)
@@ -409,14 +412,17 @@ func TestCompleteSettings(t *testing.T) {
t.Run("case=possible to remove webauthn credential if it is MFA at all times", func(t *testing.T) {
run := func(t *testing.T, spa bool) {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
id.DeleteCredentialsType(identity.CredentialsTypePassword)
id.UpsertCredentialsConfig(identity.CredentialsTypeWebAuthn, sqlxx.JSONRawMessage(`{"credentials":[{"id":"Zm9vZm9v","display_name":"foo","is_passwordless":false}]}`), 0)
require.NoError(t, reg.IdentityManager().Update(ctx, id, identity.ManagerAllowWriteProtectedTraits))
body, res := doBrowserFlow(t, spa, func(v url.Values) {
// The remove key should be set
- snapshotx.SnapshotTExcept(t, v, []string{"csrf_token"})
+ snapshotx.SnapshotTExcept(t, v, []string{
+ "csrf_token",
+ "webauthn_register_trigger",
+ })
v.Set(node.WebAuthnRemove, "666f6f666f6f")
}, id)
@@ -446,7 +452,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("case=remove all security keys", func(t *testing.T) {
run := func(t *testing.T, spa bool) {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
allCred, ok := id.GetCredentials(identity.CredentialsTypeWebAuthn)
assert.True(t, ok)
@@ -465,6 +471,13 @@ func TestCompleteSettings(t *testing.T) {
assert.Contains(t, res.Request.URL.String(), uiTS.URL)
}
assert.EqualValues(t, flow.StateSuccess, gjson.Get(body, "state").String(), body)
+
+ if spa {
+ assert.EqualValues(t, flow.ContinueWithActionRedirectBrowserToString, gjson.Get(body, "continue_with.0.action").String(), "%s", body)
+ assert.Contains(t, gjson.Get(body, "continue_with.0.redirect_browser_to").String(), uiTS.URL, "%s", body)
+ } else {
+ assert.Empty(t, gjson.Get(body, "continue_with").Array(), "%s", body)
+ }
}
actual, err := reg.Persister().GetIdentityConfidential(context.Background(), id.ID)
@@ -487,7 +500,7 @@ func TestCompleteSettings(t *testing.T) {
t.Run("case=fails with browser submit register payload is invalid", func(t *testing.T) {
run := func(t *testing.T, spa bool) {
- id := createIdentity(t, reg)
+ id := createIdentity(t, ctx, reg)
body, res := doBrowserFlow(t, spa, func(v url.Values) {
v.Set(node.WebAuthnRemove, fmt.Sprintf("%x", []byte("foofoo")))
}, id)
@@ -526,7 +539,7 @@ func TestCompleteSettings(t *testing.T) {
var id identity.Identity
require.NoError(t, json.Unmarshal(settingsFixtureSuccessIdentity, &id))
_ = reg.PrivilegedIdentityPool().DeleteIdentity(context.Background(), id.ID)
- browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, &id)
+ browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, ctx, reg, &id)
f := testhelpers.InitializeSettingsFlowViaBrowser(t, browserClient, isSPA, publicTS)
// We inject the session to replay
diff --git a/session/handler.go b/session/handler.go
index 9dab8860c773..84be9c25e1b8 100644
--- a/session/handler.go
+++ b/session/handler.go
@@ -877,6 +877,9 @@ type extendSession struct {
// return a 200 OK response with the session in the body. Returning the session as part of the response
// will be deprecated in the future and should not be relied upon.
//
+// This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those
+// scenarios. This endpoint also returns 404 errors if the session does not exist.
+//
// Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.
//
// Schemes: http, https
diff --git a/session/handler_test.go b/session/handler_test.go
index 3c61b7764832..c11a8f4a548e 100644
--- a/session/handler_test.go
+++ b/session/handler_test.go
@@ -560,7 +560,7 @@ func TestHandlerAdminSessionManagement(t *testing.T) {
})
t.Run("should redirect to public for whoami", func(t *testing.T) {
- client := testhelpers.NewHTTPClientWithSessionToken(t, reg, s)
+ client := testhelpers.NewHTTPClientWithSessionToken(t, ctx, reg, s)
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
diff --git a/session/session.go b/session/session.go
index 84f64ceec0d6..e5b826b88f2f 100644
--- a/session/session.go
+++ b/session/session.go
@@ -16,7 +16,7 @@ import (
"github.com/ory/x/httpx"
"github.com/ory/x/pagination/keysetpagination"
- "github.com/ory/x/stringsx"
+ "github.com/ory/x/pointerx"
"github.com/pkg/errors"
@@ -282,12 +282,12 @@ func (s *Session) Activate(r *http.Request, i *identity.Identity, c lifespanProv
func (s *Session) SetSessionDeviceInformation(r *http.Request) {
device := Device{
SessionID: s.ID,
- IPAddress: stringsx.GetPointer(httpx.ClientIP(r)),
+ IPAddress: pointerx.Ptr(httpx.ClientIP(r)),
}
agent := r.Header["User-Agent"]
if len(agent) > 0 {
- device.UserAgent = stringsx.GetPointer(strings.Join(agent, " "))
+ device.UserAgent = pointerx.Ptr(strings.Join(agent, " "))
}
var clientGeoLocation []string
@@ -297,7 +297,7 @@ func (s *Session) SetSessionDeviceInformation(r *http.Request) {
if r.Header.Get("Cf-Ipcountry") != "" {
clientGeoLocation = append(clientGeoLocation, r.Header.Get("Cf-Ipcountry"))
}
- device.Location = stringsx.GetPointer(strings.Join(clientGeoLocation, ", "))
+ device.Location = pointerx.Ptr(strings.Join(clientGeoLocation, ", "))
s.Devices = append(s.Devices, device)
}
diff --git a/session/test/persistence.go b/session/test/persistence.go
index 8e8cbfeb18b2..d124aa23eb4f 100644
--- a/session/test/persistence.go
+++ b/session/test/persistence.go
@@ -8,6 +8,8 @@ import (
"testing"
"time"
+ confighelpers "github.com/ory/kratos/driver/config/testhelpers"
+
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
@@ -42,8 +44,6 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface {
return func(t *testing.T) {
_, p := testhelpers.NewNetworkUnlessExisting(t, ctx, p)
- testhelpers.SetDefaultIdentitySchema(conf, "file://./stub/identity.schema.json")
-
t.Run("case=not found", func(t *testing.T) {
_, err := p.GetSession(ctx, x.NewUUID(), session.ExpandNothing)
require.Error(t, err)
@@ -611,10 +611,7 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface {
})
t.Run("extend session lifespan but min time is not yet reached", func(t *testing.T) {
- conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour*2)
- t.Cleanup(func() {
- conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, nil)
- })
+ ctx := confighelpers.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour})
var expected session.Session
require.NoError(t, faker.FakeData(&expected))
@@ -629,23 +626,19 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface {
})
t.Run("extend session lifespan", func(t *testing.T) {
- conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour)
- t.Cleanup(func() {
- conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, nil)
- })
+ ctx := confighelpers.WithConfigValues(ctx, map[string]any{config.ViperKeySessionRefreshMinTimeLeft: 2 * time.Hour})
- conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour*2)
var expected session.Session
require.NoError(t, faker.FakeData(&expected))
expected.ExpiresAt = time.Now().Add(time.Hour).UTC()
require.NoError(t, p.CreateIdentity(ctx, expected.Identity))
require.NoError(t, p.UpsertSession(ctx, &expected))
- expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt.Round(time.Minute)
+ expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt
require.NoError(t, p.ExtendSession(ctx, expected.ID))
actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing)
require.NoError(t, err)
- assert.Equal(t, expectedExpiry, actual.ExpiresAt.Round(time.Minute))
+ assert.GreaterOrEqual(t, 10*time.Second, expectedExpiry.Sub(actual.ExpiresAt).Abs())
})
t.Run("extend session lifespan on CockroachDB", func(t *testing.T) {
@@ -653,23 +646,19 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface {
t.Skip("Skipping test because driver is not CockroachDB")
}
- conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour)
- t.Cleanup(func() {
- conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, nil)
- })
+ ctx := confighelpers.WithConfigValue(ctx, config.ViperKeySessionRefreshMinTimeLeft, 2*time.Hour)
- conf.MustSet(ctx, config.ViperKeySessionRefreshMinTimeLeft, time.Hour*2)
var expected session.Session
require.NoError(t, faker.FakeData(&expected))
expected.ExpiresAt = time.Now().Add(time.Hour).UTC()
require.NoError(t, p.CreateIdentity(ctx, expected.Identity))
require.NoError(t, p.UpsertSession(ctx, &expected))
- expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt.Round(time.Minute)
+ expectedExpiry := expected.Refresh(ctx, conf).ExpiresAt
- var foundExpectedCockroachError bool
+ foundExpectedCockroachError := false
g := errgroup.Group{}
- for i := 0; i < 10; i++ {
+ for range 10 {
g.Go(func() error {
err := p.ExtendSession(ctx, expected.ID)
if errors.Is(err, sqlcon.ErrNoRows) {
@@ -683,7 +672,7 @@ func TestPersister(ctx context.Context, conf *config.Config, p interface {
actual, err := p.GetSession(ctx, expected.ID, session.ExpandNothing)
require.NoError(t, err)
- assert.Equal(t, expectedExpiry, actual.ExpiresAt.Round(time.Minute))
+ assert.LessOrEqual(t, expectedExpiry.Sub(actual.ExpiresAt).Abs(), 10*time.Second)
assert.True(t, foundExpectedCockroachError, "We expect to find a not found error caused by ... FOR UPDATE SKIP LOCKED")
})
}
diff --git a/spec/api.json b/spec/api.json
index 084bbfa5ea47..55919c611025 100644
--- a/spec/api.json
+++ b/spec/api.json
@@ -465,6 +465,7 @@
"continueWith": {
"discriminator": {
"mapping": {
+ "redirect_browser_to": "#/components/schemas/continueWithRedirectBrowserTo",
"set_ory_session_token": "#/components/schemas/continueWithSetOrySessionToken",
"show_recovery_ui": "#/components/schemas/continueWithRecoveryUi",
"show_settings_ui": "#/components/schemas/continueWithSettingsUi",
@@ -484,6 +485,9 @@
},
{
"$ref": "#/components/schemas/continueWithRecoveryUi"
+ },
+ {
+ "$ref": "#/components/schemas/continueWithRedirectBrowserTo"
}
]
},
@@ -516,7 +520,7 @@
"type": "string"
},
"url": {
- "description": "The URL of the recovery flow",
+ "description": "The URL of the recovery flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.",
"type": "string"
}
},
@@ -525,6 +529,28 @@
],
"type": "object"
},
+ "continueWithRedirectBrowserTo": {
+ "description": "Indicates, that the UI flow could be continued by showing a recovery ui",
+ "properties": {
+ "action": {
+ "description": "Action will always be `redirect_browser_to`\nredirect_browser_to ContinueWithActionRedirectBrowserToString",
+ "enum": [
+ "redirect_browser_to"
+ ],
+ "type": "string",
+ "x-go-enum-desc": "redirect_browser_to ContinueWithActionRedirectBrowserToString"
+ },
+ "redirect_browser_to": {
+ "description": "The URL to redirect the browser to",
+ "type": "string"
+ }
+ },
+ "required": [
+ "action",
+ "redirect_browser_to"
+ ],
+ "type": "object"
+ },
"continueWithSetOrySessionToken": {
"description": "Indicates that a session was issued, and the application should use this token for authenticated requests",
"properties": {
@@ -574,6 +600,10 @@
"description": "The ID of the settings flow",
"format": "uuid",
"type": "string"
+ },
+ "url": {
+ "description": "The URL of the settings flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.",
+ "type": "string"
}
},
"required": [
@@ -610,7 +640,7 @@
"type": "string"
},
"url": {
- "description": "The URL of the verification flow",
+ "description": "The URL of the verification flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.",
"type": "string"
},
"verifiable_address": {
@@ -1080,6 +1110,10 @@
"hashed_password": {
"description": "HashedPassword is a hash-representation of the password.",
"type": "string"
+ },
+ "use_password_migration_hook": {
+ "description": "UsePasswordMigrationHook is set to true if the password should be migrated\nusing the password migration hook. If set, and the HashedPassword is empty, a\nwebhook will be called during login to migrate the password.",
+ "type": "boolean"
}
},
"title": "CredentialsPassword is contains the configuration for credentials of the type password.",
@@ -1228,6 +1262,10 @@
"password": {
"description": "The password in plain text if no hash is available.",
"type": "string"
+ },
+ "use_password_migration_hook": {
+ "description": "If set to true, the password will be migrated using the password migration hook.",
+ "type": "boolean"
}
},
"type": "object"
@@ -2169,7 +2207,7 @@
"$ref": "#/components/schemas/uiNodeAttributes"
},
"group": {
- "description": "Group specifies which group (e.g. password authenticator) this node belongs to.\ndefault DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup",
+ "description": "Group specifies which group (e.g. password authenticator) this node belongs to.\ndefault DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup\nidentifier_first IdentifierFirstGroup",
"enum": [
"default",
"password",
@@ -2180,10 +2218,11 @@
"totp",
"lookup_secret",
"webauthn",
- "passkey"
+ "passkey",
+ "identifier_first"
],
"type": "string",
- "x-go-enum-desc": "default DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup"
+ "x-go-enum-desc": "default DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup\nidentifier_first IdentifierFirstGroup"
},
"messages": {
"$ref": "#/components/schemas/uiTexts"
@@ -2345,6 +2384,11 @@
"label": {
"$ref": "#/components/schemas/uiText"
},
+ "maxlength": {
+ "description": "MaxLength may contain the input's maximum length.",
+ "format": "int64",
+ "type": "integer"
+ },
"name": {
"description": "The input's element name.",
"type": "string"
@@ -2362,13 +2406,39 @@
"x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script"
},
"onclick": {
- "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.",
+ "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.",
"type": "string"
},
+ "onclickTrigger": {
+ "description": "OnClickTrigger may contain a WebAuthn trigger which should be executed on click.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration",
+ "enum": [
+ "oryWebAuthnRegistration",
+ "oryWebAuthnLogin",
+ "oryPasskeyLogin",
+ "oryPasskeyLoginAutocompleteInit",
+ "oryPasskeyRegistration",
+ "oryPasskeySettingsRegistration"
+ ],
+ "type": "string",
+ "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration"
+ },
"onload": {
- "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.",
+ "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.",
"type": "string"
},
+ "onloadTrigger": {
+ "description": "OnLoadTrigger may contain a WebAuthn trigger which should be executed on load.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration",
+ "enum": [
+ "oryWebAuthnRegistration",
+ "oryWebAuthnLogin",
+ "oryPasskeyLogin",
+ "oryPasskeyLoginAutocompleteInit",
+ "oryPasskeyRegistration",
+ "oryPasskeySettingsRegistration"
+ ],
+ "type": "string",
+ "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration"
+ },
"pattern": {
"description": "The input's pattern.",
"type": "string"
@@ -2597,6 +2667,7 @@
"discriminator": {
"mapping": {
"code": "#/components/schemas/updateLoginFlowWithCodeMethod",
+ "identifier_first": "#/components/schemas/updateLoginFlowWithIdentifierFirstMethod",
"lookup_secret": "#/components/schemas/updateLoginFlowWithLookupSecretMethod",
"oidc": "#/components/schemas/updateLoginFlowWithOidcMethod",
"passkey": "#/components/schemas/updateLoginFlowWithPasskeyMethod",
@@ -2627,6 +2698,9 @@
},
{
"$ref": "#/components/schemas/updateLoginFlowWithPasskeyMethod"
+ },
+ {
+ "$ref": "#/components/schemas/updateLoginFlowWithIdentifierFirstMethod"
}
]
},
@@ -2664,6 +2738,32 @@
],
"type": "object"
},
+ "updateLoginFlowWithIdentifierFirstMethod": {
+ "description": "Update Login Flow with Multi-Step Method",
+ "properties": {
+ "csrf_token": {
+ "description": "Sending the anti-csrf token is only required for browser login flows.",
+ "type": "string"
+ },
+ "identifier": {
+ "description": "Identifier is the email or username of the user trying to log in.",
+ "type": "string"
+ },
+ "method": {
+ "description": "Method should be set to \"password\" when logging in using the identifier and password strategy.",
+ "type": "string"
+ },
+ "transient_payload": {
+ "description": "Transient data to pass along to any webhooks",
+ "type": "object"
+ }
+ },
+ "required": [
+ "method",
+ "identifier"
+ ],
+ "type": "object"
+ },
"updateLoginFlowWithLookupSecretMethod": {
"description": "Update Login Flow with Lookup Secret Method",
"properties": {
@@ -2929,8 +3029,9 @@
"mapping": {
"code": "#/components/schemas/updateRegistrationFlowWithCodeMethod",
"oidc": "#/components/schemas/updateRegistrationFlowWithOidcMethod",
- "passKey": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod",
+ "passkey": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod",
"password": "#/components/schemas/updateRegistrationFlowWithPasswordMethod",
+ "profile": "#/components/schemas/updateRegistrationFlowWithProfileMethod",
"webauthn": "#/components/schemas/updateRegistrationFlowWithWebAuthnMethod"
},
"propertyName": "method"
@@ -2950,6 +3051,9 @@
},
{
"$ref": "#/components/schemas/updateRegistrationFlowWithPasskeyMethod"
+ },
+ {
+ "$ref": "#/components/schemas/updateRegistrationFlowWithProfileMethod"
}
]
},
@@ -4355,7 +4459,7 @@
},
"/admin/identities/{id}/credentials/{type}": {
"delete": {
- "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type\nYou can only delete second factor (aal2) credentials.",
+ "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type.\nYou cannot delete password or code auth credentials through this API.",
"operationId": "deleteIdentityCredentials",
"parameters": [
{
@@ -4368,7 +4472,7 @@
}
},
{
- "description": "Type is the type of credentials to be deleted.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode",
+ "description": "Type is the type of credentials to delete.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode",
"in": "path",
"name": "type",
"required": true,
@@ -4388,6 +4492,14 @@
"type": "string"
},
"x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode"
+ },
+ {
+ "description": "Identifier is the identifier of the OIDC credential to delete.\nFind the identifier by calling the `GET /admin/identities/{id}?include_credential=oidc` endpoint.",
+ "in": "query",
+ "name": "identifier",
+ "schema": {
+ "type": "string"
+ }
}
],
"responses": {
@@ -4969,7 +5081,7 @@
},
"/admin/sessions/{id}/extend": {
"patch": {
- "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.",
+ "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nThis endpoint ignores consecutive requests to extend the same session and returns a 404 error in those\nscenarios. This endpoint also returns 404 errors if the session does not exist.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.",
"operationId": "extendSession",
"parameters": [
{
diff --git a/spec/swagger.json b/spec/swagger.json
index 8ccd39801919..e1c0f8b1f8d3 100755
--- a/spec/swagger.json
+++ b/spec/swagger.json
@@ -662,7 +662,7 @@
"oryAccessToken": []
}
],
- "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type\nYou can only delete second factor (aal2) credentials.",
+ "description": "Delete an [identity](https://www.ory.sh/docs/kratos/concepts/identity-user-model) credential by its type.\nYou cannot delete password or code auth credentials through this API.",
"consumes": [
"application/json"
],
@@ -701,10 +701,16 @@
],
"type": "string",
"x-go-enum-desc": "password CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode",
- "description": "Type is the type of credentials to be deleted.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode",
+ "description": "Type is the type of credentials to delete.\npassword CredentialsTypePassword\noidc CredentialsTypeOIDC\ntotp CredentialsTypeTOTP\nlookup_secret CredentialsTypeLookup\nwebauthn CredentialsTypeWebAuthn\ncode CredentialsTypeCodeAuth\npasskey CredentialsTypePasskey\nprofile CredentialsTypeProfile\nlink_recovery CredentialsTypeRecoveryLink CredentialsTypeRecoveryLink is a special credential type linked to the link strategy (recovery flow). It is not used within the credentials object itself.\ncode_recovery CredentialsTypeRecoveryCode",
"name": "type",
"in": "path",
"required": true
+ },
+ {
+ "type": "string",
+ "description": "Identifier is the identifier of the OIDC credential to delete.\nFind the identifier by calling the `GET /admin/identities/{id}?include_credential=oidc` endpoint.",
+ "name": "identifier",
+ "in": "query"
}
],
"responses": {
@@ -1188,7 +1194,7 @@
"oryAccessToken": []
}
],
- "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.",
+ "description": "Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it\nwill only extend the session after the specified time has passed.\n\nThis endpoint returns per default a 204 No Content response on success. Older Ory Network projects may\nreturn a 200 OK response with the session in the body. Returning the session as part of the response\nwill be deprecated in the future and should not be relied upon.\n\nThis endpoint ignores consecutive requests to extend the same session and returns a 404 error in those\nscenarios. This endpoint also returns 404 errors if the session does not exist.\n\nRetrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.",
"schemes": [
"http",
"https"
@@ -3648,7 +3654,29 @@
"format": "uuid"
},
"url": {
- "description": "The URL of the recovery flow",
+ "description": "The URL of the recovery flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.",
+ "type": "string"
+ }
+ }
+ },
+ "continueWithRedirectBrowserTo": {
+ "description": "Indicates, that the UI flow could be continued by showing a recovery ui",
+ "type": "object",
+ "required": [
+ "action",
+ "redirect_browser_to"
+ ],
+ "properties": {
+ "action": {
+ "description": "Action will always be `redirect_browser_to`\nredirect_browser_to ContinueWithActionRedirectBrowserToString",
+ "type": "string",
+ "enum": [
+ "redirect_browser_to"
+ ],
+ "x-go-enum-desc": "redirect_browser_to ContinueWithActionRedirectBrowserToString"
+ },
+ "redirect_browser_to": {
+ "description": "The URL to redirect the browser to",
"type": "string"
}
}
@@ -3706,6 +3734,10 @@
"description": "The ID of the settings flow",
"type": "string",
"format": "uuid"
+ },
+ "url": {
+ "description": "The URL of the settings flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.",
+ "type": "string"
}
}
},
@@ -3743,7 +3775,7 @@
"format": "uuid"
},
"url": {
- "description": "The URL of the verification flow",
+ "description": "The URL of the verification flow\n\nIf this value is set, redirect the user's browser to this URL. This value is typically unset for native clients / API flows.",
"type": "string"
},
"verifiable_address": {
@@ -4205,6 +4237,10 @@
"hashed_password": {
"description": "HashedPassword is a hash-representation of the password.",
"type": "string"
+ },
+ "use_password_migration_hook": {
+ "description": "UsePasswordMigrationHook is set to true if the password should be migrated\nusing the password migration hook. If set, and the HashedPassword is empty, a\nwebhook will be called during login to migrate the password.",
+ "type": "boolean"
}
}
},
@@ -4353,6 +4389,10 @@
"password": {
"description": "The password in plain text if no hash is available.",
"type": "string"
+ },
+ "use_password_migration_hook": {
+ "description": "If set to true, the password will be migrated using the password migration hook.",
+ "type": "boolean"
}
}
},
@@ -5276,7 +5316,7 @@
"$ref": "#/definitions/uiNodeAttributes"
},
"group": {
- "description": "Group specifies which group (e.g. password authenticator) this node belongs to.\ndefault DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup",
+ "description": "Group specifies which group (e.g. password authenticator) this node belongs to.\ndefault DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup\nidentifier_first IdentifierFirstGroup",
"type": "string",
"enum": [
"default",
@@ -5288,9 +5328,10 @@
"totp",
"lookup_secret",
"webauthn",
- "passkey"
+ "passkey",
+ "identifier_first"
],
- "x-go-enum-desc": "default DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup"
+ "x-go-enum-desc": "default DefaultGroup\npassword PasswordGroup\noidc OpenIDConnectGroup\nprofile ProfileGroup\nlink LinkGroup\ncode CodeGroup\ntotp TOTPGroup\nlookup_secret LookupGroup\nwebauthn WebAuthnGroup\npasskey PasskeyGroup\nidentifier_first IdentifierFirstGroup"
},
"messages": {
"$ref": "#/definitions/uiTexts"
@@ -5424,6 +5465,11 @@
"label": {
"$ref": "#/definitions/uiText"
},
+ "maxlength": {
+ "description": "MaxLength may contain the input's maximum length.",
+ "type": "integer",
+ "format": "int64"
+ },
"name": {
"description": "The input's element name.",
"type": "string"
@@ -5441,13 +5487,39 @@
"x-go-enum-desc": "text Text\ninput Input\nimg Image\na Anchor\nscript Script"
},
"onclick": {
- "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.",
+ "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.",
"type": "string"
},
+ "onclickTrigger": {
+ "description": "OnClickTrigger may contain a WebAuthn trigger which should be executed on click.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration",
+ "type": "string",
+ "enum": [
+ "oryWebAuthnRegistration",
+ "oryWebAuthnLogin",
+ "oryPasskeyLogin",
+ "oryPasskeyLoginAutocompleteInit",
+ "oryPasskeyRegistration",
+ "oryPasskeySettingsRegistration"
+ ],
+ "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration"
+ },
"onload": {
- "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.",
+ "description": "OnLoad may contain javascript which should be executed on load. This is primarily\nused for WebAuthn.\n\nDeprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.",
"type": "string"
},
+ "onloadTrigger": {
+ "description": "OnLoadTrigger may contain a WebAuthn trigger which should be executed on load.\n\nThe trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.\noryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration",
+ "type": "string",
+ "enum": [
+ "oryWebAuthnRegistration",
+ "oryWebAuthnLogin",
+ "oryPasskeyLogin",
+ "oryPasskeyLoginAutocompleteInit",
+ "oryPasskeyRegistration",
+ "oryPasskeySettingsRegistration"
+ ],
+ "x-go-enum-desc": "oryWebAuthnRegistration WebAuthnTriggersWebAuthnRegistration\noryWebAuthnLogin WebAuthnTriggersWebAuthnLogin\noryPasskeyLogin WebAuthnTriggersPasskeyLogin\noryPasskeyLoginAutocompleteInit WebAuthnTriggersPasskeyLoginAutocompleteInit\noryPasskeyRegistration WebAuthnTriggersPasskeyRegistration\noryPasskeySettingsRegistration WebAuthnTriggersPasskeySettingsRegistration"
+ },
"pattern": {
"description": "The input's pattern.",
"type": "string"
@@ -5703,6 +5775,32 @@
}
}
},
+ "updateLoginFlowWithIdentifierFirstMethod": {
+ "description": "Update Login Flow with Multi-Step Method",
+ "type": "object",
+ "required": [
+ "method",
+ "identifier"
+ ],
+ "properties": {
+ "csrf_token": {
+ "description": "Sending the anti-csrf token is only required for browser login flows.",
+ "type": "string"
+ },
+ "identifier": {
+ "description": "Identifier is the email or username of the user trying to log in.",
+ "type": "string"
+ },
+ "method": {
+ "description": "Method should be set to \"password\" when logging in using the identifier and password strategy.",
+ "type": "string"
+ },
+ "transient_payload": {
+ "description": "Transient data to pass along to any webhooks",
+ "type": "object"
+ }
+ }
+ },
"updateLoginFlowWithLookupSecretMethod": {
"description": "Update Login Flow with Lookup Secret Method",
"type": "object",
diff --git a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts
index 477a149f9b26..3310a966627f 100644
--- a/test/e2e/cypress/integration/profiles/code/login/error.spec.ts
+++ b/test/e2e/cypress/integration/profiles/code/login/error.spec.ts
@@ -83,7 +83,7 @@ context("Login error messages with code method", () => {
"An email containing a code has been sent to the email address you provided",
)
- cy.get(Selectors[app]["code"]).type("invalid-code")
+ cy.get(Selectors[app]["code"]).type("123456")
cy.submitCodeForm(app)
cy.get('[data-testid="ui/message/4010008"]').should(
@@ -113,7 +113,7 @@ context("Login error messages with code method", () => {
.type(gen.email(), { force: true })
}
- cy.get(Selectors[app]["code"]).type("invalid-code")
+ cy.get(Selectors[app]["code"]).type("123456")
cy.submitCodeForm(app)
if (app !== "express") {
@@ -147,7 +147,7 @@ context("Login error messages with code method", () => {
)
}
- cy.get(Selectors[app]["code"]).type("invalid-code")
+ cy.get(Selectors[app]["code"]).type("123456")
cy.removeAttribute([Selectors[app]["identity"]], "required")
cy.get(Selectors[app]["identity"]).type("{selectall}{backspace}", {
diff --git a/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts
index ef435991f736..4570c1c5cc60 100644
--- a/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts
+++ b/test/e2e/cypress/integration/profiles/code/registration/error.spec.ts
@@ -74,7 +74,7 @@ context("Registration error messages with code method", () => {
"An email containing a code has been sent to the email address you provided",
)
- cy.get(Selectors[app]["code"]).type("invalid-code")
+ cy.get(Selectors[app]["code"]).type("123456")
cy.submitCodeForm(app)
cy.get('[data-testid="ui/message/4040003"]').should(
@@ -111,7 +111,7 @@ context("Registration error messages with code method", () => {
.type("changed-email@email.com", { force: true })
}
- cy.get(Selectors[app]["code"]).type("invalid-code")
+ cy.get(Selectors[app]["code"]).type("123456")
cy.submitCodeForm(app)
if (app !== "express") {
@@ -174,7 +174,7 @@ context("Registration error messages with code method", () => {
})
cy.removeAttribute([Selectors[app]["email"]], "required")
}
- cy.get(Selectors[app]["code"]).type("invalid-code")
+ cy.get(Selectors[app]["code"]).type("123456")
cy.submitCodeForm(app)
diff --git a/test/e2e/cypress/integration/profiles/mfa/code.spec.ts b/test/e2e/cypress/integration/profiles/mfa/code.spec.ts
index 7961aa850de9..c7d29c0561c2 100644
--- a/test/e2e/cypress/integration/profiles/mfa/code.spec.ts
+++ b/test/e2e/cypress/integration/profiles/mfa/code.spec.ts
@@ -58,7 +58,7 @@ context("2FA code", () => {
cy.get("input[name='code']").should("be.visible")
cy.getLoginCodeFromEmail(email).then((code) => {
cy.get("input[name='code']").type(code)
- cy.contains("Submit").click()
+ cy.contains("Continue").click()
})
cy.getSession({
@@ -88,10 +88,10 @@ context("2FA code", () => {
cy.get("input[name='code']").should("be.visible")
cy.get("input[name='code']").type("123456")
- cy.contains("Submit").click()
+ cy.contains("Continue").click()
cy.getLoginCodeFromEmail(email).then((code) => {
cy.get("input[name='code']").type(code)
- cy.contains("Submit").click()
+ cy.contains("Continue").click()
})
cy.getSession({
diff --git a/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts
index 8e381e7acf5d..153b3332dd81 100644
--- a/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts
+++ b/test/e2e/cypress/integration/profiles/oidc/login/success.spec.ts
@@ -70,7 +70,7 @@ context("Social Sign In Successes", () => {
cy.visit(settings)
cy.get('[value="hydra"]')
.should("have.attr", "name", "unlink")
- .should("contain.text", "Unlink hydra")
+ .should("contain.text", "Unlink Ory")
})
it("should be able to sign up with redirects", () => {
diff --git a/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts b/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts
index 674e24e0d668..5f22ae886338 100644
--- a/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts
+++ b/test/e2e/cypress/integration/profiles/oidc/settings/success.spec.ts
@@ -94,7 +94,7 @@ context("Social Sign In Settings Success", () => {
cy.get('[value="hydra"]')
.should("have.attr", "name", "unlink")
- .should("contain.text", "Unlink hydra")
+ .should("contain.text", "Unlink Ory")
})
it("should link google", () => {
diff --git a/test/e2e/cypress/support/commands.ts b/test/e2e/cypress/support/commands.ts
index 0b4584646abc..89b5c7cb15c5 100644
--- a/test/e2e/cypress/support/commands.ts
+++ b/test/e2e/cypress/support/commands.ts
@@ -17,7 +17,7 @@ import {
import dayjs from "dayjs"
import YAML from "yamljs"
import { MailMessage, Strategy } from "."
-import { OryKratosConfiguration } from "./config"
+import { OryKratosConfiguration } from "../../shared/config"
import { UiNode } from "@ory/kratos-client"
import { ConfigBuilder } from "./configHelpers"
@@ -429,7 +429,7 @@ Cypress.Commands.add(
f.group === "default" &&
"name" in f.attributes &&
f.attributes.name === "traits.email",
- ).attributes.value,
+ )?.attributes.value,
).to.eq(email)
return cy
diff --git a/test/e2e/cypress/support/configHelpers.ts b/test/e2e/cypress/support/configHelpers.ts
index c8ccf05b70d2..0fc72864294a 100644
--- a/test/e2e/cypress/support/configHelpers.ts
+++ b/test/e2e/cypress/support/configHelpers.ts
@@ -1,7 +1,7 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
-import { OryKratosConfiguration } from "./config"
+import { OryKratosConfiguration } from "../../shared/config"
export class ConfigBuilder {
constructor(readonly config: OryKratosConfiguration) {}
diff --git a/test/e2e/cypress/support/index.d.ts b/test/e2e/cypress/support/index.d.ts
index 9cfeb083f12a..a6a7120937de 100644
--- a/test/e2e/cypress/support/index.d.ts
+++ b/test/e2e/cypress/support/index.d.ts
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { Session as KratosSession } from "@ory/kratos-client"
-import { OryKratosConfiguration } from "./config"
+import { OryKratosConfiguration } from "../../shared/config"
import { ConfigBuilder } from "./configHelpers"
export interface MailMessage {
diff --git a/test/e2e/cypress/tsconfig.json b/test/e2e/cypress/tsconfig.json
index 6042605a6887..dd9b96adae48 100644
--- a/test/e2e/cypress/tsconfig.json
+++ b/test/e2e/cypress/tsconfig.json
@@ -2,15 +2,9 @@
"compilerOptions": {
"baseUrl": "../../../node_modules",
"target": "es5",
- "lib": [
- "es2015",
- "dom"
- ],
+ "lib": ["es2015", "dom"],
"types": ["cypress", "node"],
- "esModuleInterop": true,
+ "esModuleInterop": true
},
- "include": [
- "**/*.ts",
- "support/index.ts",
- ],
+ "include": ["**/*.ts", "support/index.ts", "../shared/config.d.ts"]
}
diff --git a/test/e2e/package-lock.json b/test/e2e/package-lock.json
index f7f7bd88539a..f6452591bda9 100644
--- a/test/e2e/package-lock.json
+++ b/test/e2e/package-lock.json
@@ -8,13 +8,14 @@
"name": "@ory/kratos-e2e-suite",
"version": "0.0.1",
"dependencies": {
- "@faker-js/faker": "7.6.0",
+ "@faker-js/faker": "8.4.1",
"async-retry": "1.3.3",
- "mailhog": "4.16.0"
+ "mailhog": "4.16.0",
+ "promise-retry": "^2.0.1"
},
"devDependencies": {
- "@ory/kratos-client": "0.0.0-next.8d3b018594f7",
- "@playwright/test": "1.34.0",
+ "@ory/kratos-client": "1.2.0",
+ "@playwright/test": "1.44.1",
"@types/async-retry": "1.4.5",
"@types/node": "16.9.6",
"@types/yamljs": "0.2.31",
@@ -98,12 +99,19 @@
}
},
"node_modules/@faker-js/faker": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz",
- "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==",
+ "version": "8.4.1",
+ "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz",
+ "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fakerjs"
+ }
+ ],
+ "license": "MIT",
"engines": {
- "node": ">=14.0.0",
- "npm": ">=6.0.0"
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0",
+ "npm": ">=6.14.13"
}
},
"node_modules/@hapi/hoek": {
@@ -128,12 +136,13 @@
"dev": true
},
"node_modules/@ory/kratos-client": {
- "version": "0.0.0-next.8d3b018594f7",
- "resolved": "https://registry.npmjs.org/@ory/kratos-client/-/kratos-client-0.0.0-next.8d3b018594f7.tgz",
- "integrity": "sha512-TkpjBo6Z6UUEJIJCR2EDdpKVDNgQHzwDWZbOjz3xTOUoGipMBykvIfluP58Jwkpt2rIXUkt9+L+u1mFFvD/tqA==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ory/kratos-client/-/kratos-client-1.2.0.tgz",
+ "integrity": "sha512-W6jFkVEjnoq5ylGOvYOOaNvEZ1cGSEN/YJsZTcBVye81nQtW5R7QWClvNsJVD1LjwgWGMVKWglrFlfHvvkKnmg==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
- "axios": "^0.21.1"
+ "axios": "^1.6.1"
}
},
"node_modules/@otplib/core": {
@@ -184,22 +193,19 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.34.0",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.34.0.tgz",
- "integrity": "sha512-GIALJVODOIrMflLV54H3Cow635OfrTwOu24ZTDyKC66uchtFX2NcCRq83cLdakMjZKYK78lODNLQSYBj2OgaTw==",
+ "version": "1.44.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
+ "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
- "@types/node": "*",
- "playwright-core": "1.34.0"
+ "playwright": "1.44.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
- "node": ">=14"
- },
- "optionalDependencies": {
- "fsevents": "2.3.2"
+ "node": ">=16"
}
},
"node_modules/@sideway/address": {
@@ -534,14 +540,39 @@
"dev": true
},
"node_modules/axios": {
- "version": "0.21.4",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
- "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
+ "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios/node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "follow-redirects": "^1.14.0"
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
}
},
+ "node_modules/axios/node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1121,6 +1152,11 @@
"node": ">=8.6"
}
},
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="
+ },
"node_modules/es5-ext": {
"version": "0.10.62",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz",
@@ -1304,9 +1340,9 @@
}
},
"node_modules/follow-redirects": {
- "version": "1.14.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
- "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true,
"funding": [
{
@@ -1314,6 +1350,7 @@
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
+ "license": "MIT",
"engines": {
"node": ">=4.0"
},
@@ -1373,6 +1410,7 @@
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -2336,13 +2374,36 @@
"node": ">=0.10.0"
}
},
+ "node_modules/playwright": {
+ "version": "1.44.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
+ "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.44.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
"node_modules/playwright-core": {
- "version": "1.34.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.0.tgz",
- "integrity": "sha512-fMUY1+iR6kYbJF/EsOOqzBA99ZHXbw9sYPNjwA4X/oV0hVF/1aGlWYBGPVUEqxBkGANDKMziYoOdKGU5DIP5Gg==",
+ "version": "1.44.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
+ "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
"engines": {
- "node": ">=14"
+ "node": ">=16"
}
},
"node_modules/prettier": {
@@ -2381,6 +2442,26 @@
"node": ">= 0.6.0"
}
},
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/promise-retry/node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
@@ -3091,9 +3172,9 @@
}
},
"@faker-js/faker": {
- "version": "7.6.0",
- "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz",
- "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw=="
+ "version": "8.4.1",
+ "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz",
+ "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg=="
},
"@hapi/hoek": {
"version": "9.3.0",
@@ -3117,12 +3198,12 @@
"dev": true
},
"@ory/kratos-client": {
- "version": "0.0.0-next.8d3b018594f7",
- "resolved": "https://registry.npmjs.org/@ory/kratos-client/-/kratos-client-0.0.0-next.8d3b018594f7.tgz",
- "integrity": "sha512-TkpjBo6Z6UUEJIJCR2EDdpKVDNgQHzwDWZbOjz3xTOUoGipMBykvIfluP58Jwkpt2rIXUkt9+L+u1mFFvD/tqA==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ory/kratos-client/-/kratos-client-1.2.0.tgz",
+ "integrity": "sha512-W6jFkVEjnoq5ylGOvYOOaNvEZ1cGSEN/YJsZTcBVye81nQtW5R7QWClvNsJVD1LjwgWGMVKWglrFlfHvvkKnmg==",
"dev": true,
"requires": {
- "axios": "^0.21.1"
+ "axios": "^1.6.1"
}
},
"@otplib/core": {
@@ -3173,14 +3254,12 @@
}
},
"@playwright/test": {
- "version": "1.34.0",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.34.0.tgz",
- "integrity": "sha512-GIALJVODOIrMflLV54H3Cow635OfrTwOu24ZTDyKC66uchtFX2NcCRq83cLdakMjZKYK78lODNLQSYBj2OgaTw==",
+ "version": "1.44.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
+ "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true,
"requires": {
- "@types/node": "*",
- "fsevents": "2.3.2",
- "playwright-core": "1.34.0"
+ "playwright": "1.44.1"
}
},
"@sideway/address": {
@@ -3459,12 +3538,33 @@
"dev": true
},
"axios": {
- "version": "0.21.4",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
- "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
+ "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"dev": true,
"requires": {
- "follow-redirects": "^1.14.0"
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ },
+ "dependencies": {
+ "form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dev": true,
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ }
+ },
+ "proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true
+ }
}
},
"balanced-match": {
@@ -3914,6 +4014,11 @@
"ansi-colors": "^4.1.1"
}
},
+ "err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="
+ },
"es5-ext": {
"version": "0.10.62",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz",
@@ -4066,9 +4171,9 @@
}
},
"follow-redirects": {
- "version": "1.14.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
- "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true
},
"forever-agent": {
@@ -4825,10 +4930,20 @@
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
"dev": true
},
+ "playwright": {
+ "version": "1.44.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
+ "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
+ "dev": true,
+ "requires": {
+ "fsevents": "2.3.2",
+ "playwright-core": "1.44.1"
+ }
+ },
"playwright-core": {
- "version": "1.34.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.0.tgz",
- "integrity": "sha512-fMUY1+iR6kYbJF/EsOOqzBA99ZHXbw9sYPNjwA4X/oV0hVF/1aGlWYBGPVUEqxBkGANDKMziYoOdKGU5DIP5Gg==",
+ "version": "1.44.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
+ "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"dev": true
},
"prettier": {
@@ -4849,6 +4964,22 @@
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"dev": true
},
+ "promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "requires": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "dependencies": {
+ "retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="
+ }
+ }
+ },
"proxy-from-env": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
diff --git a/test/e2e/package.json b/test/e2e/package.json
index d4106cbb8aa1..05b04db6880a 100644
--- a/test/e2e/package.json
+++ b/test/e2e/package.json
@@ -11,13 +11,14 @@
"wait-on": "wait-on"
},
"dependencies": {
- "@faker-js/faker": "7.6.0",
+ "@faker-js/faker": "8.4.1",
"async-retry": "1.3.3",
- "mailhog": "4.16.0"
+ "mailhog": "4.16.0",
+ "promise-retry": "^2.0.1"
},
"devDependencies": {
- "@ory/kratos-client": "0.0.0-next.8d3b018594f7",
- "@playwright/test": "1.34.0",
+ "@ory/kratos-client": "1.2.0",
+ "@playwright/test": "1.44.1",
"@types/async-retry": "1.4.5",
"@types/node": "16.9.6",
"@types/yamljs": "0.2.31",
diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts
index 71a67dfd8795..a3f81c73060d 100644
--- a/test/e2e/playwright.config.ts
+++ b/test/e2e/playwright.config.ts
@@ -4,7 +4,7 @@
import { defineConfig, devices } from "@playwright/test"
import * as dotenv from "dotenv"
-dotenv.config({ path: "playwright/playwright.env" })
+dotenv.config({ path: __dirname + "/playwright/playwright.env" })
/**
* See https://playwright.dev/docs/test-configuration.
@@ -17,19 +17,28 @@ export default defineConfig({
workers: 1,
reporter: process.env.CI ? [["github"], ["html"], ["list"]] : "html",
- globalSetup: "./playwright/setup/global_setup.ts",
-
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
trace: process.env.CI ? "retain-on-failure" : "on",
- baseURL: "http://localhost:19006",
},
/* Configure projects for major browsers */
projects: [
{
- name: "Mobile Chrome",
- use: { ...devices["Pixel 5"] },
+ name: "mobile-chrome",
+ testMatch: "mobile/**/*.spec.ts",
+ use: {
+ ...devices["Pixel 5"],
+ baseURL: "http://localhost:19006",
+ },
+ },
+ {
+ name: "chromium",
+ testMatch: "desktop/**/*.spec.ts",
+ use: {
+ ...devices["Desktop Chrome"],
+ baseURL: "http://localhost:4455",
+ },
},
],
@@ -42,7 +51,6 @@ export default defineConfig({
].join(" && "),
cwd: "../..",
url: "http://localhost:4433/health/ready",
- reuseExistingServer: false,
env: {
DSN: dbToDsn(),
COURIER_SMTP_CONNECTION_URI:
diff --git a/test/e2e/playwright/actions/login.ts b/test/e2e/playwright/actions/login.ts
new file mode 100644
index 000000000000..806f16466dc3
--- /dev/null
+++ b/test/e2e/playwright/actions/login.ts
@@ -0,0 +1,47 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { APIRequestContext } from "@playwright/test"
+import { findCsrfToken } from "../lib/helper"
+import { LoginFlow, Session } from "@ory/kratos-client"
+import { expectJSONResponse } from "../lib/request"
+import { expect } from "../fixtures"
+
+export async function loginWithPassword(
+ user: { password: string; traits: { email: string } },
+ r: APIRequestContext,
+ baseUrl: string,
+): Promise {
+ const { ui } = await expectJSONResponse(
+ await r.get(baseUrl + "/self-service/login/browser", {
+ headers: {
+ Accept: "application/json",
+ },
+ }),
+ {
+ message: "Initializing login flow failed",
+ },
+ )
+
+ const res = await r.post(ui.action, {
+ headers: {
+ Accept: "application/json",
+ },
+ data: {
+ identifier: user.traits.email,
+ password: user.password,
+ method: "password",
+ csrf_token: findCsrfToken(ui),
+ },
+ })
+ const { session } = await expectJSONResponse<{ session: Session }>(res)
+ expect(session?.identity?.traits.email).toEqual(user.traits.email)
+ expect(
+ res.headersArray().find(
+ ({ name, value }) =>
+ name.toLowerCase() === "set-cookie" &&
+ (value.indexOf("ory_session_") > -1 || // Ory Network
+ value.indexOf("ory_kratos_session") > -1), // Locally hosted
+ ),
+ ).toBeDefined()
+}
diff --git a/test/e2e/playwright/actions/mail.ts b/test/e2e/playwright/actions/mail.ts
index 871608bc204d..172c6d8ab849 100644
--- a/test/e2e/playwright/actions/mail.ts
+++ b/test/e2e/playwright/actions/mail.ts
@@ -8,14 +8,29 @@ const mh = mailhog({
basePath: "http://localhost:8025/api",
})
-export function search(...props: Parameters) {
+type searchProps = {
+ query: string
+ kind: "to" | "from" | "containing"
+ /**
+ *
+ * @param message an email message
+ * @returns decide whether to include the message in the result
+ */
+ filter?: (message: mailhog.Message) => boolean
+}
+
+export function search({ query, kind, filter }: searchProps) {
return retry(
async () => {
- const res = await mh.search(...props)
+ const res = await mh.search(query, kind)
if (res.total === 0) {
throw new Error("no emails found")
}
- return res.items
+ const result = filter ? res.items.filter(filter) : res.items
+ if (result.length === 0) {
+ throw new Error("no emails found")
+ }
+ return result
},
{
retries: 3,
diff --git a/test/e2e/playwright/actions/session.ts b/test/e2e/playwright/actions/session.ts
new file mode 100644
index 000000000000..5d77b6de7b59
--- /dev/null
+++ b/test/e2e/playwright/actions/session.ts
@@ -0,0 +1,38 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { APIRequestContext, expect } from "@playwright/test"
+import { Session } from "@ory/kratos-client"
+
+export async function hasSession(
+ r: APIRequestContext,
+ kratosPublicURL: string,
+): Promise {
+ const resp = await r.get(kratosPublicURL + "/sessions/whoami", {
+ failOnStatusCode: true,
+ })
+ const session = await resp.json()
+ expect(session).toBeDefined()
+ expect(session.active).toBe(true)
+}
+
+export async function getSession(
+ r: APIRequestContext,
+ kratosPublicURL: string,
+): Promise {
+ const resp = await r.get(kratosPublicURL + "/sessions/whoami", {
+ failOnStatusCode: true,
+ })
+ return resp.json()
+}
+
+export async function hasNoSession(
+ r: APIRequestContext,
+ kratosPublicURL: string,
+): Promise {
+ const resp = await r.get(kratosPublicURL + "/sessions/whoami", {
+ failOnStatusCode: false,
+ })
+ expect(resp.status()).toBe(401)
+ return resp.json()
+}
diff --git a/test/e2e/playwright/fixtures/index.ts b/test/e2e/playwright/fixtures/index.ts
index 3b1264e84c0a..b915dd4d937f 100644
--- a/test/e2e/playwright/fixtures/index.ts
+++ b/test/e2e/playwright/fixtures/index.ts
@@ -1,13 +1,24 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
+import { faker } from "@faker-js/faker"
import { Identity } from "@ory/kratos-client"
-import { test as base, expect } from "@playwright/test"
-import { OryKratosConfiguration } from "../../cypress/support/config"
+import {
+ CDPSession,
+ test as base,
+ expect as baseExpect,
+ APIRequestContext,
+ Page,
+} from "@playwright/test"
+import { writeFile } from "fs/promises"
import { merge } from "lodash"
+import { OryKratosConfiguration } from "../../shared/config"
import { default_config } from "../setup/default_config"
-import { writeFile } from "fs/promises"
-import { faker } from "@faker-js/faker"
+import { APIResponse } from "playwright-core"
+import { SessionWithResponse } from "../types"
+import { retryOptions } from "../lib/request"
+import promiseRetry from "promise-retry"
+import { Protocol } from "playwright-core/types/protocol"
// from https://stackoverflow.com/questions/61132262/typescript-deep-partial
type DeepPartial = T extends object
@@ -17,12 +28,23 @@ type DeepPartial = T extends object
: T
type TestFixtures = {
- identity: Identity
+ identity: { oryIdentity: Identity; email: string; password: string }
configOverride: DeepPartial
- config: void
+ config: OryKratosConfiguration
+ virtualAuthenticatorOptions: Partial
+ pageCDPSession: CDPSession
+ virtualAuthenticator: Protocol.WebAuthn.addVirtualAuthenticatorReturnValue
}
-type WorkerFixtures = {}
+type WorkerFixtures = {
+ kratosAdminURL: string
+ kratosPublicURL: string
+ mode:
+ | "reconfigure_kratos"
+ | "reconfigure_ory_network_project"
+ | "existing_kratos"
+ | "existing_ory_network_project"
+}
export const test = base.extend({
configOverride: {},
@@ -34,9 +56,11 @@ export const test = base.extend({
const configRevision = await resp.body()
+ const fileDirectory = __dirname + "/../.."
+
await writeFile(
- "playwright/kratos.config.json",
- JSON.stringify(configToWrite),
+ fileDirectory + "/playwright/kratos.config.json",
+ JSON.stringify(configToWrite, null, 2),
)
await expect(async () => {
const resp = await request.get("http://localhost:4434/health/config")
@@ -44,21 +68,166 @@ export const test = base.extend({
expect(updatedRevision).not.toBe(configRevision)
}).toPass()
- await use()
+ await use(configToWrite)
},
{ auto: true },
],
- identity: async ({ request }, use) => {
+ virtualAuthenticatorOptions: undefined,
+ pageCDPSession: async ({ page }, use) => {
+ const cdpSession = await page.context().newCDPSession(page)
+ await use(cdpSession)
+ await cdpSession.detach()
+ },
+ virtualAuthenticator: async (
+ { pageCDPSession, virtualAuthenticatorOptions },
+ use,
+ ) => {
+ await pageCDPSession.send("WebAuthn.enable")
+ const { authenticatorId } = await pageCDPSession.send(
+ "WebAuthn.addVirtualAuthenticator",
+ {
+ options: {
+ protocol: "ctap2",
+ transport: "internal",
+ hasResidentKey: true,
+ hasUserVerification: true,
+ isUserVerified: true,
+ ...virtualAuthenticatorOptions,
+ },
+ },
+ )
+ await use({ authenticatorId })
+ await pageCDPSession.send("WebAuthn.removeVirtualAuthenticator", {
+ authenticatorId,
+ })
+
+ await pageCDPSession.send("WebAuthn.disable")
+ },
+ identity: async ({ request }, use, i) => {
+ const email = faker.internet.email({ provider: "ory.sh" })
+ const password = faker.internet.password()
const resp = await request.post("http://localhost:4434/admin/identities", {
data: {
schema_id: "email",
traits: {
- email: faker.internet.email(undefined, undefined, "ory.sh"),
+ email,
website: faker.internet.url(),
},
+
+ credentials: {
+ password: {
+ config: {
+ password,
+ },
+ },
+ },
},
})
+ const oryIdentity = await resp.json()
+ i.attach("identity", {
+ body: JSON.stringify(oryIdentity, null, 2),
+ contentType: "application/json",
+ })
expect(resp.status()).toBe(201)
- await use(await resp.json())
+ await use({
+ oryIdentity,
+ email,
+ password,
+ })
},
+ kratosAdminURL: ["http://localhost:4434", { option: true, scope: "worker" }],
+ kratosPublicURL: ["http://localhost:4433", { option: true, scope: "worker" }],
+})
+
+export const expect = baseExpect.extend({
+ toHaveSession,
+ toMatchResponseData,
})
+
+async function toHaveSession(
+ requestOrPage: APIRequestContext | Page,
+ baseUrl: string,
+) {
+ let r: APIRequestContext
+ if ("request" in requestOrPage) {
+ r = requestOrPage.request
+ } else {
+ r = requestOrPage
+ }
+ let pass = true
+
+ let responseData: string
+ let response: APIResponse = null
+ try {
+ const result = await promiseRetry(
+ () =>
+ r
+ .get(baseUrl + "/sessions/whoami", {
+ failOnStatusCode: false,
+ })
+ .then(
+ async (res: APIResponse): Promise => {
+ return {
+ session: await res.json(),
+ response: res,
+ }
+ },
+ ),
+ retryOptions,
+ )
+ pass = !!result.session.active
+ responseData = await result.response.text()
+ response = result.response
+ } catch (e) {
+ pass = false
+ responseData = JSON.stringify(e.message, undefined, 2)
+ }
+
+ const message = () =>
+ this.utils.matcherHint("toHaveSession", undefined, undefined, {
+ isNot: this.isNot,
+ }) +
+ `\n
+ \n
+ Expected: ${this.isNot ? "not" : ""} to have session\n
+ Session data received: ${responseData}\n
+ Headers: ${JSON.stringify(response?.headers(), null, 2)}\n
+ `
+
+ return {
+ message,
+ pass,
+ name: "toHaveSession",
+ }
+}
+
+async function toMatchResponseData(
+ res: APIResponse,
+ options: {
+ statusCode?: number
+ failureHint?: string
+ },
+) {
+ const body = await res.text()
+ const statusCode = options.statusCode ?? 200
+ const failureHint = options.failureHint ?? ""
+ const message = () =>
+ this.utils.matcherHint("toMatch", undefined, undefined, {
+ isNot: this.isNot,
+ }) +
+ `\n
+ ${failureHint}
+ \n
+ Expected: ${this.isNot ? "not" : ""} to match\n
+ Status Code: ${statusCode}\n
+ Body: ${body}\n
+ Headers: ${JSON.stringify(res.headers(), null, 2)}\n
+ URL: ${JSON.stringify(res.url(), null, 2)}\n
+ `
+
+ return {
+ message,
+ pass: res.status() === statusCode,
+ name: "toMatch",
+ }
+}
diff --git a/test/e2e/playwright/kratos.base-config.json b/test/e2e/playwright/kratos.base-config.json
index 83b24587bd61..e9abcfd7f4f0 100644
--- a/test/e2e/playwright/kratos.base-config.json
+++ b/test/e2e/playwright/kratos.base-config.json
@@ -4,6 +4,10 @@
{
"id": "default",
"url": "file://test/e2e/profiles/oidc/identity.traits.schema.json"
+ },
+ {
+ "id": "email",
+ "url": "file://test/e2e/profiles/email/identity.traits.schema.json"
}
]
},
diff --git a/test/e2e/playwright/lib/helper.ts b/test/e2e/playwright/lib/helper.ts
index 929b39b74573..da5fb76c13ba 100644
--- a/test/e2e/playwright/lib/helper.ts
+++ b/test/e2e/playwright/lib/helper.ts
@@ -2,6 +2,13 @@
// SPDX-License-Identifier: Apache-2.0
import { Message } from "mailhog"
+import {
+ UiContainer,
+ UiNodeAttributes,
+ UiNodeInputAttributes,
+} from "@ory/kratos-client"
+import { expect } from "../fixtures"
+import { LoginFlowStyle, OryKratosConfiguration } from "../../shared/config"
export const codeRegex = /(\d{6})/
@@ -18,3 +25,50 @@ export function extractCode(mail: Message) {
}
return null
}
+
+export function findCsrfToken(ui: UiContainer) {
+ const csrf = ui.nodes
+ .filter((node) => isUiNodeInputAttributes(node.attributes))
+ // Since we filter all non-input attributes, the following as is ok:
+ .map(
+ (node): UiNodeInputAttributes => node.attributes as UiNodeInputAttributes,
+ )
+ .find(({ name }) => name === "csrf_token")?.value
+ expect(csrf).toBeDefined()
+ return csrf
+}
+
+export function isUiNodeInputAttributes(
+ attrs: UiNodeAttributes,
+): attrs is UiNodeInputAttributes & {
+ node_type: "input"
+} {
+ return attrs.node_type === "input"
+}
+
+export const toConfig = ({
+ style = "identifier_first",
+ mitigateEnumeration = false,
+ selfservice,
+}: {
+ style?: LoginFlowStyle
+ mitigateEnumeration?: boolean
+ selfservice?: Partial
+}) => ({
+ selfservice: {
+ default_browser_return_url: "http://localhost:4455/welcome",
+ ...selfservice,
+ flows: {
+ login: {
+ ...selfservice?.flows?.login,
+ style,
+ },
+ ...selfservice?.flows,
+ },
+ },
+ security: {
+ account_enumeration: {
+ mitigate: mitigateEnumeration,
+ },
+ },
+})
diff --git a/test/e2e/playwright/lib/request.ts b/test/e2e/playwright/lib/request.ts
new file mode 100644
index 000000000000..5732e5a34aca
--- /dev/null
+++ b/test/e2e/playwright/lib/request.ts
@@ -0,0 +1,34 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { APIResponse } from "playwright-core"
+import { expect } from "../fixtures"
+import { OperationOptions } from "retry"
+
+export type RetryOptions = OperationOptions
+
+export const retryOptions: RetryOptions = {
+ retries: 20,
+ factor: 1,
+ maxTimeout: 500,
+ minTimeout: 250,
+ randomize: false,
+}
+
+export async function expectJSONResponse(
+ res: APIResponse,
+ { statusCode = 200, message }: { statusCode?: number; message?: string } = {},
+): Promise {
+ await expect(res).toMatchResponseData({
+ statusCode,
+ failureHint: message,
+ })
+ try {
+ return (await res.json()) as T
+ } catch (e) {
+ const body = await res.text()
+ throw Error(
+ `Expected to be able to parse body as json: ${e} (body: ${body})`,
+ )
+ }
+}
diff --git a/test/e2e/playwright/models/elements/login.ts b/test/e2e/playwright/models/elements/login.ts
new file mode 100644
index 000000000000..baae9b11834a
--- /dev/null
+++ b/test/e2e/playwright/models/elements/login.ts
@@ -0,0 +1,246 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { expect, Locator, Page } from "@playwright/test"
+import { createInputLocator, InputLocator } from "../../selectors/input"
+import { URLSearchParams } from "node:url"
+import { OryKratosConfiguration } from "../../../shared/config"
+
+enum LoginStyle {
+ IdentifierFirst = "identifier_first",
+ Unified = "unified",
+}
+
+type SubmitOptions = {
+ submitWithKeyboard?: boolean
+ waitForURL?: string | RegExp
+}
+
+export class LoginPage {
+ public submitPassword: Locator
+ public github: Locator
+ public google: Locator
+ public signup: Locator
+
+ public identifier: InputLocator
+ public password: InputLocator
+ public totpInput: InputLocator
+ public totpSubmit: Locator
+ public lookupInput: InputLocator
+ public lookupSubmit: Locator
+ public codeSubmit = this.page.locator('button[type="submit"][value="code"]')
+ public codeInput = createInputLocator(this.page, "code")
+
+ public alert: Locator
+
+ constructor(readonly page: Page, readonly config: OryKratosConfiguration) {
+ this.identifier = createInputLocator(page, "identifier")
+ this.password = createInputLocator(page, "password")
+ this.totpInput = createInputLocator(page, "totp_code")
+ this.lookupInput = createInputLocator(page, "lookup_secret")
+
+ this.submitPassword = page.locator(
+ '[type="submit"][name="method"][value="password"]',
+ )
+
+ this.github = page.locator('[name="provider"][value="github"]')
+ this.google = page.locator('[name="provider"][value="google"]')
+
+ this.totpSubmit = page.locator('[name="method"][value="totp"]')
+ this.lookupSubmit = page.locator('[name="method"][value="lookup_secret"]')
+
+ this.signup = page.locator('[data-testid="signup-link"]')
+
+ // this.submitHydra = page.locator('[name="provider"][value="hydra"]')
+ // this.forgotPasswordLink = page.locator(
+ // "[data-testid='forgot-password-link']",
+ // )
+ // this.logoutLink = page.locator("[data-testid='logout-link']")
+ }
+
+ async submitIdentifierFirst(identifier: string) {
+ await this.inputField("identifier").fill(identifier)
+ await this.submit("identifier_first", {
+ waitForURL: new RegExp(this.config.selfservice.flows.login.ui_url),
+ })
+ }
+
+ async loginWithPassword(
+ identifier: string,
+ password: string,
+ opts?: SubmitOptions,
+ ) {
+ switch (this.config.selfservice.flows.login.style) {
+ case LoginStyle.IdentifierFirst:
+ await this.submitIdentifierFirst(identifier)
+ break
+ case LoginStyle.Unified:
+ await this.inputField("identifier").fill(identifier)
+ break
+ }
+
+ await this.inputField("password").fill(password)
+ await this.submit("password", opts)
+ }
+
+ async triggerLoginWithCode(identifier: string, opts?: SubmitOptions) {
+ switch (this.config.selfservice.flows.login.style) {
+ case LoginStyle.IdentifierFirst:
+ await this.submitIdentifierFirst(identifier)
+ break
+ case LoginStyle.Unified:
+ await this.inputField("identifier").fill(identifier)
+ break
+ }
+
+ await this.codeSubmit.click()
+ }
+
+ async open({
+ aal,
+ refresh,
+ }: {
+ aal?: string
+ refresh?: boolean
+ } = {}) {
+ const p = new URLSearchParams()
+ if (refresh) {
+ p.append("refresh", "true")
+ }
+
+ if (aal) {
+ p.append("aal", aal)
+ }
+
+ await Promise.all([
+ this.page.goto(
+ this.config.selfservice.flows.login.ui_url + "?" + p.toString(),
+ ),
+ this.isReady(),
+ this.page.waitForURL((url) =>
+ url.toString().includes(this.config.selfservice.flows.login.ui_url),
+ ),
+ ])
+ await this.isReady()
+ }
+
+ async isReady() {
+ await expect(this.inputField("csrf_token").nth(0)).toBeHidden()
+ }
+
+ submitMethod(method: string) {
+ switch (method) {
+ case "google":
+ case "github":
+ case "hydra":
+ return this.page.locator(`[name="provider"][value="${method}"]`)
+ }
+ return this.page.locator(`[name="method"][value="${method}"]`)
+ }
+
+ inputField(name: string) {
+ return this.page.locator(`input[name=${name}]`)
+ }
+
+ async submit(method: string, opts?: SubmitOptions) {
+ const nav = opts?.waitForURL
+ ? this.page.waitForURL(opts.waitForURL)
+ : Promise.resolve()
+ if (opts?.submitWithKeyboard) {
+ await this.page.keyboard.press("Enter")
+ } else {
+ await this.submitMethod(method).click()
+ }
+
+ await nav
+ }
+
+ //
+ // async submitPasswordForm(
+ // id: string,
+ // password: string,
+ // expectURL: string | RegExp,
+ // options: {
+ // submitWithKeyboard?: boolean
+ // style?: LoginStyle
+ // } = {
+ // submitWithKeyboard: false,
+ // style: LoginStyle.OneStep,
+ // },
+ // ) {
+ // await this.isReady()
+ // await this.inputField("identifier").fill(id)
+ //
+ // if (options.style === LoginStyle.IdentifierFirst) {
+ // await this.submitMethod("identifier_first").click()
+ // await this.inputField("password").fill(password)
+ // } else {
+ // await this.inputField("password").fill(password)
+ // }
+ //
+ // const nav = this.page.waitForURL(expectURL)
+ //
+ // if (submitWithKeyboard) {
+ // await this.page.keyboard.press("Enter")
+ // } else {
+ // await this.submitPassword.click()
+ // }
+ //
+ // await nav
+ // }
+ //
+ // readonly baseURL: string
+ // readonly submitHydra: Locator
+ // readonly forgotPasswordLink: Locator
+ // readonly logoutLink: Locator
+ //
+ // async goto(returnTo?: string, refresh?: boolean) {
+ // const u = new URL(routes.hosted.login(this.baseURL))
+ // if (returnTo) {
+ // u.searchParams.append("return_to", returnTo)
+ // }
+ // if (refresh) {
+ // u.searchParams.append("refresh", refresh.toString())
+ // }
+ // await this.page.goto(u.toString())
+ // await this.isReady()
+ // }
+ //
+ // async loginWithHydra(email: string, password: string) {
+ // await this.submitHydra.click()
+ // await this.page.waitForURL(new RegExp(OIDC_PROVIDER))
+ //
+ // await this.page.locator("input[name=email]").fill(email)
+ // await this.page.locator("input[name=password]").fill(password)
+ //
+ // await this.page.locator("input[name=submit][id=accept]").click()
+ // }
+ //
+ // async loginWithOIDC(email = generateEmail(), password = generatePassword()) {
+ // await this.page.fill('[name="email"]', email)
+ // await this.page.fill('[name="password"]', password)
+ // await this.page.click("#accept")
+ // }
+ //
+ // async loginAndAcceptConsent(
+ // email = generateEmail(),
+ // password = generatePassword(),
+ // {rememberConsent = true, rememberLogin = false} = {},
+ // ) {
+ // await this.page.fill('[name="email"]', email)
+ // await this.page.fill('[name="password"]', password)
+ // rememberLogin && (await this.page.check('[name="remember"]'))
+ // await this.page.click("#accept")
+ //
+ // await this.page.click("#offline")
+ // await this.page.click("#openid")
+ // rememberConsent && (await this.page.check("[name=remember]"))
+ // await this.page.click("#accept")
+ //
+ // return email
+ // }
+ //
+ // async expectAlert(id: string) {
+ // await this.page.getByTestId(`ui/message/${id}`).waitFor()
+ // }
+}
diff --git a/test/e2e/playwright/selectors/input.ts b/test/e2e/playwright/selectors/input.ts
new file mode 100644
index 000000000000..c4f1f35d0271
--- /dev/null
+++ b/test/e2e/playwright/selectors/input.ts
@@ -0,0 +1,19 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { Locator, Page } from "@playwright/test"
+
+export interface InputLocator {
+ input: Locator
+ message: Locator
+ label: Locator
+}
+
+export const createInputLocator = (page: Page, field: string): InputLocator => {
+ const prefix = `[data-testid="node/input/${field}"]`
+ return {
+ input: page.locator(`${prefix} input`),
+ label: page.locator(`${prefix} label`),
+ message: page.locator(`${prefix} p`),
+ }
+}
diff --git a/test/e2e/playwright/setup/default_config.ts b/test/e2e/playwright/setup/default_config.ts
index b9249917b039..5e4f5d1e2e73 100644
--- a/test/e2e/playwright/setup/default_config.ts
+++ b/test/e2e/playwright/setup/default_config.ts
@@ -1,7 +1,7 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
-import { OryKratosConfiguration } from "../../cypress/support/config"
+import { OryKratosConfiguration } from "../../shared/config"
export const default_config: OryKratosConfiguration = {
dsn: "",
@@ -11,6 +11,10 @@ export const default_config: OryKratosConfiguration = {
id: "default",
url: "file://test/e2e/profiles/oidc/identity.traits.schema.json",
},
+ {
+ id: "email",
+ url: "file://test/e2e/profiles/email/identity.traits.schema.json",
+ },
],
},
serve: {
@@ -40,7 +44,7 @@ export const default_config: OryKratosConfiguration = {
cipher: ["secret-thirty-two-character-long"],
},
selfservice: {
- default_browser_return_url: "http://localhost:4455/",
+ default_browser_return_url: "http://localhost:4455/welcome",
allowed_return_urls: [
"http://localhost:4455",
"http://localhost:19006",
diff --git a/test/e2e/playwright/setup/global_setup.ts b/test/e2e/playwright/setup/global_setup.ts
deleted file mode 100644
index 92a42432fc96..000000000000
--- a/test/e2e/playwright/setup/global_setup.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright © 2023 Ory Corp
-// SPDX-License-Identifier: Apache-2.0
-
-import fs from "fs"
-import { default_config } from "./default_config"
-
-export default async function globalSetup() {
- await fs.promises.writeFile(
- "playwright/kratos.config.json",
- JSON.stringify(default_config),
- )
-}
diff --git a/test/e2e/playwright/tests/desktop/identifier_first/code.login.spec.ts b/test/e2e/playwright/tests/desktop/identifier_first/code.login.spec.ts
new file mode 100644
index 000000000000..b94dbf29669c
--- /dev/null
+++ b/test/e2e/playwright/tests/desktop/identifier_first/code.login.spec.ts
@@ -0,0 +1,224 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { expect } from "@playwright/test"
+import { search } from "../../../actions/mail"
+import { getSession, hasNoSession, hasSession } from "../../../actions/session"
+import { test } from "../../../fixtures"
+import { extractCode, toConfig } from "../../../lib/helper"
+import { LoginPage } from "../../../models/elements/login"
+
+test.describe.parallel("account enumeration protection off", () => {
+ test.use({
+ configOverride: toConfig({
+ style: "identifier_first",
+ mitigateEnumeration: false,
+ selfservice: {
+ methods: {
+ code: {
+ passwordless_enabled: true,
+ },
+ },
+ },
+ }),
+ })
+
+ test("login fails because user does not exist", async ({ page, config }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.submitIdentifierFirst("i@donot.exist")
+
+ await expect(
+ page.locator('[data-testid="ui/message/4000037"]'),
+ "expect account not exist message to be shown",
+ ).toBeVisible()
+ })
+
+ test("login with wrong code fails", async ({
+ page,
+ identity,
+ kratosPublicURL,
+ config,
+ }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.triggerLoginWithCode(identity.email)
+
+ await login.codeInput.input.fill("123123")
+
+ await login.codeSubmit.getByText("Continue").click()
+
+ await hasNoSession(page.request, kratosPublicURL)
+ await expect(
+ page.locator('[data-testid="ui/message/4010008"]'),
+ "expect to be shown a wrong code error",
+ ).toBeVisible()
+ })
+
+ test("login succeeds", async ({
+ page,
+ identity,
+ config,
+ kratosPublicURL,
+ }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.triggerLoginWithCode(identity.email)
+
+ const mails = await search({ query: identity.email, kind: "to" })
+ expect(mails).toHaveLength(1)
+
+ const code = extractCode(mails[0])
+
+ await login.codeInput.input.fill(code)
+
+ await login.codeSubmit.getByText("Continue").click()
+
+ await hasSession(page.request, kratosPublicURL)
+ })
+})
+
+test.describe("account enumeration protection on", () => {
+ test.use({
+ configOverride: toConfig({
+ style: "identifier_first",
+ mitigateEnumeration: true,
+ selfservice: {
+ methods: {
+ code: {
+ passwordless_enabled: true,
+ },
+ },
+ },
+ }),
+ })
+
+ test("login fails because user does not exist", async ({ page, config }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.submitIdentifierFirst("i@donot.exist")
+
+ await expect(
+ page.locator('button[name="method"][value="code"]'),
+ "expect to show the code form",
+ ).toBeVisible()
+ })
+
+ test("login with wrong code fails", async ({
+ page,
+ identity,
+ kratosPublicURL,
+ config,
+ }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.triggerLoginWithCode(identity.email)
+
+ await login.codeInput.input.fill("123123")
+
+ await login.codeSubmit.getByText("Continue").click()
+
+ await hasNoSession(page.request, kratosPublicURL)
+ await expect(
+ page.locator('[data-testid="ui/message/4010008"]'),
+ "expect to be shown a wrong code error",
+ ).toBeVisible()
+ })
+
+ test("login succeeds", async ({
+ page,
+ identity,
+ config,
+ kratosPublicURL,
+ }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.triggerLoginWithCode(identity.email)
+
+ const mails = await search({ query: identity.email, kind: "to" })
+ expect(mails).toHaveLength(1)
+
+ const code = extractCode(mails[0])
+
+ await login.codeInput.input.fill(code)
+
+ await login.codeSubmit.getByText("Continue").click()
+
+ await hasSession(page.request, kratosPublicURL)
+ })
+})
+
+test.describe(() => {
+ test.use({
+ configOverride: toConfig({
+ style: "identifier_first",
+ mitigateEnumeration: false,
+ selfservice: {
+ methods: {
+ code: {
+ passwordless_enabled: true,
+ },
+ },
+ },
+ }),
+ })
+ test("refresh", async ({ page, identity, config, kratosPublicURL }) => {
+ const login = new LoginPage(page, config)
+
+ const [initialSession, initialCode] =
+ await test.step("initial login", async () => {
+ await login.open()
+ await login.triggerLoginWithCode(identity.email)
+
+ const mails = await search({ query: identity.email, kind: "to" })
+ expect(mails).toHaveLength(1)
+
+ const code = extractCode(mails[0])
+
+ await login.codeInput.input.fill(code)
+
+ await login.codeSubmit.getByText("Continue").click()
+
+ const session = await getSession(page.request, kratosPublicURL)
+ expect(session).toBeDefined()
+ expect(session.active).toBe(true)
+ return [session, code]
+ })
+
+ await login.open({
+ refresh: true,
+ })
+ await login.inputField("identifier").fill(identity.email)
+ await login.submit("code")
+
+ const mails = await search({
+ query: identity.email,
+ kind: "to",
+ filter: (m) => !m.html.includes(initialCode),
+ })
+ expect(mails).toHaveLength(1)
+
+ const code = extractCode(mails[0])
+
+ await login.codeInput.input.fill(code)
+
+ await login.codeSubmit.getByText("Continue").click()
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+
+ const newSession = await getSession(page.request, kratosPublicURL)
+ expect(newSession).toBeDefined()
+ expect(newSession.active).toBe(true)
+
+ expect(initialSession.authenticated_at).not.toEqual(
+ newSession.authenticated_at,
+ )
+ })
+})
diff --git a/test/e2e/playwright/tests/desktop/identifier_first/oidc.login.spec.ts b/test/e2e/playwright/tests/desktop/identifier_first/oidc.login.spec.ts
new file mode 100644
index 000000000000..cf0a19d23a87
--- /dev/null
+++ b/test/e2e/playwright/tests/desktop/identifier_first/oidc.login.spec.ts
@@ -0,0 +1,162 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { faker } from "@faker-js/faker"
+import { expect, Page } from "@playwright/test"
+import { getSession, hasSession } from "../../../actions/session"
+import { test } from "../../../fixtures"
+import { toConfig } from "../../../lib/helper"
+import { LoginPage } from "../../../models/elements/login"
+import { OryKratosConfiguration } from "../../../../shared/config"
+
+async function loginHydra(page: Page) {
+ return test.step("login with hydra", async () => {
+ await page
+ .locator("input[name=username]")
+ .fill(faker.internet.email({ provider: "ory.sh" }))
+ await page.locator("button[name=action][value=accept]").click()
+ await page.locator("#offline").check()
+ await page.locator("#openid").check()
+
+ await page.locator("input[name=website]").fill(faker.internet.url())
+
+ await page.locator("button[name=action][value=accept]").click()
+ })
+}
+
+async function registerWithHydra(
+ page: Page,
+ config: OryKratosConfiguration,
+ kratosPublicURL: string,
+) {
+ return await test.step("register", async () => {
+ await page.goto("/registration")
+
+ await page.locator(`button[name=provider][value=hydra]`).click()
+
+ const email = faker.internet.email({ provider: "ory.sh" })
+ await page.locator("input[name=username]").fill(email)
+ await page.locator("#remember").check()
+ await page.locator("button[name=action][value=accept]").click()
+ await page.locator("#offline").check()
+ await page.locator("#openid").check()
+
+ await page.locator("input[name=website]").fill(faker.internet.url())
+
+ await page.locator("button[name=action][value=accept]").click()
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+ await page.context().clearCookies({
+ domain: new URL(kratosPublicURL).hostname,
+ })
+
+ await expect(
+ getSession(page.request, kratosPublicURL),
+ ).rejects.toThrowError()
+ return email
+ })
+}
+
+for (const mitigateEnumeration of [true, false]) {
+ test.describe(`account enumeration protection ${
+ mitigateEnumeration ? "on" : "off"
+ }`, () => {
+ test.use({
+ configOverride: toConfig({ mitigateEnumeration }),
+ })
+
+ test("login", async ({ page, config, kratosPublicURL }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await page.locator(`button[name=provider][value=hydra]`).click()
+
+ await loginHydra(page)
+
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+
+ await hasSession(page.request, kratosPublicURL)
+ })
+
+ test("oidc sign in on second step", async ({
+ page,
+ config,
+ kratosPublicURL,
+ }) => {
+ const email = await registerWithHydra(page, config, kratosPublicURL)
+
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.submitIdentifierFirst(email)
+
+ // If account enumeration is mitigated, we should see the password method,
+ // because the identity has not set up a password
+ await expect(
+ page.locator('button[name="method"][value="password"]'),
+ "hide the password method",
+ ).toBeVisible({ visible: mitigateEnumeration })
+
+ await page.locator(`button[name=provider][value=hydra]`).click()
+
+ await loginHydra(page)
+
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+
+ const session = await getSession(page.request, kratosPublicURL)
+ expect(session).toBeDefined()
+ expect(session.active).toBe(true)
+ })
+ })
+}
+
+test("login with refresh", async ({ page, config, kratosPublicURL }) => {
+ await registerWithHydra(page, config, kratosPublicURL)
+
+ const login = new LoginPage(page, config)
+
+ const initialSession = await test.step("initial login", async () => {
+ await login.open()
+ await page.locator(`button[name=provider][value=hydra]`).click()
+
+ await loginHydra(page)
+
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+ return await getSession(page.request, kratosPublicURL)
+ })
+
+ // This is required, because OIDC issues a new session on refresh (TODO), and MySQL does not store sub second timestamps, so we need to wait a bit
+ await page.waitForTimeout(1000)
+ await test.step("refresh login", async () => {
+ await login.open({
+ refresh: true,
+ })
+
+ await expect(
+ page.locator('[data-testid="ui/message/1010003"]'),
+ "show the refresh message",
+ ).toBeVisible()
+
+ await page.locator(`button[name=provider][value=hydra]`).click()
+
+ await loginHydra(page)
+
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+ const newSession = await getSession(page.request, kratosPublicURL)
+ // expect(newSession.authentication_methods).toHaveLength(
+ // initialSession.authentication_methods.length + 1,
+ // )
+ expect(newSession.authenticated_at).not.toBe(
+ initialSession.authenticated_at,
+ )
+ })
+})
diff --git a/test/e2e/playwright/tests/desktop/identifier_first/passkeys.login.spec.ts b/test/e2e/playwright/tests/desktop/identifier_first/passkeys.login.spec.ts
new file mode 100644
index 000000000000..ecd4fe0ceaa5
--- /dev/null
+++ b/test/e2e/playwright/tests/desktop/identifier_first/passkeys.login.spec.ts
@@ -0,0 +1,238 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { faker } from "@faker-js/faker"
+import { CDPSession, expect, Page } from "@playwright/test"
+import { OryKratosConfiguration } from "../../../../shared/config"
+import { getSession } from "../../../actions/session"
+import { test } from "../../../fixtures"
+import { toConfig } from "../../../lib/helper"
+import { LoginPage } from "../../../models/elements/login"
+
+async function toggleAutomaticPresenceSimulation(
+ cdpSession: CDPSession,
+ authenticatorId: string,
+ enabled: boolean,
+) {
+ await cdpSession.send("WebAuthn.setAutomaticPresenceSimulation", {
+ authenticatorId,
+ enabled,
+ })
+}
+
+async function registerWithPasskey(
+ page: Page,
+ pageCDPSession: CDPSession,
+ config: OryKratosConfiguration,
+ authenticatorId: string,
+ simulatePresence: boolean,
+) {
+ return await test.step("create webauthn identity", async () => {
+ await page.goto("/registration")
+ const identifier = faker.internet.email()
+ await page.locator(`input[name="traits.email"]`).fill(identifier)
+ await page
+ .locator(`input[name="traits.website"]`)
+ .fill(faker.internet.url())
+ await page.locator("button[name=method][value=profile]").click()
+
+ await toggleAutomaticPresenceSimulation(
+ pageCDPSession,
+ authenticatorId,
+ true,
+ )
+ await page.locator("button[name=passkey_register_trigger]").click()
+
+ await toggleAutomaticPresenceSimulation(
+ pageCDPSession,
+ authenticatorId,
+ simulatePresence,
+ )
+
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+ return identifier
+ })
+}
+
+const passkeyConfig = {
+ methods: {
+ passkey: {
+ enabled: true,
+ config: {
+ rp: {
+ display_name: "ORY",
+ id: "localhost",
+ origins: ["http://localhost:4455"],
+ },
+ },
+ },
+ },
+}
+
+for (const mitigateEnumeration of [true, false]) {
+ test.describe(`account enumeration protection ${
+ mitigateEnumeration ? "on" : "off"
+ }`, () => {
+ test.use({
+ configOverride: toConfig({
+ mitigateEnumeration,
+ style: "identifier_first",
+ selfservice: passkeyConfig,
+ }),
+ })
+
+ for (const simulatePresence of [true, false]) {
+ test.describe(`${
+ simulatePresence ? "with" : "without"
+ } automatic presence proof`, () => {
+ test.use({
+ virtualAuthenticatorOptions: {
+ automaticPresenceSimulation: simulatePresence,
+ // hasResidentKey: simulatePresence,
+ },
+ })
+ test("login", async ({
+ config,
+ page,
+ kratosPublicURL,
+ virtualAuthenticator,
+ pageCDPSession,
+ }) => {
+ const identifier = await registerWithPasskey(
+ page,
+ pageCDPSession,
+ config,
+ virtualAuthenticator.authenticatorId,
+ simulatePresence,
+ )
+ await page.context().clearCookies({})
+
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ if (!simulatePresence) {
+ await login.submitIdentifierFirst(identifier)
+
+ const passkeyLoginTrigger = page.locator(
+ "button[name=passkey_login_trigger]",
+ )
+ await passkeyLoginTrigger.waitFor()
+
+ await page.waitForLoadState("load")
+
+ await toggleAutomaticPresenceSimulation(
+ pageCDPSession,
+ virtualAuthenticator.authenticatorId,
+ true,
+ )
+
+ await passkeyLoginTrigger.click()
+
+ await toggleAutomaticPresenceSimulation(
+ pageCDPSession,
+ virtualAuthenticator.authenticatorId,
+ false,
+ )
+ }
+
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+
+ await expect(
+ getSession(page.request, kratosPublicURL),
+ ).resolves.toMatchObject({
+ active: true,
+ identity: {
+ traits: {
+ email: identifier,
+ },
+ },
+ })
+ })
+ })
+ }
+ })
+}
+
+test.describe("without automatic presence simulation", () => {
+ test.use({
+ virtualAuthenticatorOptions: {
+ automaticPresenceSimulation: false,
+ },
+ configOverride: toConfig({
+ selfservice: passkeyConfig,
+ }),
+ })
+ test("login with refresh", async ({
+ page,
+ config,
+ kratosPublicURL,
+ pageCDPSession,
+ virtualAuthenticator,
+ }) => {
+ const identifier = await registerWithPasskey(
+ page,
+ pageCDPSession,
+ config,
+ virtualAuthenticator.authenticatorId,
+ true,
+ )
+
+ const login = new LoginPage(page, config)
+ // Due to resetting automatic presence simulating to "true" in the previous step,
+ // opening the login page automatically triggers the passkey login
+ await login.open()
+
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+
+ await expect(
+ getSession(page.request, kratosPublicURL),
+ ).resolves.toMatchObject({
+ active: true,
+ identity: {
+ traits: {
+ email: identifier,
+ },
+ },
+ })
+
+ await login.open({
+ refresh: true,
+ })
+
+ await expect(
+ page.locator('[data-testid="ui/message/1010003"]'),
+ "show the refresh message",
+ ).toBeVisible()
+
+ const initialSession = await getSession(page.request, kratosPublicURL)
+
+ const passkeyLoginTrigger = page.locator(
+ "button[name=passkey_login_trigger]",
+ )
+ await passkeyLoginTrigger.waitFor()
+
+ await page.waitForLoadState("load")
+
+ await toggleAutomaticPresenceSimulation(
+ pageCDPSession,
+ virtualAuthenticator.authenticatorId,
+ true,
+ )
+
+ await passkeyLoginTrigger.click()
+ await page.waitForURL(
+ new RegExp(config.selfservice.default_browser_return_url),
+ )
+ const newSession = await getSession(page.request, kratosPublicURL)
+
+ expect(newSession.authentication_methods).toHaveLength(
+ initialSession.authentication_methods.length + 1,
+ )
+ })
+})
diff --git a/test/e2e/playwright/tests/desktop/identifier_first/password.login.spec.ts b/test/e2e/playwright/tests/desktop/identifier_first/password.login.spec.ts
new file mode 100644
index 000000000000..982c2ead22d6
--- /dev/null
+++ b/test/e2e/playwright/tests/desktop/identifier_first/password.login.spec.ts
@@ -0,0 +1,219 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { expect } from "@playwright/test"
+import { loginWithPassword } from "../../../actions/login"
+import { getSession, hasNoSession, hasSession } from "../../../actions/session"
+import { test } from "../../../fixtures"
+import { toConfig } from "../../../lib/helper"
+import { LoginPage } from "../../../models/elements/login"
+
+// These can run in parallel because they use the same config.
+test.describe("account enumeration protection off", () => {
+ test.use({
+ configOverride: toConfig({
+ style: "identifier_first",
+ mitigateEnumeration: false,
+ }),
+ })
+
+ test.describe.configure({ mode: "parallel" })
+
+ test("login fails because user does not exist", async ({ page, config }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.submitIdentifierFirst("i@donot.exist")
+
+ await expect(
+ page.locator('[data-testid="ui/message/4000037"]'),
+ "expect account not exist message to be shown",
+ ).toBeVisible()
+ })
+
+ test("login with wrong password fails", async ({
+ page,
+ identity,
+ kratosPublicURL,
+ config,
+ }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.loginWithPassword(identity.email, "wrong-password")
+ await login.isReady()
+
+ await hasNoSession(page.request, kratosPublicURL)
+ await expect(
+ page.locator('[data-testid="ui/message/4000006"]'),
+ "expect to be shown a credentials do not exist error",
+ ).toBeVisible()
+ })
+
+ test("login succeeds", async ({
+ page,
+ identity,
+ config,
+ kratosPublicURL,
+ }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.inputField("identifier").fill(identity.email)
+ await login.submit("identifier_first", {
+ waitForURL: new RegExp(config.selfservice.flows.login.ui_url),
+ })
+
+ await login.inputField("password").fill(identity.password)
+ await login.submit("password", {
+ waitForURL: new RegExp(config.selfservice.default_browser_return_url),
+ })
+
+ await hasSession(page.request, kratosPublicURL)
+ })
+
+ test("login with refresh", async ({
+ page,
+ config,
+ identity,
+ kratosPublicURL,
+ }) => {
+ await loginWithPassword(
+ {
+ password: identity.password,
+ traits: {
+ email: identity.email,
+ },
+ },
+ page.request,
+ kratosPublicURL,
+ )
+
+ const login = new LoginPage(page, config)
+ await login.open({
+ refresh: true,
+ })
+
+ await expect(
+ page.locator('[data-testid="ui/message/1010003"]'),
+ "show the refresh message",
+ ).toBeVisible()
+
+ const initialSession = await getSession(page.request, kratosPublicURL)
+ await login.inputField("password").fill(identity.password)
+ await login.submit("password", {
+ waitForURL: new RegExp(config.selfservice.default_browser_return_url),
+ })
+
+ const newSession = await getSession(page.request, kratosPublicURL)
+
+ expect(newSession.authentication_methods).toHaveLength(
+ initialSession.authentication_methods.length + 1,
+ )
+ })
+})
+
+test.describe("account enumeration protection on", () => {
+ test.use({
+ configOverride: toConfig({
+ style: "identifier_first",
+ mitigateEnumeration: true,
+ }),
+ })
+
+ test.describe.configure({ mode: "parallel" })
+ test("login fails because user does not exist", async ({ page, config }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.submitIdentifierFirst("i@donot.exist")
+
+ await expect(
+ page.locator('button[name="method"][value="password"]'),
+ "expect to show the password form",
+ ).toBeVisible()
+ })
+
+ test("login with wrong password fails", async ({
+ page,
+ identity,
+ kratosPublicURL,
+ config,
+ }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.loginWithPassword(identity.email, "wrong-password")
+ await login.isReady()
+
+ await hasNoSession(page.request, kratosPublicURL)
+ await expect(
+ page.locator('[data-testid="ui/message/4000006"]'),
+ "expect to be shown a credentials do not exist error",
+ ).toBeVisible()
+ })
+
+ test("login succeeds", async ({
+ page,
+ // projectFrontendClient,
+ identity,
+ config,
+ kratosPublicURL,
+ }) => {
+ const login = new LoginPage(page, config)
+ await login.open()
+
+ await login.inputField("identifier").fill(identity.email)
+ await login.submit("identifier_first", {
+ waitForURL: new RegExp(config.selfservice.flows.login.ui_url),
+ })
+
+ await login.inputField("password").fill(identity.password)
+ await login.submit("password", {
+ waitForURL: new RegExp(config.selfservice.default_browser_return_url),
+ })
+
+ await hasSession(page.request, kratosPublicURL)
+ })
+
+ test("login with refresh", async ({
+ page,
+ config,
+ identity,
+ kratosPublicURL,
+ }) => {
+ await loginWithPassword(
+ {
+ password: identity.password,
+ traits: {
+ email: identity.email,
+ },
+ },
+ page.request,
+ kratosPublicURL,
+ )
+
+ const login = new LoginPage(page, config)
+ await login.open({
+ refresh: true,
+ })
+
+ await expect(
+ page.locator('[data-testid="ui/message/1010003"]'),
+ "show the refresh message",
+ ).toBeVisible()
+
+ const initialSession = await getSession(page.request, kratosPublicURL)
+
+ await login.inputField("password").fill(identity.password)
+ await login.submit("password", {
+ waitForURL: new RegExp(config.selfservice.default_browser_return_url),
+ })
+
+ const newSession = await getSession(page.request, kratosPublicURL)
+
+ expect(newSession.authentication_methods).toHaveLength(
+ initialSession.authentication_methods.length + 1,
+ )
+ })
+})
diff --git a/test/e2e/playwright/tests/app_login.spec.ts b/test/e2e/playwright/tests/mobile/app_login.spec.ts
similarity index 97%
rename from test/e2e/playwright/tests/app_login.spec.ts
rename to test/e2e/playwright/tests/mobile/app_login.spec.ts
index 600a49a48ce1..327fbbf3e54a 100644
--- a/test/e2e/playwright/tests/app_login.spec.ts
+++ b/test/e2e/playwright/tests/mobile/app_login.spec.ts
@@ -1,7 +1,8 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
-import { test, expect, Page } from "@playwright/test"
+import { expect, Page } from "@playwright/test"
+import { test } from "../../fixtures"
test.describe.configure({ mode: "parallel" })
diff --git a/test/e2e/playwright/tests/app_recovery.spec.ts b/test/e2e/playwright/tests/mobile/app_recovery.spec.ts
similarity index 78%
rename from test/e2e/playwright/tests/app_recovery.spec.ts
rename to test/e2e/playwright/tests/mobile/app_recovery.spec.ts
index 629abd3c05bc..c065e0965443 100644
--- a/test/e2e/playwright/tests/app_recovery.spec.ts
+++ b/test/e2e/playwright/tests/mobile/app_recovery.spec.ts
@@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
import { expect } from "@playwright/test"
-import { test } from "../fixtures"
-import { search } from "../actions/mail"
-import { extractCode } from "../lib/helper"
+import { test } from "../../fixtures"
+import { search } from "../../actions/mail"
+import { extractCode } from "../../lib/helper"
const schemaConfig = {
default_schema_id: "email",
@@ -31,11 +31,11 @@ test.describe("Recovery", () => {
test("succeeds with a valid email address", async ({ page, identity }) => {
await page.goto("/Recovery")
- await page.getByTestId("email").fill(identity.traits.email)
+ await page.getByTestId("email").fill(identity.email)
await page.getByTestId("submit-form").click()
await expect(page.getByTestId("ui/message/1060003")).toBeVisible()
- const mails = await search(identity.traits.email, "to")
+ const mails = await search({ query: identity.email, kind: "to" })
expect(mails).toHaveLength(1)
const code = extractCode(mails[0])
@@ -43,13 +43,13 @@ test.describe("Recovery", () => {
await test.step("enter wrong code", async () => {
await page.getByTestId("code").fill(wrongCode)
- await page.getByText("Submit").click()
+ await page.getByText("Continue").click()
await expect(page.getByTestId("ui/message/4060006")).toBeVisible()
})
await test.step("enter correct code", async () => {
await page.getByTestId("code").fill(code)
- await page.getByText("Submit").click()
+ await page.getByText("Continue").click()
await page.waitForURL(/Settings/)
await expect(page.getByTestId("ui/message/1060001").first()).toBeVisible()
})
@@ -58,13 +58,13 @@ test.describe("Recovery", () => {
test("wrong email address does not get sent", async ({ page, identity }) => {
await page.goto("/Recovery")
- const wrongEmailAddress = "wrong-" + identity.traits.email
+ const wrongEmailAddress = "wrong-" + identity.email
await page.getByTestId("email").fill(wrongEmailAddress)
await page.getByTestId("submit-form").click()
await expect(page.getByTestId("ui/message/1060003")).toBeVisible()
try {
- await search(identity.traits.email, "to")
+ await search({ query: identity.email, kind: "to" })
expect(false).toBeTruthy()
} catch (e) {
// this is expected
@@ -74,11 +74,11 @@ test.describe("Recovery", () => {
test("fails with an invalid code", async ({ page, identity }) => {
await page.goto("/Recovery")
- await page.getByTestId("email").fill(identity.traits.email)
+ await page.getByTestId("email").fill(identity.email)
await page.getByTestId("submit-form").click()
await page.getByTestId("ui/message/1060003").isVisible()
- const mails = await search(identity.traits.email, "to")
+ const mails = await search({ query: identity.email, kind: "to" })
expect(mails).toHaveLength(1)
const code = extractCode(mails[0])
@@ -87,14 +87,14 @@ test.describe("Recovery", () => {
await test.step("enter wrong repeatedly", async () => {
for (let i = 0; i < 10; i++) {
await page.getByTestId("code").fill(wrongCode)
- await page.getByText("Submit", { exact: true }).click()
+ await page.getByText("Continue", { exact: true }).click()
await expect(page.getByTestId("ui/message/4060006")).toBeVisible()
}
})
await test.step("enter correct code fails", async () => {
await page.getByTestId("code").fill(code)
- await page.getByText("Submit", { exact: true }).click()
+ await page.getByText("Continue", { exact: true }).click()
await expect(page.getByTestId("ui/message/4060006")).toBeVisible()
})
})
@@ -123,17 +123,17 @@ test.describe("Recovery", () => {
test("fails with an expired code", async ({ page, identity }) => {
await page.goto("/Recovery")
- await page.getByTestId("email").fill(identity.traits.email)
+ await page.getByTestId("email").fill(identity.email)
await page.getByTestId("submit-form").click()
await page.getByTestId("ui/message/1060003").isVisible()
- const mails = await search(identity.traits.email, "to")
+ const mails = await search({ query: identity.email, kind: "to" })
expect(mails).toHaveLength(1)
const code = extractCode(mails[0])
await page.getByTestId("code").fill(code)
- await page.getByText("Submit", { exact: true }).click()
+ await page.getByText("Continue", { exact: true }).click()
await expect(page.getByTestId("email")).toBeVisible()
})
})
diff --git a/test/e2e/playwright/types/index.ts b/test/e2e/playwright/types/index.ts
new file mode 100644
index 000000000000..ddb3431774c0
--- /dev/null
+++ b/test/e2e/playwright/types/index.ts
@@ -0,0 +1,10 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+import { APIResponse } from "playwright-core"
+import { Session } from "@ory/kratos-client"
+
+export type SessionWithResponse = {
+ session: Session
+ response: APIResponse
+}
diff --git a/test/e2e/profiles/code/.kratos.yml b/test/e2e/profiles/code/.kratos.yml
index 3e98857e1628..a98e4979e730 100644
--- a/test/e2e/profiles/code/.kratos.yml
+++ b/test/e2e/profiles/code/.kratos.yml
@@ -38,7 +38,6 @@ selfservice:
enabled: true
code:
passwordless_enabled: true
- passwordless_login_fallback_enabled: false
enabled: true
config:
lifespan: 1h
diff --git a/test/e2e/profiles/mobile/.kratos.yml b/test/e2e/profiles/mobile/.kratos.yml
index c0a46e57c197..ab927737337d 100644
--- a/test/e2e/profiles/mobile/.kratos.yml
+++ b/test/e2e/profiles/mobile/.kratos.yml
@@ -19,7 +19,6 @@ selfservice:
verification:
enabled: false
-
methods:
totp:
enabled: true
diff --git a/test/e2e/profiles/two-steps/.kratos.yml b/test/e2e/profiles/two-steps/.kratos.yml
index d23dd0bce07c..f5271792c0fc 100644
--- a/test/e2e/profiles/two-steps/.kratos.yml
+++ b/test/e2e/profiles/two-steps/.kratos.yml
@@ -66,7 +66,6 @@ selfservice:
code:
enabled: true
passwordless_enabled: true
- passwordless_login_fallback_enabled: false
config:
lifespan: 1h
diff --git a/test/e2e/render-kratos-config.sh b/test/e2e/render-kratos-config.sh
index 5867904216e2..b1895ecd882c 100755
--- a/test/e2e/render-kratos-config.sh
+++ b/test/e2e/render-kratos-config.sh
@@ -10,8 +10,8 @@ ory_x_version="$(cd $dir/../..; go list -f '{{.Version}}' -m github.com/ory/x)"
curl -s https://raw.githubusercontent.com/ory/x/$ory_x_version/otelx/config.schema.json > $dir/.tracing-config.schema.json
-(cd $dir; sed "s!ory://tracing-config!.tracing-config.schema.json!g;" $dir/../../embedx/config.schema.json | npx json2ts --strictIndexSignatures > $dir/cypress/support/config.d.ts)
+(cd $dir; sed "s!ory://tracing-config!.tracing-config.schema.json!g;" $dir/../../embedx/config.schema.json | npx json2ts --strictIndexSignatures > $dir/shared/config.d.ts)
rm $dir/.tracing-config.schema.json
-make format
+(cd $dir/../..; make format)
diff --git a/test/e2e/cypress/support/config.d.ts b/test/e2e/shared/config.d.ts
similarity index 88%
rename from test/e2e/cypress/support/config.d.ts
rename to test/e2e/shared/config.d.ts
index c7e9742aed39..9f3d60368bc7 100644
--- a/test/e2e/cypress/support/config.d.ts
+++ b/test/e2e/shared/config.d.ts
@@ -53,10 +53,18 @@ export type ProvideLoginHintsOnFailedRegistration = boolean
* URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).
*/
export type RegistrationUIURL = string
+/**
+ * Two-step registration is a significantly improved sign up flow and recommended when using more than one sign up methods. To revert to one-step registration, set this to `true`.
+ */
+export type DisableTwoStepRegistration = boolean
/**
* URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).
*/
export type LoginUIURL = string
+/**
+ * The style of the login flow. If set to `unified` the login flow will be a one-step process. If set to `identifier_first` (experimental!) the login flow will first ask for the identifier and then the credentials.
+ */
+export type LoginFlowStyle = "unified" | "identifier_first"
/**
* If set to true will enable [Email and Phone Verification and Account Activation](https://www.ory.sh/kratos/docs/self-service/flows/verify-email-account-activation/).
*/
@@ -110,14 +118,6 @@ export type EnablesLinkMethod = boolean
export type OverrideTheBaseURLWhichShouldBeUsedAsTheBaseForRecoveryAndVerificationLinks =
string
export type HowLongALinkIsValidFor = string
-export type EnablesLoginAndRegistrationWithTheCodeMethod = boolean
-export type EnablesLoginFlowsCodeMethodToFulfilMFARequests = boolean
-/**
- * This setting allows the code method to always login a user with code if they have registered with another authentication method such as password or social sign in.
- */
-export type PasswordlessLoginFallbackEnabled = boolean
-export type EnablesCodeMethod = boolean
-export type HowLongACodeIsValidFor = string
export type EnablesUsernameEmailAndPasswordMethod = boolean
/**
* Allows changing the default HIBP host to a self hosted version.
@@ -143,6 +143,16 @@ export type MinimumPasswordLength = number
* If set to false the password validation does not check for similarity between the password and the user identifier.
*/
export type EnablePasswordIdentifierSimilarityCheck = boolean
+/**
+ * If set to true will enable password migration.
+ */
+export type EnablePasswordMigration = boolean
+/**
+ * Define which auth mechanism the Web-Hook should use
+ */
+export type AuthMechanisms =
+ | WebHookAuthApiKeyProperties
+ | WebHookAuthBasicAuthProperties
export type EnablesTheTOTPMethod = boolean
/**
* The issuer (e.g. a domain name) will be shown in the TOTP app (e.g. Google Authenticator). It helps the user differentiate between different codes.
@@ -178,6 +188,19 @@ export type RelyingPartyRPConfig =
origins: string[]
[k: string]: unknown | undefined
}
+export type EnablesThePasskeyMethod = boolean
+/**
+ * A name to help the user identify this RP.
+ */
+export type RelyingPartyDisplayName = string
+/**
+ * The id must be a subset of the domain currently in the browser.
+ */
+export type RelyingPartyIdentifier = string
+/**
+ * A list of explicit RP origins. If left empty, this defaults to either `origin` or `id`, prepended with the current protocol schema (HTTP or HTTPS).
+ */
+export type RelyingPartyOrigins = string[]
export type EnablesOpenIDConnectMethod = boolean
/**
* Can be used to modify the base URL for OAuth2 Redirect URLs. If unset, the Public Base URL will be used.
@@ -202,6 +225,7 @@ export type SelfServiceOIDCProvider = SelfServiceOIDCProvider1 & {
requested_claims?: OpenIDConnectClaims
organization_id?: OrganizationID
additional_id_token_audiences?: AdditionalClientIdsAllowedWhenUsingIDTokenSubmission
+ claims_source?: ClaimsSource
}
export type SelfServiceOIDCProvider1 = {
[k: string]: unknown | undefined
@@ -209,7 +233,7 @@ export type SelfServiceOIDCProvider1 = {
[k: string]: unknown | undefined
}
/**
- * Can be one of github, github-app, gitlab, generic, google, microsoft, discord, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon.
+ * Can be one of github, github-app, gitlab, generic, google, microsoft, discord, salesforce, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon.
*/
export type Provider =
| "github"
@@ -219,6 +243,7 @@ export type Provider =
| "google"
| "microsoft"
| "discord"
+ | "salesforce"
| "slack"
| "facebook"
| "auth0"
@@ -230,7 +255,9 @@ export type Provider =
| "dingtalk"
| "patreon"
| "linkedin"
+ | "linkedin_v2"
| "lark"
+ | "x"
export type OptionalStringWhichWillBeUsedWhenGeneratingLabelsForUIButtons =
string
/**
@@ -262,6 +289,10 @@ export type ApplePrivateKey = string
*/
export type OrganizationID = string
export type AdditionalClientIdsAllowedWhenUsingIDTokenSubmission = string[]
+/**
+ * Can be either `userinfo` (calls the userinfo endpoint to get the claims) or `id_token` (takes the claims from the id token). It defaults to `id_token`
+ */
+export type ClaimsSource = "id_token" | "userinfo"
/**
* A list and configuration of OAuth2 and OpenID Connect providers Ory Kratos should integrate with.
*/
@@ -297,7 +328,7 @@ export type HTTPAddressOfAPIEndpoint = string
/**
* Define which auth mechanism to use for auth with the HTTP email provider
*/
-export type AuthMechanisms =
+export type AuthMechanisms1 =
| WebHookAuthApiKeyProperties
| WebHookAuthBasicAuthProperties
/**
@@ -335,7 +366,7 @@ export type HTTPAddressOfAPIEndpoint1 = string
/**
* Define which auth mechanism to use for auth with the SMS provider
*/
-export type AuthMechanisms1 =
+export type AuthMechanisms2 =
| WebHookAuthApiKeyProperties
| WebHookAuthBasicAuthProperties
/**
@@ -517,14 +548,26 @@ export type AddExemptURLsToPrivateIPRanges = string[]
* If enabled allows Ory Sessions to be cached. Only effective in the Ory Network.
*/
export type EnableOrySessionsCaching = boolean
+/**
+ * Set how long Ory Sessions are cached on the edge. If unset, the session expiry will be used. Only effective in the Ory Network.
+ */
+export type SetOrySessionEdgeCachingMaximumAge = string
/**
* If enabled allows new flow transitions using `continue_with` items.
*/
export type EnableNewFlowTransitionsUsingContinueWithItems = boolean
/**
- * Secifies which organizations are available. Only effective in the Ory Network.
+ * If enabled allows faster session extension by skipping the session lookup. Disabling this feature will be deprecated in the future.
+ */
+export type EnableFasterSessionExtension = boolean
+/**
+ * Please use selfservice.methods.b2b instead. This key will be removed. Only effective in the Ory Network.
*/
export type Organizations = unknown[]
+/**
+ * A fallback URL template used when looking up identity schemas.
+ */
+export type FallbackURLTemplateForIdentitySchemas = string
export interface OryKratosConfiguration2 {
selfservice: {
@@ -551,10 +594,12 @@ export interface OryKratosConfiguration2 {
lifespan?: string
before?: SelfServiceBeforeRegistration
after?: SelfServiceAfterRegistration
+ enable_legacy_one_step?: DisableTwoStepRegistration
}
login?: {
ui_url?: LoginUIURL
lifespan?: string
+ style?: LoginFlowStyle
before?: SelfServiceBeforeLogin
after?: SelfServiceAfterLogin
}
@@ -565,6 +610,7 @@ export interface OryKratosConfiguration2 {
}
}
methods?: {
+ b2b?: SingleSignOnForB2B
profile?: {
enabled?: EnablesProfileManagementMethod
}
@@ -572,13 +618,22 @@ export interface OryKratosConfiguration2 {
enabled?: EnablesLinkMethod
config?: LinkConfiguration
}
- code?: {
- passwordless_enabled?: EnablesLoginAndRegistrationWithTheCodeMethod
- mfa_enabled?: EnablesLoginFlowsCodeMethodToFulfilMFARequests
- passwordless_login_fallback_enabled?: PasswordlessLoginFallbackEnabled
- enabled?: EnablesCodeMethod
- config?: CodeConfiguration
- }
+ code?:
+ | {
+ passwordless_enabled?: true
+ mfa_enabled?: false
+ [k: string]: unknown | undefined
+ }
+ | {
+ mfa_enabled?: true
+ passwordless_enabled?: false
+ [k: string]: unknown | undefined
+ }
+ | {
+ mfa_enabled?: false
+ passwordless_enabled?: false
+ [k: string]: unknown | undefined
+ }
password?: {
enabled?: EnablesUsernameEmailAndPasswordMethod
config?: PasswordConfiguration
@@ -594,6 +649,10 @@ export interface OryKratosConfiguration2 {
enabled?: EnablesTheWebAuthnMethod
config?: WebAuthnConfiguration
}
+ passkey?: {
+ enabled?: EnablesThePasskeyMethod
+ config?: PasskeyConfiguration
+ }
oidc?: SpecifyOpenIDConnectAndOAuth2Configuration
}
}
@@ -701,6 +760,16 @@ export interface OryKratosConfiguration2 {
}
earliest_possible_extend?: EarliestPossibleSessionExtension
}
+ security?: {
+ account_enumeration?: {
+ /**
+ * Mitigate account enumeration by making it harder to figure out if an identifier (email, phone number) exists or not. Enabling this setting degrades user experience. This setting does not mitigate all possible attack vectors yet.
+ */
+ mitigate?: boolean
+ [k: string]: unknown | undefined
+ }
+ [k: string]: unknown | undefined
+ }
version?: TheKratosVersionThisConfigIsWrittenFor
dev?: boolean
help?: boolean
@@ -720,6 +789,7 @@ export interface OryKratosConfiguration2 {
clients?: GlobalOutgoingNetworkSettings
feature_flags?: FeatureFlags
organizations?: Organizations
+ enterprise?: EnterpriseFeatures
}
export interface SelfServiceAfterSettings {
default_browser_return_url?: RedirectBrowsersToSetURLPerDefault
@@ -727,6 +797,7 @@ export interface SelfServiceAfterSettings {
totp?: SelfServiceAfterSettingsAuthMethod
oidc?: SelfServiceAfterSettingsAuthMethod
webauthn?: SelfServiceAfterSettingsAuthMethod
+ passkey?: SelfServiceAfterSettingsAuthMethod
lookup_secret?: SelfServiceAfterSettingsAuthMethod
profile?: SelfServiceAfterSettingsMethod
hooks?: SelfServiceHooks
@@ -762,6 +833,7 @@ export interface SelfServiceAfterRegistration {
default_browser_return_url?: RedirectBrowsersToSetURLPerDefault
password?: SelfServiceAfterRegistrationMethod
webauthn?: SelfServiceAfterRegistrationMethod
+ passkey?: SelfServiceAfterRegistrationMethod
oidc?: SelfServiceAfterRegistrationMethod
code?: SelfServiceAfterRegistrationMethod
hooks?: SelfServiceHooks
@@ -788,6 +860,7 @@ export interface SelfServiceAfterLogin {
default_browser_return_url?: RedirectBrowsersToSetURLPerDefault
password?: SelfServiceAfterDefaultLoginMethod
webauthn?: SelfServiceAfterDefaultLoginMethod
+ passkey?: SelfServiceAfterDefaultLoginMethod
oidc?: SelfServiceAfterOIDCLoginMethod
code?: SelfServiceAfterDefaultLoginMethod
totp?: SelfServiceAfterDefaultLoginMethod
@@ -796,6 +869,8 @@ export interface SelfServiceAfterLogin {
| SelfServiceWebHook
| SelfServiceSessionRevokerHook
| SelfServiceRequireVerifiedAddressHook
+ | SelfServiceVerificationHook
+ | SelfServiceShowVerificationUIHook
| B2BSSOHook
)[]
}
@@ -805,11 +880,16 @@ export interface SelfServiceAfterDefaultLoginMethod {
| SelfServiceSessionRevokerHook
| SelfServiceRequireVerifiedAddressHook
| SelfServiceWebHook
+ | SelfServiceVerificationHook
+ | SelfServiceShowVerificationUIHook
)[]
}
export interface SelfServiceRequireVerifiedAddressHook {
hook: "require_verified_address"
}
+export interface SelfServiceVerificationHook {
+ hook: "verification"
+}
export interface SelfServiceAfterOIDCLoginMethod {
default_browser_return_url?: RedirectBrowsersToSetURLPerDefault
hooks?: (
@@ -851,6 +931,25 @@ export interface SelfServiceAfterRecovery {
export interface SelfServiceBeforeRecovery {
hooks?: SelfServiceHooks
}
+/**
+ * Single Sign-On for B2B allows your customers to bring their own (workforce) identity server (e.g. OneLogin). This feature is not available in the open source licensed code.
+ */
+export interface SingleSignOnForB2B {
+ config?: {
+ organizations?: {
+ /**
+ * The ID of the organization.
+ */
+ id?: string
+ /**
+ * The label of the organization.
+ */
+ label?: string
+ domains?: string[]
+ [k: string]: unknown | undefined
+ }[]
+ }
+}
/**
* Additional configuration for the link strategy.
*/
@@ -859,13 +958,6 @@ export interface LinkConfiguration {
lifespan?: HowLongALinkIsValidFor
[k: string]: unknown | undefined
}
-/**
- * Additional configuration for the code strategy.
- */
-export interface CodeConfiguration {
- lifespan?: HowLongACodeIsValidFor
- [k: string]: unknown | undefined
-}
/**
* Define how passwords are validated.
*/
@@ -876,6 +968,61 @@ export interface PasswordConfiguration {
ignore_network_errors?: IgnoreLookupNetworkErrors
min_password_length?: MinimumPasswordLength
identifier_similarity_check_enabled?: EnablePasswordIdentifierSimilarityCheck
+ migrate_hook?: {
+ enabled?: EnablePasswordMigration
+ config?: {
+ /**
+ * The URL the password migration hook should call
+ */
+ url?: string
+ /**
+ * The HTTP method to use (GET, POST, etc).
+ */
+ method?: "POST"
+ /**
+ * The HTTP headers that must be applied to the password migration hook.
+ */
+ headers?: {
+ [k: string]: string | undefined
+ }
+ /**
+ * Emit tracing events for this hook on delivery or error
+ */
+ emit_analytics_event?: boolean
+ auth?: AuthMechanisms
+ additionalProperties?: false
+ }
+ }
+}
+export interface WebHookAuthApiKeyProperties {
+ type: "api_key"
+ config: {
+ /**
+ * The name of the api key
+ */
+ name: string
+ /**
+ * The value of the api key
+ */
+ value: string
+ /**
+ * How the api key should be transferred
+ */
+ in: "header" | "cookie"
+ }
+}
+export interface WebHookAuthBasicAuthProperties {
+ type: "basic_auth"
+ config: {
+ /**
+ * user name for basic auth
+ */
+ user: string
+ /**
+ * password for basic auth
+ */
+ password: string
+ }
}
export interface TOTPConfiguration {
issuer?: TOTPIssuer
@@ -884,6 +1031,15 @@ export interface WebAuthnConfiguration {
passwordless?: UseForPasswordlessFlows
rp?: RelyingPartyRPConfig
}
+export interface PasskeyConfiguration {
+ rp?: RelyingPartyRPConfig1
+}
+export interface RelyingPartyRPConfig1 {
+ display_name: RelyingPartyDisplayName
+ id: RelyingPartyIdentifier
+ origins?: RelyingPartyOrigins
+ [k: string]: unknown | undefined
+}
export interface SpecifyOpenIDConnectAndOAuth2Configuration {
enabled?: EnablesOpenIDConnectMethod
config?: {
@@ -963,6 +1119,7 @@ export interface CourierConfiguration {
login_code?: {
valid?: {
email: EmailCourierTemplate
+ sms?: SmsCourierTemplate
}
}
}
@@ -1043,44 +1200,14 @@ export interface HttpRequestConfig {
* URI pointing to the jsonnet template used for payload generation. Only used for those HTTP methods, which support HTTP body payloads
*/
body?: string
- auth?: AuthMechanisms
+ auth?: AuthMechanisms1
additionalProperties?: false
}
-export interface WebHookAuthApiKeyProperties {
- type: "api_key"
- config: {
- /**
- * The name of the api key
- */
- name: string
- /**
- * The value of the api key
- */
- value: string
- /**
- * How the api key should be transferred
- */
- in: "header" | "cookie"
- }
-}
-export interface WebHookAuthBasicAuthProperties {
- type: "basic_auth"
- config: {
- /**
- * user name for basic auth
- */
- user: string
- /**
- * password for basic auth
- */
- password: string
- }
-}
/**
* Configures outgoing emails using the SMTP protocol.
*/
export interface SMTPConfiguration {
- connection_uri: SMTPConnectionString
+ connection_uri?: SMTPConnectionString
client_cert_path?: SMTPClientCertificatePath
client_key_path?: SMTPClientPrivateKeyPath
from_address?: SMTPSenderAddress
@@ -1119,7 +1246,7 @@ export interface SMSSenderConfiguration {
* URI pointing to the jsonnet template used for payload generation. Only used for those HTTP methods, which support HTTP body payloads
*/
body?: string
- auth?: AuthMechanisms1
+ auth?: AuthMechanisms2
additionalProperties?: false
}
}
@@ -1375,5 +1502,13 @@ export interface GlobalHTTPClientConfiguration {
}
export interface FeatureFlags {
cacheable_sessions?: EnableOrySessionsCaching
+ cacheable_sessions_max_age?: SetOrySessionEdgeCachingMaximumAge
use_continue_with_transitions?: EnableNewFlowTransitionsUsingContinueWithItems
+ faster_session_extend?: EnableFasterSessionExtension
+}
+/**
+ * Specifies enterprise features. Only effective in the Ory Network or with a valid license.
+ */
+export interface EnterpriseFeatures {
+ identity_schema_fallback_url_template?: FallbackURLTemplateForIdentitySchemas
}
diff --git a/text/id.go b/text/id.go
index a466caec0f8c..edff417a2738 100644
--- a/text/id.go
+++ b/text/id.go
@@ -31,6 +31,7 @@ const (
InfoSelfServiceLoginCodeMFA // 1010019
InfoSelfServiceLoginCodeMFAHint // 1010020
InfoSelfServiceLoginPasskey // 1010021
+ InfoSelfServiceLoginPassword // 1010022
)
const (
@@ -86,21 +87,21 @@ const (
)
const (
- InfoNodeLabel ID = 1070000 + iota // 1070000
- InfoNodeLabelInputPassword // 1070001
- InfoNodeLabelGenerated // 1070002
- InfoNodeLabelSave // 1070003
- InfoNodeLabelID // 1070004
- InfoNodeLabelSubmit // 1070005
- InfoNodeLabelVerifyOTP // 1070006
- InfoNodeLabelEmail // 1070007
- InfoNodeLabelResendOTP // 1070008
- InfoNodeLabelContinue // 1070009
- InfoNodeLabelRecoveryCode // 1070010
- InfoNodeLabelVerificationCode // 1070011
- InfoNodeLabelRegistrationCode // 1070012
- InfoNodeLabelLoginCode // 1070013
- InfoNodeLabelLoginAndLinkCredential
+ InfoNodeLabel ID = 1070000 + iota // 1070000
+ InfoNodeLabelInputPassword // 1070001
+ InfoNodeLabelGenerated // 1070002
+ InfoNodeLabelSave // 1070003
+ InfoNodeLabelID // 1070004
+ InfoNodeLabelSubmit // 1070005
+ InfoNodeLabelVerifyOTP // 1070006
+ InfoNodeLabelEmail // 1070007
+ InfoNodeLabelResendOTP // 1070008
+ InfoNodeLabelContinue // 1070009
+ InfoNodeLabelRecoveryCode // 1070010
+ InfoNodeLabelVerificationCode // 1070011
+ InfoNodeLabelRegistrationCode // 1070012
+ InfoNodeLabelLoginCode // 1070013
+ InfoNodeLabelLoginAndLinkCredential // 1070014
)
const (
@@ -148,6 +149,7 @@ const (
ErrorValidationPasswordTooManyBreaches
ErrorValidationNoCodeUser
ErrorValidationTraitsMismatch
+ ErrorValidationAccountNotFound
)
const (
diff --git a/text/message_login.go b/text/message_login.go
index ec627458a028..9312a21e97a2 100644
--- a/text/message_login.go
+++ b/text/message_login.go
@@ -89,6 +89,14 @@ func NewInfoLoginTOTP() *Message {
}
}
+func NewInfoLoginPassword() *Message {
+ return &Message{
+ ID: InfoSelfServiceLoginPassword,
+ Text: "Sign in with password",
+ Type: Info,
+ }
+}
+
func NewInfoLoginLookup() *Message {
return &Message{
ID: InfoLoginLookup,
@@ -182,7 +190,7 @@ func NewErrorValidationVerificationNoStrategyFound() *Message {
func NewInfoSelfServiceLoginWebAuthn() *Message {
return &Message{
ID: InfoSelfServiceLoginWebAuthn,
- Text: "Use security key",
+ Text: "Sign in with hardware key",
Type: Info,
}
}
@@ -198,7 +206,7 @@ func NewInfoSelfServiceLoginPasskey() *Message {
func NewInfoSelfServiceContinueLoginWebAuthn() *Message {
return &Message{
ID: InfoSelfServiceLoginContinueWebAuthn,
- Text: "Continue with security key",
+ Text: "Sign in with hardware key",
Type: Info,
}
}
@@ -239,7 +247,7 @@ func NewInfoSelfServiceLoginCode() *Message {
return &Message{
ID: InfoSelfServiceLoginCode,
Type: Info,
- Text: "Sign in with code",
+ Text: "Send sign in code",
}
}
diff --git a/text/message_validation.go b/text/message_validation.go
index c10fddead805..2fd1e4c2d28d 100644
--- a/text/message_validation.go
+++ b/text/message_validation.go
@@ -257,6 +257,14 @@ func NewErrorValidationInvalidCredentials() *Message {
}
}
+func NewErrorValidationAccountNotFound() *Message {
+ return &Message{
+ ID: ErrorValidationAccountNotFound,
+ Text: "This account does not exist or has no login method configured.",
+ Type: Error,
+ }
+}
+
func NewErrorValidationDuplicateCredentials() *Message {
return &Message{
ID: ErrorValidationDuplicateCredentials,
diff --git a/ui/node/attributes.go b/ui/node/attributes.go
index 9611b5828dff..6ad55d46a0e2 100644
--- a/ui/node/attributes.go
+++ b/ui/node/attributes.go
@@ -3,7 +3,12 @@
package node
-import "github.com/ory/kratos/text"
+import (
+ "fmt"
+
+ "github.com/ory/kratos/text"
+ "github.com/ory/kratos/x/webauthnx/js"
+)
const (
InputAttributeTypeText UiNodeInputAttributeType = "text"
@@ -53,6 +58,9 @@ type Attributes interface {
// swagger:ignore
GetNodeType() UiNodeType
+
+ // swagger:ignore
+ Matches(other Attributes) bool
}
// InputAttributes represents the attributes of an input node
@@ -91,12 +99,29 @@ type InputAttributes struct {
// OnClick may contain javascript which should be executed on click. This is primarily
// used for WebAuthn.
+ //
+ // Deprecated: Using OnClick requires the use of eval() which is a security risk. Use OnClickTrigger instead.
OnClick string `json:"onclick,omitempty"`
+ // OnClickTrigger may contain a WebAuthn trigger which should be executed on click.
+ //
+ // The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.
+ OnClickTrigger js.WebAuthnTriggers `json:"onclickTrigger,omitempty"`
+
// OnLoad may contain javascript which should be executed on load. This is primarily
// used for WebAuthn.
+ //
+ // Deprecated: Using OnLoad requires the use of eval() which is a security risk. Use OnLoadTrigger instead.
OnLoad string `json:"onload,omitempty"`
+ // OnLoadTrigger may contain a WebAuthn trigger which should be executed on load.
+ //
+ // The trigger maps to a JavaScript function provided by Ory, which triggers actions such as PassKey registration or login.
+ OnLoadTrigger js.WebAuthnTriggers `json:"onloadTrigger,omitempty"`
+
+ // MaxLength may contain the input's maximum length.
+ MaxLength int `json:"maxlength,omitempty"`
+
// NodeType represents this node's types. It is a mirror of `node.type` and
// is primarily used to allow compatibility with OpenAPI 3.0. In this struct it technically always is "input".
//
@@ -267,6 +292,99 @@ func (a *ScriptAttributes) ID() string {
return a.Identifier
}
+func (a *InputAttributes) Matches(other Attributes) bool {
+ ot, ok := other.(*InputAttributes)
+ if !ok {
+ return false
+ }
+
+ if len(ot.ID()) > 0 && a.ID() != ot.ID() {
+ return false
+ }
+
+ if len(ot.Type) > 0 && a.Type != ot.Type {
+ return false
+ }
+
+ if ot.FieldValue != nil && fmt.Sprintf("%v", a.FieldValue) != fmt.Sprintf("%v", ot.FieldValue) {
+ return false
+ }
+
+ if len(ot.Name) > 0 && a.Name != ot.Name {
+ return false
+ }
+
+ return true
+}
+
+func (a *ImageAttributes) Matches(other Attributes) bool {
+ ot, ok := other.(*ImageAttributes)
+ if !ok {
+ return false
+ }
+
+ if len(ot.ID()) > 0 && a.ID() != ot.ID() {
+ return false
+ }
+
+ if len(ot.Source) > 0 && a.Source != ot.Source {
+ return false
+ }
+
+ return true
+}
+
+func (a *AnchorAttributes) Matches(other Attributes) bool {
+ ot, ok := other.(*AnchorAttributes)
+ if !ok {
+ return false
+ }
+
+ if len(ot.ID()) > 0 && a.ID() != ot.ID() {
+ return false
+ }
+
+ if len(ot.HREF) > 0 && a.HREF != ot.HREF {
+ return false
+ }
+
+ return true
+}
+
+func (a *TextAttributes) Matches(other Attributes) bool {
+ ot, ok := other.(*TextAttributes)
+ if !ok {
+ return false
+ }
+
+ if len(ot.ID()) > 0 && a.ID() != ot.ID() {
+ return false
+ }
+
+ return true
+}
+
+func (a *ScriptAttributes) Matches(other Attributes) bool {
+ ot, ok := other.(*ScriptAttributes)
+ if !ok {
+ return false
+ }
+
+ if len(ot.ID()) > 0 && a.ID() != ot.ID() {
+ return false
+ }
+
+ if ot.Type != "" && a.Type != ot.Type {
+ return false
+ }
+
+ if ot.Source != "" && a.Source != ot.Source {
+ return false
+ }
+
+ return true
+}
+
func (a *InputAttributes) SetValue(value interface{}) {
a.FieldValue = value
}
diff --git a/ui/node/attributes_input.go b/ui/node/attributes_input.go
index b63ac9365a51..176c1a25b42e 100644
--- a/ui/node/attributes_input.go
+++ b/ui/node/attributes_input.go
@@ -38,6 +38,12 @@ func WithRequiredInputAttribute(a *InputAttributes) {
a.Required = true
}
+func WithMaxLengthInputAttribute(maxLength int) func(a *InputAttributes) {
+ return func(a *InputAttributes) {
+ a.MaxLength = maxLength
+ }
+}
+
func WithInputAttributes(f func(a *InputAttributes)) func(a *InputAttributes) {
return func(a *InputAttributes) {
f(a)
diff --git a/ui/node/attributes_test.go b/ui/node/attributes_test.go
index 218919e1145e..62c2316d3c9f 100644
--- a/ui/node/attributes_test.go
+++ b/ui/node/attributes_test.go
@@ -21,6 +21,70 @@ func TestIDs(t *testing.T) {
assert.EqualValues(t, "foo", (&ScriptAttributes{Identifier: "foo"}).ID())
}
+func TestMatchesAnchorAttributes(t *testing.T) {
+ assert.True(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&AnchorAttributes{Identifier: "foo"}))
+ assert.True(t, (&AnchorAttributes{HREF: "bar"}).Matches(&AnchorAttributes{HREF: "bar"}))
+ assert.False(t, (&AnchorAttributes{HREF: "foo"}).Matches(&AnchorAttributes{HREF: "bar"}))
+ assert.False(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&AnchorAttributes{HREF: "bar"}))
+
+ assert.True(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "foo", HREF: "bar"}))
+ assert.False(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "foo", HREF: "baz"}))
+ assert.False(t, (&AnchorAttributes{Identifier: "foo", HREF: "bar"}).Matches(&AnchorAttributes{Identifier: "bar", HREF: "bar"}))
+
+ assert.False(t, (&AnchorAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"}))
+}
+
+func TestMatchesImageAttributes(t *testing.T) {
+ assert.True(t, (&ImageAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Identifier: "foo"}))
+ assert.True(t, (&ImageAttributes{Source: "bar"}).Matches(&ImageAttributes{Source: "bar"}))
+ assert.False(t, (&ImageAttributes{Source: "foo"}).Matches(&ImageAttributes{Source: "bar"}))
+ assert.False(t, (&ImageAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Source: "bar"}))
+
+ assert.True(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "foo", Source: "bar"}))
+ assert.False(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "foo", Source: "baz"}))
+ assert.False(t, (&ImageAttributes{Identifier: "foo", Source: "bar"}).Matches(&ImageAttributes{Identifier: "bar", Source: "bar"}))
+
+ assert.False(t, (&ImageAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"}))
+}
+
+func TestMatchesInputAttributes(t *testing.T) {
+ // Test when other is not of type *InputAttributes
+ var attr Attributes = &ImageAttributes{}
+ inputAttr := &InputAttributes{Name: "foo"}
+ assert.False(t, inputAttr.Matches(attr))
+
+ // Test when ID is different
+ attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText}
+ inputAttr = &InputAttributes{Name: "bar", Type: InputAttributeTypeText}
+ assert.False(t, inputAttr.Matches(attr))
+
+ // Test when Type is different
+ attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText}
+ inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeNumber}
+ assert.False(t, inputAttr.Matches(attr))
+
+ // Test when FieldValue is different
+ attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"}
+ inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "baz"}
+ assert.False(t, inputAttr.Matches(attr))
+
+ // Test when Name is different
+ attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText}
+ inputAttr = &InputAttributes{Name: "bar", Type: InputAttributeTypeText}
+ assert.False(t, inputAttr.Matches(attr))
+
+ // Test when all fields are the same
+ attr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"}
+ inputAttr = &InputAttributes{Name: "foo", Type: InputAttributeTypeText, FieldValue: "bar"}
+ assert.True(t, inputAttr.Matches(attr))
+}
+
+func TestMatchesTextAttributes(t *testing.T) {
+ assert.True(t, (&TextAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"}))
+ assert.True(t, (&TextAttributes{Identifier: "foo"}).Matches(&TextAttributes{Identifier: "foo"}))
+ assert.False(t, (&TextAttributes{Identifier: "foo"}).Matches(&ImageAttributes{Identifier: "foo"}))
+}
+
func TestNodeEncode(t *testing.T) {
script := jsonx.TestMarshalJSONString(t, &Node{Attributes: &ScriptAttributes{}})
assert.EqualValues(t, Script, gjson.Get(script, "attributes.node_type").String())
diff --git a/ui/node/node.go b/ui/node/node.go
index e08295b827f4..66e1b72fa8b6 100644
--- a/ui/node/node.go
+++ b/ui/node/node.go
@@ -39,16 +39,17 @@ func (t UiNodeType) String() string {
type UiNodeGroup string
const (
- DefaultGroup UiNodeGroup = "default"
- PasswordGroup UiNodeGroup = "password"
- OpenIDConnectGroup UiNodeGroup = "oidc"
- ProfileGroup UiNodeGroup = "profile"
- LinkGroup UiNodeGroup = "link"
- CodeGroup UiNodeGroup = "code"
- TOTPGroup UiNodeGroup = "totp"
- LookupGroup UiNodeGroup = "lookup_secret"
- WebAuthnGroup UiNodeGroup = "webauthn"
- PasskeyGroup UiNodeGroup = "passkey"
+ DefaultGroup UiNodeGroup = "default"
+ PasswordGroup UiNodeGroup = "password"
+ OpenIDConnectGroup UiNodeGroup = "oidc"
+ ProfileGroup UiNodeGroup = "profile"
+ LinkGroup UiNodeGroup = "link"
+ CodeGroup UiNodeGroup = "code"
+ TOTPGroup UiNodeGroup = "totp"
+ LookupGroup UiNodeGroup = "lookup_secret"
+ WebAuthnGroup UiNodeGroup = "webauthn"
+ PasskeyGroup UiNodeGroup = "passkey"
+ IdentifierFirstGroup UiNodeGroup = "identifier_first"
)
func (g UiNodeGroup) String() string {
@@ -218,6 +219,7 @@ func SortUseOrder(keysInOrder []string) func(*sortOptions) {
options.keysInOrder = keysInOrder
}
}
+
func SortUseOrderAppend(keysInOrder []string) func(*sortOptions) {
return func(options *sortOptions) {
options.keysInOrderAppend = keysInOrder
@@ -353,6 +355,37 @@ func (n *Nodes) Append(node *Node) {
*n = append(*n, node)
}
+func (n *Nodes) RemoveMatching(node *Node) {
+ if n == nil {
+ return
+ }
+
+ var r Nodes
+ for k, v := range *n {
+ if !(*n)[k].Matches(node) {
+ r = append(r, v)
+ }
+ }
+
+ *n = r
+}
+
+func (n *Node) Matches(needle *Node) bool {
+ if len(needle.ID()) > 0 && n.ID() != needle.ID() {
+ return false
+ }
+
+ if needle.Type != "" && n.Type != needle.Type {
+ return false
+ }
+
+ if needle.Group != "" && n.Group != needle.Group {
+ return false
+ }
+
+ return n.Attributes.Matches(needle.Attributes)
+}
+
func (n *Node) UnmarshalJSON(data []byte) error {
var attr Attributes
switch t := gjson.GetBytes(data, "type").String(); UiNodeType(t) {
diff --git a/ui/node/node_test.go b/ui/node/node_test.go
index f8867b98c2a3..e8a85bc9cd9f 100644
--- a/ui/node/node_test.go
+++ b/ui/node/node_test.go
@@ -11,6 +11,8 @@ import (
"path/filepath"
"testing"
+ "github.com/ory/kratos/text"
+
"github.com/ory/x/assertx"
"github.com/ory/kratos/corpx"
@@ -193,3 +195,64 @@ func TestNodeJSON(t *testing.T) {
require.EqualError(t, json.NewDecoder(bytes.NewReader(json.RawMessage(`{"type": "foo"}`))).Decode(&n), "unexpected node type: foo")
})
}
+
+func TestMatchesNode(t *testing.T) {
+ // Test when ID is different
+ node1 := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
+ node2 := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}}
+ assert.False(t, node1.Matches(node2))
+
+ // Test when Type is different
+ node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
+ node2 = &node.Node{Type: node.Text, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
+ assert.False(t, node1.Matches(node2))
+
+ // Test when Group is different
+ node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
+ node2 = &node.Node{Type: node.Input, Group: node.OpenIDConnectGroup, Attributes: &node.InputAttributes{Name: "foo"}}
+ assert.False(t, node1.Matches(node2))
+
+ // Test when all fields are the same
+ node1 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
+ node2 = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}}
+ assert.True(t, node1.Matches(node2))
+}
+
+func TestRemoveMatchingNodes(t *testing.T) {
+ nodes := node.Nodes{
+ &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "foo"}},
+ &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}},
+ &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "baz"}},
+ }
+
+ // Test when node to remove is present
+ nodeToRemove := &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "bar"}}
+ nodes.RemoveMatching(nodeToRemove)
+ assert.Len(t, nodes, 2)
+ for _, n := range nodes {
+ assert.NotEqual(t, nodeToRemove.ID(), n.ID())
+ }
+
+ // Test when node to remove is not present
+ nodeToRemove = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "qux"}}
+ nodes.RemoveMatching(nodeToRemove)
+ assert.Len(t, nodes, 2) // length should remain the same
+
+ // Test when node to remove is present
+ nodeToRemove = &node.Node{Type: node.Input, Group: node.PasswordGroup, Attributes: &node.InputAttributes{Name: "baz"}}
+ ui := &container.Container{
+ Nodes: nodes,
+ }
+
+ ui.GetNodes().RemoveMatching(nodeToRemove)
+ assert.Len(t, *ui.GetNodes(), 1)
+ for _, n := range *ui.GetNodes() {
+ assert.NotEqual(t, "bar", n.ID())
+ assert.NotEqual(t, "baz", n.ID())
+ }
+
+ ui.Nodes.Append(node.NewInputField("method", "foo", "bar", node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelContinue()))
+ assert.NotNil(t, ui.Nodes.Find("method"))
+ ui.GetNodes().RemoveMatching(node.NewInputField("method", "foo", "bar", node.InputAttributeTypeSubmit))
+ assert.Nil(t, ui.Nodes.Find("method"))
+}
diff --git a/x/redir_test.go b/x/redir_test.go
index bbb8417f8b91..1c4a191b429b 100644
--- a/x/redir_test.go
+++ b/x/redir_test.go
@@ -4,7 +4,6 @@
package x_test
import (
- "context"
"fmt"
"io"
"net/http"
@@ -12,6 +11,8 @@ import (
"strings"
"testing"
+ "github.com/ory/x/configx"
+
"github.com/julienschmidt/httprouter"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -22,17 +23,16 @@ import (
)
func TestRedirectToPublicAdminRoute(t *testing.T) {
- ctx := context.Background()
- conf, reg := internal.NewFastRegistryWithMocks(t)
pub := x.NewRouterPublic()
adm := x.NewRouterAdmin()
adminTS := httptest.NewServer(adm)
pubTS := httptest.NewServer(pub)
t.Cleanup(pubTS.Close)
t.Cleanup(adminTS.Close)
-
- conf.MustSet(ctx, config.ViperKeyAdminBaseURL, adminTS.URL)
- conf.MustSet(ctx, config.ViperKeyPublicBaseURL, pubTS.URL)
+ _, reg := internal.NewFastRegistryWithMocks(t, configx.WithValues(map[string]any{
+ config.ViperKeyAdminBaseURL: adminTS.URL,
+ config.ViperKeyPublicBaseURL: pubTS.URL,
+ }))
pub.POST("/privileged", x.RedirectToAdminRoute(reg))
pub.POST("/admin/privileged", x.RedirectToAdminRoute(reg))
diff --git a/x/webauthnx/js/trigger.go b/x/webauthnx/js/trigger.go
new file mode 100644
index 000000000000..7b236191ce8e
--- /dev/null
+++ b/x/webauthnx/js/trigger.go
@@ -0,0 +1,22 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package js
+
+import "fmt"
+
+// swagger:enum WebAuthnTriggers
+type WebAuthnTriggers string
+
+const (
+ WebAuthnTriggersWebAuthnRegistration WebAuthnTriggers = "oryWebAuthnRegistration"
+ WebAuthnTriggersWebAuthnLogin WebAuthnTriggers = "oryWebAuthnLogin"
+ WebAuthnTriggersPasskeyLogin WebAuthnTriggers = "oryPasskeyLogin"
+ WebAuthnTriggersPasskeyLoginAutocompleteInit WebAuthnTriggers = "oryPasskeyLoginAutocompleteInit"
+ WebAuthnTriggersPasskeyRegistration WebAuthnTriggers = "oryPasskeyRegistration"
+ WebAuthnTriggersPasskeySettingsRegistration WebAuthnTriggers = "oryPasskeySettingsRegistration"
+)
+
+func (r WebAuthnTriggers) String() string {
+ return fmt.Sprintf("window.%s", string(r))
+}
diff --git a/x/webauthnx/js/trigger_test.go b/x/webauthnx/js/trigger_test.go
new file mode 100644
index 000000000000..97f9dc00ee77
--- /dev/null
+++ b/x/webauthnx/js/trigger_test.go
@@ -0,0 +1,14 @@
+// Copyright © 2024 Ory Corp
+// SPDX-License-Identifier: Apache-2.0
+
+package js
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestToString(t *testing.T) {
+ assert.Equal(t, "window.oryWebAuthnRegistration", WebAuthnTriggersWebAuthnRegistration.String())
+}
diff --git a/x/webauthnx/js/webauthn.js b/x/webauthnx/js/webauthn.js
index 61a7cb8f976d..638bd4ece082 100644
--- a/x/webauthnx/js/webauthn.js
+++ b/x/webauthnx/js/webauthn.js
@@ -32,7 +32,7 @@
}
function __oryWebAuthnLogin(
- opt,
+ options,
resultQuerySelector = '*[name="webauthn_login"]',
triggerQuerySelector = '*[name="webauthn_login_trigger"]',
) {
@@ -40,6 +40,12 @@
alert("This browser does not support WebAuthn!")
}
+ const triggerEl = document.querySelector(triggerQuerySelector)
+ let opt = options
+ if (!opt) {
+ opt = JSON.parse(triggerEl.value)
+ }
+
opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge)
opt.publicKey.allowCredentials = opt.publicKey.allowCredentials.map(
function (value) {
@@ -71,7 +77,7 @@
},
})
- document.querySelector(triggerQuerySelector).closest("form").submit()
+ triggerEl.closest("form").submit()
})
.catch((err) => {
alert(err)
@@ -79,7 +85,7 @@
}
function __oryWebAuthnRegistration(
- opt,
+ options,
resultQuerySelector = '*[name="webauthn_register"]',
triggerQuerySelector = '*[name="webauthn_register_trigger"]',
) {
@@ -87,6 +93,12 @@
alert("This browser does not support WebAuthn!")
}
+ const triggerEl = document.querySelector(triggerQuerySelector)
+ let opt = options
+ if (!opt) {
+ opt = JSON.parse(triggerEl.value)
+ }
+
opt.publicKey.user.id = __oryWebAuthnBufferDecode(opt.publicKey.user.id)
opt.publicKey.challenge = __oryWebAuthnBufferDecode(opt.publicKey.challenge)
@@ -118,14 +130,14 @@
},
})
- document.querySelector(triggerQuerySelector).closest("form").submit()
+ triggerEl.closest("form").submit()
})
.catch((err) => {
alert(err)
})
}
- window.__oryPasskeyLoginAutocompleteInit = async function () {
+ async function __oryPasskeyLoginAutocompleteInit () {
const dataEl = document.getElementsByName("passkey_challenge")[0]
const resultEl = document.getElementsByName("passkey_login")[0]
const identifierEl = document.getElementsByName("identifier")[0]
@@ -195,7 +207,7 @@
})
}
- window.__oryPasskeyLogin = function () {
+ function __oryPasskeyLogin () {
const dataEl = document.getElementsByName("passkey_challenge")[0]
const resultEl = document.getElementsByName("passkey_login")[0]
@@ -262,7 +274,7 @@
})
}
- window.__oryPasskeyRegistration = function () {
+ function __oryPasskeyRegistration () {
const dataEl = document.getElementsByName("passkey_create_data")[0]
const resultEl = document.getElementsByName("passkey_register")[0]
@@ -373,8 +385,21 @@
})
}
+ // Deprecated naming with underscores - kept for support with Ory Elements v0
window.__oryWebAuthnLogin = __oryWebAuthnLogin
window.__oryWebAuthnRegistration = __oryWebAuthnRegistration
window.__oryPasskeySettingsRegistration = __oryPasskeySettingsRegistration
+ window.__oryPasskeyLogin = __oryPasskeyLogin
+ window.__oryPasskeyRegistration = __oryPasskeyRegistration
+ window.__oryPasskeyLoginAutocompleteInit = __oryPasskeyLoginAutocompleteInit
+
+ // Current naming - use with Ory Elements v1
+ window.oryWebAuthnLogin = __oryWebAuthnLogin
+ window.oryWebAuthnRegistration = __oryWebAuthnRegistration
+ window.oryPasskeySettingsRegistration = __oryPasskeySettingsRegistration
+ window.oryPasskeyLogin = __oryPasskeyLogin
+ window.oryPasskeyRegistration = __oryPasskeyRegistration
+ window.oryPasskeyLoginAutocompleteInit = __oryPasskeyLoginAutocompleteInit
+
window.__oryWebAuthnInitialized = true
})()
diff --git a/x/webauthnx/nodes.go b/x/webauthnx/nodes.go
index 76fac1c397cd..309f49f80e9d 100644
--- a/x/webauthnx/nodes.go
+++ b/x/webauthnx/nodes.go
@@ -10,6 +10,8 @@ import (
"fmt"
"net/url"
+ "github.com/ory/kratos/x/webauthnx/js"
+
"github.com/ory/x/stringsx"
"github.com/ory/x/urlx"
@@ -21,7 +23,10 @@ import (
func NewWebAuthnConnectionTrigger(options string) *node.Node {
return node.NewInputField(node.WebAuthnRegisterTrigger, "", node.WebAuthnGroup,
node.InputAttributeTypeButton, node.WithInputAttributes(func(a *node.InputAttributes) {
- a.OnClick = "window.__oryWebAuthnRegistration(" + options + ")"
+ //nolint:staticcheck
+ a.OnClick = fmt.Sprintf("%s(%s)", js.WebAuthnTriggersWebAuthnRegistration, options)
+ a.OnClickTrigger = js.WebAuthnTriggersWebAuthnRegistration
+ a.FieldValue = options
}))
}
@@ -44,7 +49,10 @@ func NewWebAuthnConnectionInput() *node.Node {
func NewWebAuthnLoginTrigger(options string) *node.Node {
return node.NewInputField(node.WebAuthnLoginTrigger, "", node.WebAuthnGroup,
node.InputAttributeTypeButton, node.WithInputAttributes(func(a *node.InputAttributes) {
- a.OnClick = "window.__oryWebAuthnLogin(" + options + ")"
+ //nolint:staticcheck
+ a.OnClick = fmt.Sprintf("%s(%s)", js.WebAuthnTriggersWebAuthnLogin, options)
+ a.FieldValue = options
+ a.OnClickTrigger = js.WebAuthnTriggersWebAuthnLogin
}))
}