Repository: ory/kratos
Pull request #2394 (2022-04-13 09:58:31)

feat: add support for netID provider 
<!--
Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request.

This text will be included in the changelog. If applicable, include links to documentation or pieces of code.
If your change includes breaking changes please add a codeblock documenting the breaking change:

```
BREAKING CHANGES: This patch changes the behavior of configuration item `foo` to do bar. To keep the existing
behavior please do baz.
```
-->

## Related issue(s)
Added support for NetID as a separate provider in Kratos config.
As discussed with @aeneasr added also screen record of the working flow.
I have also docs https://github.com/ory/docs/pull/755 describing how to set it up. 

Note: due to tech limitations of NetID redirect URIs for local testing I had to use https and fake domain `test.local` on my machine - which complicated a bit the local setup but that's a different stOry ;-) 
<!--
If this pull request

1. is a fix for a known bug, link the issue where the bug was reported in the format of `#1234`;
2. is a fix for a previously unknown bug, explain the bug and how to reproduce it in this pull request;
2. implements a new feature, link the issue containing the design document in the format of `#1234`;
3. improves the documentation, no issue reference is required.

Pull requests introducing new features, which do not have a design document linked are more likely to be rejected and take on average 2-8 weeks longer to
get merged.

You can discuss changes with maintainers either in the Github Discusssions in this repository or
join the [Ory Chat](https://www.ory.sh/chat).
-->

## Checklist

<!--
Put an `x` in the boxes that apply. You can also fill these out after creating the PR.

Please be aware that pull requests must have all boxes ticked in order to be merged.

If you're unsure about any of them, don't hesitate to ask. We're here to help!
-->

- [ ] I have read the [contributing guidelines](../blob/master/CONTRIBUTING.md).
- [ ] I have referenced an issue containing the design document if my change
      introduces a new feature.
- [ ] I am following the
      [contributing code guidelines](../blob/master/CONTRIBUTING.md#contributing-code).
- [ ] I have read the [security policy](../security/policy).
- [ ] I confirm that this pull request does not address a security
      vulnerability. If this pull request addresses a security. vulnerability, I
      confirm that I got green light (please contact
      [security@ory.sh](mailto:security@ory.sh)) from the maintainers to push
      the changes.
- [ ] I have added tests that prove my fix is effective or that my feature
      works.
- [ ] I have added or changed [the documentation](docs/docs).

## Further Comments

<!--
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution
you did and what alternatives you considered, etc...
-->
https://user-images.githubusercontent.com/17701416/163158221-150c9c74-7696-4b7a-b7c3-1967e9230e80.mp4


codecov[bot]: # [Codecov](https://codecov.io/gh/ory/kratos/pull/2394?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ory) Report
> Merging [#2394](https://codecov.io/gh/ory/kratos/pull/2394?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ory) (bfe5a2e) into [master](https://codecov.io/gh/ory/kratos/commit/aee2b1ed1189b57fcbb1aaa456444d5121be94b1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ory) (aee2b1e) will **decrease** coverage by `0.23%`.
> The diff coverage is `0.00%`.

```diff
@@            Coverage Diff             @@
##           master    #2394      +/-   ##
==========================================
- Coverage   76.56%   76.33%   -0.24%     
==========================================
  Files         318      319       +1     
  Lines       17298    17348      +50     
==========================================
- Hits        13244    13242       -2     
- Misses       3121     3172      +51     
- Partials      933      934       +1     
```


| [Impacted Files](https://codecov.io/gh/ory/kratos/pull/2394?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ory) | Coverage Δ | |
|---|---|---|
| [selfservice/strategy/oidc/provider\_config.go](https://codecov.io/gh/ory/kratos/pull/2394/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ory#diff-c2VsZnNlcnZpY2Uvc3RyYXRlZ3kvb2lkYy9wcm92aWRlcl9jb25maWcuZ28=) | `35.55% <0.00%> (-1.66%)` | :arrow_down: |
| [selfservice/strategy/oidc/provider\_netid.go](https://codecov.io/gh/ory/kratos/pull/2394/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ory#diff-c2VsZnNlcnZpY2Uvc3RyYXRlZ3kvb2lkYy9wcm92aWRlcl9uZXRpZC5nbw==) | `0.00% <0.00%> (ø)` | |
| [persistence/sql/persister\_courier.go](https://codecov.io/gh/ory/kratos/pull/2394/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ory#diff-cGVyc2lzdGVuY2Uvc3FsL3BlcnNpc3Rlcl9jb3VyaWVyLmdv) | `85.00% <0.00%> (-3.34%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/ory/kratos/pull/2394?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ory).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ory)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/ory/kratos/pull/2394?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ory). Last update [aee2b1e...bfe5a2e](https://codecov.io/gh/ory/kratos/pull/2394?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ory). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ory).

piotrmsc: @aeneasr I have added changes according to your comments, I also rebased with master, could you take a look?

Patch:
diff --git a/.schema/openapi/patches/schema.yaml b/.schema/openapi/patches/schema.yaml
index db683b55f..a06a4e495 100644
--- a/.schema/openapi/patches/schema.yaml
+++ b/.schema/openapi/patches/schema.yaml
@@ -8,8 +8,8 @@
     mapping:
       input: "#/components/schemas/uiNodeInputAttributes"
       text: "#/components/schemas/uiNodeTextAttributes"
-      image: "#/components/schemas/uiNodeImageAttributes"
-      anchor: "#/components/schemas/uiNodeAnchorAttributes"
+      img: "#/components/schemas/uiNodeImageAttributes"
+      a: "#/components/schemas/uiNodeAnchorAttributes"
       script: "#/components/schemas/uiNodeScriptAttributes"
 - op: add
   path: /components/schemas/uiNodeAttributes/oneOf
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 63d361711..4b0c00bde 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@
 <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
 **Table of Contents**
 
-- [ (2022-04-14)](#2022-04-14)
+- [ (2022-04-16)](#2022-04-16)
   - [Breaking Changes](#breaking-changes)
     - [Bug Fixes](#bug-fixes)
     - [Code Refactoring](#code-refactoring)
@@ -250,7 +250,7 @@
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
 
-# [](https://github.com/ory/kratos/compare/v0.9.0-alpha.3...v) (2022-04-14)
+# [](https://github.com/ory/kratos/compare/v0.9.0-alpha.3...v) (2022-04-16)
 ## Breaking Changes
 
 This patch moves several CLI command to comply with the Ory CLI command structure:
@@ -283,6 +283,7 @@ This patch moves several CLI command to comply with the Ory CLI command structur
 ### Bug Fixes
 
 * Add legacy session value ([ecfd052](https://github.com/ory/kratos/commit/ecfd05216f5ebb70f1617595d2d398cf1fa3c660)), closes [#2398](https://github.com/ory/kratos/issues/2398)
+* Avoid excessive memory allocations in HIBP cache ([#2389](https://github.com/ory/kratos/issues/2389)) ([ee2d410](https://github.com/ory/kratos/commit/ee2d41057a7e6cb2c57c6304c2e7bbf5ad7c56da)), closes [#2354](https://github.com/ory/kratos/issues/2354)
 * Change SQLite database mode to 0600 ([#2344](https://github.com/ory/kratos/issues/2344)) ([0e5d3b7](https://github.com/ory/kratos/commit/0e5d3b7726a8923fbc2a4c10ec18f0ba97ffbcff)):
 
     The default mode is 0644, which is allows broader access than necessary.
@@ -292,6 +293,7 @@ This patch moves several CLI command to comply with the Ory CLI command structur
 * **Dockerfile:** Use existing builder base image ([#2390](https://github.com/ory/kratos/issues/2390)) ([37de25a](https://github.com/ory/kratos/commit/37de25a541a24e03407ecf344fb750775e48c782))
 * Load return_to and append to errors ([#2333](https://github.com/ory/kratos/issues/2333)) ([5efe4a3](https://github.com/ory/kratos/commit/5efe4a33e35e74d248d4eec43dc901b7b6334037)), closes [#2275](https://github.com/ory/kratos/issues/2275) [#2279](https://github.com/ory/kratos/issues/2279) [#2285](https://github.com/ory/kratos/issues/2285)
 * Refresh is always false when session exists ([d3436d7](https://github.com/ory/kratos/commit/d3436d7fa17589d91e25c9f0bd66bc3bb5b150fa)), closes [#2341](https://github.com/ory/kratos/issues/2341)
+* **sdk:** Improved OpenAPI specifications for UI nodes ([#2375](https://github.com/ory/kratos/issues/2375)) ([a42a0f7](https://github.com/ory/kratos/commit/a42a0f772af3625c457032d6dcc34289a62acc61)), closes [#2357](https://github.com/ory/kratos/issues/2357)
 * Serve.admin.request_log.disable_for_health behaviour ([#2399](https://github.com/ory/kratos/issues/2399)) ([0a381fa](https://github.com/ory/kratos/commit/0a381fa3d702f77e614d0492dafa3ac2cd102c7e))
 * Use `path` instead of `filepath` to join http route paths ([16b1244](https://github.com/ory/kratos/commit/16b12449c841bf7a237fe436b884b4b5012cd022)), closes [#2292](https://github.com/ory/kratos/issues/2292)
 
diff --git a/Makefile b/Makefile
index 0359a659c..a1ab49a47 100644
--- a/Makefile
+++ b/Makefile
@@ -39,6 +39,14 @@ $(call make-lint-dependency)
 docs/cli:
 		go run ./cmd/clidoc/. .
 
+.PHONY: docs/api
+docs/api:
+		npx @redocly/openapi-cli preview-docs spec/api.json
+
+.PHONY: docs/swagger
+docs/swagger:
+		npx @redocly/openapi-cli preview-docs spec/swagger.json
+
 .bin/ory: Makefile
 		bash <(curl https://raw.githubusercontent.com/ory/meta/master/install.sh) -d -b .bin ory v0.1.14
 		touch -a -m .bin/ory
diff --git a/README.md b/README.md
index f9dd72b5c..999c5b716 100644
--- a/README.md
+++ b/README.md
@@ -578,3 +578,12 @@ To prepare documentation tests, run `npm i` to install
 
 - test all documentation: <code type="make/command">make test-docs</code>
 - test an individual file: <code type="npm/installed-executable">text-run</code>
+
+#### Preview API documentation
+
+- update the SDK including the OpenAPI specification:
+  <code type="make/command">make sdk</code>
+- run preview server for API documentation: <code type="make/command">make
+  docs/api</code>
+- run preview server for swagger documentation: <code type="make/command">make
+  docs/swagger</code>
diff --git a/driver/registry_default.go b/driver/registry_default.go
index 36d324a9b..c474d098f 100644
--- a/driver/registry_default.go
+++ b/driver/registry_default.go
@@ -414,7 +414,11 @@ func (m *RegistryDefault) Hasher() hash.Hasher {
 
 func (m *RegistryDefault) PasswordValidator() password2.Validator {
 	if m.passwordValidator == nil {
-		m.passwordValidator = password2.NewDefaultPasswordValidatorStrategy(m)
+		var err error
+		m.passwordValidator, err = password2.NewDefaultPasswordValidatorStrategy(m)
+		if err != nil {
+			m.Logger().WithError(err).Fatal("could not initialize DefaultPasswordValidator")
+		}
 	}
 	return m.passwordValidator
 }
diff --git a/embedx/config.schema.json b/embedx/config.schema.json
index d9b3129d9..270936e4b 100644
--- a/embedx/config.schema.json
+++ b/embedx/config.schema.json
@@ -339,7 +339,8 @@
             "vk",
             "yandex",
             "apple",
-            "spotify"
+            "spotify",
+            "netid"
           ],
           "examples": [
             "google"
diff --git a/go.mod b/go.mod
index 4cf33e840..b2feabd10 100644
--- a/go.mod
+++ b/go.mod
@@ -31,6 +31,7 @@ require (
 	github.com/cortesi/modd v0.0.0-20210323234521-b35eddab86cc
 	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.0
 	github.com/duo-labs/webauthn v0.0.0-20220330035159-03696f3d4499
 	github.com/fatih/color v1.13.0
 	github.com/form3tech-oss/jwt-go v3.2.3+incompatible
diff --git a/internal/httpclient/api/openapi.yaml b/internal/httpclient/api/openapi.yaml
index bd5633433..4a3feb650 100644
--- a/internal/httpclient/api/openapi.yaml
+++ b/internal/httpclient/api/openapi.yaml
@@ -3461,8 +3461,8 @@ components:
               id: 0
               text: text
               type: type
-            type: type
-            group: group
+            type: text
+            group: default
           - meta:
               label:
                 context: '{}'
@@ -3478,8 +3478,8 @@ components:
               id: 0
               text: text
               type: type
-            type: type
-            group: group
+            type: text
+            group: default
           method: method
           action: action
           messages:
@@ -3593,8 +3593,8 @@ components:
               id: 0
               text: text
               type: type
-            type: type
-            group: group
+            type: text
+            group: default
           - meta:
               label:
                 context: '{}'
@@ -3610,8 +3610,8 @@ components:
               id: 0
               text: text
               type: type
-            type: type
-            group: group
+            type: text
+            group: default
           method: method
           action: action
           messages:
@@ -3670,6 +3670,7 @@ components:
       - issued_at
       - request_url
       - state
+      - type
       - ui
       title: A Recovery Flow
       type: object
@@ -3727,8 +3728,8 @@ components:
               id: 0
               text: text
               type: type
-            type: type
-            group: group
+            type: text
+            group: default
           - meta:
               label:
                 context: '{}'
@@ -3744,8 +3745,8 @@ components:
               id: 0
               text: text
               type: type
-            type: type
-            group: group
+            type: text
+            group: default
           method: method
           action: action
           messages:
@@ -3797,6 +3798,7 @@ components:
       - id
       - issued_at
       - request_url
+      - type
       - ui
       type: object
     selfServiceSettingsFlow:
@@ -3824,8 +3826,8 @@ components:
               id: 0
               text: text
               type: type
-            type: type
-            group: group
+            type: text
+            group: default
           - meta:
               label:
                 context: '{}'
@@ -3841,8 +3843,8 @@ components:
               id: 0
               text: text
               type: type
-            type: type
-            group: group
+            type: text
+            group: default
           method: method
           action: action
           messages:
@@ -3949,6 +3951,7 @@ components:
       - issued_at
       - request_url
       - state
+      - type
       - ui
       title: Flow represents a Settings Flow
       type: object
@@ -3988,8 +3991,8 @@ components:
               id: 0
               text: text
               type: type
-            type: type
-            group: group
+            type: text
+            group: default
           - meta:
               label:
                 context: '{}'
@@ -4005,8 +4008,8 @@ components:
               id: 0
               text: text
               type: type
-            type: type
-            group: group
+            type: text
+            group: default
           method: method
           action: action
           messages:
@@ -4964,8 +4967,8 @@ components:
             id: 0
             text: text
             type: type
-          type: type
-          group: group
+          type: text
+          group: default
         - meta:
             label:
               context: '{}'
@@ -4981,8 +4984,8 @@ components:
             id: 0
             text: text
             type: type
-          type: type
-          group: group
+          type: text
+          group: default
         method: method
         action: action
         messages:
@@ -5035,12 +5038,23 @@ components:
           id: 0
           text: text
           type: type
-        type: type
-        group: group
+        type: text
+        group: default
       properties:
         attributes:
           $ref: '#/components/schemas/uiNodeAttributes'
         group:
+          description: Group specifies which group (e.g. password authenticator) this
+            node belongs to.
+          enum:
+          - default
+          - password
+          - oidc
+          - profile
+          - link
+          - totp
+          - lookup_secret
+          - webauthn
           type: string
         messages:
           items:
@@ -5049,6 +5063,13 @@ components:
         meta:
           $ref: '#/components/schemas/uiNodeMeta'
         type:
+          description: The node's type
+          enum:
+          - text
+          - input
+          - img
+          - a
+          - script
           type: string
       required:
       - attributes
@@ -5070,6 +5091,15 @@ components:
           description: A unique identifier
           type: string
         node_type:
+          description: |-
+            NodeType represents this node's types. It is a mirror of `node.type` and
+            is primarily used to allow compatibility with OpenAPI 3.0.
+          enum:
+          - text
+          - input
+          - img
+          - a
+          - script
           type: string
         title:
           $ref: '#/components/schemas/uiText'
@@ -5083,8 +5113,8 @@ components:
     uiNodeAttributes:
       discriminator:
         mapping:
-          anchor: '#/components/schemas/uiNodeAnchorAttributes'
-          image: '#/components/schemas/uiNodeImageAttributes'
+          a: '#/components/schemas/uiNodeAnchorAttributes'
+          img: '#/components/schemas/uiNodeImageAttributes'
           input: '#/components/schemas/uiNodeInputAttributes'
           script: '#/components/schemas/uiNodeScriptAttributes'
           text: '#/components/schemas/uiNodeTextAttributes'
@@ -5096,8 +5126,6 @@ components:
       - $ref: '#/components/schemas/uiNodeAnchorAttributes'
       - $ref: '#/components/schemas/uiNodeScriptAttributes'
       title: Attributes represents a list of attributes (e.g. `href="foo"` for links).
-    uiNodeGroup:
-      type: string
     uiNodeImageAttributes:
       properties:
         height:
@@ -5108,6 +5136,15 @@ components:
           description: A unique identifier
           type: string
         node_type:
+          description: |-
+            NodeType represents this node's types. It is a mirror of `node.type` and
+            is primarily used to allow compatibility with OpenAPI 3.0.
+          enum:
+          - text
+          - input
+          - img
+          - a
+          - script
           type: string
         src:
           description: |-
@@ -5141,6 +5178,15 @@ components:
           description: The input's element name.
           type: string
         node_type:
+          description: |-
+            NodeType represents this node's types. It is a mirror of `node.type` and
+            is primarily used to allow compatibility with OpenAPI 3.0.
+          enum:
+          - text
+          - input
+          - img
+          - a
+          - script
           type: string
         onclick:
           description: |-
@@ -5194,6 +5240,15 @@ components:
           description: The script's integrity hash
           type: string
         node_type:
+          description: |-
+            NodeType represents this node's types. It is a mirror of `node.type` and
+            is primarily used to allow compatibility with OpenAPI 3.0.
+          enum:
+          - text
+          - input
+          - img
+          - a
+          - script
           type: string
         nonce:
           description: |-
@@ -5230,6 +5285,15 @@ components:
           description: A unique identifier
           type: string
         node_type:
+          description: |-
+            NodeType represents this node's types. It is a mirror of `node.type` and
+            is primarily used to allow compatibility with OpenAPI 3.0.
+          enum:
+          - text
+          - input
+          - img
+          - a
+          - script
           type: string
         text:
           $ref: '#/components/schemas/uiText'
@@ -5239,8 +5303,6 @@ components:
       - text
       title: TextAttributes represents the attributes of a text node.
       type: object
-    uiNodeType:
-      type: string
     uiNodes:
       items:
         $ref: '#/components/schemas/uiNode'
diff --git a/internal/httpclient/docs/SelfServiceRecoveryFlow.md b/internal/httpclient/docs/SelfServiceRecoveryFlow.md
index d42afbf4c..68d613914 100644
--- a/internal/httpclient/docs/SelfServiceRecoveryFlow.md
+++ b/internal/httpclient/docs/SelfServiceRecoveryFlow.md
@@ -11,14 +11,14 @@ Name | Type | Description | Notes
 **RequestUrl** | **string** | RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL&#39;s path or query for example. | 
 **ReturnTo** | Pointer to **string** | ReturnTo contains the requested return_to URL. | [optional] 
 **State** | [**SelfServiceRecoveryFlowState**](SelfServiceRecoveryFlowState.md) |  | 
-**Type** | Pointer to **string** | The flow type can either be &#x60;api&#x60; or &#x60;browser&#x60;. | [optional] 
+**Type** | **string** | The flow type can either be &#x60;api&#x60; or &#x60;browser&#x60;. | 
 **Ui** | [**UiContainer**](UiContainer.md) |  | 
 
 ## Methods
 
 ### NewSelfServiceRecoveryFlow
 
-`func NewSelfServiceRecoveryFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state SelfServiceRecoveryFlowState, ui UiContainer, ) *SelfServiceRecoveryFlow`
+`func NewSelfServiceRecoveryFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state SelfServiceRecoveryFlowState, type_ string, ui UiContainer, ) *SelfServiceRecoveryFlow`
 
 NewSelfServiceRecoveryFlow instantiates a new SelfServiceRecoveryFlow object
 This constructor will assign default values to properties that have it defined,
@@ -202,11 +202,6 @@ and a boolean to check if the value has been set.
 
 SetType sets Type field to given value.
 
-### HasType
-
-`func (o *SelfServiceRecoveryFlow) HasType() bool`
-
-HasType returns a boolean if a field has been set.
 
 ### GetUi
 
diff --git a/internal/httpclient/docs/SelfServiceRegistrationFlow.md b/internal/httpclient/docs/SelfServiceRegistrationFlow.md
index e80ea4084..bc7901df2 100644
--- a/internal/httpclient/docs/SelfServiceRegistrationFlow.md
+++ b/internal/httpclient/docs/SelfServiceRegistrationFlow.md
@@ -10,14 +10,14 @@ Name | Type | Description | Notes
 **IssuedAt** | **time.Time** | IssuedAt is the time (UTC) when the flow occurred. | 
 **RequestUrl** | **string** | RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL&#39;s path or query for example. | 
 **ReturnTo** | Pointer to **string** | ReturnTo contains the requested return_to URL. | [optional] 
-**Type** | Pointer to **string** | The flow type can either be &#x60;api&#x60; or &#x60;browser&#x60;. | [optional] 
+**Type** | **string** | The flow type can either be &#x60;api&#x60; or &#x60;browser&#x60;. | 
 **Ui** | [**UiContainer**](UiContainer.md) |  | 
 
 ## Methods
 
 ### NewSelfServiceRegistrationFlow
 
-`func NewSelfServiceRegistrationFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, ui UiContainer, ) *SelfServiceRegistrationFlow`
+`func NewSelfServiceRegistrationFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, type_ string, ui UiContainer, ) *SelfServiceRegistrationFlow`
 
 NewSelfServiceRegistrationFlow instantiates a new SelfServiceRegistrationFlow object
 This constructor will assign default values to properties that have it defined,
@@ -181,11 +181,6 @@ and a boolean to check if the value has been set.
 
 SetType sets Type field to given value.
 
-### HasType
-
-`func (o *SelfServiceRegistrationFlow) HasType() bool`
-
-HasType returns a boolean if a field has been set.
 
 ### GetUi
 
diff --git a/internal/httpclient/docs/SelfServiceSettingsFlow.md b/internal/httpclient/docs/SelfServiceSettingsFlow.md
index 78d134879..2628c8ea6 100644
--- a/internal/httpclient/docs/SelfServiceSettingsFlow.md
+++ b/internal/httpclient/docs/SelfServiceSettingsFlow.md
@@ -12,14 +12,14 @@ Name | Type | Description | Notes
 **RequestUrl** | **string** | RequestURL is the initial URL that was requested from Ory Kratos. It can be used to forward information contained in the URL&#39;s path or query for example. | 
 **ReturnTo** | Pointer to **string** | ReturnTo contains the requested return_to URL. | [optional] 
 **State** | [**SelfServiceSettingsFlowState**](SelfServiceSettingsFlowState.md) |  | 
-**Type** | Pointer to **string** | The flow type can either be &#x60;api&#x60; or &#x60;browser&#x60;. | [optional] 
+**Type** | **string** | The flow type can either be &#x60;api&#x60; or &#x60;browser&#x60;. | 
 **Ui** | [**UiContainer**](UiContainer.md) |  | 
 
 ## Methods
 
 ### NewSelfServiceSettingsFlow
 
-`func NewSelfServiceSettingsFlow(expiresAt time.Time, id string, identity Identity, issuedAt time.Time, requestUrl string, state SelfServiceSettingsFlowState, ui UiContainer, ) *SelfServiceSettingsFlow`
+`func NewSelfServiceSettingsFlow(expiresAt time.Time, id string, identity Identity, issuedAt time.Time, requestUrl string, state SelfServiceSettingsFlowState, type_ string, ui UiContainer, ) *SelfServiceSettingsFlow`
 
 NewSelfServiceSettingsFlow instantiates a new SelfServiceSettingsFlow object
 This constructor will assign default values to properties that have it defined,
@@ -223,11 +223,6 @@ and a boolean to check if the value has been set.
 
 SetType sets Type field to given value.
 
-### HasType
-
-`func (o *SelfServiceSettingsFlow) HasType() bool`
-
-HasType returns a boolean if a field has been set.
 
 ### GetUi
 
diff --git a/internal/httpclient/docs/UiNode.md b/internal/httpclient/docs/UiNode.md
index 59a6f949d..956290963 100644
--- a/internal/httpclient/docs/UiNode.md
+++ b/internal/httpclient/docs/UiNode.md
@@ -5,10 +5,10 @@
 Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
 **Attributes** | [**UiNodeAttributes**](UiNodeAttributes.md) |  | 
-**Group** | **string** |  | 
+**Group** | **string** | Group specifies which group (e.g. password authenticator) this node belongs to. | 
 **Messages** | [**[]UiText**](UiText.md) |  | 
 **Meta** | [**UiNodeMeta**](UiNodeMeta.md) |  | 
-**Type** | **string** |  | 
+**Type** | **string** | The node&#39;s type | 
 
 ## Methods
 
diff --git a/internal/httpclient/docs/UiNodeAnchorAttributes.md b/internal/httpclient/docs/UiNodeAnchorAttributes.md
index b3f178380..44099ac60 100644
--- a/internal/httpclient/docs/UiNodeAnchorAttributes.md
+++ b/internal/httpclient/docs/UiNodeAnchorAttributes.md
@@ -6,7 +6,7 @@ Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
 **Href** | **string** | The link&#39;s href (destination) URL.  format: uri | 
 **Id** | **string** | A unique identifier | 
-**NodeType** | **string** |  | 
+**NodeType** | **string** | NodeType represents this node&#39;s types. It is a mirror of &#x60;node.type&#x60; and is primarily used to allow compatibility with OpenAPI 3.0. | 
 **Title** | [**UiText**](UiText.md) |  | 
 
 ## Methods
diff --git a/internal/httpclient/docs/UiNodeAttributes.md b/internal/httpclient/docs/UiNodeAttributes.md
index b2c451fd8..03760d3ec 100644
--- a/internal/httpclient/docs/UiNodeAttributes.md
+++ b/internal/httpclient/docs/UiNodeAttributes.md
@@ -7,7 +7,7 @@ Name | Type | Description | Notes
 **Disabled** | **bool** | Sets the input&#39;s disabled field to true or false. | 
 **Label** | Pointer to [**UiText**](UiText.md) |  | [optional] 
 **Name** | **string** | The input&#39;s element name. | 
-**NodeType** | **string** |  | 
+**NodeType** | **string** | NodeType represents this node&#39;s types. It is a mirror of &#x60;node.type&#x60; and is primarily used to allow compatibility with OpenAPI 3.0. | 
 **Onclick** | Pointer to **string** | OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. | [optional] 
 **Pattern** | Pointer to **string** | The input&#39;s pattern. | [optional] 
 **Required** | Pointer to **bool** | Mark this input field as required. | [optional] 
diff --git a/internal/httpclient/docs/UiNodeImageAttributes.md b/internal/httpclient/docs/UiNodeImageAttributes.md
index e8138b78c..f79dc91c4 100644
--- a/internal/httpclient/docs/UiNodeImageAttributes.md
+++ b/internal/httpclient/docs/UiNodeImageAttributes.md
@@ -6,7 +6,7 @@ Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
 **Height** | **int64** | Height of the image | 
 **Id** | **string** | A unique identifier | 
-**NodeType** | **string** |  | 
+**NodeType** | **string** | NodeType represents this node&#39;s types. It is a mirror of &#x60;node.type&#x60; and is primarily used to allow compatibility with OpenAPI 3.0. | 
 **Src** | **string** | The image&#39;s source URL.  format: uri | 
 **Width** | **int64** | Width of the image | 
 
diff --git a/internal/httpclient/docs/UiNodeInputAttributes.md b/internal/httpclient/docs/UiNodeInputAttributes.md
index 45b6439d1..d684252a7 100644
--- a/internal/httpclient/docs/UiNodeInputAttributes.md
+++ b/internal/httpclient/docs/UiNodeInputAttributes.md
@@ -7,7 +7,7 @@ Name | Type | Description | Notes
 **Disabled** | **bool** | Sets the input&#39;s disabled field to true or false. | 
 **Label** | Pointer to [**UiText**](UiText.md) |  | [optional] 
 **Name** | **string** | The input&#39;s element name. | 
-**NodeType** | **string** |  | 
+**NodeType** | **string** | NodeType represents this node&#39;s types. It is a mirror of &#x60;node.type&#x60; and is primarily used to allow compatibility with OpenAPI 3.0. | 
 **Onclick** | Pointer to **string** | OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn. | [optional] 
 **Pattern** | Pointer to **string** | The input&#39;s pattern. | [optional] 
 **Required** | Pointer to **bool** | Mark this input field as required. | [optional] 
diff --git a/internal/httpclient/docs/UiNodeScriptAttributes.md b/internal/httpclient/docs/UiNodeScriptAttributes.md
index d6dfa8576..54dd12571 100644
--- a/internal/httpclient/docs/UiNodeScriptAttributes.md
+++ b/internal/httpclient/docs/UiNodeScriptAttributes.md
@@ -8,7 +8,7 @@ Name | Type | Description | Notes
 **Crossorigin** | **string** | The script cross origin policy | 
 **Id** | **string** | A unique identifier | 
 **Integrity** | **string** | The script&#39;s integrity hash | 
-**NodeType** | **string** |  | 
+**NodeType** | **string** | NodeType represents this node&#39;s types. It is a mirror of &#x60;node.type&#x60; and is primarily used to allow compatibility with OpenAPI 3.0. | 
 **Nonce** | **string** | Nonce for CSP  A nonce you may want to use to improve your Content Security Policy. You do not have to use this value but if you want to improve your CSP policies you may use it. You can also choose to use your own nonce value! | 
 **Referrerpolicy** | **string** | The script referrer policy | 
 **Src** | **string** | The script source | 
diff --git a/internal/httpclient/docs/UiNodeTextAttributes.md b/internal/httpclient/docs/UiNodeTextAttributes.md
index 2eb29b2fc..6139132c1 100644
--- a/internal/httpclient/docs/UiNodeTextAttributes.md
+++ b/internal/httpclient/docs/UiNodeTextAttributes.md
@@ -5,7 +5,7 @@
 Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
 **Id** | **string** | A unique identifier | 
-**NodeType** | **string** |  | 
+**NodeType** | **string** | NodeType represents this node&#39;s types. It is a mirror of &#x60;node.type&#x60; and is primarily used to allow compatibility with OpenAPI 3.0. | 
 **Text** | [**UiText**](UiText.md) |  | 
 
 ## Methods
diff --git a/internal/httpclient/model_self_service_recovery_flow.go b/internal/httpclient/model_self_service_recovery_flow.go
index 311c4d213..996e5f0a1 100644
--- a/internal/httpclient/model_self_service_recovery_flow.go
+++ b/internal/httpclient/model_self_service_recovery_flow.go
@@ -31,7 +31,7 @@ type SelfServiceRecoveryFlow struct {
 	ReturnTo *string                      `json:"return_to,omitempty"`
 	State    SelfServiceRecoveryFlowState `json:"state"`
 	// The flow type can either be `api` or `browser`.
-	Type *string     `json:"type,omitempty"`
+	Type string      `json:"type"`
 	Ui   UiContainer `json:"ui"`
 }
 
@@ -39,13 +39,14 @@ type SelfServiceRecoveryFlow struct {
 // 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 NewSelfServiceRecoveryFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state SelfServiceRecoveryFlowState, ui UiContainer) *SelfServiceRecoveryFlow {
+func NewSelfServiceRecoveryFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, state SelfServiceRecoveryFlowState, type_ string, ui UiContainer) *SelfServiceRecoveryFlow {
 	this := SelfServiceRecoveryFlow{}
 	this.ExpiresAt = expiresAt
 	this.Id = id
 	this.IssuedAt = issuedAt
 	this.RequestUrl = requestUrl
 	this.State = state
+	this.Type = type_
 	this.Ui = ui
 	return &this
 }
@@ -242,36 +243,28 @@ func (o *SelfServiceRecoveryFlow) SetState(v SelfServiceRecoveryFlowState) {
 	o.State = v
 }
 
-// GetType returns the Type field value if set, zero value otherwise.
+// GetType returns the Type field value
 func (o *SelfServiceRecoveryFlow) GetType() string {
-	if o == nil || o.Type == nil {
+	if o == nil {
 		var ret string
 		return ret
 	}
-	return *o.Type
+
+	return o.Type
 }
 
-// GetTypeOk returns a tuple with the Type field value if set, nil otherwise
+// GetTypeOk returns a tuple with the Type field value
 // and a boolean to check if the value has been set.
 func (o *SelfServiceRecoveryFlow) GetTypeOk() (*string, bool) {
-	if o == nil || o.Type == nil {
+	if o == nil {
 		return nil, false
 	}
-	return o.Type, true
-}
-
-// HasType returns a boolean if a field has been set.
-func (o *SelfServiceRecoveryFlow) HasType() bool {
-	if o != nil && o.Type != nil {
-		return true
-	}
-
-	return false
+	return &o.Type, true
 }
 
-// SetType gets a reference to the given string and assigns it to the Type field.
+// SetType sets field value
 func (o *SelfServiceRecoveryFlow) SetType(v string) {
-	o.Type = &v
+	o.Type = v
 }
 
 // GetUi returns the Ui field value
@@ -321,7 +314,7 @@ func (o SelfServiceRecoveryFlow) MarshalJSON() ([]byte, error) {
 	if true {
 		toSerialize["state"] = o.State
 	}
-	if o.Type != nil {
+	if true {
 		toSerialize["type"] = o.Type
 	}
 	if true {
diff --git a/internal/httpclient/model_self_service_registration_flow.go b/internal/httpclient/model_self_service_registration_flow.go
index 31c6c78c4..4e42e7b99 100644
--- a/internal/httpclient/model_self_service_registration_flow.go
+++ b/internal/httpclient/model_self_service_registration_flow.go
@@ -29,7 +29,7 @@ type SelfServiceRegistrationFlow struct {
 	// ReturnTo contains the requested return_to URL.
 	ReturnTo *string `json:"return_to,omitempty"`
 	// The flow type can either be `api` or `browser`.
-	Type *string     `json:"type,omitempty"`
+	Type string      `json:"type"`
 	Ui   UiContainer `json:"ui"`
 }
 
@@ -37,12 +37,13 @@ type SelfServiceRegistrationFlow struct {
 // 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 NewSelfServiceRegistrationFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, ui UiContainer) *SelfServiceRegistrationFlow {
+func NewSelfServiceRegistrationFlow(expiresAt time.Time, id string, issuedAt time.Time, requestUrl string, type_ string, ui UiContainer) *SelfServiceRegistrationFlow {
 	this := SelfServiceRegistrationFlow{}
 	this.ExpiresAt = expiresAt
 	this.Id = id
 	this.IssuedAt = issuedAt
 	this.RequestUrl = requestUrl
+	this.Type = type_
 	this.Ui = ui
 	return &this
 }
@@ -215,36 +216,28 @@ func (o *SelfServiceRegistrationFlow) SetReturnTo(v string) {
 	o.ReturnTo = &v
 }
 
-// GetType returns the Type field value if set, zero value otherwise.
+// GetType returns the Type field value
 func (o *SelfServiceRegistrationFlow) GetType() string {
-	if o == nil || o.Type == nil {
+	if o == nil {
 		var ret string
 		return ret
 	}
-	return *o.Type
+
+	return o.Type
 }
 
-// GetTypeOk returns a tuple with the Type field value if set, nil otherwise
+// GetTypeOk returns a tuple with the Type field value
 // and a boolean to check if the value has been set.
 func (o *SelfServiceRegistrationFlow) GetTypeOk() (*string, bool) {
-	if o == nil || o.Type == nil {
+	if o == nil {
 		return nil, false
 	}
-	return o.Type, true
-}
-
-// HasType returns a boolean if a field has been set.
-func (o *SelfServiceRegistrationFlow) HasType() bool {
-	if o != nil && o.Type != nil {
-		return true
-	}
-
-	return false
+	return &o.Type, true
 }
 
-// SetType gets a reference to the given string and assigns it to the Type field.
+// SetType sets field value
 func (o *SelfServiceRegistrationFlow) SetType(v string) {
-	o.Type = &v
+	o.Type = v
 }
 
 // GetUi returns the Ui field value
@@ -291,7 +284,7 @@ func (o SelfServiceRegistrationFlow) MarshalJSON() ([]byte, error) {
 	if o.ReturnTo != nil {
 		toSerialize["return_to"] = o.ReturnTo
 	}
-	if o.Type != nil {
+	if true {
 		toSerialize["type"] = o.Type
 	}
 	if true {
diff --git a/internal/httpclient/model_self_service_settings_flow.go b/internal/httpclient/model_self_service_settings_flow.go
index 550fffeae..275a7d9b8 100644
--- a/internal/httpclient/model_self_service_settings_flow.go
+++ b/internal/httpclient/model_self_service_settings_flow.go
@@ -32,7 +32,7 @@ type SelfServiceSettingsFlow struct {
 	ReturnTo *string                      `json:"return_to,omitempty"`
 	State    SelfServiceSettingsFlowState `json:"state"`
 	// The flow type can either be `api` or `browser`.
-	Type *string     `json:"type,omitempty"`
+	Type string      `json:"type"`
 	Ui   UiContainer `json:"ui"`
 }
 
@@ -40,7 +40,7 @@ type SelfServiceSettingsFlow struct {
 // 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 NewSelfServiceSettingsFlow(expiresAt time.Time, id string, identity Identity, issuedAt time.Time, requestUrl string, state SelfServiceSettingsFlowState, ui UiContainer) *SelfServiceSettingsFlow {
+func NewSelfServiceSettingsFlow(expiresAt time.Time, id string, identity Identity, issuedAt time.Time, requestUrl string, state SelfServiceSettingsFlowState, type_ string, ui UiContainer) *SelfServiceSettingsFlow {
 	this := SelfServiceSettingsFlow{}
 	this.ExpiresAt = expiresAt
 	this.Id = id
@@ -48,6 +48,7 @@ func NewSelfServiceSettingsFlow(expiresAt time.Time, id string, identity Identit
 	this.IssuedAt = issuedAt
 	this.RequestUrl = requestUrl
 	this.State = state
+	this.Type = type_
 	this.Ui = ui
 	return &this
 }
@@ -268,36 +269,28 @@ func (o *SelfServiceSettingsFlow) SetState(v SelfServiceSettingsFlowState) {
 	o.State = v
 }
 
-// GetType returns the Type field value if set, zero value otherwise.
+// GetType returns the Type field value
 func (o *SelfServiceSettingsFlow) GetType() string {
-	if o == nil || o.Type == nil {
+	if o == nil {
 		var ret string
 		return ret
 	}
-	return *o.Type
+
+	return o.Type
 }
 
-// GetTypeOk returns a tuple with the Type field value if set, nil otherwise
+// GetTypeOk returns a tuple with the Type field value
 // and a boolean to check if the value has been set.
 func (o *SelfServiceSettingsFlow) GetTypeOk() (*string, bool) {
-	if o == nil || o.Type == nil {
+	if o == nil {
 		return nil, false
 	}
-	return o.Type, true
-}
-
-// HasType returns a boolean if a field has been set.
-func (o *SelfServiceSettingsFlow) HasType() bool {
-	if o != nil && o.Type != nil {
-		return true
-	}
-
-	return false
+	return &o.Type, true
 }
 
-// SetType gets a reference to the given string and assigns it to the Type field.
+// SetType sets field value
 func (o *SelfServiceSettingsFlow) SetType(v string) {
-	o.Type = &v
+	o.Type = v
 }
 
 // GetUi returns the Ui field value
@@ -350,7 +343,7 @@ func (o SelfServiceSettingsFlow) MarshalJSON() ([]byte, error) {
 	if true {
 		toSerialize["state"] = o.State
 	}
-	if o.Type != nil {
+	if true {
 		toSerialize["type"] = o.Type
 	}
 	if true {
diff --git a/internal/httpclient/model_ui_node.go b/internal/httpclient/model_ui_node.go
index 6c94ae0b6..e94114166 100644
--- a/internal/httpclient/model_ui_node.go
+++ b/internal/httpclient/model_ui_node.go
@@ -18,10 +18,12 @@ import (
 // UiNode Nodes are represented as HTML elements or their native UI equivalents. For example, a node can be an `<img>` tag, or an `<input element>` but also `some plain text`.
 type UiNode struct {
 	Attributes UiNodeAttributes `json:"attributes"`
-	Group      string           `json:"group"`
-	Messages   []UiText         `json:"messages"`
-	Meta       UiNodeMeta       `json:"meta"`
-	Type       string           `json:"type"`
+	// Group specifies which group (e.g. password authenticator) this node belongs to.
+	Group    string     `json:"group"`
+	Messages []UiText   `json:"messages"`
+	Meta     UiNodeMeta `json:"meta"`
+	// The node's type
+	Type string `json:"type"`
 }
 
 // NewUiNode instantiates a new UiNode object
diff --git a/internal/httpclient/model_ui_node_anchor_attributes.go b/internal/httpclient/model_ui_node_anchor_attributes.go
index 0e544f319..e995d86cb 100644
--- a/internal/httpclient/model_ui_node_anchor_attributes.go
+++ b/internal/httpclient/model_ui_node_anchor_attributes.go
@@ -20,7 +20,8 @@ type UiNodeAnchorAttributes struct {
 	// The link's href (destination) URL.  format: uri
 	Href string `json:"href"`
 	// A unique identifier
-	Id       string `json:"id"`
+	Id string `json:"id"`
+	// NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0.
 	NodeType string `json:"node_type"`
 	Title    UiText `json:"title"`
 }
diff --git a/internal/httpclient/model_ui_node_image_attributes.go b/internal/httpclient/model_ui_node_image_attributes.go
index 418eb0a72..be775ff21 100644
--- a/internal/httpclient/model_ui_node_image_attributes.go
+++ b/internal/httpclient/model_ui_node_image_attributes.go
@@ -20,7 +20,8 @@ type UiNodeImageAttributes struct {
 	// Height of the image
 	Height int64 `json:"height"`
 	// A unique identifier
-	Id       string `json:"id"`
+	Id string `json:"id"`
+	// NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0.
 	NodeType string `json:"node_type"`
 	// The image's source URL.  format: uri
 	Src string `json:"src"`
diff --git a/internal/httpclient/model_ui_node_input_attributes.go b/internal/httpclient/model_ui_node_input_attributes.go
index bee7c2245..ff7360de8 100644
--- a/internal/httpclient/model_ui_node_input_attributes.go
+++ b/internal/httpclient/model_ui_node_input_attributes.go
@@ -21,7 +21,8 @@ type UiNodeInputAttributes struct {
 	Disabled bool    `json:"disabled"`
 	Label    *UiText `json:"label,omitempty"`
 	// The input's element name.
-	Name     string `json:"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.
 	NodeType string `json:"node_type"`
 	// OnClick may contain javascript which should be executed on click. This is primarily used for WebAuthn.
 	Onclick *string `json:"onclick,omitempty"`
diff --git a/internal/httpclient/model_ui_node_script_attributes.go b/internal/httpclient/model_ui_node_script_attributes.go
index a077823a7..d2d668bb1 100644
--- a/internal/httpclient/model_ui_node_script_attributes.go
+++ b/internal/httpclient/model_ui_node_script_attributes.go
@@ -25,7 +25,8 @@ type UiNodeScriptAttributes struct {
 	Id string `json:"id"`
 	// The script's integrity hash
 	Integrity string `json:"integrity"`
-	NodeType  string `json:"node_type"`
+	// NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0.
+	NodeType string `json:"node_type"`
 	// Nonce for CSP  A nonce you may want to use to improve your Content Security Policy. You do not have to use this value but if you want to improve your CSP policies you may use it. You can also choose to use your own nonce value!
 	Nonce string `json:"nonce"`
 	// The script referrer policy
diff --git a/internal/httpclient/model_ui_node_text_attributes.go b/internal/httpclient/model_ui_node_text_attributes.go
index 4fea508c1..b2d0066ae 100644
--- a/internal/httpclient/model_ui_node_text_attributes.go
+++ b/internal/httpclient/model_ui_node_text_attributes.go
@@ -18,7 +18,8 @@ import (
 // UiNodeTextAttributes struct for UiNodeTextAttributes
 type UiNodeTextAttributes struct {
 	// A unique identifier
-	Id       string `json:"id"`
+	Id string `json:"id"`
+	// NodeType represents this node's types. It is a mirror of `node.type` and is primarily used to allow compatibility with OpenAPI 3.0.
 	NodeType string `json:"node_type"`
 	Text     UiText `json:"text"`
 }
diff --git a/selfservice/flow/login/error.go b/selfservice/flow/login/error.go
index 67f873bf4..eb7562986 100644
--- a/selfservice/flow/login/error.go
+++ b/selfservice/flow/login/error.go
@@ -70,7 +70,7 @@ func (s *ErrorHandler) PrepareReplacementForExpiredFlow(w http.ResponseWriter, r
 	return e.WithFlow(a), nil
 }
 
-func (s *ErrorHandler) WriteFlowError(w http.ResponseWriter, r *http.Request, f *Flow, group node.Group, err error) {
+func (s *ErrorHandler) WriteFlowError(w http.ResponseWriter, r *http.Request, f *Flow, group node.UiNodeGroup, err error) {
 	s.d.Audit().
 		WithError(err).
 		WithRequest(r).
diff --git a/selfservice/flow/login/sort.go b/selfservice/flow/login/sort.go
index c98a5834e..816f6a53a 100644
--- a/selfservice/flow/login/sort.go
+++ b/selfservice/flow/login/sort.go
@@ -8,7 +8,7 @@ import (
 
 func sortNodes(ctx context.Context, n node.Nodes) error {
 	return n.SortBySchema(ctx,
-		node.SortByGroups([]node.Group{
+		node.SortByGroups([]node.UiNodeGroup{
 			node.OpenIDConnectGroup,
 			node.DefaultGroup,
 			node.WebAuthnGroup,
diff --git a/selfservice/flow/login/strategy.go b/selfservice/flow/login/strategy.go
index 6956cebc3..ff7fd8c24 100644
--- a/selfservice/flow/login/strategy.go
+++ b/selfservice/flow/login/strategy.go
@@ -15,7 +15,7 @@ import (
 
 type Strategy interface {
 	ID() identity.CredentialsType
-	NodeGroup() node.Group
+	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, ss *session.Session) (i *identity.Identity, err error)
diff --git a/selfservice/flow/recovery/error.go b/selfservice/flow/recovery/error.go
index 4785945dc..826528e9b 100644
--- a/selfservice/flow/recovery/error.go
+++ b/selfservice/flow/recovery/error.go
@@ -54,7 +54,7 @@ func (s *ErrorHandler) WriteFlowError(
 	w http.ResponseWriter,
 	r *http.Request,
 	f *Flow,
-	group node.Group,
+	group node.UiNodeGroup,
 	err error,
 ) {
 	s.d.Audit().
diff --git a/selfservice/flow/recovery/flow.go b/selfservice/flow/recovery/flow.go
index cc13b7064..48d8f0de1 100644
--- a/selfservice/flow/recovery/flow.go
+++ b/selfservice/flow/recovery/flow.go
@@ -38,6 +38,8 @@ type Flow struct {
 	ID uuid.UUID `json:"id" db:"id" faker:"-"`
 
 	// Type represents the flow's type which can be either "api" or "browser", depending on the flow interaction.
+	//
+	// required: true
 	Type flow.Type `json:"type" db:"type" faker:"flow_type"`
 
 	// ExpiresAt is the time (UTC) when the request expires. If the user still wishes to update the setting,
diff --git a/selfservice/flow/recovery/handler.go b/selfservice/flow/recovery/handler.go
index d3da91a41..d998de2f5 100644
--- a/selfservice/flow/recovery/handler.go
+++ b/selfservice/flow/recovery/handler.go
@@ -370,7 +370,7 @@ func (h *Handler) submitFlow(w http.ResponseWriter, r *http.Request, ps httprout
 		return
 	}
 
-	var g node.Group
+	var g node.UiNodeGroup
 	var found bool
 	for _, ss := range h.d.AllRecoveryStrategies() {
 		err := ss.Recover(w, r, f)
diff --git a/selfservice/flow/recovery/strategy.go b/selfservice/flow/recovery/strategy.go
index 4c38c1719..c8e3ecbf5 100644
--- a/selfservice/flow/recovery/strategy.go
+++ b/selfservice/flow/recovery/strategy.go
@@ -18,7 +18,7 @@ const (
 type (
 	Strategy interface {
 		RecoveryStrategyID() string
-		RecoveryNodeGroup() node.Group
+		RecoveryNodeGroup() node.UiNodeGroup
 		PopulateRecoveryMethod(*http.Request, *Flow) error
 		Recover(w http.ResponseWriter, r *http.Request, f *Flow) (err error)
 	}
diff --git a/selfservice/flow/registration/error.go b/selfservice/flow/registration/error.go
index cd0a07ec6..0a132ef5f 100644
--- a/selfservice/flow/registration/error.go
+++ b/selfservice/flow/registration/error.go
@@ -66,7 +66,7 @@ func (s *ErrorHandler) WriteFlowError(
 	w http.ResponseWriter,
 	r *http.Request,
 	f *Flow,
-	group node.Group,
+	group node.UiNodeGroup,
 	err error,
 ) {
 	s.d.Audit().
diff --git a/selfservice/flow/registration/flow.go b/selfservice/flow/registration/flow.go
index c442c0208..b2d21cad4 100644
--- a/selfservice/flow/registration/flow.go
+++ b/selfservice/flow/registration/flow.go
@@ -37,6 +37,8 @@ type Flow struct {
 	ID uuid.UUID `json:"id" faker:"-" db:"id"`
 
 	// Type represents the flow's type which can be either "api" or "browser", depending on the flow interaction.
+	//
+	// required: true
 	Type flow.Type `json:"type" db:"type" faker:"flow_type"`
 
 	// ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to log in,
diff --git a/selfservice/flow/registration/sort.go b/selfservice/flow/registration/sort.go
index de247112c..f22f085c0 100644
--- a/selfservice/flow/registration/sort.go
+++ b/selfservice/flow/registration/sort.go
@@ -9,7 +9,7 @@ import (
 func SortNodes(ctx context.Context, n node.Nodes, schemaRef string) error {
 	return n.SortBySchema(ctx,
 		node.SortBySchema(schemaRef),
-		node.SortByGroups([]node.Group{
+		node.SortByGroups([]node.UiNodeGroup{
 			node.DefaultGroup,
 			node.OpenIDConnectGroup,
 			node.WebAuthnGroup,
diff --git a/selfservice/flow/registration/strategy.go b/selfservice/flow/registration/strategy.go
index 79fdf0594..1085ee544 100644
--- a/selfservice/flow/registration/strategy.go
+++ b/selfservice/flow/registration/strategy.go
@@ -14,7 +14,7 @@ import (
 
 type Strategy interface {
 	ID() identity.CredentialsType
-	NodeGroup() node.Group
+	NodeGroup() node.UiNodeGroup
 	RegisterRegistrationRoutes(*x.RouterPublic)
 	PopulateRegistrationMethod(r *http.Request, sr *Flow) error
 	Register(w http.ResponseWriter, r *http.Request, f *Flow, i *identity.Identity) (err error)
diff --git a/selfservice/flow/settings/error.go b/selfservice/flow/settings/error.go
index 086f7611f..1432ba720 100644
--- a/selfservice/flow/settings/error.go
+++ b/selfservice/flow/settings/error.go
@@ -115,7 +115,7 @@ func (s *ErrorHandler) PrepareReplacementForExpiredFlow(w http.ResponseWriter, r
 func (s *ErrorHandler) WriteFlowError(
 	w http.ResponseWriter,
 	r *http.Request,
-	group node.Group,
+	group node.UiNodeGroup,
 	f *Flow,
 	id *identity.Identity,
 	err error,
diff --git a/selfservice/flow/settings/flow.go b/selfservice/flow/settings/flow.go
index 496df8078..7a3b783f6 100644
--- a/selfservice/flow/settings/flow.go
+++ b/selfservice/flow/settings/flow.go
@@ -50,6 +50,8 @@ type Flow struct {
 	ID uuid.UUID `json:"id" db:"id" faker:"-"`
 
 	// Type represents the flow's type which can be either "api" or "browser", depending on the flow interaction.
+	//
+	// required: true
 	Type flow.Type `json:"type" db:"type" faker:"flow_type"`
 
 	// ExpiresAt is the time (UTC) when the flow expires. If the user still wishes to update the setting,
diff --git a/selfservice/flow/settings/sort.go b/selfservice/flow/settings/sort.go
index fb8777c0a..2cc779898 100644
--- a/selfservice/flow/settings/sort.go
+++ b/selfservice/flow/settings/sort.go
@@ -9,7 +9,7 @@ import (
 func sortNodes(ctx context.Context, n node.Nodes, schemaRef string) error {
 	return n.SortBySchema(ctx,
 		node.SortBySchema(schemaRef),
-		node.SortByGroups([]node.Group{
+		node.SortByGroups([]node.UiNodeGroup{
 			node.DefaultGroup,
 			node.ProfileGroup,
 			node.PasswordGroup,
diff --git a/selfservice/flow/settings/strategy.go b/selfservice/flow/settings/strategy.go
index 787d75709..3e199457a 100644
--- a/selfservice/flow/settings/strategy.go
+++ b/selfservice/flow/settings/strategy.go
@@ -23,7 +23,7 @@ var pkgName = reflect.TypeOf(Strategies{}).PkgPath()
 
 type Strategy interface {
 	SettingsStrategyID() string
-	NodeGroup() node.Group
+	NodeGroup() node.UiNodeGroup
 	RegisterSettingsRoutes(*x.RouterPublic)
 	PopulateSettingsMethod(*http.Request, *identity.Identity, *Flow) error
 	Settings(w http.ResponseWriter, r *http.Request, f *Flow, s *session.Session) (*UpdateContext, error)
diff --git a/selfservice/flow/verification/error.go b/selfservice/flow/verification/error.go
index 9fcd5a12c..879d13f95 100644
--- a/selfservice/flow/verification/error.go
+++ b/selfservice/flow/verification/error.go
@@ -51,7 +51,7 @@ func (s *ErrorHandler) WriteFlowError(
 	w http.ResponseWriter,
 	r *http.Request,
 	f *Flow,
-	group node.Group,
+	group node.UiNodeGroup,
 	err error,
 ) {
 	s.d.Audit().
diff --git a/selfservice/flow/verification/flow.go b/selfservice/flow/verification/flow.go
index a5d190c97..aadde5903 100644
--- a/selfservice/flow/verification/flow.go
+++ b/selfservice/flow/verification/flow.go
@@ -39,6 +39,7 @@ type Flow struct {
 	ID uuid.UUID `json:"id" db:"id" faker:"-"`
 
 	// Type represents the flow's type which can be either "api" or "browser", depending on the flow interaction.
+	//
 	// required: true
 	Type flow.Type `json:"type" db:"type" faker:"flow_type"`
 
diff --git a/selfservice/flow/verification/handler.go b/selfservice/flow/verification/handler.go
index c62ede047..41b816bb2 100644
--- a/selfservice/flow/verification/handler.go
+++ b/selfservice/flow/verification/handler.go
@@ -352,7 +352,7 @@ func (h *Handler) submitFlow(w http.ResponseWriter, r *http.Request, ps httprout
 		return
 	}
 
-	var g node.Group
+	var g node.UiNodeGroup
 	var found bool
 	for _, ss := range h.d.AllVerificationStrategies() {
 		err := ss.Verify(w, r, f)
diff --git a/selfservice/flow/verification/strategy.go b/selfservice/flow/verification/strategy.go
index 4e8aed9e1..96d958940 100644
--- a/selfservice/flow/verification/strategy.go
+++ b/selfservice/flow/verification/strategy.go
@@ -18,7 +18,7 @@ const (
 type (
 	Strategy interface {
 		VerificationStrategyID() string
-		VerificationNodeGroup() node.Group
+		VerificationNodeGroup() node.UiNodeGroup
 		PopulateVerificationMethod(*http.Request, *Flow) error
 		Verify(w http.ResponseWriter, r *http.Request, f *Flow) (err error)
 	}
diff --git a/selfservice/strategy/link/strategy.go b/selfservice/strategy/link/strategy.go
index ab20019ce..e9b02cd88 100644
--- a/selfservice/strategy/link/strategy.go
+++ b/selfservice/strategy/link/strategy.go
@@ -79,10 +79,10 @@ func NewStrategy(d strategyDependencies) *Strategy {
 	return &Strategy{d: d, dx: decoderx.NewHTTP()}
 }
 
-func (s *Strategy) RecoveryNodeGroup() node.Group {
-	return node.RecoveryLinkGroup
+func (s *Strategy) RecoveryNodeGroup() node.UiNodeGroup {
+	return node.LinkGroup
 }
 
-func (s *Strategy) VerificationNodeGroup() node.Group {
-	return node.VerificationLinkGroup
+func (s *Strategy) VerificationNodeGroup() node.UiNodeGroup {
+	return node.LinkGroup
 }
diff --git a/selfservice/strategy/link/strategy_recovery.go b/selfservice/strategy/link/strategy_recovery.go
index 1965ba17d..df95f40c1 100644
--- a/selfservice/strategy/link/strategy_recovery.go
+++ b/selfservice/strategy/link/strategy_recovery.go
@@ -49,9 +49,9 @@ func (s *Strategy) PopulateRecoveryMethod(r *http.Request, f *recovery.Flow) err
 	f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
 	f.UI.GetNodes().Upsert(
 		// v0.5: form.Field{Name: "email", Type: "email", Required: true},
-		node.NewInputField("email", nil, node.RecoveryLinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()),
+		node.NewInputField("email", nil, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()),
 	)
-	f.UI.GetNodes().Append(node.NewInputField("method", s.RecoveryStrategyID(), node.RecoveryLinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSubmit()))
+	f.UI.GetNodes().Append(node.NewInputField("method", s.RecoveryStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSubmit()))
 
 	return nil
 }
@@ -394,7 +394,7 @@ func (s *Strategy) retryRecoveryFlowWithError(w http.ResponseWriter, r *http.Req
 	if expired := new(flow.ExpiredError); errors.As(recErr, &expired) {
 		return s.retryRecoveryFlowWithMessage(w, r, ft, text.NewErrorValidationRecoveryFlowExpired(expired.Ago))
 	} else {
-		if err := req.UI.ParseError(node.RecoveryLinkGroup, recErr); err != nil {
+		if err := req.UI.ParseError(node.LinkGroup, recErr); err != nil {
 			return err
 		}
 	}
@@ -437,7 +437,7 @@ func (s *Strategy) recoveryHandleFormSubmission(w http.ResponseWriter, r *http.R
 	f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
 	f.UI.GetNodes().Upsert(
 		// v0.5: form.Field{Name: "email", Type: "email", Required: true, Value: body.Body.Email}
-		node.NewInputField("email", body.Email, node.RecoveryLinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute),
+		node.NewInputField("email", body.Email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute),
 	)
 
 	f.Active = sqlxx.NullString(s.RecoveryNodeGroup())
@@ -483,7 +483,7 @@ func (s *Strategy) HandleRecoveryError(w http.ResponseWriter, r *http.Request, r
 		req.UI.SetCSRF(s.d.GenerateCSRFToken(r))
 		req.UI.GetNodes().Upsert(
 			// v0.5: form.Field{Name: "email", Type: "email", Required: true, Value: body.Body.Email}
-			node.NewInputField("email", email, node.RecoveryLinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute),
+			node.NewInputField("email", email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute),
 		)
 	}
 
diff --git a/selfservice/strategy/link/strategy_verification.go b/selfservice/strategy/link/strategy_verification.go
index dd89e81bd..552f44e06 100644
--- a/selfservice/strategy/link/strategy_verification.go
+++ b/selfservice/strategy/link/strategy_verification.go
@@ -34,9 +34,9 @@ func (s *Strategy) PopulateVerificationMethod(r *http.Request, f *verification.F
 	f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
 	f.UI.GetNodes().Upsert(
 		// v0.5: form.Field{Name: "email", Type: "email", Required: true}
-		node.NewInputField("email", nil, node.VerificationLinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()),
+		node.NewInputField("email", nil, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute).WithMetaLabel(text.NewInfoNodeInputEmail()),
 	)
-	f.UI.GetNodes().Append(node.NewInputField("method", s.VerificationStrategyID(), node.VerificationLinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSubmit()))
+	f.UI.GetNodes().Append(node.NewInputField("method", s.VerificationStrategyID(), node.LinkGroup, node.InputAttributeTypeSubmit).WithMetaLabel(text.NewInfoNodeLabelSubmit()))
 	return nil
 }
 
@@ -75,7 +75,7 @@ func (s *Strategy) handleVerificationError(w http.ResponseWriter, r *http.Reques
 		f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
 		f.UI.GetNodes().Upsert(
 			// v0.5: form.Field{Name: "email", Type: "email", Required: true, Value: body.Body.Email}
-			node.NewInputField("email", body.Email, node.VerificationLinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute),
+			node.NewInputField("email", body.Email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute),
 		)
 	}
 
@@ -166,7 +166,7 @@ func (s *Strategy) verificationHandleFormSubmission(w http.ResponseWriter, r *ht
 	f.UI.SetCSRF(s.d.GenerateCSRFToken(r))
 	f.UI.GetNodes().Upsert(
 		// v0.5: form.Field{Name: "email", Type: "email", Required: true, Value: body.Body.Email}
-		node.NewInputField("email", body.Email, node.VerificationLinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute),
+		node.NewInputField("email", body.Email, node.LinkGroup, node.InputAttributeTypeEmail, node.WithRequiredInputAttribute),
 	)
 
 	f.Active = sqlxx.NullString(s.VerificationNodeGroup())
@@ -305,7 +305,7 @@ func (s *Strategy) retryVerificationFlowWithError(w http.ResponseWriter, r *http
 	if expired := new(flow.ExpiredError); errors.As(verErr, &expired) {
 		return s.retryVerificationFlowWithMessage(w, r, ft, text.NewErrorValidationVerificationFlowExpired(expired.Ago))
 	} else {
-		if err := f.UI.ParseError(node.RecoveryLinkGroup, verErr); err != nil {
+		if err := f.UI.ParseError(node.LinkGroup, verErr); err != nil {
 			return err
 		}
 	}
diff --git a/selfservice/strategy/lookup/strategy.go b/selfservice/strategy/lookup/strategy.go
index 30ab83bf4..12c78ffd4 100644
--- a/selfservice/strategy/lookup/strategy.go
+++ b/selfservice/strategy/lookup/strategy.go
@@ -97,7 +97,7 @@ func (s *Strategy) ID() identity.CredentialsType {
 	return identity.CredentialsTypeLookup
 }
 
-func (s *Strategy) NodeGroup() node.Group {
+func (s *Strategy) NodeGroup() node.UiNodeGroup {
 	return node.LookupGroup
 }
 
diff --git a/selfservice/strategy/oidc/provider_config.go b/selfservice/strategy/oidc/provider_config.go
index f0f48660e..a321a3870 100644
--- a/selfservice/strategy/oidc/provider_config.go
+++ b/selfservice/strategy/oidc/provider_config.go
@@ -150,6 +150,8 @@ func (c ConfigurationCollection) Provider(id string, reg dependencies) (Provider
 				return NewProviderApple(&p, reg), nil
 			case addProviderName("spotify"):
 				return NewProviderSpotify(&p, reg), nil
+			case addProviderName("netid"):
+				return NewProviderNetID(&p, reg), nil
 			}
 			return nil, errors.Errorf("provider type %s is not supported, supported are: %v", p.Provider, providerNames)
 		}
diff --git a/selfservice/strategy/oidc/provider_netid.go b/selfservice/strategy/oidc/provider_netid.go
new file mode 100644
index 000000000..3587ef158
--- /dev/null
+++ b/selfservice/strategy/oidc/provider_netid.go
@@ -0,0 +1,96 @@
+package oidc
+
+import (
+	"context"
+	"encoding/json"
+	"net/url"
+
+	"github.com/hashicorp/go-retryablehttp"
+	"github.com/pkg/errors"
+	"golang.org/x/oauth2"
+
+	"github.com/ory/x/urlx"
+
+	"github.com/ory/herodot"
+	"github.com/ory/x/httpx"
+)
+
+const (
+	defaultBrokerScheme = "https"
+	defaultBrokerHost   = "broker.netid.de"
+)
+
+type ProviderNetID struct {
+	*ProviderGenericOIDC
+}
+
+func NewProviderNetID(
+	config *Configuration,
+	reg dependencies,
+) *ProviderNetID {
+	return &ProviderNetID{
+		ProviderGenericOIDC: &ProviderGenericOIDC{
+			config: config,
+			reg:    reg,
+		},
+	}
+}
+
+func (n *ProviderNetID) OAuth2(ctx context.Context) (*oauth2.Config, error) {
+	return n.oAuth2(ctx)
+}
+
+func (n *ProviderNetID) oAuth2(ctx context.Context) (*oauth2.Config, error) {
+	u := n.brokerURL()
+
+	authURL := urlx.AppendPaths(u, "/authorize")
+	tokenURL := urlx.AppendPaths(u, "/token")
+
+	return &oauth2.Config{
+		ClientID:     n.config.ClientID,
+		ClientSecret: n.config.ClientSecret,
+		Endpoint: oauth2.Endpoint{
+			AuthURL:  authURL.String(),
+			TokenURL: tokenURL.String(),
+		},
+		Scopes:      n.config.Scope,
+		RedirectURL: n.config.Redir(n.reg.Config(ctx).OIDCRedirectURIBase()),
+	}, nil
+
+}
+
+func (n *ProviderNetID) Claims(ctx context.Context, exchange *oauth2.Token) (*Claims, error) {
+	o, err := n.OAuth2(ctx)
+	if err != nil {
+		return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
+	}
+
+	client := n.reg.HTTPClient(ctx, httpx.ResilientClientDisallowInternalIPs(), httpx.ResilientClientWithClient(o.Client(ctx, exchange)))
+
+	u := n.brokerURL()
+	if err != nil {
+		return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
+	}
+	userInfoURL := urlx.AppendPaths(u, "/userinfo")
+	req, err := retryablehttp.NewRequest("GET", userInfoURL.String(), nil)
+	if err != nil {
+		return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
+	}
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
+	}
+	defer resp.Body.Close()
+
+	var claims Claims
+	if err := json.NewDecoder(resp.Body).Decode(&claims); err != nil {
+		return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
+	}
+
+	return &claims, nil
+}
+
+func (n *ProviderNetID) brokerURL() *url.URL {
+	return &url.URL{Scheme: defaultBrokerScheme, Host: defaultBrokerHost}
+}
diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go
index ae7fa66ff..176e25944 100644
--- a/selfservice/strategy/oidc/strategy.go
+++ b/selfservice/strategy/oidc/strategy.go
@@ -461,7 +461,7 @@ func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, f flow.Fl
 	return err
 }
 
-func (s *Strategy) NodeGroup() node.Group {
+func (s *Strategy) NodeGroup() node.UiNodeGroup {
 	return node.OpenIDConnectGroup
 }
 
diff --git a/selfservice/strategy/password/strategy.go b/selfservice/strategy/password/strategy.go
index fa8437d8c..866d72793 100644
--- a/selfservice/strategy/password/strategy.go
+++ b/selfservice/strategy/password/strategy.go
@@ -111,6 +111,6 @@ func (s *Strategy) CompletedAuthenticationMethod(ctx context.Context) session.Au
 	}
 }
 
-func (s *Strategy) NodeGroup() node.Group {
+func (s *Strategy) NodeGroup() node.UiNodeGroup {
 	return node.PasswordGroup
 }
diff --git a/selfservice/strategy/password/validator.go b/selfservice/strategy/password/validator.go
index ad8088b05..ba7d71f37 100644
--- a/selfservice/strategy/password/validator.go
+++ b/selfservice/strategy/password/validator.go
@@ -10,10 +10,10 @@ import (
 	"net/http"
 	"strconv"
 	"strings"
-	"sync"
 	"time"
 
 	"github.com/arbovm/levenshtein"
+	"github.com/dgraph-io/ristretto"
 	"github.com/hashicorp/go-retryablehttp"
 	"github.com/pkg/errors"
 
@@ -22,6 +22,8 @@ import (
 	"github.com/ory/x/httpx"
 )
 
+const hashCacheItemTTL = time.Hour
+
 // Validator implements a validation strategy for passwords. One example is that the password
 // has to have at least 6 characters and at least one lower and one uppercase password.
 type Validator interface {
@@ -49,10 +51,9 @@ var ErrUnexpectedStatusCode = errors.New("unexpected status code")
 // [haveibeenpwnd](https://haveibeenpwned.com/API/v2#SearchingPwnedPasswordsByRange) service to check if the
 // password has been breached in a previous data leak using k-anonymity.
 type DefaultPasswordValidator struct {
-	sync.RWMutex
 	reg    validatorDependencies
 	Client *retryablehttp.Client
-	hashes map[string]int64
+	hashes *ristretto.Cache
 
 	minIdentifierPasswordDist            int
 	maxIdentifierPasswordSubstrThreshold float32
@@ -62,12 +63,22 @@ type validatorDependencies interface {
 	config.Provider
 }
 
-func NewDefaultPasswordValidatorStrategy(reg validatorDependencies) *DefaultPasswordValidator {
+func NewDefaultPasswordValidatorStrategy(reg validatorDependencies) (*DefaultPasswordValidator, error) {
+	cache, err := ristretto.NewCache(&ristretto.Config{
+		NumCounters:        10 * 10000,
+		MaxCost:            60 * 10000, // BCrypt hash size is 60 bytes
+		BufferItems:        64,
+		IgnoreInternalCost: true,
+	})
+	// sanity check - this should never happen unless above configuration variables are invalid
+	if err != nil {
+		return nil, errors.Wrap(err, "error while setting up validator cache")
+	}
 	return &DefaultPasswordValidator{
 		Client:                    httpx.NewResilientClient(httpx.ResilientClientWithConnectionTimeout(time.Second)),
 		reg:                       reg,
-		hashes:                    map[string]int64{},
-		minIdentifierPasswordDist: 5, maxIdentifierPasswordSubstrThreshold: 0.5}
+		hashes:                    cache,
+		minIdentifierPasswordDist: 5, maxIdentifierPasswordSubstrThreshold: 0.5}, nil
 }
 
 func b20(src []byte) string {
@@ -109,9 +120,7 @@ func (s *DefaultPasswordValidator) fetch(hpw []byte, apiDNSName string) error {
 		return errors.Wrapf(ErrUnexpectedStatusCode, "%d", res.StatusCode)
 	}
 
-	s.Lock()
-	s.hashes[b20(hpw)] = 0
-	s.Unlock()
+	s.hashes.SetWithTTL(b20(hpw), 0, 1, hashCacheItemTTL)
 
 	sc := bufio.NewScanner(res.Body)
 	for sc.Scan() {
@@ -130,9 +139,7 @@ func (s *DefaultPasswordValidator) fetch(hpw []byte, apiDNSName string) error {
 			}
 		}
 
-		s.Lock()
-		s.hashes[(prefix + result[0])] = count
-		s.Unlock()
+		s.hashes.SetWithTTL(prefix+result[0], count, 1, hashCacheItemTTL)
 	}
 
 	if err := sc.Err(); err != nil {
@@ -169,10 +176,7 @@ func (s *DefaultPasswordValidator) Validate(ctx context.Context, identifier, pas
 	}
 	hpw := h.Sum(nil)
 
-	s.RLock()
-	c, ok := s.hashes[b20(hpw)]
-	s.RUnlock()
-
+	c, ok := s.hashes.Get(b20(hpw))
 	if !ok {
 		err := s.fetch(hpw, passwordPolicyConfig.HaveIBeenPwnedHost)
 		if (errors.Is(err, ErrNetworkFailure) || errors.Is(err, ErrUnexpectedStatusCode)) && passwordPolicyConfig.IgnoreNetworkErrors {
@@ -184,7 +188,8 @@ func (s *DefaultPasswordValidator) Validate(ctx context.Context, identifier, pas
 		return s.Validate(ctx, identifier, password)
 	}
 
-	if c > int64(s.reg.Config(ctx).PasswordPolicyConfig().MaxBreaches) {
+	v, ok := c.(int64)
+	if ok && v > int64(s.reg.Config(ctx).PasswordPolicyConfig().MaxBreaches) {
 		return errors.New("the password has been found in data breaches and must no longer be used")
 	}
 
diff --git a/selfservice/strategy/profile/strategy.go b/selfservice/strategy/profile/strategy.go
index 748e503ee..1f53dc3ff 100644
--- a/selfservice/strategy/profile/strategy.go
+++ b/selfservice/strategy/profile/strategy.go
@@ -276,6 +276,6 @@ func (s *Strategy) newSettingsProfileDecoder(ctx context.Context, i *identity.Id
 	return o, nil
 }
 
-func (s *Strategy) NodeGroup() node.Group {
+func (s *Strategy) NodeGroup() node.UiNodeGroup {
 	return node.ProfileGroup
 }
diff --git a/selfservice/strategy/totp/strategy.go b/selfservice/strategy/totp/strategy.go
index c54b66938..ebc47ead8 100644
--- a/selfservice/strategy/totp/strategy.go
+++ b/selfservice/strategy/totp/strategy.go
@@ -100,7 +100,7 @@ func (s *Strategy) ID() identity.CredentialsType {
 	return identity.CredentialsTypeTOTP
 }
 
-func (s *Strategy) NodeGroup() node.Group {
+func (s *Strategy) NodeGroup() node.UiNodeGroup {
 	return node.TOTPGroup
 }
 
diff --git a/selfservice/strategy/webauthn/strategy.go b/selfservice/strategy/webauthn/strategy.go
index b001fb22f..3b8f53d25 100644
--- a/selfservice/strategy/webauthn/strategy.go
+++ b/selfservice/strategy/webauthn/strategy.go
@@ -107,7 +107,7 @@ func (s *Strategy) ID() identity.CredentialsType {
 	return identity.CredentialsTypeWebAuthn
 }
 
-func (s *Strategy) NodeGroup() node.Group {
+func (s *Strategy) NodeGroup() node.UiNodeGroup {
 	return node.WebAuthnGroup
 }
 
diff --git a/spec/api.json b/spec/api.json
index 84cc6e6f9..f1589876d 100755
--- a/spec/api.json
+++ b/spec/api.json
@@ -897,6 +897,7 @@
         },
         "required": [
           "id",
+          "type",
           "expires_at",
           "issued_at",
           "request_url",
@@ -968,6 +969,7 @@
         },
         "required": [
           "id",
+          "type",
           "expires_at",
           "issued_at",
           "request_url",
@@ -1018,6 +1020,7 @@
         },
         "required": [
           "id",
+          "type",
           "expires_at",
           "issued_at",
           "request_url",
@@ -1823,7 +1826,18 @@
             "$ref": "#/components/schemas/uiNodeAttributes"
           },
           "group": {
-            "$ref": "#/components/schemas/uiNodeGroup"
+            "description": "Group specifies which group (e.g. password authenticator) this node belongs to.",
+            "enum": [
+              "default",
+              "password",
+              "oidc",
+              "profile",
+              "link",
+              "totp",
+              "lookup_secret",
+              "webauthn"
+            ],
+            "type": "string"
           },
           "messages": {
             "$ref": "#/components/schemas/uiTexts"
@@ -1832,7 +1846,15 @@
             "$ref": "#/components/schemas/uiNodeMeta"
           },
           "type": {
-            "$ref": "#/components/schemas/uiNodeType"
+            "description": "The node's type",
+            "enum": [
+              "text",
+              "input",
+              "img",
+              "a",
+              "script"
+            ],
+            "type": "string"
           }
         },
         "required": [
@@ -1856,7 +1878,15 @@
             "type": "string"
           },
           "node_type": {
-            "$ref": "#/components/schemas/uiNodeType"
+            "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0.",
+            "enum": [
+              "text",
+              "input",
+              "img",
+              "a",
+              "script"
+            ],
+            "type": "string"
           },
           "title": {
             "$ref": "#/components/schemas/uiText"
@@ -1874,8 +1904,8 @@
       "uiNodeAttributes": {
         "discriminator": {
           "mapping": {
-            "anchor": "#/components/schemas/uiNodeAnchorAttributes",
-            "image": "#/components/schemas/uiNodeImageAttributes",
+            "a": "#/components/schemas/uiNodeAnchorAttributes",
+            "img": "#/components/schemas/uiNodeImageAttributes",
             "input": "#/components/schemas/uiNodeInputAttributes",
             "script": "#/components/schemas/uiNodeScriptAttributes",
             "text": "#/components/schemas/uiNodeTextAttributes"
@@ -1901,9 +1931,6 @@
         ],
         "title": "Attributes represents a list of attributes (e.g. `href=\"foo\"` for links)."
       },
-      "uiNodeGroup": {
-        "type": "string"
-      },
       "uiNodeImageAttributes": {
         "properties": {
           "height": {
@@ -1916,7 +1943,15 @@
             "type": "string"
           },
           "node_type": {
-            "$ref": "#/components/schemas/uiNodeType"
+            "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0.",
+            "enum": [
+              "text",
+              "input",
+              "img",
+              "a",
+              "script"
+            ],
+            "type": "string"
           },
           "src": {
             "description": "The image's source URL.\n\nformat: uri",
@@ -1956,7 +1991,15 @@
             "type": "string"
           },
           "node_type": {
-            "$ref": "#/components/schemas/uiNodeType"
+            "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0.",
+            "enum": [
+              "text",
+              "input",
+              "img",
+              "a",
+              "script"
+            ],
+            "type": "string"
           },
           "onclick": {
             "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.",
@@ -2015,7 +2058,15 @@
             "type": "string"
           },
           "node_type": {
-            "$ref": "#/components/schemas/uiNodeType"
+            "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0.",
+            "enum": [
+              "text",
+              "input",
+              "img",
+              "a",
+              "script"
+            ],
+            "type": "string"
           },
           "nonce": {
             "description": "Nonce for CSP\n\nA nonce you may want to use to improve your Content Security Policy.\nYou do not have to use this value but if you want to improve your CSP\npolicies you may use it. You can also choose to use your own nonce value!",
@@ -2055,7 +2106,15 @@
             "type": "string"
           },
           "node_type": {
-            "$ref": "#/components/schemas/uiNodeType"
+            "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0.",
+            "enum": [
+              "text",
+              "input",
+              "img",
+              "a",
+              "script"
+            ],
+            "type": "string"
           },
           "text": {
             "$ref": "#/components/schemas/uiText"
@@ -2069,9 +2128,6 @@
         "title": "TextAttributes represents the attributes of a text node.",
         "type": "object"
       },
-      "uiNodeType": {
-        "type": "string"
-      },
       "uiNodes": {
         "items": {
           "$ref": "#/components/schemas/uiNode"
diff --git a/spec/swagger.json b/spec/swagger.json
index 712814b06..559aa3492 100755
--- a/spec/swagger.json
+++ b/spec/swagger.json
@@ -3183,6 +3183,7 @@
       "title": "A Recovery Flow",
       "required": [
         "id",
+        "type",
         "expires_at",
         "issued_at",
         "request_url",
@@ -3252,6 +3253,7 @@
       "type": "object",
       "required": [
         "id",
+        "type",
         "expires_at",
         "issued_at",
         "request_url",
@@ -3296,6 +3298,7 @@
       "title": "Flow represents a Settings Flow",
       "required": [
         "id",
+        "type",
         "expires_at",
         "issued_at",
         "request_url",
@@ -4031,7 +4034,18 @@
           "$ref": "#/definitions/uiNodeAttributes"
         },
         "group": {
-          "$ref": "#/definitions/uiNodeGroup"
+          "description": "Group specifies which group (e.g. password authenticator) this node belongs to.",
+          "type": "string",
+          "enum": [
+            "default",
+            "password",
+            "oidc",
+            "profile",
+            "link",
+            "totp",
+            "lookup_secret",
+            "webauthn"
+          ]
         },
         "messages": {
           "$ref": "#/definitions/uiTexts"
@@ -4040,7 +4054,15 @@
           "$ref": "#/definitions/uiNodeMeta"
         },
         "type": {
-          "$ref": "#/definitions/uiNodeType"
+          "description": "The node's type",
+          "type": "string",
+          "enum": [
+            "text",
+            "input",
+            "img",
+            "a",
+            "script"
+          ]
         }
       }
     },
@@ -4063,7 +4085,15 @@
           "type": "string"
         },
         "node_type": {
-          "$ref": "#/definitions/uiNodeType"
+          "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0.",
+          "type": "string",
+          "enum": [
+            "text",
+            "input",
+            "img",
+            "a",
+            "script"
+          ]
         },
         "title": {
           "$ref": "#/definitions/uiText"
@@ -4074,9 +4104,6 @@
       "type": "object",
       "title": "Attributes represents a list of attributes (e.g. `href=\"foo\"` for links)."
     },
-    "uiNodeGroup": {
-      "type": "string"
-    },
     "uiNodeImageAttributes": {
       "type": "object",
       "title": "ImageAttributes represents the attributes of an image node.",
@@ -4098,7 +4125,15 @@
           "type": "string"
         },
         "node_type": {
-          "$ref": "#/definitions/uiNodeType"
+          "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0.",
+          "type": "string",
+          "enum": [
+            "text",
+            "input",
+            "img",
+            "a",
+            "script"
+          ]
         },
         "src": {
           "description": "The image's source URL.\n\nformat: uri",
@@ -4136,7 +4171,15 @@
           "type": "string"
         },
         "node_type": {
-          "$ref": "#/definitions/uiNodeType"
+          "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0.",
+          "type": "string",
+          "enum": [
+            "text",
+            "input",
+            "img",
+            "a",
+            "script"
+          ]
         },
         "onclick": {
           "description": "OnClick may contain javascript which should be executed on click. This is primarily\nused for WebAuthn.",
@@ -4201,7 +4244,15 @@
           "type": "string"
         },
         "node_type": {
-          "$ref": "#/definitions/uiNodeType"
+          "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0.",
+          "type": "string",
+          "enum": [
+            "text",
+            "input",
+            "img",
+            "a",
+            "script"
+          ]
         },
         "nonce": {
           "description": "Nonce for CSP\n\nA nonce you may want to use to improve your Content Security Policy.\nYou do not have to use this value but if you want to improve your CSP\npolicies you may use it. You can also choose to use your own nonce value!",
@@ -4235,16 +4286,21 @@
           "type": "string"
         },
         "node_type": {
-          "$ref": "#/definitions/uiNodeType"
+          "description": "NodeType represents this node's types. It is a mirror of `node.type` and\nis primarily used to allow compatibility with OpenAPI 3.0.",
+          "type": "string",
+          "enum": [
+            "text",
+            "input",
+            "img",
+            "a",
+            "script"
+          ]
         },
         "text": {
           "$ref": "#/definitions/uiText"
         }
       }
     },
-    "uiNodeType": {
-      "type": "string"
-    },
     "uiNodes": {
       "type": "array",
       "items": {
diff --git a/ui/container/container.go b/ui/container/container.go
index 6d9c0a3b3..c64682ac1 100644
--- a/ui/container/container.go
+++ b/ui/container/container.go
@@ -68,7 +68,7 @@ func New(action string) *Container {
 
 // NewFromHTTPRequest creates a new Container and populates fields by parsing the HTTP Request body.
 // A jsonSchemaRef needs to be added to allow HTTP Form Post Body parsing.
-func NewFromHTTPRequest(r *http.Request, group node.Group, action string, compiler decoderx.HTTPDecoderOption) (*Container, error) {
+func NewFromHTTPRequest(r *http.Request, group node.UiNodeGroup, action string, compiler decoderx.HTTPDecoderOption) (*Container, error) {
 	c := New(action)
 	raw := json.RawMessage(`{}`)
 	if err := decoder.Decode(r, &raw, compiler); err != nil {
@@ -82,7 +82,7 @@ func NewFromHTTPRequest(r *http.Request, group node.Group, action string, compil
 }
 
 // NewFromJSON creates a UI Container based on the provided JSON struct.
-func NewFromJSON(action string, group node.Group, raw json.RawMessage, prefix string) *Container {
+func NewFromJSON(action string, group node.UiNodeGroup, raw json.RawMessage, prefix string) *Container {
 	c := New(action)
 	c.UpdateNodeValuesFromJSON(raw, prefix, group)
 	return c
@@ -90,7 +90,7 @@ func NewFromJSON(action string, group node.Group, raw json.RawMessage, prefix st
 
 // NewFromJSONSchema creates a new Container and populates the fields
 // using the provided JSON Schema.
-func NewFromJSONSchema(ctx context.Context, action string, group node.Group, jsonSchemaRef, prefix string, compiler *jsonschema.Compiler) (*Container, error) {
+func NewFromJSONSchema(ctx context.Context, action string, group node.UiNodeGroup, jsonSchemaRef, prefix string, compiler *jsonschema.Compiler) (*Container, error) {
 	c := New(action)
 	nodes, err := NodesFromJSONSchema(ctx, group, jsonSchemaRef, prefix, compiler)
 	if err != nil {
@@ -101,7 +101,7 @@ func NewFromJSONSchema(ctx context.Context, action string, group node.Group, jso
 	return c, nil
 }
 
-func NodesFromJSONSchema(ctx context.Context, group node.Group, jsonSchemaRef, prefix string, compiler *jsonschema.Compiler) (node.Nodes, error) {
+func NodesFromJSONSchema(ctx context.Context, group node.UiNodeGroup, jsonSchemaRef, prefix string, compiler *jsonschema.Compiler) (node.Nodes, error) {
 	paths, err := jsonschemax.ListPaths(ctx, jsonSchemaRef, compiler)
 	if err != nil {
 		return nil, err
@@ -150,7 +150,7 @@ func (c *Container) Reset(exclude ...string) {
 // formUI Container, the error is returned.
 //
 // This method DOES NOT touch the values of the node values/names, only its errors.
-func (c *Container) ParseError(group node.Group, err error) error {
+func (c *Container) ParseError(group node.UiNodeGroup, err error) error {
 	if e := richError(nil); errors.As(err, &e) {
 		if e.StatusCode() == http.StatusBadRequest {
 			c.AddMessage(group, text.NewValidationErrorGeneric(e.Reason()))
@@ -195,7 +195,7 @@ func (c *Container) ParseError(group node.Group, err error) error {
 }
 
 // UpdateNodeValuesFromJSON sets the container's fields to the provided values.
-func (c *Container) UpdateNodeValuesFromJSON(raw json.RawMessage, prefix string, group node.Group) {
+func (c *Container) UpdateNodeValuesFromJSON(raw json.RawMessage, prefix string, group node.UiNodeGroup) {
 	for k, v := range jsonx.Flatten(raw) {
 		k = addPrefix(k, prefix, ".")
 
@@ -236,7 +236,7 @@ func (c *Container) SetValue(id string, n *node.Node) {
 
 // AddMessage adds the provided error, and if a non-empty names list is set,
 // adds the error on the corresponding field.
-func (c *Container) AddMessage(group node.Group, err *text.Message, setForFields ...string) {
+func (c *Container) AddMessage(group node.UiNodeGroup, err *text.Message, setForFields ...string) {
 	if len(stringslice.TrimSpaceEmptyFilter(setForFields)) == 0 {
 		c.Messages = append(c.Messages, *err)
 		return
diff --git a/ui/container/types.go b/ui/container/types.go
index 170635bda..593ba6656 100644
--- a/ui/container/types.go
+++ b/ui/container/types.go
@@ -9,7 +9,7 @@ type ErrorParser interface {
 	// ParseError type asserts the given error and sets the forms's errors or a
 	// field's errors and if the error is not something to be handled by the
 	// formUI Container itself, the error is returned for further propagation (e.g. showing a 502 status code).
-	ParseError(group node.Group, err error) error
+	ParseError(group node.UiNodeGroup, err error) error
 }
 
 type NodeSetter interface {
diff --git a/ui/node/attributes.go b/ui/node/attributes.go
index 0d7d7ba36..f59135f50 100644
--- a/ui/node/attributes.go
+++ b/ui/node/attributes.go
@@ -36,7 +36,7 @@ type Attributes interface {
 	GetValue() interface{}
 
 	// swagger:ignore
-	GetNodeType() Type
+	GetNodeType() UiNodeType
 }
 
 // InputAttributes represents the attributes of an input node
@@ -78,7 +78,7 @@ type InputAttributes struct {
 	// is primarily used to allow compatibility with OpenAPI 3.0.
 	//
 	// required: true
-	NodeType Type `json:"node_type"`
+	NodeType UiNodeType `json:"node_type"`
 }
 
 // ImageAttributes represents the attributes of an image node.
@@ -110,7 +110,7 @@ type ImageAttributes struct {
 	// is primarily used to allow compatibility with OpenAPI 3.0.
 	//
 	// required: true
-	NodeType Type `json:"node_type"`
+	NodeType UiNodeType `json:"node_type"`
 }
 
 // AnchorAttributes represents the attributes of an anchor node.
@@ -137,7 +137,7 @@ type AnchorAttributes struct {
 	// is primarily used to allow compatibility with OpenAPI 3.0.
 	//
 	// required: true
-	NodeType Type `json:"node_type"`
+	NodeType UiNodeType `json:"node_type"`
 }
 
 // TextAttributes represents the attributes of a text node.
@@ -159,7 +159,7 @@ type TextAttributes struct {
 	// is primarily used to allow compatibility with OpenAPI 3.0.
 	//
 	// required: true
-	NodeType Type `json:"node_type"`
+	NodeType UiNodeType `json:"node_type"`
 }
 
 // ScriptAttributes represent script nodes which load javascript.
@@ -214,7 +214,7 @@ type ScriptAttributes struct {
 	// is primarily used to allow compatibility with OpenAPI 3.0.
 	//
 	// required: true
-	NodeType Type `json:"node_type"`
+	NodeType UiNodeType `json:"node_type"`
 }
 
 var (
@@ -301,22 +301,22 @@ func (a *TextAttributes) Reset() {
 func (a *ScriptAttributes) Reset() {
 }
 
-func (a *InputAttributes) GetNodeType() Type {
+func (a *InputAttributes) GetNodeType() UiNodeType {
 	return a.NodeType
 }
 
-func (a *ImageAttributes) GetNodeType() Type {
+func (a *ImageAttributes) GetNodeType() UiNodeType {
 	return a.NodeType
 }
 
-func (a *AnchorAttributes) GetNodeType() Type {
+func (a *AnchorAttributes) GetNodeType() UiNodeType {
 	return a.NodeType
 }
 
-func (a *TextAttributes) GetNodeType() Type {
+func (a *TextAttributes) GetNodeType() UiNodeType {
 	return a.NodeType
 }
 
-func (a *ScriptAttributes) GetNodeType() Type {
+func (a *ScriptAttributes) GetNodeType() UiNodeType {
 	return a.NodeType
 }
diff --git a/ui/node/attributes_input.go b/ui/node/attributes_input.go
index b57c2c6c1..bfe3e78ec 100644
--- a/ui/node/attributes_input.go
+++ b/ui/node/attributes_input.go
@@ -80,7 +80,7 @@ func applyScriptAttributes(opts ScriptAttributesModifiers, attributes *ScriptAtt
 	return attributes
 }
 
-func NewInputFieldFromJSON(name string, value interface{}, group Group, opts ...InputAttributesModifier) *Node {
+func NewInputFieldFromJSON(name string, value interface{}, group UiNodeGroup, opts ...InputAttributesModifier) *Node {
 	return &Node{
 		Type:       Input,
 		Group:      group,
@@ -89,7 +89,7 @@ func NewInputFieldFromJSON(name string, value interface{}, group Group, opts ...
 	}
 }
 
-func NewInputField(name string, value interface{}, group Group, inputType InputAttributeType, opts ...InputAttributesModifier) *Node {
+func NewInputField(name string, value interface{}, group UiNodeGroup, inputType InputAttributeType, opts ...InputAttributesModifier) *Node {
 	return &Node{
 		Type:       Input,
 		Group:      group,
@@ -98,7 +98,7 @@ func NewInputField(name string, value interface{}, group Group, inputType InputA
 	}
 }
 
-func NewImageField(id string, src string, group Group, opts ...ImageAttributesModifier) *Node {
+func NewImageField(id string, src string, group UiNodeGroup, opts ...ImageAttributesModifier) *Node {
 	return &Node{
 		Type:       Image,
 		Group:      group,
@@ -107,7 +107,7 @@ func NewImageField(id string, src string, group Group, opts ...ImageAttributesMo
 	}
 }
 
-func NewTextField(id string, text *text.Message, group Group) *Node {
+func NewTextField(id string, text *text.Message, group UiNodeGroup) *Node {
 	return &Node{
 		Type:       Text,
 		Group:      group,
@@ -116,7 +116,7 @@ func NewTextField(id string, text *text.Message, group Group) *Node {
 	}
 }
 
-func NewAnchorField(id string, href string, group Group, title *text.Message) *Node {
+func NewAnchorField(id string, href string, group UiNodeGroup, title *text.Message) *Node {
 	return &Node{
 		Type:       Anchor,
 		Group:      group,
@@ -125,7 +125,7 @@ func NewAnchorField(id string, href string, group Group, title *text.Message) *N
 	}
 }
 
-func NewScriptField(name string, src string, group Group, integrity string, opts ...ScriptAttributesModifier) *Node {
+func NewScriptField(name string, src string, group UiNodeGroup, integrity string, opts ...ScriptAttributesModifier) *Node {
 	return &Node{
 		Type:  Script,
 		Group: group,
@@ -143,7 +143,7 @@ func NewScriptField(name string, src string, group Group, integrity string, opts
 	}
 }
 
-func NewInputFieldFromSchema(name string, group Group, p jsonschemax.Path, opts ...InputAttributesModifier) *Node {
+func NewInputFieldFromSchema(name string, group UiNodeGroup, p jsonschemax.Path, opts ...InputAttributesModifier) *Node {
 	attr := &InputAttributes{
 		Name:     name,
 		Type:     toFormType(p.Name, p.Type),
diff --git a/ui/node/node.go b/ui/node/node.go
index b0cf0fdca..edc38c6c2 100644
--- a/ui/node/node.go
+++ b/ui/node/node.go
@@ -17,34 +17,39 @@ import (
 	"github.com/ory/x/stringslice"
 )
 
-// swagger:model uiNodeType
-type Type string
+// swagger:enum UiNodeType
+type UiNodeType string
 
-// swagger:model uiNodeGroup
-type Group string
+const (
+	Text   UiNodeType = "text"
+	Input  UiNodeType = "input"
+	Image  UiNodeType = "img"
+	Anchor UiNodeType = "a"
+	Script UiNodeType = "script"
+)
 
-func (g Group) String() string {
-	return string(g)
+func (t UiNodeType) String() string {
+	return string(t)
 }
 
+// swagger:enum UiNodeGroup
+type UiNodeGroup string
+
 const (
-	DefaultGroup          Group = "default"
-	PasswordGroup         Group = "password"
-	OpenIDConnectGroup    Group = "oidc"
-	ProfileGroup          Group = "profile"
-	RecoveryLinkGroup     Group = "link"
-	VerificationLinkGroup Group = "link"
-	TOTPGroup             Group = "totp"
-	LookupGroup           Group = "lookup_secret"
-	WebAuthnGroup         Group = "webauthn"
-
-	Text   Type = "text"
-	Input  Type = "input"
-	Image  Type = "img"
-	Anchor Type = "a"
-	Script Type = "script"
+	DefaultGroup       UiNodeGroup = "default"
+	PasswordGroup      UiNodeGroup = "password"
+	OpenIDConnectGroup UiNodeGroup = "oidc"
+	ProfileGroup       UiNodeGroup = "profile"
+	LinkGroup          UiNodeGroup = "link"
+	TOTPGroup          UiNodeGroup = "totp"
+	LookupGroup        UiNodeGroup = "lookup_secret"
+	WebAuthnGroup      UiNodeGroup = "webauthn"
 )
 
+func (g UiNodeGroup) String() string {
+	return string(g)
+}
+
 // swagger:model uiNodes
 type Nodes []*Node
 
@@ -57,15 +62,13 @@ type Nodes []*Node
 type Node struct {
 	// The node's type
 	//
-	// Can be one of: text, input, img, a
-	//
 	// required: true
-	Type Type `json:"type" faker:"-"`
+	Type UiNodeType `json:"type" faker:"-"`
 
 	// Group specifies which group (e.g. password authenticator) this node belongs to.
 	//
 	// required: true
-	Group Group `json:"group"`
+	Group UiNodeGroup `json:"group"`
 
 	// The node's attributes.
 	//
@@ -106,8 +109,8 @@ type Meta struct {
 
 // Used for en/decoding the Attributes field.
 type jsonRawNode struct {
-	Type       Type          `json:"type"`
-	Group      Group         `json:"group"`
+	Type       UiNodeType    `json:"type"`
+	Group      UiNodeGroup   `json:"group"`
 	Attributes Attributes    `json:"attributes"`
 	Messages   text.Messages `json:"messages"`
 	Meta       *Meta         `json:"meta"`
@@ -190,7 +193,7 @@ type sortOptions struct {
 
 type SortOption func(*sortOptions)
 
-func SortByGroups(orderByGroups []Group) func(*sortOptions) {
+func SortByGroups(orderByGroups []UiNodeGroup) func(*sortOptions) {
 	return func(options *sortOptions) {
 		options.orderByGroups = make([]string, len(orderByGroups))
 		for k := range orderByGroups {
@@ -347,7 +350,7 @@ func (n *Nodes) Append(node *Node) {
 
 func (n *Node) UnmarshalJSON(data []byte) error {
 	var attr Attributes
-	switch t := gjson.GetBytes(data, "type").String(); Type(t) {
+	switch t := gjson.GetBytes(data, "type").String(); UiNodeType(t) {
 	case Text:
 		attr = &TextAttributes{
 			NodeType: Text,
@@ -388,7 +391,7 @@ func (n *Node) UnmarshalJSON(data []byte) error {
 }
 
 func (n *Node) MarshalJSON() ([]byte, error) {
-	var t Type
+	var t UiNodeType
 	if n.Attributes != nil {
 		switch attr := n.Attributes.(type) {
 		case *TextAttributes:


Test patch:
diff --git a/selfservice/flow/login/error_test.go b/selfservice/flow/login/error_test.go
index 08b294663..a7de240fe 100644
--- a/selfservice/flow/login/error_test.go
+++ b/selfservice/flow/login/error_test.go
@@ -51,7 +51,7 @@ func TestHandleError(t *testing.T) {
 
 	var loginFlow *login.Flow
 	var flowError error
-	var ct node.Group
+	var ct node.UiNodeGroup
 	router.GET("/error", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
 		h.WriteFlowError(w, r, loginFlow, ct, flowError)
 	})
diff --git a/selfservice/flow/recovery/error_test.go b/selfservice/flow/recovery/error_test.go
index a932342e4..e467a40bb 100644
--- a/selfservice/flow/recovery/error_test.go
+++ b/selfservice/flow/recovery/error_test.go
@@ -52,7 +52,7 @@ func TestHandleError(t *testing.T) {
 
 	var recoveryFlow *recovery.Flow
 	var flowError error
-	var methodName node.Group
+	var methodName node.UiNodeGroup
 	router.GET("/error", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
 		h.WriteFlowError(w, r, recoveryFlow, methodName, flowError)
 	})
@@ -195,7 +195,7 @@ func TestHandleError(t *testing.T) {
 
 			recoveryFlow = &recovery.Flow{Type: flow.TypeBrowser}
 			flowError = flow.NewFlowExpiredError(anHourAgo)
-			methodName = node.RecoveryLinkGroup
+			methodName = node.LinkGroup
 
 			lf, _ := expectRecoveryUI(t)
 			require.Len(t, lf.UI.Messages, 1, "%s", jsonx.TestMarshalJSONString(t, lf))
@@ -207,7 +207,7 @@ func TestHandleError(t *testing.T) {
 
 			recoveryFlow = newFlow(t, time.Minute, flow.TypeBrowser)
 			flowError = schema.NewInvalidCredentialsError()
-			methodName = node.RecoveryLinkGroup
+			methodName = node.LinkGroup
 
 			lf, _ := expectRecoveryUI(t)
 			require.NotEmpty(t, lf.UI, x.MustEncodeJSON(t, lf))
@@ -220,7 +220,7 @@ func TestHandleError(t *testing.T) {
 
 			recoveryFlow = newFlow(t, time.Minute, flow.TypeBrowser)
 			flowError = herodot.ErrInternalServerError.WithReason("system error")
-			methodName = node.RecoveryLinkGroup
+			methodName = node.LinkGroup
 
 			sse, _ := expectErrorUI(t)
 			assertx.EqualAsJSON(t, flowError, sse)
diff --git a/selfservice/flow/registration/error_test.go b/selfservice/flow/registration/error_test.go
index 7ba809409..3e1b1fd57 100644
--- a/selfservice/flow/registration/error_test.go
+++ b/selfservice/flow/registration/error_test.go
@@ -54,7 +54,7 @@ func TestHandleError(t *testing.T) {
 
 	var registrationFlow *registration.Flow
 	var flowError error
-	var group node.Group
+	var group node.UiNodeGroup
 	router.GET("/error", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
 		h.WriteFlowError(w, r, registrationFlow, group, flowError)
 	})
diff --git a/selfservice/flow/settings/error_test.go b/selfservice/flow/settings/error_test.go
index 9a7f8cefe..e446b5e83 100644
--- a/selfservice/flow/settings/error_test.go
+++ b/selfservice/flow/settings/error_test.go
@@ -57,7 +57,7 @@ func TestHandleError(t *testing.T) {
 
 	var settingsFlow *settings.Flow
 	var flowError error
-	var flowMethod node.Group
+	var flowMethod node.UiNodeGroup
 	var id identity.Identity
 	require.NoError(t, faker.FakeData(&id))
 	id.SchemaID = "default"
diff --git a/selfservice/flow/verification/error_test.go b/selfservice/flow/verification/error_test.go
index 969a32f68..21c990626 100644
--- a/selfservice/flow/verification/error_test.go
+++ b/selfservice/flow/verification/error_test.go
@@ -52,7 +52,7 @@ func TestHandleError(t *testing.T) {
 
 	var verificationFlow *verification.Flow
 	var flowError error
-	var methodName node.Group
+	var methodName node.UiNodeGroup
 	router.GET("/error", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
 		h.WriteFlowError(w, r, verificationFlow, methodName, flowError)
 	})
@@ -195,7 +195,7 @@ func TestHandleError(t *testing.T) {
 
 			verificationFlow = &verification.Flow{Type: flow.TypeBrowser}
 			flowError = flow.NewFlowExpiredError(anHourAgo)
-			methodName = node.VerificationLinkGroup
+			methodName = node.LinkGroup
 
 			lf, _ := expectVerificationUI(t)
 			require.Len(t, lf.UI.Messages, 1, "%s", jsonx.TestMarshalJSONString(t, lf))
@@ -207,7 +207,7 @@ func TestHandleError(t *testing.T) {
 
 			verificationFlow = newFlow(t, time.Minute, flow.TypeBrowser)
 			flowError = schema.NewInvalidCredentialsError()
-			methodName = node.VerificationLinkGroup
+			methodName = node.LinkGroup
 
 			lf, _ := expectVerificationUI(t)
 			require.NotEmpty(t, lf.UI, x.MustEncodeJSON(t, lf))
@@ -220,7 +220,7 @@ func TestHandleError(t *testing.T) {
 
 			verificationFlow = newFlow(t, time.Minute, flow.TypeBrowser)
 			flowError = herodot.ErrInternalServerError.WithReason("system error")
-			methodName = node.VerificationLinkGroup
+			methodName = node.LinkGroup
 
 			sse, _ := expectErrorUI(t)
 			assertx.EqualAsJSON(t, flowError, sse)
diff --git a/selfservice/strategy/link/strategy_recovery_test.go b/selfservice/strategy/link/strategy_recovery_test.go
index 5763bde40..8d47dc903 100644
--- a/selfservice/strategy/link/strategy_recovery_test.go
+++ b/selfservice/strategy/link/strategy_recovery_test.go
@@ -251,7 +251,7 @@ func TestRecovery(t *testing.T) {
 
 	t.Run("description=should require an email to be sent", func(t *testing.T) {
 		var check = func(t *testing.T, actual string) {
-			assert.EqualValues(t, node.RecoveryLinkGroup, gjson.Get(actual, "active").String(), "%s", actual)
+			assert.EqualValues(t, node.LinkGroup, gjson.Get(actual, "active").String(), "%s", actual)
 			assert.EqualValues(t, "Property email is missing.",
 				gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(),
 				"%s", actual)
@@ -276,7 +276,7 @@ func TestRecovery(t *testing.T) {
 
 	t.Run("description=should require a valid email to be sent", func(t *testing.T) {
 		var check = func(t *testing.T, actual string, value string) {
-			assert.EqualValues(t, node.RecoveryLinkGroup, gjson.Get(actual, "active").String(), "%s", actual)
+			assert.EqualValues(t, node.LinkGroup, gjson.Get(actual, "active").String(), "%s", actual)
 			assert.EqualValues(t, fmt.Sprintf("%q is not valid \"email\"", value),
 				gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(),
 				"%s", actual)
@@ -356,7 +356,7 @@ func TestRecovery(t *testing.T) {
 	t.Run("description=should try to recover an email that does not exist", func(t *testing.T) {
 		var email string
 		var check = func(t *testing.T, actual string) {
-			assert.EqualValues(t, node.RecoveryLinkGroup, gjson.Get(actual, "active").String(), "%s", actual)
+			assert.EqualValues(t, node.LinkGroup, gjson.Get(actual, "active").String(), "%s", actual)
 			assert.EqualValues(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual)
 			assertx.EqualAsJSON(t, text.NewRecoveryEmailSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw))
 
@@ -443,7 +443,7 @@ func TestRecovery(t *testing.T) {
 			assert.Nil(t, addr.VerifiedAt)
 			assert.Equal(t, identity.VerifiableAddressStatusPending, addr.Status)
 
-			assert.EqualValues(t, node.RecoveryLinkGroup, gjson.Get(recoverySubmissionResponse, "active").String(), "%s", recoverySubmissionResponse)
+			assert.EqualValues(t, node.LinkGroup, gjson.Get(recoverySubmissionResponse, "active").String(), "%s", recoverySubmissionResponse)
 			assert.EqualValues(t, recoveryEmail, gjson.Get(recoverySubmissionResponse, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", recoverySubmissionResponse)
 			require.Len(t, gjson.Get(recoverySubmissionResponse, "ui.messages").Array(), 1, "%s", recoverySubmissionResponse)
 			assertx.EqualAsJSON(t, text.NewRecoveryEmailSent(), json.RawMessage(gjson.Get(recoverySubmissionResponse, "ui.messages.0").Raw))
diff --git a/selfservice/strategy/link/strategy_verification_test.go b/selfservice/strategy/link/strategy_verification_test.go
index b0d10a45e..d1d064a90 100644
--- a/selfservice/strategy/link/strategy_verification_test.go
+++ b/selfservice/strategy/link/strategy_verification_test.go
@@ -112,7 +112,7 @@ func TestVerification(t *testing.T) {
 
 	t.Run("description=should require an email to be sent", func(t *testing.T) {
 		var check = func(t *testing.T, actual string) {
-			assert.EqualValues(t, string(node.VerificationLinkGroup), gjson.Get(actual, "active").String(), "%s", actual)
+			assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual)
 			assert.EqualValues(t, "Property email is missing.",
 				gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(),
 				"%s", actual)
@@ -137,7 +137,7 @@ func TestVerification(t *testing.T) {
 
 	t.Run("description=should require a valid email to be sent", func(t *testing.T) {
 		var check = func(t *testing.T, actual string, value string) {
-			assert.EqualValues(t, string(node.VerificationLinkGroup), gjson.Get(actual, "active").String(), "%s", actual)
+			assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual)
 			assert.EqualValues(t, fmt.Sprintf("%q is not valid \"email\"", value),
 				gjson.Get(actual, "ui.nodes.#(attributes.name==email).messages.0.text").String(),
 				"%s", actual)
@@ -165,7 +165,7 @@ func TestVerification(t *testing.T) {
 	t.Run("description=should try to verify an email that does not exist", func(t *testing.T) {
 		var email string
 		var check = func(t *testing.T, actual string) {
-			assert.EqualValues(t, string(node.VerificationLinkGroup), gjson.Get(actual, "active").String(), "%s", actual)
+			assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual)
 			assert.EqualValues(t, email, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual)
 			assertx.EqualAsJSON(t, text.NewVerificationEmailSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw))
 
@@ -260,7 +260,7 @@ func TestVerification(t *testing.T) {
 
 	t.Run("description=should verify an email address", func(t *testing.T) {
 		var check = func(t *testing.T, actual string) {
-			assert.EqualValues(t, string(node.VerificationLinkGroup), gjson.Get(actual, "active").String(), "%s", actual)
+			assert.EqualValues(t, string(node.LinkGroup), gjson.Get(actual, "active").String(), "%s", actual)
 			assert.EqualValues(t, verificationEmail, gjson.Get(actual, "ui.nodes.#(attributes.name==email).attributes.value").String(), "%s", actual)
 			assertx.EqualAsJSON(t, text.NewVerificationEmailSent(), json.RawMessage(gjson.Get(actual, "ui.messages.0").Raw))
 
diff --git a/selfservice/strategy/oidc/provider_private_net_test.go b/selfservice/strategy/oidc/provider_private_net_test.go
index a18e0bd99..caa4d0ff9 100644
--- a/selfservice/strategy/oidc/provider_private_net_test.go
+++ b/selfservice/strategy/oidc/provider_private_net_test.go
@@ -71,6 +71,7 @@ func TestProviderPrivateIP(t *testing.T) {
 		// Spotify uses a fixed token URL and does not use the issuer.
 		// VK uses a fixed token URL and does not use the issuer.
 		// Yandex uses a fixed token URL and does not use the issuer.
+		// NetID uses a fixed token URL and does not use the issuer.
 	} {
 		t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
 			p := tc.p(tc.c)
diff --git a/selfservice/strategy/password/validator_test.go b/selfservice/strategy/password/validator_test.go
index 13577fad8..1430ca00d 100644
--- a/selfservice/strategy/password/validator_test.go
+++ b/selfservice/strategy/password/validator_test.go
@@ -28,7 +28,7 @@ func TestDefaultPasswordValidationStrategy(t *testing.T) {
 
 	t.Run("default strategy", func(t *testing.T) {
 		_, reg := internal.NewFastRegistryWithMocks(t)
-		s := password.NewDefaultPasswordValidatorStrategy(reg)
+		s, _ := password.NewDefaultPasswordValidatorStrategy(reg)
 		for k, tc := range []struct {
 			id   string
 			pw   string
@@ -80,7 +80,7 @@ func TestDefaultPasswordValidationStrategy(t *testing.T) {
 
 	t.Run("failure cases", func(t *testing.T) {
 		conf, reg := internal.NewFastRegistryWithMocks(t)
-		s := password.NewDefaultPasswordValidatorStrategy(reg)
+		s, _ := password.NewDefaultPasswordValidatorStrategy(reg)
 		fakeClient := NewFakeHTTPClient()
 		s.Client = httpx.NewResilientClient(httpx.ResilientClientWithClient(&fakeClient.Client), httpx.ResilientClientWithMaxRetry(1), httpx.ResilientClientWithConnectionTimeout(time.Millisecond), httpx.ResilientClientWithMaxRetryWait(time.Millisecond))
 
@@ -117,7 +117,7 @@ func TestDefaultPasswordValidationStrategy(t *testing.T) {
 
 	t.Run("max breaches", func(t *testing.T) {
 		conf, reg := internal.NewFastRegistryWithMocks(t)
-		s := password.NewDefaultPasswordValidatorStrategy(reg)
+		s, _ := password.NewDefaultPasswordValidatorStrategy(reg)
 		fakeClient := NewFakeHTTPClient()
 		s.Client = httpx.NewResilientClient(httpx.ResilientClientWithClient(&fakeClient.Client), httpx.ResilientClientWithMaxRetry(1), httpx.ResilientClientWithConnectionTimeout(time.Millisecond))
 
@@ -194,7 +194,7 @@ func TestChangeHaveIBeenPwnedValidationHost(t *testing.T) {
 	testServer.StartTLS()
 	testServerURL, _ := url.Parse(testServer.URL)
 	conf, reg := internal.NewFastRegistryWithMocks(t)
-	s := password.NewDefaultPasswordValidatorStrategy(reg)
+	s, _ := password.NewDefaultPasswordValidatorStrategy(reg)
 	conf.MustSet(config.ViperKeyPasswordHaveIBeenPwnedHost, testServerURL.Host)
 
 	fakeClient := NewFakeHTTPClient()
@@ -211,7 +211,7 @@ func TestChangeHaveIBeenPwnedValidationHost(t *testing.T) {
 
 func TestDisableHaveIBeenPwnedValidationHost(t *testing.T) {
 	conf, reg := internal.NewFastRegistryWithMocks(t)
-	s := password.NewDefaultPasswordValidatorStrategy(reg)
+	s, _ := password.NewDefaultPasswordValidatorStrategy(reg)
 	conf.MustSet(config.ViperKeyPasswordHaveIBeenPwnedEnabled, false)
 
 	fakeClient := NewFakeHTTPClient()
@@ -225,7 +225,7 @@ func TestDisableHaveIBeenPwnedValidationHost(t *testing.T) {
 
 func TestChangeMinPasswordLength(t *testing.T) {
 	conf, reg := internal.NewFastRegistryWithMocks(t)
-	s := password.NewDefaultPasswordValidatorStrategy(reg)
+	s, _ := password.NewDefaultPasswordValidatorStrategy(reg)
 	conf.MustSet(config.ViperKeyPasswordMinLength, 10)
 
 	t.Run("case=should not fail if password is longer than min length", func(t *testing.T) {
@@ -239,7 +239,7 @@ func TestChangeMinPasswordLength(t *testing.T) {
 
 func TestChangeIdentifierSimilarityCheckEnabled(t *testing.T) {
 	conf, reg := internal.NewFastRegistryWithMocks(t)
-	s := password.NewDefaultPasswordValidatorStrategy(reg)
+	s, _ := password.NewDefaultPasswordValidatorStrategy(reg)
 
 	t.Run("case=should not fail if password is similar to identifier", func(t *testing.T) {
 		conf.MustSet(config.ViperKeyPasswordIdentifierSimilarityCheckEnabled, false)
diff --git a/ui/node/attributes_test.go b/ui/node/attributes_test.go
index f4c3624c3..a158cdcca 100644
--- a/ui/node/attributes_test.go
+++ b/ui/node/attributes_test.go
@@ -41,7 +41,7 @@ func TestNodeEncode(t *testing.T) {
 }
 
 func TestNodeDecode(t *testing.T) {
-	for _, kind := range []Type{
+	for _, kind := range []UiNodeType{
 		Text,
 		Input,
 		Image,
diff --git a/ui/node/node_test.go b/ui/node/node_test.go
index 493825943..2a26eced9 100644
--- a/ui/node/node_test.go
+++ b/ui/node/node_test.go
@@ -49,19 +49,19 @@ func TestNodesSort(t *testing.T) {
 		"1.json": {
 			node.SortUseOrder([]string{"password_identifier"}),
 			node.SortUpdateOrder(node.PasswordLoginOrder),
-			node.SortByGroups([]node.Group{
+			node.SortByGroups([]node.UiNodeGroup{
 				node.DefaultGroup,
 				node.ProfileGroup,
 				node.OpenIDConnectGroup,
 				node.PasswordGroup,
-				node.RecoveryLinkGroup,
-				node.VerificationLinkGroup,
+				node.LinkGroup,
+				node.LinkGroup,
 			}),
 		},
 		"2.json": {
 			node.SortBySchema(filepath.Join("fixtures/sort/schema", "2.json")),
 			node.SortUpdateOrder(node.PasswordLoginOrder),
-			node.SortByGroups([]node.Group{
+			node.SortByGroups([]node.UiNodeGroup{
 				node.DefaultGroup,
 				node.OpenIDConnectGroup,
 				node.PasswordGroup,
@@ -69,7 +69,7 @@ func TestNodesSort(t *testing.T) {
 		},
 		"3.json": {
 			node.SortBySchema(filepath.Join("fixtures/sort/schema", "3.json")),
-			node.SortByGroups([]node.Group{
+			node.SortByGroups([]node.UiNodeGroup{
 				node.DefaultGroup,
 				node.OpenIDConnectGroup,
 				node.PasswordGroup,
@@ -77,7 +77,7 @@ func TestNodesSort(t *testing.T) {
 		},
 		"4.json": {
 			node.SortBySchema(filepath.Join("fixtures/sort/schema", "4.json")),
-			node.SortByGroups([]node.Group{
+			node.SortByGroups([]node.UiNodeGroup{
 				node.DefaultGroup,
 				node.ProfileGroup,
 				node.PasswordGroup,