Repository: ory/kratos
Pull request #196 (2020-01-28 14:56:54)

Resolve build issues with CGO
## Related issue

<!--
Please link the GitHub issue this pull request resolves in the format of `#1234`. If you discussed this change
with a maintainer, please mention her/him using the `@` syntax (e.g. `@aeneasr`).

If this change neither resolves an existing issue nor has sign-off from one of the maintainers, there is a
chance substantial changes will be requested or that the changes will be rejected.

You can discuss changes with maintainers either in the [ORY Community Forums](https://community.ory.sh/) or
join the [ORY Chat](https://www.ory.sh/chat).
-->

## Proposed changes

<!--
Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request.
-->

## Checklist

<!--
Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of
them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.
-->

- [ ] I have read the [contributing guidelines](../blob/master/CONTRIBUTING.md)
- [ ] 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 necessary documentation within the code base (if appropriate)
- [ ] I have documented my changes in the [developer guide](https://github.com/ory/docs) (if appropriate)

## 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...
-->



Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 1c650d8ec..73c60dd3d 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -3,7 +3,7 @@ version: 2.1
 orbs:
   sdk: ory/sdk@0.1.13
   changelog: ory/changelog@0.0.5
-  goreleaser: ory/goreleaser@0.0.8
+  goreleaser: ory/goreleaser@0.0.20
   nancy: ory/nancy@0.0.8
 
 jobs:
@@ -63,7 +63,7 @@ jobs:
         image: cockroachdb/cockroach:v19.2.0
         command: start --insecure
       -
-        image: oryd/hydra:v1.0.0
+        image: oryd/hydra:v1.2.2
         environment:
           - DSN=memory
           - URLS_SELF_ISSUER=http://127.0.0.1:4444/
@@ -142,6 +142,12 @@ workflows:
           filters:
             tags:
               only: /.*/
+# Uncomment this to enable the dry-release test pipeline. Useful for debugging builds.
+#
+#      - goreleaser/release-dry:
+#          filters:
+#            tags:
+#              only: /.*/
       - goreleaser/release:
           requires:
             - goreleaser/test
diff --git a/Dockerfile b/.docker/Dockerfile
similarity index 99%
rename from Dockerfile
rename to .docker/Dockerfile
index 7e200b350..111e724ae 100644
--- a/Dockerfile
+++ b/.docker/Dockerfile
@@ -1,6 +1,7 @@
 FROM alpine:3.11
 
 RUN apk add -U --no-cache ca-certificates
+
 COPY kratos /usr/bin/kratos
 
 USER 1000
diff --git a/Dockerfile-build b/.docker/Dockerfile-build
similarity index 100%
rename from Dockerfile-build
rename to .docker/Dockerfile-build
diff --git a/.docker/Dockerfile-goreleaser b/.docker/Dockerfile-goreleaser
new file mode 100644
index 000000000..51b5301a6
--- /dev/null
+++ b/.docker/Dockerfile-goreleaser
@@ -0,0 +1,11 @@
+FROM oryd/xgoreleaser:1.13.5
+
+WORKDIR /go/src/github.com/ory/kratos
+
+ADD . .
+
+ENV RELEASE_NAME=v0.0.0-alpha.1
+ENTRYPOINT ["goreleaser"]
+CMD ["--snapshot", "--skip-publish", "--rm-dist"]
+
+# docker build -f .docker/Dockerfile-goreleaser -t build .; docker run --rm build
diff --git a/.docker/Dockerfile-sqlite b/.docker/Dockerfile-sqlite
new file mode 100644
index 000000000..8345cce78
--- /dev/null
+++ b/.docker/Dockerfile-sqlite
@@ -0,0 +1,10 @@
+FROM alpine:3.11
+
+RUN apk add -U --no-cache ca-certificates
+
+COPY kratos-sqlite-libmusl /usr/bin/kratos
+
+USER 1000
+
+ENTRYPOINT ["kratos"]
+CMD ["serve"]
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 9929d8b71..119e578ed 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -4,7 +4,6 @@ project_name: kratos
 
 env:
   - GO111MODULE=on
-  - CGO_ENABLED=1
 
 before:
   hooks:
@@ -12,20 +11,90 @@ before:
 
 builds:
   -
-    id: kratos
+    id: kratos-sqlite-darwin
     flags:
       - -a
+      - -tags
+      - sqlite
     ldflags:
       - -s -w -X github.com/ory/kratos/cmd.Version={{.Env.RELEASE_NAME}} -X github.com/ory/kratos/cmd.Commit={{.FullCommit}} -X github.com/ory/kratos/cmd.Date={{.Date}}
-    binary: kratos
+      # - "-extldflags '-static'"
+    binary: kratos-sqlite
+    env:
+      - CGO_ENABLED=1
+      - CC=o64-clang
+      - CXX=o64-clang++
+    goarch:
+      - amd64
+    goos:
+      - darwin
+
+  -
+    id: kratos-sqlite-linux
+    flags:
+      - -a
+      - -tags
+      - sqlite
+    ldflags:
+      - -s -w -X github.com/ory/kratos/cmd.Version={{.Env.RELEASE_NAME}} -X github.com/ory/kratos/cmd.Commit={{.FullCommit}} -X github.com/ory/kratos/cmd.Date={{.Date}}
+    binary: kratos-sqlite
+    env:
+      - CGO_ENABLED=1
+    goarch:
+      - amd64
+    goos:
+      - linux
+  -
+    id: kratos-sqlite-linux-libmusl
+    flags:
+      - -a
+      - -tags
+      - sqlite
+    ldflags:
+      - -s -w -X github.com/ory/kratos/cmd.Version={{.Env.RELEASE_NAME}} -X github.com/ory/kratos/cmd.Commit={{.FullCommit}} -X github.com/ory/kratos/cmd.Date={{.Date}}
+    binary: kratos-sqlite-libmusl
     env:
       - CGO_ENABLED=1
+      - CC=musl-gcc
+    goarch:
+      - amd64
+    goos:
+      - linux
+  -
+    id: kratos-sqlite-windows
+    flags:
+      - -a
+      - -tags
+      - sqlite
+    ldflags:
+      - -s -w -X github.com/ory/kratos/cmd.Version={{.Env.RELEASE_NAME}} -X github.com/ory/kratos/cmd.Commit={{.FullCommit}} -X github.com/ory/kratos/cmd.Date={{.Date}}
+      - "-extldflags '-static'"
+    binary: kratos-sqlite
+    env:
+      - CGO_ENABLED=1
+      - CC=x86_64-w64-mingw32-gcc
+      - CXX=x86_64-w64-mingw32-g++
+    goarch:
+      - amd64
+    goos:
+      - windows
+
+  -
+    id: kratos
+    ldflags:
+      - -a
+      - -s -w -X github.com/ory/kratos/cmd.Version={{.Env.RELEASE_NAME}} -X github.com/ory/kratos/cmd.Commit={{.FullCommit}} -X github.com/ory/kratos/cmd.Date={{.Date}}
+    binary: kratos
+    env:
+      - CGO_ENABLED=0
     goarch:
       - amd64
       - 386
       - arm
       - arm64
     goarm:
+      - 5
+      - 6
       - 7
     goos:
       - freebsd
@@ -34,29 +103,60 @@ builds:
       - darwin
 
 archives:
-  - builds:
+  - id: kratos-sqlite
+    builds:
+      - kratos-sqlite-windows
+      - kratos-sqlite-macos
+      - kratos-sqlite-linux
+    replacements:
+      darwin: macos
+      386: 32bit
+      amd64: 64bit
+      arm: arm32
+    format_overrides:
+      -
+        goos: windows
+        format: zip
+    files:
+      - README.md
+      - LICENSE
+    name_template: "{{ .ProjectName }}_sqlite_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
+
+  - id: kratos
+    builds:
       - kratos
-    id: kratos-archive
     replacements:
-      darwin: macOS
-      linux: Linux
-      windows: Windows
-      386: 32-bit
-      amd64: 64-bit
+      darwin: macos
+      386: 32bit
+      amd64: 64bit
+      arm: arm32
     format_overrides:
       - goos: windows
         format: zip
+    files:
+      - README.md
+      - LICENSE
+
+  - id: kratos-brew
+    builds:
+      - kratos-sqlite-macos
+    replacements:
+      darwin: macos
 
 snapshot:
   name_template: "{{ .Tag }}-next"
+
 changelog:
   sort: asc
+  filters:
+    exclude:
+      - "CI skip"
 brews:
   - github:
       owner: ory
       name: homebrew-kratos
     ids:
-    - kratos-archive
+    - kratos-brew
     homepage:  https://www.ory.sh
     commit_author:
       name: ory
@@ -71,8 +171,24 @@ scoop:
     name: ory
     email: hi@ory.sh
 
+checksum:
+  algorithm: sha256
+
 dockers:
-  - dockerfile: Dockerfile
+  - dockerfile: .docker/Dockerfile-sqlite
+    builds:
+      - kratos-sqlite-linux-libmusl
+    binaries:
+      - kratos-sqlite-libmusl
+    image_templates:
+      - "oryd/kratos:v{{ .Major }}-sqlite"
+      - "oryd/kratos:v{{ .Major }}.{{ .Minor }}-sqlite"
+      - "oryd/kratos:v{{ .Major }}.{{ .Minor }}.{{ .Patch }}-sqlite"
+      - "oryd/kratos:{{ .Env.DOCKER_SHORT_TAG }}-sqlite"
+      - "oryd/kratos:latest-sqlite"
+  - dockerfile: .docker/Dockerfile
+    builds:
+      - kratos
     binaries:
       - kratos
     image_templates:
@@ -85,3 +201,6 @@ dockers:
 release:
   prerelease: auto
   name_template: "{{.Env.RELEASE_NAME}}"
+  ids:
+    - kratos-sqlite
+    - kratos
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e69de29bb..132c88c09 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -0,0 +1,1 @@
+## Changelog
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 6190eddf8..e62655de4 100644
--- a/Makefile
+++ b/Makefile
@@ -5,6 +5,10 @@ ifeq (, $(shell which golangci-lint))
     curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
 endif
 
+.PHONY: tools
+tools:
+		go install github.com/ory/go-acc github.com/ory/x/tools/listx github.com/go-swagger/go-swagger/cmd/swagger github.com/go-bindata/go-bindata/go-bindata github.com/sqs/goreturns github.com/ory/sdk/swagutil
+
 .PHONY: build
 build:
 		make sqlbin
@@ -55,21 +59,15 @@ test-resetdb:
 		docker kill kratos_test_database_cockroach || true
 		docker rm -f kratos_test_database_mysql || true
 		docker rm -f kratos_test_database_postgres || true
-		docker run --rm --name kratos_test_database_postgres -p 3445:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=postgres -d postgres:9.6
 		docker rm -f kratos_test_database_cockroach || true
 		docker run --rm --name kratos_test_database_mysql -p 3444:3306 -e MYSQL_ROOT_PASSWORD=secret -d mysql:5.7
-		docker run --rm --name kratos_test_database_postgres -p 3445:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=kratos -d postgres:9.6
+		docker run --rm --name kratos_test_database_postgres -p 3445:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=postgres -d postgres:9.6
 		docker run --rm --name kratos_test_database_cockroach -p 3446:26257 -d cockroachdb/cockroach:v2.1.6 start --insecure
 
 .PHONY: test
 test:
-		go test -short -tags sqlite ./...
-
-.PHONY: test-integration
-test-integration:
 		go test -tags sqlite ./...
 
-
 # Generates the SDKs
 .PHONY: sdk
 sdk:
@@ -87,6 +85,16 @@ quickstart:
 		docker-compose -f quickstart.yml up --build --force-recreate
 
 quickstart-dev:
-		docker build -f Dockerfile-build -t oryd/kratos:latest .
+		docker build -f .docker/Dockerfile-build -t oryd/kratos:latest .
 		docker pull oryd/kratos-selfservice-ui-node:latest
 		docker-compose -f quickstart.yml up --build --force-recreate
+
+# Formats the code
+.PHONY: format
+format:
+		$$(go env GOPATH)/bin/goreturns -w -local github.com/ory $$($$(go env GOPATH)/bin/listx .)
+
+# Runs tests in short mode, without database adapters
+.PHONY: docker
+docker:
+		docker build -f .docker/Dockerfile-build -t oryd/kratos:latest .
\ No newline at end of file
diff --git a/README.md b/README.md
index a5ab0f218..eced2f556 100644
--- a/README.md
+++ b/README.md
@@ -22,38 +22,114 @@
 
 ORY Kratos is the first and only cloud native Identity and User Management System in the world. Finally, it is no longer necessary to implement a User Login process for the umpteenth time! 
 
-ORY Kratos includes:
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**
+
+- [What is ORY Kratos?](#what-is-ory-kratos)
+  - [Who's using it?](#whos-using-it)
+- [Getting Started](#getting-started)
+  - [Quickstart](#quickstart)
+  - [Installation](#installation)
+- [Ecosystem](#ecosystem)
+  - [ORY Security Console: Administrative User Interface](#ory-security-console-administrative-user-interface)
+  - [ORY Oathkeeper: Identity & Access Proxy](#ory-oathkeeper-identity--access-proxy)
+  - [ORY Keto: Access Control Policies as a Server](#ory-keto-access-control-policies-as-a-server)
+- [Security](#security)
+  - [Disclosing vulnerabilities](#disclosing-vulnerabilities)
+- [Telemetry](#telemetry)
+- [Documentation](#documentation)
+  - [Guide](#guide)
+  - [HTTP API documentation](#http-api-documentation)
+  - [Upgrading and Changelog](#upgrading-and-changelog)
+  - [Command line documentation](#command-line-documentation)
+  - [Develop](#develop)
+    - [Dependencies](#dependencies)
+      - [Install Tools](#install-tools)
+    - [Formatting Code](#formatting-code)
+    - [Running Tests](#running-tests)
+      - [Short Tests](#short-tests)
+      - [Regular Tests](#regular-tests)
+    - [Build Docker](#build-docker)
 
-- **User login and registration** using a variety of configurable authentication mechanisms: **Username/Email + Password**, **Social Sign In** ("Sign in with GitHub, Google, ..."), with or without password and others.
-- **Multi-factor authentication** supporting a wide range of protocols such as [Google Authenticator](https://en.wikipedia.org/wiki/Google_Authenticator) (formalized as [RFC 6238](https://tools.ietf.org/html/rfc6238) and [IETF RFC 4226](https://tools.ietf.org/html/rfc4226)).
-- **Account verification** and **account recovery** by several methods: E-Mail, recovery codes, ...
-- **Storing user information** in a way that does not enforce *our* normative data model on *you*. With ORY Kratos developers define the dataset users may store using [JSON Schema](https://json-schema.org/). ORY Kratos supports multiple identity types - every identity can have its own JSON Schema - even versioned!
-- **Headless UI** - instead of learning some custom, and probably inferior, template engine, developers can choose their own! ORY Kratos is all APIs. UI's can be programmed in various languages (JavaScript, Node, Java, PHP, ...) and numerous framework (React, Vue, Angular, ...)! Check out the [reference UI implementation](https://github.com/ory/kratos-selfservice-ui-node) - it's less than 100 lines of code!
-- **Workflow engine** to decide what happens after, for example, a user signs up (redirect somewhere? require activation before login? issue session right away?) as well as to notify other systems on certain actions ("create a Stripe account after sign up", "synchronize with newsletter", ...).
-- ... and of course many more features that will be discussed outside of the Introduction in the Ory Kratos documentation [here](https://www.ory.sh/docs/next/kratos).
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
 
-## Timeline
+## What is ORY Kratos?
 
-ORY Kratos is **not yet released** and is **undergoing continuous and active development**. The core featureset is done but several more steps are required before version 0.0.1 can be released. To find out the current progress, planned features for each milestone, and more information please refer to [milestones](https://github.com/ory/kratos/milestones).
+ORY Kratos is an API-first Identity and User Management system that is built
+according to
+[cloud architecture best practices](https://www.ory.sh/docs/next/ecosystem/software-architecture-philosophy).
+It implements core use cases that almost every software application needs to
+deal with:
 
-## What's different?
+- **Self-service Login and Registration**: Allow end-users to create and sign
+  into accounts (we call them **identities**) using Username / Email and
+  password combinations, Social Sign In ("Sign in with Google, GitHub"),
+  Passwordless flows, and others.
+- **Multi-Factor Authentication (MFA/2FA)**: Support protocols such as TOTP
+  ([RFC 6238](https://tools.ietf.org/html/rfc6238) and
+  [IETF RFC 4226](https://tools.ietf.org/html/rfc4226) - better known as
+  [Google Authenticator](https://en.wikipedia.org/wiki/Google_Authenticator))
+- **Account Verification**: Verify that an E-Mail address, phone number, or
+  physical address actually belong to that identity.
+- **Account Recovery**: Recover access using "Forgot Password" flows, Security
+  Codes (in case of MKFA device loss), and others.
+- **Profile and Account Management**: Update passwords, personal details, email
+  addresses, linked social profiles using secure flows.
+- **Admin APIs**: Import, update, delete identities.
 
-> This section is a work in progress.
+We highly recommend reading the [ORY Kratos introduction docs](https://www.ory.sh/docs/next/kratos/)
+to learn more about ORY Krato's background, feature set, and differentiation
+from other products.
 
-- There is no templating as with other full-stack solutions. You implement a "login, registration, ... UI" that interacts with ORY Kratos. For instance:
+### Who's using it?
 
-   * _Want Progressive Registration?_       _No problem._ 
+<!--BEGIN ADOPTERS-->
+<!--END ADOPTERS-->
 
-   * _Just need a username on sign up?_      _Sure!_ 
+## Getting Started
 
-   * _How about your favorite pet name as a required sign up field?_      _Of course!_
+To get started, head over to the [ORY Kratos Documentation](https://www.ory.sh/docs/next/kratos).
 
-- While other solutions support an API-driven approach, they leave you with the burden of making things secure, e.g. CSRF Tokens, storing state, and so on. In ORY Kratos, all of this is done using - among others - HTTP Redirection.
-- ORY Kratos does not need OAuth2 and OpenID Connect. We know that big players in the market have tried selling you OAuth2 and OpenID Connect for years as "the most secure" and "a very easy to use" protocol. Fact is, OAuth2 and OpenID Connect are not designed for first-party use ("I just want people to be able to log into my mobile app"). ORY Kratos makes integration a one-minute process using a HTTP Reverse Proxy. [Reverse proxy] (https://en.wikipedia.org/wiki/Reverse_proxy) (https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html) **Include links to docs here.** Ory Hydra is a OAuth2 Server and OpenID Certified™ OpenID Connect Provider written in Go - cloud native, security-first, open source API security for your infrastructure. If the goal is to become the new "Sign in with Google" provider, use ORY Hydra and natively integrate with ORY Kratos!
-- The "Sign Up" and "Login" process is customisable. The developer determines next step after sign up and login, for instance:
-"Redirect the user to a certain page?" 
-"Create a Stripe account?" or
-"Require account activation via email before being allowed to sign in?"
+### Quickstart
+
+The **[ORY Kratos Quickstart](https://www.ory.sh/docs/next/kratos/quickstart)** teaches you ORY Kratos basics
+and sets up an example based on Docker Compose in less than five minutes.
+
+### Installation
+
+Head over to the [ORY Developer Documentation](https://www.ory.sh/docs/next/kratos/install.md) to learn how to install ORY Kratos on Linux, macOS, Windows, and Docker and how to build ORY Kratos from source.
+
+## Ecosystem
+
+<a href="https://console.ory.sh/">
+    <img align="right" width="30%" src="docs/images/sec-console.png" alt="ORY Security Console">
+</a>
+
+### ORY Security Console: Administrative User Interface
+
+The [ORY Security Console](https://console.ory.sh/) is a visual admin interface for managing ORY Kratos,
+ORY Oathkeeper, and ORY Keto.
+
+### ORY Oathkeeper: Identity & Access Proxy
+
+[ORY Oathkeeper](https://github.com/ory/oathkeeper) is a BeyondCorp/Zero Trust Identity & Access Proxy (IAP) built
+on top of OAuth2 and ORY Kratos.
+
+### ORY Keto: Access Control Policies as a Server
+
+[ORY Keto](https://github.com/ory/keto) is a policy decision point. It uses a set of access control policies, similar
+to AWS IAM Policies, in order to determine whether a subject (user, application, service, car, ...) is authorized to
+perform a certain action on a resource.
+
+## Security
+
+Running identity infrastructure requires [attention and knowledge of thread models](https://www.ory.sh/docs/next/kratos/concepts/security).
+
+### Disclosing vulnerabilities
+
+If you think you found a security vulnerability, please refrain from posting it publicly on the forums, the chat, or GitHub
+and send us an email to [hi@ory.am](mailto:hi@ory.sh) instead.
 
 ## Telemetry
 
@@ -78,3 +154,86 @@ changes in [UPGRADE.md](./UPGRADE.md) and [CHANGELOG.md](./CHANGELOG.md).
 ### Command line documentation
 
 Run `kratos -h` or `kratos help`.
+
+### Develop
+
+We encourage all contributions and encourage you to read our [contribution guidelines](./CONTRIBUTING.md)
+
+#### Dependencies
+
+You need Go 1.13+ with `GO111MODULE=on` and (for the test suites):
+
+- Docker and Docker Compose
+- Makefile
+- NodeJS / npm
+
+It is possible to develop ORY Kratos on Windows, but please be aware that all guides assume a Unix shell like bash or zsh.
+
+##### Install Tools
+
+When cloning ORY Kratos, run `make tools`. It will download several required dependencies. If you haven't run the command
+in a while it's probably a good idea to run it again.
+
+#### Formatting Code
+
+You can format all code using `make format`. Our CI checks if your code is properly formatted.
+
+#### Running Tests
+
+There are three types of tests you can run:
+
+- Short tests (do not require a SQL database like PostgreSQL)
+- Regular tests (do require PostgreSQL, MySQL, CockroachDB)
+- End to end tests (do require databases and will use a test browser)
+
+##### Short Tests
+
+Short tests run fairly quickly. You can either test all of the code at once
+
+```shell script
+go test -short -tags sqlite ./...
+```
+
+or test just a specific module:
+
+```shell script
+cd client; go test -tags sqlite -short .
+```
+
+##### Regular Tests
+
+Regular tests require a database set up. Our test suite is able to work with docker directly (using [ory/dockertest](https://github.com/ory/dockertest))
+but we encourage to use the Makefile instead. Using dockertest can bloat the number of Docker Images on your system
+and are quite slow. Instead we recommend doing:
+
+```shell script
+make test
+```
+
+Please be aware that `make test` recreates the databases every time you run `make test`. This can be annoying if
+you are trying to fix something very specific and need the database tests all the time. In that case we
+suggest that you initialize the databases with:
+
+```shell script
+make resetdb
+export TEST_DATABASE_MYSQL='mysql://root:secret@(127.0.0.1:3444)/mysql?parseTime=true'
+export TEST_DATABASE_POSTGRESQL='postgres://postgres:secret@127.0.0.1:3445/kratos?sslmode=disable'
+export TEST_DATABASE_COCKROACHDB='cockroach://root@127.0.0.1:3446/defaultdb?sslmode=disable'
+```
+
+Then you can run `go test` as often as you'd like:
+
+```shell script
+go test -tags sqlite ./...
+
+# or in a module:
+cd client; go test  -tags sqlite  .
+```
+
+#### Build Docker
+
+You can build a development Docker Image using:
+
+```shell script
+make docker
+```
diff --git a/docs/api.swagger.json b/docs/api.swagger.json
index 052e463cf..f6988fd01 100755
--- a/docs/api.swagger.json
+++ b/docs/api.swagger.json
@@ -269,6 +269,7 @@
         "parameters": [
           {
             "type": "string",
+            "x-go-name": "Request",
             "description": "Request should be set to the value of the `request` query parameter\nby the profile management UI.",
             "name": "request",
             "in": "query",
@@ -416,8 +417,8 @@
     },
     "formFields": {
       "description": "Fields contains multiple fields",
-      "type": "object",
-      "additionalProperties": {
+      "type": "array",
+      "items": {
         "$ref": "#/definitions/formField"
       }
     },
diff --git a/go.mod b/go.mod
index d4c85a154..d13d050c0 100644
--- a/go.mod
+++ b/go.mod
@@ -23,7 +23,6 @@ require (
 	github.com/gobuffalo/httptest v1.0.2
 	github.com/gobuffalo/packr v1.22.0
 	github.com/gobuffalo/packr/v2 v2.7.1
-	github.com/gobuffalo/pop v4.8.4+incompatible
 	github.com/gobuffalo/pop/v5 v5.0.7
 	github.com/gobuffalo/tags v2.1.7+incompatible // indirect
 	github.com/gobuffalo/uuid v2.0.5+incompatible
diff --git a/go.sum b/go.sum
index a55ed705e..d6c7144bf 100644
--- a/go.sum
+++ b/go.sum
@@ -263,9 +263,6 @@ github.com/gobuffalo/flect v0.0.0-20190117212819-a62e61d96794 h1:HZOs07hF3AmoaUj
 github.com/gobuffalo/flect v0.0.0-20190117212819-a62e61d96794/go.mod h1:397QT6v05LkZkn07oJXXT6y9FCfwC8Pug0WA2/2mE9k=
 github.com/gobuffalo/flect v0.1.5 h1:xpKq9ap8MbYfhuPCF0dBH854Gp9CxZjr/IocxELFflo=
 github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
-github.com/gobuffalo/flect v0.1.6/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
-github.com/gobuffalo/flect v0.1.7 h1:qQqM2eGdM6tJX8yHKYBM0wVHBLjUT7Qs6uk5jnAhOwI=
-github.com/gobuffalo/flect v0.1.7/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
 github.com/gobuffalo/flect v0.2.0 h1:EWCvMGGxOjsgwlWaP+f4+Hh6yrrte7JeFL2S6b+0hdM=
 github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
 github.com/gobuffalo/genny v0.0.0-20180924032338-7af3a40f2252/go.mod h1:tUTQOogrr7tAQnhajMSH6rv1BVev34H2sa1xNHMy94g=
@@ -306,8 +303,6 @@ github.com/gobuffalo/gogen v0.2.0/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTD
 github.com/gobuffalo/helpers v0.2.2/go.mod h1:xYbzUdCUpVzLwLnqV8HIjT6hmG0Cs7YIBCJkNM597jw=
 github.com/gobuffalo/helpers v0.2.4 h1:K5pXrMiSn8qJpMuv/q853i+WI2o47wS8ehD6NGF0hkE=
 github.com/gobuffalo/helpers v0.2.4/go.mod h1:NX7v27yxPDOPTgUFYmJ5ow37EbxdoLraucOGvMNawyk=
-github.com/gobuffalo/helpers v0.4.0 h1:DR/iYihrVCXv1cYeIGSK3EZz2CljO+DqDLQPWZAod9c=
-github.com/gobuffalo/helpers v0.4.0/go.mod h1:2q/ZnVxCehM4/y1bNz3+wXsvWvWUY+iTUr7mPC6QqGQ=
 github.com/gobuffalo/helpers v0.6.0 h1:CL1xOSGeKCaKD1IUpo4RfrkDU83kmkMG4H3dXAS7dw0=
 github.com/gobuffalo/helpers v0.6.0/go.mod h1:pncVrer7x/KRvnL5aJABLAuT/RhKRR9klL6dkUOhyv8=
 github.com/gobuffalo/httptest v1.0.2 h1:LWp2khlgA697h4BIYWW2aRxvB93jMnBrbakQ/r2KLzs=
@@ -415,8 +410,6 @@ github.com/gobuffalo/pop v4.8.2+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVD
 github.com/gobuffalo/pop v4.8.3+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
 github.com/gobuffalo/pop v4.8.4+incompatible h1:5v15ZgICK3MFTU90QRqCaqDUf4wcriIbws1hqpYL2Xo=
 github.com/gobuffalo/pop v4.8.4+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
-github.com/gobuffalo/pop v4.13.0+incompatible h1:sE2C/dmCENwLhSfXyFXk3re4dzBbSkH/Ut62g6N1zDw=
-github.com/gobuffalo/pop v4.13.0+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg=
 github.com/gobuffalo/pop/v5 v5.0.7 h1:QKFkUE2aPDkmM0jXv9u/96SFUDPVj5JYvJlOwXm40WY=
 github.com/gobuffalo/pop/v5 v5.0.7/go.mod h1:RRoSowwfxkKnCw8C2vr5EnCzs879Jj4fDH5fmwpWcEg=
 github.com/gobuffalo/release v1.0.35/go.mod h1:VtHFAKs61vO3wboCec5xr9JPTjYyWYcvaM3lclkc4x4=
@@ -547,6 +540,7 @@ github.com/jackc/pgconn v1.2.1 h1:+73KD6pbtv6Dbs6/rqlSRUa8XffPlW6YBd1hyFLpwuA=
 github.com/jackc/pgconn v1.2.1/go.mod h1:GgY/Lbj1VonNaVdNUHs9AwWom3yP2eymFQ1C8z9r/Lk=
 github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
 github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
+github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA=
 github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
@@ -676,8 +670,6 @@ github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK86
 github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
 github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/mattn/go-sqlite3 v1.13.0 h1:LnJI81JidiW9r7pS/hXe6cFeO5EXNq7KbfvoJLRI69c=
-github.com/mattn/go-sqlite3 v1.13.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-sqlite3 v2.0.2+incompatible h1:qzw9c2GNT8UFrgWNDhCTqRqYUSmu/Dav/9Z58LGpk7U=
 github.com/mattn/go-sqlite3 v2.0.2+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/goveralls v0.0.2 h1:7eJB6EqsPhRVxvwEXGnqdO2sJI0PTsrWoTMXEk9/OQc=
diff --git a/internal/httpclient/models/form_fields.go b/internal/httpclient/models/form_fields.go
index 215b0c8f9..f7c877fd3 100644
--- a/internal/httpclient/models/form_fields.go
+++ b/internal/httpclient/models/form_fields.go
@@ -6,27 +6,32 @@ package models
 // Editing this file might prove futile when you re-run the swagger generate command
 
 import (
+	"strconv"
+
 	strfmt "github.com/go-openapi/strfmt"
 
 	"github.com/go-openapi/errors"
-	"github.com/go-openapi/validate"
+	"github.com/go-openapi/swag"
 )
 
 // FormFields Fields contains multiple fields
 // swagger:model formFields
-type FormFields map[string]FormField
+type FormFields []*FormField
 
 // Validate validates this form fields
 func (m FormFields) Validate(formats strfmt.Registry) error {
 	var res []error
 
-	for k := range m {
-
-		if err := validate.Required(k, "body", m[k]); err != nil {
-			return err
+	for i := 0; i < len(m); i++ {
+		if swag.IsZero(m[i]) { // not required
+			continue
 		}
-		if val, ok := m[k]; ok {
-			if err := val.Validate(formats); err != nil {
+
+		if m[i] != nil {
+			if err := m[i].Validate(formats); err != nil {
+				if ve, ok := err.(*errors.Validation); ok {
+					return ve.ValidateName(strconv.Itoa(i))
+				}
 				return err
 			}
 		}
diff --git a/internal/httpclient/models/login_request_method.go b/internal/httpclient/models/login_request_method.go
index c9938f84d..ff13f554e 100644
--- a/internal/httpclient/models/login_request_method.go
+++ b/internal/httpclient/models/login_request_method.go
@@ -27,10 +27,6 @@ type LoginRequestMethod struct {
 func (m *LoginRequestMethod) Validate(formats strfmt.Registry) error {
 	var res []error
 
-	if err := m.validateConfig(formats); err != nil {
-		res = append(res, err)
-	}
-
 	if err := m.validateMethod(formats); err != nil {
 		res = append(res, err)
 	}
@@ -41,24 +37,6 @@ func (m *LoginRequestMethod) Validate(formats strfmt.Registry) error {
 	return nil
 }
 
-func (m *LoginRequestMethod) validateConfig(formats strfmt.Registry) error {
-
-	if swag.IsZero(m.Config) { // not required
-		return nil
-	}
-
-	if m.Config != nil {
-		if err := m.Config.Validate(formats); err != nil {
-			if ve, ok := err.(*errors.Validation); ok {
-				return ve.ValidateName("config")
-			}
-			return err
-		}
-	}
-
-	return nil
-}
-
 func (m *LoginRequestMethod) validateMethod(formats strfmt.Registry) error {
 
 	if swag.IsZero(m.Method) { // not required
diff --git a/selfservice/flow/profile/handler.go b/selfservice/flow/profile/handler.go
index 26057e771..eb8a9fbd6 100644
--- a/selfservice/flow/profile/handler.go
+++ b/selfservice/flow/profile/handler.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"net/http"
 	"net/url"
+	"sort"
 
 	"github.com/ory/kratos/schema"
 
@@ -198,6 +199,7 @@ func (h *Handler) fetchUpdateProfileRequest(w http.ResponseWriter, r *http.Reque
 		Value:    rid,
 	})
 	ar.Form.SetCSRF(nosurf.Token(r))
+	sort.Sort(ar.Form.Fields)
 	h.d.Writer().Write(w, r, ar)
 }
 
@@ -341,11 +343,12 @@ func (h *Handler) completeProfileManagementFlow(w http.ResponseWriter, r *http.R
 
 	ar.Form.Reset()
 	ar.UpdateSuccessful = true
-	for name, field := range form.NewHTMLFormFromJSON("", json.RawMessage(i.Traits), "traits").Fields {
-		ar.Form.SetField(name, field)
+	for _, field := range form.NewHTMLFormFromJSON("", json.RawMessage(i.Traits), "traits").Fields {
+		ar.Form.SetField(field.Name, field)
 	}
 	ar.Form.SetValue("request", r.Form.Get("request"))
 	ar.Form.SetCSRF(nosurf.Token(r))
+	sort.Sort(ar.Form.Fields)
 
 	if err := h.d.ProfileRequestPersister().UpdateProfileRequest(r.Context(), ar); err != nil {
 		h.handleProfileManagementError(w, r, ar, i.Traits, err)
@@ -366,12 +369,13 @@ func (h *Handler) handleProfileManagementError(w http.ResponseWriter, r *http.Re
 		rr.UpdateSuccessful = false
 
 		if traits != nil {
-			for name, field := range form.NewHTMLFormFromJSON("", json.RawMessage(traits), "traits").Fields {
-				rr.Form.SetField(name, field)
+			for _, field := range form.NewHTMLFormFromJSON("", json.RawMessage(traits), "traits").Fields {
+				rr.Form.SetField(field.Name, field)
 			}
 		}
 		rr.Form.SetValue("request", r.Form.Get("request"))
 		rr.Form.SetCSRF(nosurf.Token(r))
+		sort.Sort(rr.Form.Fields)
 	}
 
 	h.d.ProfileRequestRequestErrorHandler().HandleProfileManagementError(w, r, identity.CredentialsTypePassword, rr, err)
diff --git a/selfservice/form/fields.go b/selfservice/form/fields.go
index 4ab90e7ab..ec67b862e 100644
--- a/selfservice/form/fields.go
+++ b/selfservice/form/fields.go
@@ -8,7 +8,7 @@ import (
 // Fields contains multiple fields
 //
 // swagger:model formFields
-type Fields map[string]Field
+type Fields []Field
 
 // Field represents a HTML Form Field
 //
@@ -32,6 +32,18 @@ func (f *Field) Reset() {
 	f.Value = nil
 }
 
+func (ff Fields) Len() int {
+	return len(ff)
+}
+
+func (ff Fields) Swap(i, j int) {
+	ff[i], ff[j] = ff[j], ff[i]
+}
+
+func (ff Fields) Less(i, j int) bool {
+	return ff[i].Name < ff[j].Name
+}
+
 func toFormType(n string, i interface{}) string {
 	switch n {
 	case CSRFTokenName:
diff --git a/selfservice/form/html_form.go b/selfservice/form/html_form.go
index da6110840..b5cc5a2c4 100644
--- a/selfservice/form/html_form.go
+++ b/selfservice/form/html_form.go
@@ -4,6 +4,7 @@ import (
 	"database/sql/driver"
 	"encoding/json"
 	"net/http"
+	"sort"
 	"sync"
 
 	"github.com/santhosh-tekuri/jsonschema/v2"
@@ -71,6 +72,7 @@ func NewHTMLFormFromRequestBody(r *http.Request, action string, compiler decoder
 	for k, v := range jsonx.Flatten(raw) {
 		c.SetValue(k, v)
 	}
+	sort.Sort(c.Fields)
 
 	return c, nil
 }
@@ -84,6 +86,8 @@ func NewHTMLFormFromJSON(action string, raw json.RawMessage, prefix string) *HTM
 		}
 		c.SetValue(k, v)
 	}
+	sort.Sort(c.Fields)
+
 	return c
 }
 
@@ -98,11 +102,12 @@ func NewHTMLFormFromJSONSchema(action, jsonSchemaRef, prefix string) (*HTMLForm,
 	c := NewHTMLForm(action)
 	for _, value := range paths {
 		name := addPrefix(value.Name, prefix, ".")
-		c.Fields[name] = Field{
+		c.Fields = append(c.Fields, Field{
 			Name: name,
 			Type: toFormType(value.Name, value.Type),
-		}
+		})
 	}
+	sort.Sort(c.Fields)
 
 	return c, nil
 }
@@ -179,15 +184,32 @@ func (c *HTMLForm) SetValues(values map[string]interface{}) {
 	}
 }
 
+// getField returns a pointer to the field with the given name.
+func (c *HTMLForm) getField(name string) *Field {
+	// to prevent blocks we don't use c.defaults() here
+	if c.Fields == nil {
+		return nil
+	}
+
+	for i := range c.Fields {
+		if c.Fields[i].Name == name {
+			return &c.Fields[i]
+		}
+	}
+
+	return nil
+}
+
 // SetRequired sets the container's fields required.
 func (c *HTMLForm) SetRequired(fields ...string) {
 	c.defaults()
+	c.Lock()
+	defer c.Unlock()
+
 	for _, field := range fields {
-		ff, ok := c.Fields[field]
-		if !ok {
-			continue
+		if f := c.getField(field); f != nil {
+			f.Required = true
 		}
-		ff.Required = true
 	}
 }
 
@@ -197,29 +219,38 @@ func (c *HTMLForm) Unset(name string) {
 	c.Lock()
 	defer c.Unlock()
 
-	delete(c.Fields, name)
+	for i := range c.Fields {
+		if c.Fields[i].Name == name {
+			c.Fields = append(c.Fields[:i], c.Fields[i+1:]...)
+			return
+		}
+	}
 }
 
 // SetCSRF sets the CSRF value using e.g. nosurf.Token(r).
 func (c *HTMLForm) SetCSRF(token string) {
-	c.defaults()
-	c.Lock()
-	defer c.Unlock()
-
-	c.Fields[CSRFTokenName] = Field{
+	c.SetField(CSRFTokenName, Field{
 		Name:     CSRFTokenName,
 		Type:     "hidden",
 		Required: true,
 		Value:    token,
-	}
+	})
 }
 
 // SetField sets a field.
-func (c *HTMLForm) SetField(name string, field Field) {
+func (c *HTMLForm) SetField(oldName string, field Field) {
 	c.defaults()
 	c.Lock()
 	defer c.Unlock()
-	c.Fields[name] = field
+
+	for i := range c.Fields {
+		if c.Fields[i].Name == oldName {
+			c.Fields[i] = field
+			return
+		}
+	}
+
+	c.Fields = append(c.Fields, field)
 }
 
 // SetValue sets a container's field to the provided name and value.
@@ -228,21 +259,16 @@ func (c *HTMLForm) SetValue(name string, value interface{}) {
 	c.Lock()
 	defer c.Unlock()
 
-	ff, ok := c.Fields[name]
-	if !ok {
-		c.Fields[name] = Field{
-			Name:  name,
-			Type:  toFormType(name, value),
-			Value: value,
-		}
-	}
-
-	ff.Name = name
-	ff.Value = value
-	if len(ff.Type) == 0 {
-		ff.Type = toFormType(name, value)
+	if f := c.getField(name); f != nil {
+		f.Value = value
+		f.Type = toFormType(name, value)
+		return
 	}
-	c.Fields[name] = ff
+	c.Fields = append(c.Fields, Field{
+		Name:  name,
+		Value: value,
+		Type:  toFormType(name, value),
+	})
 }
 
 // AddError adds the provided error, and if a non-empty names list is set,
@@ -258,17 +284,15 @@ func (c *HTMLForm) AddError(err *Error, names ...string) {
 	}
 
 	for _, name := range names {
-		ff, ok := c.Fields[name]
-		if !ok {
-			c.Fields[name] = Field{
-				Name:   name,
-				Errors: []Error{*err},
-			}
+		if ff := c.getField(name); ff != nil {
+			ff.Errors = append(ff.Errors, *err)
 			continue
 		}
 
-		ff.Errors = append(ff.Errors, *err)
-		c.Fields[name] = ff
+		c.Fields = append(c.Fields, Field{
+			Name:   name,
+			Errors: []Error{*err},
+		})
 	}
 }
 
diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go
index 93e7ff591..dff737030 100644
--- a/selfservice/strategy/oidc/strategy.go
+++ b/selfservice/strategy/oidc/strategy.go
@@ -534,8 +534,8 @@ func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, rid uuid.
 			method.Config.Reset()
 
 			if traits != nil {
-				for name, field := range form.NewHTMLFormFromJSON("", traits, "traits").Fields {
-					method.Config.SetField(name, field)
+				for _, field := range form.NewHTMLFormFromJSON("", traits, "traits").Fields {
+					method.Config.SetField(field.Name, field)
 				}
 			}
 
diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go
index 7c58cb372..e7335ff90 100644
--- a/selfservice/strategy/password/login.go
+++ b/selfservice/strategy/password/login.go
@@ -119,12 +119,12 @@ func (s *Strategy) PopulateLoginMethod(r *http.Request, sr *login.Request) error
 		Action: action.String(),
 		Method: "POST",
 		Fields: form.Fields{
-			"identifier": {
+			{
 				Name:     "identifier",
 				Type:     "text",
 				Required: true,
 			},
-			"password": {
+			{
 				Name:     "password",
 				Type:     "password",
 				Required: true,
diff --git a/selfservice/strategy/password/registration.go b/selfservice/strategy/password/registration.go
index 2c757e23b..c63f39916 100644
--- a/selfservice/strategy/password/registration.go
+++ b/selfservice/strategy/password/registration.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
+	"sort"
 
 	"github.com/ory/kratos/driver/configuration"
 
@@ -63,8 +64,8 @@ func (s *Strategy) handleRegistrationError(w http.ResponseWriter, r *http.Reques
 			method.Config.Reset()
 
 			if p != nil {
-				for name, field := range form.NewHTMLFormFromJSON("", p.Traits, "traits").Fields {
-					method.Config.SetField(name, field)
+				for _, field := range form.NewHTMLFormFromJSON("", p.Traits, "traits").Fields {
+					method.Config.SetField(field.Name, field)
 				}
 			}
 
@@ -214,6 +215,7 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, sr *registration.
 	htmlf.Method = "POST"
 	htmlf.SetCSRF(s.cg(r))
 	htmlf.SetField("password", form.Field{Name: "password", Type: "password", Required: true})
+	sort.Sort(htmlf.Fields)
 
 	sr.Methods[identity.CredentialsTypePassword] = &registration.RequestMethod{
 		Method: identity.CredentialsTypePassword,


Test patch:
diff --git a/selfservice/flow/login/handler_test.go b/selfservice/flow/login/handler_test.go
index a62776944..23ddc29e2 100644
--- a/selfservice/flow/login/handler_test.go
+++ b/selfservice/flow/login/handler_test.go
@@ -107,7 +107,7 @@ func TestLoginHandler(t *testing.T) {
 		require.NoError(t, err)
 
 		assert.Equal(t, "password", gjson.GetBytes(body, "methods.password.method").String(), "%s", body)
-		assert.NotEmpty(t, gjson.GetBytes(body, "methods.password.config.fields.csrf_token.value").String(), "%s", body)
+		assert.NotEmpty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==csrf_token).value").String(), "%s", body)
 		assert.NotEmpty(t, gjson.GetBytes(body, "id").String(), "%s", body)
 		assert.Empty(t, gjson.GetBytes(body, "headers").Value(), "%s", body)
 		assert.Contains(t, gjson.GetBytes(body, "methods.password.config.action").String(), gjson.GetBytes(body, "id").String(), "%s", body)
diff --git a/selfservice/flow/profile/handler_test.go b/selfservice/flow/profile/handler_test.go
index ddeac828f..36d316d5b 100644
--- a/selfservice/flow/profile/handler_test.go
+++ b/selfservice/flow/profile/handler_test.go
@@ -205,17 +205,26 @@ func TestUpdateProfile(t *testing.T) {
 		assert.Equal(t, primaryIdentity.TraitsSchemaID, pr.Payload.Identity.TraitsSchemaID)
 		assert.Equal(t, kratos.URL+profile.BrowserProfilePath, pr.Payload.RequestURL)
 
-		require.NotEmpty(t, pr.Payload.Form.Fields[form.CSRFTokenName])
-		delete(pr.Payload.Form.Fields, form.CSRFTokenName)
+		found := false
+		for i := range pr.Payload.Form.Fields {
+			if pr.Payload.Form.Fields[i].Name == form.CSRFTokenName {
+				found = true
+				require.NotEmpty(t, pr.Payload.Form.Fields[i])
+				pr.Payload.Form.Fields = append(pr.Payload.Form.Fields[:i], pr.Payload.Form.Fields[i+1:]...)
+				break
+			}
+		}
+		require.True(t, found)
+
 		assert.Equal(t, &models.Form{
 			Action: kratos.URL + profile.BrowserProfilePath,
 			Method: "POST",
 			Fields: models.FormFields{
-				"request":        models.FormField{Name: "request", Required: true, Type: "hidden", Value: rid},
-				"traits.stringy": models.FormField{Name: "traits.stringy", Required: false, Type: "text", Value: "foobar"},
-				"traits.numby":   models.FormField{Name: "traits.numby", Required: false, Type: "number", Value: json.Number("2.5")},
-				"traits.booly":   models.FormField{Name: "traits.booly", Required: false, Type: "checkbox", Value: false},
-				"traits.email":   models.FormField{Name: "traits.email", Required: false, Type: "text", Value: "john@doe.com"},
+				&models.FormField{Name: "request", Required: true, Type: "hidden", Value: rid},
+				&models.FormField{Name: "traits.booly", Required: false, Type: "checkbox", Value: false},
+				&models.FormField{Name: "traits.email", Required: false, Type: "text", Value: "john@doe.com"},
+				&models.FormField{Name: "traits.numby", Required: false, Type: "number", Value: json.Number("2.5")},
+				&models.FormField{Name: "traits.stringy", Required: false, Type: "text", Value: "foobar"},
 			},
 		}, pr.Payload.Form)
 	})
@@ -245,11 +254,11 @@ func TestUpdateProfile(t *testing.T) {
 		values.Set("traits.stringy", "bazbar") // it should still override new values!
 		actual, _ := submitForm(t, rs, values)
 
-		assert.NotEmpty(t, "too-short", gjson.Get(actual, "form.fields.csrf_token.value").String(), "%s", actual)
-		assert.Equal(t, "too-short", gjson.Get(actual, "form.fields.traits\\.should_long_string.value").String(), "%s", actual)
-		assert.Equal(t, "bazbar", gjson.Get(actual, "form.fields.traits\\.stringy.value").String(), "%s", actual)
-		assert.Equal(t, "2.5", gjson.Get(actual, "form.fields.traits\\.numby.value").String(), "%s", actual)
-		assert.Equal(t, "traits.should_long_string: String length must be greater than or equal to 25", gjson.Get(actual, "form.fields.traits\\.should_long_string.errors.0.message").String(), "%s", actual)
+		assert.NotEmpty(t, gjson.Get(actual, "form.fields.#(name==csrf_token).value").String(), "%s", actual)
+		assert.Equal(t, "too-short", gjson.Get(actual, "form.fields.#(name==traits.should_long_string).value").String(), "%s", actual)
+		assert.Equal(t, "bazbar", gjson.Get(actual, "form.fields.#(name==traits.stringy).value").String(), "%s", actual)
+		assert.Equal(t, "2.5", gjson.Get(actual, "form.fields.#(name==traits.numby).value").String(), "%s", actual)
+		assert.Equal(t, "traits.should_long_string: String length must be greater than or equal to 25", gjson.Get(actual, "form.fields.#(name==traits.should_long_string).errors.0.message").String(), "%s", actual)
 	})
 
 	t.Run("description=should come back with form errors if trying to update email", func(t *testing.T) {
@@ -276,10 +285,10 @@ func TestUpdateProfile(t *testing.T) {
 			actual, response := submitForm(t, rs, values)
 			assert.False(t, response.Payload.UpdateSuccessful, "%s", actual)
 
-			assert.Equal(t, "1", gjson.Get(actual, "form.fields.traits\\.should_big_number.value").String(), "%s", actual)
-			assert.Equal(t, "traits.should_big_number: Must be greater than or equal to 1200", gjson.Get(actual, "form.fields.traits\\.should_big_number.errors.0.message").String(), "%s", actual)
+			assert.Equal(t, "1", gjson.Get(actual, "form.fields.#(name==traits.should_big_number).value").String(), "%s", actual)
+			assert.Equal(t, "traits.should_big_number: Must be greater than or equal to 1200", gjson.Get(actual, "form.fields.#(name==traits.should_big_number).errors.0.message").String(), "%s", actual)
 
-			assert.Equal(t, "foobar", gjson.Get(actual, "form.fields.traits\\.stringy.value").String(), "%s", actual) // sanity check if original payload is still here
+			assert.Equal(t, "foobar", gjson.Get(actual, "form.fields.#(name==traits.stringy).value").String(), "%s", actual) // sanity check if original payload is still here
 		})
 
 		t.Run("flow=fail second update", func(t *testing.T) {
@@ -291,16 +300,16 @@ func TestUpdateProfile(t *testing.T) {
 			actual, response := submitForm(t, rs, values)
 			assert.False(t, response.Payload.UpdateSuccessful, "%s", actual)
 
-			assert.Empty(t, gjson.Get(actual, "form.fields.traits\\.should_big_number.errors.0.message").String(), "%s", actual)
-			assert.Empty(t, gjson.Get(actual, "form.fields.traits\\.should_big_number.value").String(), "%s", actual)
+			assert.Empty(t, gjson.Get(actual, "form.fields.#(name==traits.should_big_number).errors.0.message").String(), "%s", actual)
+			assert.Empty(t, gjson.Get(actual, "form.fields.#(name==traits.should_big_number).value").String(), "%s", actual)
 
-			assert.Equal(t, "short", gjson.Get(actual, "form.fields.traits\\.should_long_string.value").String(), "%s", actual)
-			assert.Equal(t, "traits.should_long_string: String length must be greater than or equal to 25", gjson.Get(actual, "form.fields.traits\\.should_long_string.errors.0.message").String(), "%s", actual)
+			assert.Equal(t, "short", gjson.Get(actual, "form.fields.#(name==traits.should_long_string).value").String(), "%s", actual)
+			assert.Equal(t, "traits.should_long_string: String length must be greater than or equal to 25", gjson.Get(actual, "form.fields.#(name==traits.should_long_string).errors.0.message").String(), "%s", actual)
 
-			assert.Equal(t, "this-is-not-a-number", gjson.Get(actual, "form.fields.traits\\.numby.value").String(), "%s", actual)
-			assert.Equal(t, "traits.numby: Invalid type. Expected: number, given: string", gjson.Get(actual, "form.fields.traits\\.numby.errors.0.message").String(), "%s", actual)
+			assert.Equal(t, "this-is-not-a-number", gjson.Get(actual, "form.fields.#(name==traits.numby).value").String(), "%s", actual)
+			assert.Equal(t, "traits.numby: Invalid type. Expected: number, given: string", gjson.Get(actual, "form.fields.#(name==traits.numby).errors.0.message").String(), "%s", actual)
 
-			assert.Equal(t, "foobar", gjson.Get(actual, "form.fields.traits\\.stringy.value").String(), "%s", actual) // sanity check if original payload is still here
+			assert.Equal(t, "foobar", gjson.Get(actual, "form.fields.#(name==traits.stringy).value").String(), "%s", actual) // sanity check if original payload is still here
 		})
 
 		t.Run("flow=succeed with final request", func(t *testing.T) {
@@ -313,15 +322,15 @@ func TestUpdateProfile(t *testing.T) {
 			actual, response := submitForm(t, rs, values)
 			assert.True(t, response.Payload.UpdateSuccessful, "%s", actual)
 
-			assert.Empty(t, gjson.Get(actual, "form.fields.traits\\.numby.errors").Value(), "%s", actual)
-			assert.Empty(t, gjson.Get(actual, "form.fields.traits\\.should_big_number.errors").Value(), "%s", actual)
-			assert.Empty(t, gjson.Get(actual, "form.fields.traits\\.should_long_string.errors").Value(), "%s", actual)
+			assert.Empty(t, gjson.Get(actual, "form.fields.#(name==traits.numby).errors").Value(), "%s", actual)
+			assert.Empty(t, gjson.Get(actual, "form.fields.#(name==traits.should_big_number).errors").Value(), "%s", actual)
+			assert.Empty(t, gjson.Get(actual, "form.fields.#(name==traits.should_long_string).errors").Value(), "%s", actual)
 
-			assert.Equal(t, 15.0, gjson.Get(actual, "form.fields.traits\\.numby.value").Value(), "%s", actual)
-			assert.Equal(t, 9001.0, gjson.Get(actual, "form.fields.traits\\.should_big_number.value").Value(), "%s", actual)
-			assert.Equal(t, "this is such a long string, amazing stuff!", gjson.Get(actual, "form.fields.traits\\.should_long_string.value").Value(), "%s", actual)
+			assert.Equal(t, 15.0, gjson.Get(actual, "form.fields.#(name==traits.numby).value").Value(), "%s", actual)
+			assert.Equal(t, 9001.0, gjson.Get(actual, "form.fields.#(name==traits.should_big_number).value").Value(), "%s", actual)
+			assert.Equal(t, "this is such a long string, amazing stuff!", gjson.Get(actual, "form.fields.#(name==traits.should_long_string).value").Value(), "%s", actual)
 
-			assert.Equal(t, "foobar", gjson.Get(actual, "form.fields.traits\\.stringy.value").String(), "%s", actual) // sanity check if original payload is still here
+			assert.Equal(t, "foobar", gjson.Get(actual, "form.fields.#(name==traits.stringy).value").String(), "%s", actual) // sanity check if original payload is still here
 		})
 
 		t.Run("flow=try another update with invalid data", func(t *testing.T) {
diff --git a/selfservice/flow/registration/handler_test.go b/selfservice/flow/registration/handler_test.go
index f3b01b09b..e8d44e6ea 100644
--- a/selfservice/flow/registration/handler_test.go
+++ b/selfservice/flow/registration/handler_test.go
@@ -109,7 +109,7 @@ func TestRegistrationHandler(t *testing.T) {
 			require.NoError(t, err)
 
 			assert.Equal(t, "password", gjson.GetBytes(body, "methods.password.method").String(), "%s", body)
-			assert.NotEmpty(t, gjson.GetBytes(body, "methods.password.config.fields.csrf_token.value").String(), "%s", body)
+			assert.NotEmpty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==csrf_token).value").String(), "%s", body)
 			assert.NotEmpty(t, gjson.GetBytes(body, "id").String(), "%s", body)
 			assert.Empty(t, gjson.GetBytes(body, "headers").Value(), "%s", body)
 			assert.Contains(t, gjson.GetBytes(body, "methods.password.config.action").String(), gjson.GetBytes(body, "id").String(), "%s", body)
diff --git a/selfservice/form/html_form_test.go b/selfservice/form/html_form_test.go
index dc2dde17a..3f64f1223 100644
--- a/selfservice/form/html_form_test.go
+++ b/selfservice/form/html_form_test.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
+	"sort"
 	"testing"
 
 	"github.com/pkg/errors"
@@ -45,10 +46,10 @@ func TestContainer(t *testing.T) {
 				r: `{"numby":1.5,"stringy":"foobar","objy":{"objy":{},"numby":1.5,"stringy":"foobar"}}`,
 				expect: &HTMLForm{
 					Fields: Fields{
-						"numby":        Field{Name: "numby", Type: "number", Value: 1.5},
-						"stringy":      Field{Name: "stringy", Type: "text", Value: "foobar"},
-						"objy.numby":   Field{Name: "objy.numby", Type: "number", Value: 1.5},
-						"objy.stringy": Field{Name: "objy.stringy", Type: "text", Value: "foobar"},
+						Field{Name: "numby", Type: "number", Value: 1.5},
+						Field{Name: "stringy", Type: "text", Value: "foobar"},
+						Field{Name: "objy.numby", Type: "number", Value: 1.5},
+						Field{Name: "objy.stringy", Type: "text", Value: "foobar"},
 					},
 				},
 			},
@@ -57,16 +58,17 @@ func TestContainer(t *testing.T) {
 				prefix: "traits",
 				expect: &HTMLForm{
 					Fields: Fields{
-						"traits.numby":        Field{Name: "traits.numby", Type: "number", Value: 1.5},
-						"traits.stringy":      Field{Name: "traits.stringy", Type: "text", Value: "foobar"},
-						"traits.objy.numby":   Field{Name: "traits.objy.numby", Type: "number", Value: 1.5},
-						"traits.objy.stringy": Field{Name: "traits.objy.stringy", Type: "text", Value: "foobar"},
+						Field{Name: "traits.numby", Type: "number", Value: 1.5},
+						Field{Name: "traits.stringy", Type: "text", Value: "foobar"},
+						Field{Name: "traits.objy.numby", Type: "number", Value: 1.5},
+						Field{Name: "traits.objy.stringy", Type: "text", Value: "foobar"},
 					},
 				},
 			},
 		} {
 			t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
 				actual := NewHTMLFormFromJSON("action", json.RawMessage(tc.r), tc.prefix)
+				sort.Sort(tc.expect.Fields)
 				assert.Equal(t, "action", actual.Action)
 				assert.EqualValues(t, tc.expect.Fields, actual.Fields)
 			})
@@ -84,10 +86,10 @@ func TestContainer(t *testing.T) {
 				r:   newJSONRequest(t, `{"numby":1.5,"stringy":"foobar","objy":{"objy":{},"numby":1.5,"stringy":"foobar"}}`),
 				expect: &HTMLForm{
 					Fields: Fields{
-						"numby":        Field{Name: "numby", Type: "number", Value: 1.5},
-						"stringy":      Field{Name: "stringy", Type: "text", Value: "foobar"},
-						"objy.numby":   Field{Name: "objy.numby", Type: "number", Value: 1.5},
-						"objy.stringy": Field{Name: "objy.stringy", Type: "text", Value: "foobar"},
+						Field{Name: "numby", Type: "number", Value: 1.5},
+						Field{Name: "stringy", Type: "text", Value: "foobar"},
+						Field{Name: "objy.numby", Type: "number", Value: 1.5},
+						Field{Name: "objy.stringy", Type: "text", Value: "foobar"},
 					},
 				},
 			},
@@ -101,10 +103,10 @@ func TestContainer(t *testing.T) {
 				}),
 				expect: &HTMLForm{
 					Fields: Fields{
-						"numby":        Field{Name: "numby", Type: "number", Value: 1.5},
-						"stringy":      Field{Name: "stringy", Type: "text", Value: "foobar"},
-						"objy.numby":   Field{Name: "objy.numby", Type: "number", Value: 1.5},
-						"objy.stringy": Field{Name: "objy.stringy", Type: "text", Value: "foobar"},
+						Field{Name: "numby", Type: "number", Value: 1.5},
+						Field{Name: "stringy", Type: "text", Value: "foobar"},
+						Field{Name: "objy.numby", Type: "number", Value: 1.5},
+						Field{Name: "objy.stringy", Type: "text", Value: "foobar"},
 					},
 				},
 			},
@@ -115,8 +117,8 @@ func TestContainer(t *testing.T) {
 				}),
 				expect: &HTMLForm{
 					Fields: Fields{
-						"meal.name": Field{Name: "meal.name", Errors: []Error{{Message: "missing properties: \"name\""}}},
-						"meal.chef": Field{Name: "meal.chef", Type: "text", Value: "aeneas"},
+						Field{Name: "meal.name", Errors: []Error{{Message: "missing properties: \"name\""}}},
+						Field{Name: "meal.chef", Type: "text", Value: "aeneas"},
 					},
 				},
 			},
@@ -124,6 +126,7 @@ func TestContainer(t *testing.T) {
 			t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
 				actual, err := NewHTMLFormFromRequestBody(tc.r, "action", decoderx.HTTPJSONSchemaCompiler(tc.ref, nil))
 				require.NoError(t, err)
+				sort.Sort(tc.expect.Fields)
 				assert.Equal(t, "action", actual.Action)
 				assert.EqualValues(t, tc.expect.Fields, actual.Fields)
 			})
@@ -141,11 +144,11 @@ func TestContainer(t *testing.T) {
 				prefix: "",
 				expect: &HTMLForm{
 					Fields: Fields{
-						"numby":        Field{Name: "numby", Type: "number"},
-						"objy.numby":   Field{Name: "objy.numby", Type: "number"},
-						"objy.objy":    Field{Name: "objy.objy", Type: "text"},
-						"objy.stringy": Field{Name: "objy.stringy", Type: "text"},
-						"stringy":      Field{Name: "stringy", Type: "text"},
+						Field{Name: "numby", Type: "number"},
+						Field{Name: "objy.numby", Type: "number"},
+						Field{Name: "objy.objy", Type: "text"},
+						Field{Name: "objy.stringy", Type: "text"},
+						Field{Name: "stringy", Type: "text"},
 					},
 				},
 			},
@@ -154,11 +157,11 @@ func TestContainer(t *testing.T) {
 				prefix: "traits",
 				expect: &HTMLForm{
 					Fields: Fields{
-						"traits.numby":        Field{Name: "traits.numby", Type: "number"},
-						"traits.objy.numby":   Field{Name: "traits.objy.numby", Type: "number"},
-						"traits.objy.objy":    Field{Name: "traits.objy.objy", Type: "text"},
-						"traits.objy.stringy": Field{Name: "traits.objy.stringy", Type: "text"},
-						"traits.stringy":      Field{Name: "traits.stringy", Type: "text"},
+						Field{Name: "traits.numby", Type: "number"},
+						Field{Name: "traits.objy.numby", Type: "number"},
+						Field{Name: "traits.objy.objy", Type: "text"},
+						Field{Name: "traits.objy.stringy", Type: "text"},
+						Field{Name: "traits.stringy", Type: "text"},
 					},
 				},
 			},
@@ -166,10 +169,10 @@ func TestContainer(t *testing.T) {
 				ref: "./stub/complex.schema.json",
 				expect: &HTMLForm{
 					Fields: Fields{
-						"fruits":     Field{Name: "fruits", Type: "text"},
-						"meal.chef":  Field{Name: "meal.chef", Type: "text"},
-						"meal.name":  Field{Name: "meal.name", Type: "text"},
-						"vegetables": Field{Name: "vegetables", Type: "text"},
+						Field{Name: "fruits", Type: "text"},
+						Field{Name: "meal.chef", Type: "text"},
+						Field{Name: "meal.name", Type: "text"},
+						Field{Name: "vegetables", Type: "text"},
 					},
 				},
 			},
@@ -177,6 +180,7 @@ func TestContainer(t *testing.T) {
 			t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
 				actual, err := NewHTMLFormFromJSONSchema("action", tc.ref, tc.prefix)
 				require.NoError(t, err)
+				sort.Sort(tc.expect.Fields)
 				assert.Equal(t, "action", actual.Action)
 				assert.EqualValues(t, tc.expect.Errors, actual.Errors)
 				assert.EqualValues(t, tc.expect.Fields, actual.Fields)
@@ -194,7 +198,7 @@ func TestContainer(t *testing.T) {
 			{err: &herodot.ErrNotFound, expectErr: true},
 			{err: herodot.ErrBadRequest.WithReason("tests"), expect: HTMLForm{Fields: Fields{}, Errors: []Error{{Message: "tests"}}}},
 			{err: schema.NewInvalidCredentialsError(), expect: HTMLForm{Fields: Fields{}, Errors: []Error{{Message: "The provided credentials are invalid. Check for spelling mistakes in your password or username, email address, or phone number."}}}},
-			{err: &jsonschema.ValidationError{Message: "test", InstancePtr: "#/foo/bar/baz"}, expect: HTMLForm{Fields: Fields{"foo.bar.baz": Field{Name: "foo.bar.baz", Type: "", Errors: []Error{{Message: "test"}}}}}},
+			{err: &jsonschema.ValidationError{Message: "test", InstancePtr: "#/foo/bar/baz"}, expect: HTMLForm{Fields: Fields{Field{Name: "foo.bar.baz", Type: "", Errors: []Error{{Message: "test"}}}}}},
 			{err: &jsonschema.ValidationError{Message: "test", InstancePtr: ""}, expect: HTMLForm{Fields: Fields{}, Errors: []Error{{Message: "test"}}}},
 		} {
 			t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
@@ -206,6 +210,7 @@ func TestContainer(t *testing.T) {
 						return
 					}
 					require.NoError(t, err)
+					sort.Sort(tc.expect.Fields)
 					assert.EqualValues(t, tc.expect.Errors, c.Errors)
 					assert.EqualValues(t, tc.expect.Fields, c.Fields)
 				}
@@ -216,8 +221,8 @@ func TestContainer(t *testing.T) {
 	t.Run("method=SetValue", func(t *testing.T) {
 		c := HTMLForm{
 			Fields: Fields{
-				"1": {Name: "1", Value: "foo"},
-				"2": {Name: "2", Value: ""},
+				{Name: "1", Value: "foo"},
+				{Name: "2", Value: ""},
 			},
 		}
 
@@ -229,33 +234,33 @@ func TestContainer(t *testing.T) {
 
 		assert.Len(t, c.Fields, 3)
 		for _, k := range []string{"1", "2", "3"} {
-			assert.EqualValues(t, fmt.Sprintf("baz%s", k), c.Fields[k].Value, "%+v", c)
+			assert.EqualValues(t, fmt.Sprintf("baz%s", k), c.getField(k).Value, "%+v", c)
 		}
 	})
 
 	t.Run("method=SetCSRF", func(t *testing.T) {
-		f := &HTMLForm{Fields: Fields{"1": {Name: "1", Value: "bar"}}}
+		f := &HTMLForm{Fields: Fields{{Name: "1", Value: "bar"}}}
 		f.SetCSRF("csrf-token")
-		assert.EqualValues(
+		assert.Contains(
 			t,
+			f.Fields,
 			Field{Name: CSRFTokenName, Value: "csrf-token", Type: "hidden", Required: true},
-			f.Fields[CSRFTokenName],
 		)
 
-		f = &HTMLForm{Fields: Fields{"1": {Name: "1", Value: "bar"}}}
+		f = &HTMLForm{Fields: Fields{{Name: "1", Value: "bar"}}}
 		f.SetCSRF("csrf-token")
-		assert.EqualValues(
+		assert.Contains(
 			t,
+			f.Fields,
 			Field{Name: CSRFTokenName, Value: "csrf-token", Type: "hidden", Required: true},
-			f.Fields[CSRFTokenName],
 		)
 	})
 
 	t.Run("method=AddError", func(t *testing.T) {
 		c := HTMLForm{
 			Fields: Fields{
-				"1": {Name: "1", Value: "foo", Errors: []Error{{Message: "foo"}}},
-				"2": {Name: "2", Value: "", Errors: []Error{}},
+				{Name: "1", Value: "foo", Errors: []Error{{Message: "foo"}}},
+				{Name: "2", Value: "", Errors: []Error{}},
 			},
 		}
 		assert.Len(t, c.Fields, 2)
@@ -267,10 +272,10 @@ func TestContainer(t *testing.T) {
 
 		assert.Len(t, c.Fields, 6)
 		for _, k := range []string{"1", "2", "3"} {
-			assert.EqualValues(t, fmt.Sprintf("baz%s", k), c.Fields[k].Errors[len(c.Fields[k].Errors)-1].Message, "%+v", c)
+			assert.EqualValues(t, fmt.Sprintf("baz%s", k), c.getField(k).Errors[len(c.getField(k).Errors)-1].Message, "%+v", c)
 		}
 		for _, k := range []string{"4", "5", "6"} {
-			assert.EqualValues(t, "baz", c.Fields[k].Errors[0].Message, "%+v", c)
+			assert.EqualValues(t, "baz", c.getField(k).Errors[0].Message, "%+v", c)
 		}
 
 		assert.Len(t, c.Errors, 1)
@@ -280,17 +285,17 @@ func TestContainer(t *testing.T) {
 	t.Run("method=Reset", func(t *testing.T) {
 		c := HTMLForm{
 			Fields: Fields{
-				"1": {Name: "1", Value: "foo", Errors: []Error{{Message: "foo"}}},
-				"2": {Name: "2", Value: "bar", Errors: []Error{{Message: "bar"}}},
+				{Name: "1", Value: "foo", Errors: []Error{{Message: "foo"}}},
+				{Name: "2", Value: "bar", Errors: []Error{{Message: "bar"}}},
 			},
 			Errors: []Error{{Message: ""}},
 		}
 		c.Reset()
 
 		assert.Empty(t, c.Errors)
-		assert.Empty(t, c.Fields["1"].Errors)
-		assert.Empty(t, c.Fields["1"].Value)
-		assert.Empty(t, c.Fields["2"].Errors)
-		assert.Empty(t, c.Fields["2"].Value)
+		assert.Empty(t, c.getField("1").Errors)
+		assert.Empty(t, c.getField("1").Value)
+		assert.Empty(t, c.getField("2").Errors)
+		assert.Empty(t, c.getField("2").Value)
 	})
 }
diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go
index be8999095..84eccfb88 100644
--- a/selfservice/strategy/oidc/strategy_test.go
+++ b/selfservice/strategy/oidc/strategy_test.go
@@ -349,7 +349,7 @@ func TestStrategy(t *testing.T) {
 		res, body := mr(t, "valid", r.ID, url.Values{})
 
 		require.Contains(t, res.Request.URL.String(), uiTS.URL, "%s", body)
-		assert.Contains(t, gjson.GetBytes(body, "methods.oidc.config.fields.traits\\.subject.errors.0").String(), "not match format 'email'", "%s", body)
+		assert.Contains(t, gjson.GetBytes(body, "methods.oidc.config.fields.#(name==traits.subject).errors.0").String(), "not match format 'email'", "%s", body)
 	})
 
 	t.Run("case=register and then login", func(t *testing.T) {
@@ -406,9 +406,9 @@ func TestStrategy(t *testing.T) {
 			res, body := mr(t, "valid", r.ID, url.Values{"traits.name": {"i"}})
 			require.Contains(t, res.Request.URL.String(), uiTS.URL, "%s", body)
 
-			assert.Equal(t, "traits.name: String length must be greater than or equal to 2", gjson.GetBytes(body, "methods.oidc.config.fields.traits\\.name.errors.0.message").String(), "%s", body) // make sure the field is being echoed
-			assert.Equal(t, "traits.name", gjson.GetBytes(body, "methods.oidc.config.fields.traits\\.name.name").String(), "%s", body)                                                               // make sure the field is being echoed
-			assert.Equal(t, "i", gjson.GetBytes(body, "methods.oidc.config.fields.traits\\.name.value").String(), "%s", body)                                                                        // make sure the field is being echoed
+			assert.Equal(t, "traits.name: String length must be greater than or equal to 2", gjson.GetBytes(body, "methods.oidc.config.fields.#(name==traits.name).errors.0.message").String(), "%s", body) // make sure the field is being echoed
+			assert.Equal(t, "traits.name", gjson.GetBytes(body, "methods.oidc.config.fields.#(name==traits.name).name").String(), "%s", body)                                                               // make sure the field is being echoed
+			assert.Equal(t, "i", gjson.GetBytes(body, "methods.oidc.config.fields.#(name==traits.name).value").String(), "%s", body)                                                                        // make sure the field is being echoed
 		})
 
 		t.Run("case=should pass registration with valid data", func(t *testing.T) {
diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go
index 54121fa38..df10c7f15 100644
--- a/selfservice/strategy/password/login_test.go
+++ b/selfservice/strategy/password/login_test.go
@@ -47,17 +47,17 @@ func nlr(exp time.Duration) *login.Request {
 						Method: "POST",
 						Action: "/action",
 						Fields: form.Fields{
-							"identifier": {
+							{
 								Name:     "identifier",
 								Type:     "text",
 								Required: true,
 							},
-							"password": {
+							{
 								Name:     "password",
 								Type:     "password",
 								Required: true,
 							},
-							form.CSRFTokenName: {
+							{
 								Name:     form.CSRFTokenName,
 								Type:     "hidden",
 								Required: true,
@@ -191,10 +191,10 @@ func TestLogin(t *testing.T) {
 				assert.Equal(t, tc.ar.ID.String(), gjson.GetBytes(body, "id").String())
 				assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String())
 				ensureFieldsExist(t, body)
-				assert.Equal(t, "identifier: identifier is required", gjson.GetBytes(body, "methods.password.config.fields.identifier.errors.0.message").String(), "%s", body)
+				assert.Equal(t, "identifier: identifier is required", gjson.GetBytes(body, "methods.password.config.fields.#(name==identifier).errors.0.message").String(), "%s", body)
 
 				// The password value should not be returned!
-				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.password.value").String())
+				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==password).value").String())
 			},
 		},
 		{
@@ -212,13 +212,13 @@ func TestLogin(t *testing.T) {
 				assert.Equal(t, tc.ar.ID.String(), gjson.GetBytes(body, "id").String())
 				assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String())
 				ensureFieldsExist(t, body)
-				assert.Equal(t, "password: password is required", gjson.GetBytes(body, "methods.password.config.fields.password.errors.0.message").String(), "%s", body)
+				assert.Equal(t, "password: password is required", gjson.GetBytes(body, "methods.password.config.fields.#(name==password).errors.0.message").String(), "%s", body)
 
-				assert.Equal(t, "anti-rf-token", gjson.GetBytes(body, "methods.password.config.fields.csrf_token.value").String())
-				assert.Equal(t, "identifier", gjson.GetBytes(body, "methods.password.config.fields.identifier.value").String(), "%s", body)
+				assert.Equal(t, "anti-rf-token", gjson.GetBytes(body, "methods.password.config.fields.#(name==csrf_token).value").String())
+				assert.Equal(t, "identifier", gjson.GetBytes(body, "methods.password.config.fields.#(name==identifier).value").String(), "%s", body)
 
 				// This must not include the password!
-				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.password.value").String())
+				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==password).value").String())
 			},
 		},
 		{
@@ -253,7 +253,7 @@ func TestLogin(t *testing.T) {
 				assert.Equal(t, schema.NewInvalidCredentialsError().(schema.ResultErrors)[0].Description(), gjson.GetBytes(body, "methods.password.config.errors.0.message").String(), "%s", body)
 
 				// This must not include the password!
-				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.password.value").String())
+				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==password).value").String())
 			},
 		},
 		{
@@ -300,12 +300,12 @@ func TestLogin(t *testing.T) {
 									Action: "/action",
 									Errors: []form.Error{{Message: "some error"}},
 									Fields: form.Fields{
-										"identifier": {
+										{
 											Value:  "baz",
 											Name:   "identifier",
 											Errors: []form.Error{{Message: "err"}},
 										},
-										"password": {
+										{
 											Value:  "bar",
 											Name:   "password",
 											Errors: []form.Error{{Message: "err"}},
@@ -330,10 +330,10 @@ func TestLogin(t *testing.T) {
 				assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String())
 				ensureFieldsExist(t, body)
 
-				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.identity.value"))
-				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.identity.error"))
+				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==identity).value"))
+				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==identity).error"))
 				assert.Empty(t, gjson.GetBytes(body, "methods.password.config.error"))
-				assert.Contains(t, gjson.GetBytes(body, "methods.password.config.fields.password.errors.0").String(), "password: password is required", "%s", body)
+				assert.Contains(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==password).errors.0").String(), "password: password is required", "%s", body)
 			},
 		},
 	} {
diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go
index ab73cacb0..8e4bed35e 100644
--- a/selfservice/strategy/password/registration_test.go
+++ b/selfservice/strategy/password/registration_test.go
@@ -8,6 +8,7 @@ import (
 	"net/http/cookiejar"
 	"net/http/httptest"
 	"net/url"
+	"sort"
 	"strings"
 	"testing"
 	"time"
@@ -39,15 +40,14 @@ func checkFormContent(t *testing.T, body []byte, requiredFields ...string) {
 // fieldNameSet checks if the fields have the right "name" set.
 func fieldNameSet(t *testing.T, body []byte, fields []string) {
 	for _, f := range fields {
-		fieldid := strings.Replace(f, ".", "\\.", -1) // we need to escape this because otherwise json path will interpret this as a nested object (it is not).
-		assert.Equal(t, gjson.GetBytes(body, fmt.Sprintf("methods.password.config.fields.%s.name", fieldid)).String(), f, "%s", body)
+		assert.Equal(t, f, gjson.GetBytes(body, fmt.Sprintf("methods.password.config.fields.#(name==%s).name", f)).String(), "%s", body)
 	}
 }
 
 // checks if some keys are not set, this should be used to catch regression issues
 func outdatedFieldsDoNotExist(t *testing.T, body []byte) {
 	for _, k := range []string{"request"} {
-		assert.Equal(t, false, gjson.GetBytes(body, fmt.Sprintf("methods.password.config.fields.%s", k)).Exists())
+		assert.Equal(t, false, gjson.GetBytes(body, fmt.Sprintf("methods.password.config.fields.#(name==%s)", k)).Exists())
 	}
 }
 
@@ -97,8 +97,8 @@ func TestRegistration(t *testing.T) {
 									Method: "POST",
 									Action: "/action",
 									Fields: form.Fields{
-										"password":   {Name: "password", Type: "password", Required: true},
-										"csrf_token": {Name: "csrf_token", Type: "hidden", Required: true, Value: "csrf-token"},
+										{Name: "password", Type: "password", Required: true},
+										{Name: "csrf_token", Type: "hidden", Required: true, Value: "csrf-token"},
 									},
 								},
 							},
@@ -160,7 +160,7 @@ func TestRegistration(t *testing.T) {
 			assert.Equal(t, rr.ID.String(), gjson.GetBytes(body, "id").String(), "%s", body)
 			assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String(), "%s", body)
 			checkFormContent(t, body, "password", "csrf_token", "traits.username", "traits.foobar")
-			assert.Contains(t, gjson.GetBytes(body, "methods.password.config.fields.password.errors.0").String(), "data breaches and must no longer be used.", "%s", body)
+			assert.Contains(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==password).errors.0").String(), "data breaches and must no longer be used.", "%s", body)
 		})
 
 		t.Run("case=should return an error because not passing validation", func(t *testing.T) {
@@ -173,7 +173,7 @@ func TestRegistration(t *testing.T) {
 			assert.Equal(t, rr.ID.String(), gjson.GetBytes(body, "id").String(), "%s", body)
 			assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String(), "%s", body)
 			checkFormContent(t, body, "password", "csrf_token", "traits.username", "traits.foobar")
-			assert.Contains(t, gjson.GetBytes(body, "methods.password.config.fields.traits\\.foobar.errors.0").String(), "foobar is required", "%s", body)
+			assert.Contains(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==traits.foobar).errors.0").String(), "foobar is required", "%s", body)
 		})
 
 		t.Run("case=should fail because schema did not specify an identifier", func(t *testing.T) {
@@ -244,11 +244,11 @@ func TestRegistration(t *testing.T) {
 									Action: "/action",
 									Errors: []form.Error{{Message: "some error"}},
 									Fields: form.Fields{
-										"traits.foo": {
+										{
 											Name: "traits.foo", Value: "bar", Type: "text",
 											Errors: []form.Error{{Message: "bar"}},
 										},
-										"password": {Name: "password"},
+										{Name: "password"},
 									},
 								},
 							},
@@ -267,10 +267,10 @@ func TestRegistration(t *testing.T) {
 			assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String(), "%s", body)
 			checkFormContent(t, body, "password", "csrf_token", "traits.username")
 
-			assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.traits\\.foo.value"), "%s", body)
-			assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.traits\\.foo.error"))
+			assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==traits.foo).value"), "%s", body)
+			assert.Empty(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==traits.foo).error"))
 			assert.Empty(t, gjson.GetBytes(body, "methods.password.config.error"))
-			assert.Contains(t, gjson.GetBytes(body, "methods.password.config.fields.traits\\.foobar.errors.0").String(), "foobar is required", "%s", body)
+			assert.Contains(t, gjson.GetBytes(body, "methods.password.config.fields.#(name==traits.foobar).errors.0").String(), "foobar is required", "%s", body)
 		})
 
 		t.Run("case=should work even if password is just numbers", func(t *testing.T) {
@@ -307,22 +307,22 @@ func TestRegistration(t *testing.T) {
 						Action: "https://foo" + password.RegistrationPath + "?request=" + sr.ID.String(),
 						Method: "POST",
 						Fields: form.Fields{
-							"password": {
+							{
 								Name:     "password",
 								Type:     "password",
 								Required: true,
 							},
-							"csrf_token": {
+							{
 								Name:     "csrf_token",
 								Type:     "hidden",
 								Required: true,
 								Value:    "nosurf",
 							},
-							"traits.foobar": {
+							{
 								Name: "traits.foobar",
 								Type: "text",
 							},
-							"traits.username": {
+							{
 								Name: "traits.username",
 								Type: "text",
 							},
@@ -331,6 +331,8 @@ func TestRegistration(t *testing.T) {
 				},
 			},
 		}
+		sort.Sort(expected.Config.RequestMethodConfigurator.(*password.RequestMethod).HTMLForm.Fields)
+
 		actual := sr.Methods[identity.CredentialsTypePassword]
 		assert.EqualValues(t, expected.Config.RequestMethodConfigurator.(*password.RequestMethod).HTMLForm, actual.Config.RequestMethodConfigurator.(*password.RequestMethod).HTMLForm)
 	})