diff --git a/.schema/api.swagger.json b/.schema/api.swagger.json
index 864355fe43da..82fd704f5b9d 100755
--- a/.schema/api.swagger.json
+++ b/.schema/api.swagger.json
@@ -402,6 +402,60 @@
}
}
},
+ "/self-service/browser/flows/recovery": {
+ "get": {
+ "description": "This endpoint initializes a browser-based account recovery flow. Once initialized, the browser will be redirected to\n`urls.recovery_ui` with the request ID set as a query parameter. If a valid user session exists, the request\nis aborted.\n\n\u003e This endpoint is NOT INTENDED for API clients and only works\nwith browsers (Chrome, Firefox, ...).\n\nMore information can be found at [ORY Kratos Account Recovery Documentation](../self-service/flows/password-reset-account-recovery).",
+ "schemes": [
+ "http",
+ "https"
+ ],
+ "tags": [
+ "public"
+ ],
+ "summary": "Initialize browser-based account recovery flow",
+ "operationId": "initializeSelfServiceRecoveryFlow",
+ "responses": {
+ "302": {
+ "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is\ntypically 201."
+ },
+ "500": {
+ "description": "genericError",
+ "schema": {
+ "$ref": "#/definitions/genericError"
+ }
+ }
+ }
+ }
+ },
+ "/self-service/browser/flows/recovery/link": {
+ "post": {
+ "description": "\u003e This endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...) and HTML Forms.\n\nMore information can be found at [ORY Kratos Account Recovery Documentation](../self-service/flows/password-reset-account-recovery).",
+ "consumes": [
+ "application/json",
+ "application/x-www-form-urlencoded"
+ ],
+ "schemes": [
+ "http",
+ "https"
+ ],
+ "tags": [
+ "public"
+ ],
+ "summary": "Complete the browser-based recovery flow using a recovery link",
+ "operationId": "completeSelfServiceBrowserRecoveryLinkStrategyFlow",
+ "responses": {
+ "302": {
+ "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is\ntypically 201."
+ },
+ "500": {
+ "description": "genericError",
+ "schema": {
+ "$ref": "#/definitions/genericError"
+ }
+ }
+ }
+ }
+ },
"/self-service/browser/flows/registration": {
"get": {
"description": "This endpoint initializes a browser-based user registration flow. Once initialized, the browser will be redirected to\n`urls.registration_ui` with the request ID set as a query parameter. If a valid user session exists already, the browser will be\nredirected to `urls.default_redirect_url`.\n\n\u003e This endpoint is NOT INTENDED for API clients and only works\nwith browsers (Chrome, Firefox, ...).\n\nMore information can be found at [ORY Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).",
@@ -458,7 +512,7 @@
},
"/self-service/browser/flows/requests/login": {
"get": {
- "description": "This endpoint returns a login request's context with, for example, error details and\nother information.\n\nWhen accessing this endpoint through ORY Kratos' Public API, ensure that cookies are set as they are required for CSRF to work. To prevent\ntoken scanning attacks, the public endpoint does not return 404 status codes to prevent scanning attacks.\n\nMore information can be found at [ORY Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).",
+ "description": "This endpoint returns a login request's context with, for example, error details and\nother information.\n\nWhen accessing this endpoint through ORY Kratos' Public API, ensure that cookies are set as they are required for CSRF to work. To prevent\ntoken scanning attacks, the public endpoint does not return 404 status codes.\n\nMore information can be found at [ORY Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).",
"produces": [
"application/json"
],
@@ -516,9 +570,69 @@
}
}
},
+ "/self-service/browser/flows/requests/recovery": {
+ "get": {
+ "description": "When accessing this endpoint through ORY Kratos' Public API, ensure that cookies are set as they are required\nfor checking the auth session. To prevent scanning attacks, the public endpoint does not return 404 status codes\nbut instead 403 or 500.\n\nMore information can be found at [ORY Kratos Account Recovery Documentation](../self-service/flows/password-reset-account-recovery).",
+ "produces": [
+ "application/json"
+ ],
+ "schemes": [
+ "http",
+ "https"
+ ],
+ "tags": [
+ "common",
+ "public",
+ "admin"
+ ],
+ "summary": "Get the request context of browser-based recovery flows",
+ "operationId": "getSelfServiceBrowserRecoveryRequest",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Request is the Login Request ID\n\nThe value for this parameter comes from `request` URL Query parameter sent to your\napplication (e.g. `/recover?request=abcde`).",
+ "name": "request",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "recoveryRequest",
+ "schema": {
+ "$ref": "#/definitions/recoveryRequest"
+ }
+ },
+ "403": {
+ "description": "genericError",
+ "schema": {
+ "$ref": "#/definitions/genericError"
+ }
+ },
+ "404": {
+ "description": "genericError",
+ "schema": {
+ "$ref": "#/definitions/genericError"
+ }
+ },
+ "410": {
+ "description": "genericError",
+ "schema": {
+ "$ref": "#/definitions/genericError"
+ }
+ },
+ "500": {
+ "description": "genericError",
+ "schema": {
+ "$ref": "#/definitions/genericError"
+ }
+ }
+ }
+ }
+ },
"/self-service/browser/flows/requests/registration": {
"get": {
- "description": "This endpoint returns a registration request's context with, for example, error details and\nother information.\n\nWhen accessing this endpoint through ORY Kratos' Public API, ensure that cookies are set as they are required for CSRF to work. To prevent\ntoken scanning attacks, the public endpoint does not return 404 status codes to prevent scanning attacks.\n\nMore information can be found at [ORY Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).",
+ "description": "This endpoint returns a registration request's context with, for example, error details and\nother information.\n\nWhen accessing this endpoint through ORY Kratos' Public API, ensure that cookies are set as they are required for CSRF to work. To prevent\ntoken scanning attacks, the public endpoint does not return 404 status codes.\n\nMore information can be found at [ORY Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).",
"produces": [
"application/json"
],
@@ -596,7 +710,7 @@
"parameters": [
{
"type": "string",
- "description": "Request is the Login Request ID\n\nThe value for this parameter comes from `request` URL Query parameter sent to your\napplication (e.g. `/login?request=abcde`).",
+ "description": "Request is the Login Request ID\n\nThe value for this parameter comes from `request` URL Query parameter sent to your\napplication (e.g. `/settingss?request=abcde`).",
"name": "request",
"in": "query",
"required": true
@@ -915,7 +1029,7 @@
},
"/self-service/errors": {
"get": {
- "description": "This endpoint returns the error associated with a user-facing self service errors.\n\nWhen accessing this endpoint through ORY Kratos' Public API, ensure that cookies are set as they are required for CSRF to work. To prevent\ntoken scanning attacks, the public endpoint does not return 404 status codes to prevent scanning attacks.\n\nMore information can be found at [ORY Kratos User User Facing Error Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-facing-errors).",
+ "description": "This endpoint returns the error associated with a user-facing self service errors.\n\nWhen accessing this endpoint through ORY Kratos' Public API, ensure that cookies are set as they are required for CSRF to work. To prevent\ntoken scanning attacks, the public endpoint does not return 404 status codes.\n\nMore information can be found at [ORY Kratos User User Facing Error Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-facing-errors).",
"produces": [
"application/json"
],
@@ -1045,6 +1159,10 @@
"$ref": "#/definitions/Error"
}
},
+ "ID": {
+ "type": "integer",
+ "format": "int64"
+ },
"Identity": {
"type": "object",
"required": [
@@ -1053,15 +1171,16 @@
"traits"
],
"properties": {
- "addresses": {
+ "id": {
+ "$ref": "#/definitions/UUID"
+ },
+ "recovery_addresses": {
+ "description": "RecoveryAddresses contains all the addresses that can be used to recover an identity.",
"type": "array",
"items": {
- "$ref": "#/definitions/VerifiableAddress"
+ "$ref": "#/definitions/RecoveryAddress"
}
},
- "id": {
- "$ref": "#/definitions/UUID"
- },
"traits": {
"$ref": "#/definitions/Traits"
},
@@ -1072,9 +1191,39 @@
"traits_schema_url": {
"description": "TraitsSchemaURL is the URL of the endpoint where the identity's traits schema can be fetched from.\n\nformat: url",
"type": "string"
+ },
+ "verifiable_addresses": {
+ "description": "VerifiableAddresses contains all the addresses that can be verified by the user.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/VerifiableAddress"
+ }
+ }
+ }
+ },
+ "Message": {
+ "type": "object",
+ "properties": {
+ "context": {
+ "type": "object"
+ },
+ "id": {
+ "$ref": "#/definitions/ID"
+ },
+ "text": {
+ "type": "string"
+ },
+ "type": {
+ "$ref": "#/definitions/Type"
}
}
},
+ "Messages": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Message"
+ }
+ },
"ProviderCredentialsConfig": {
"type": "object",
"properties": {
@@ -1086,6 +1235,28 @@
}
}
},
+ "RecoveryAddress": {
+ "type": "object",
+ "required": [
+ "id",
+ "value",
+ "via"
+ ],
+ "properties": {
+ "id": {
+ "$ref": "#/definitions/UUID"
+ },
+ "value": {
+ "type": "string"
+ },
+ "via": {
+ "$ref": "#/definitions/RecoveryAddressType"
+ }
+ }
+ },
+ "RecoveryAddressType": {
+ "type": "string"
+ },
"RequestMethodConfig": {
"type": "object",
"required": [
@@ -1114,9 +1285,15 @@
}
}
},
+ "State": {
+ "type": "string"
+ },
"Traits": {
"type": "object"
},
+ "Type": {
+ "type": "string"
+ },
"UUID": {
"type": "string",
"format": "uuid4"
@@ -1282,7 +1459,9 @@
},
"details": {
"type": "object",
- "additionalProperties": true
+ "additionalProperties": {
+ "type": "object"
+ }
},
"message": {
"type": "string"
@@ -1412,6 +1591,67 @@
}
}
},
+ "recoveryRequest": {
+ "description": "This request is used when an identity wants to recover their account.\n\nWe recommend reading the [Account Recovery Documentation](../self-service/flows/password-reset-account-recovery)",
+ "type": "object",
+ "title": "Request presents a recovery request",
+ "required": [
+ "id",
+ "expires_at",
+ "issued_at",
+ "request_url",
+ "methods",
+ "state"
+ ],
+ "properties": {
+ "active": {
+ "description": "Active, if set, contains the registration method that is being used. It is initially\nnot set.",
+ "type": "string"
+ },
+ "expires_at": {
+ "description": "ExpiresAt is the time (UTC) when the request expires. If the user still wishes to update the setting,\na new request has to be initiated.",
+ "type": "string",
+ "format": "date-time"
+ },
+ "id": {
+ "$ref": "#/definitions/UUID"
+ },
+ "issued_at": {
+ "description": "IssuedAt is the time (UTC) when the request occurred.",
+ "type": "string",
+ "format": "date-time"
+ },
+ "messages": {
+ "$ref": "#/definitions/Messages"
+ },
+ "methods": {
+ "description": "Methods contains context for all account recovery methods. If a registration request has been\nprocessed, but for example the password is incorrect, this will contain error messages.",
+ "type": "object",
+ "additionalProperties": {
+ "$ref": "#/definitions/recoveryRequestMethod"
+ }
+ },
+ "request_url": {
+ "description": "RequestURL is the initial URL that was requested from ORY Kratos. It can be used\nto forward information contained in the URL's path or query for example.",
+ "type": "string"
+ },
+ "state": {
+ "$ref": "#/definitions/State"
+ }
+ }
+ },
+ "recoveryRequestMethod": {
+ "type": "object",
+ "properties": {
+ "config": {
+ "$ref": "#/definitions/RequestMethodConfig"
+ },
+ "method": {
+ "description": "Method contains the request credentials type.",
+ "type": "string"
+ }
+ }
+ },
"registrationRequest": {
"type": "object",
"required": [
@@ -1561,6 +1801,9 @@
"type": "string",
"format": "date-time"
},
+ "messages": {
+ "$ref": "#/definitions/Messages"
+ },
"methods": {
"description": "Methods contains context for all enabled registration methods. If a registration request has been\nprocessed, but for example the password is incorrect, this will contain error messages.",
"type": "object",
@@ -1573,7 +1816,7 @@
"type": "string"
},
"update_successful": {
- "description": "UpdateSuccessful, if true, indicates that the settings request has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a request with invalid (e.g. \"please use a valid phone number\") data was sent.",
+ "description": "Success, if true, indicates that the settings request has been updated successfully with the provided data.\nDone will stay true when repeatedly checking. If set to true, done will revert back to false only\nwhen a request with invalid (e.g. \"please use a valid phone number\") data was sent.",
"type": "boolean"
}
}
diff --git a/.schema/config.schema.json b/.schema/config.schema.json
index 9773fb444e5c..e3fc8f60a511 100644
--- a/.schema/config.schema.json
+++ b/.schema/config.schema.json
@@ -396,13 +396,18 @@
"1m",
"1s"
]
- },
- "link_lifespan": {
- "title": "Self-Service Verification Link Lifespan",
- "description": "Sets how long the verification link (e.g. the one sent via email) is valid for.",
+ }
+ }
+ },
+ "recovery": {
+ "type": "object",
+ "properties": {
+ "request_lifespan": {
+ "title": "Self-Service Verification Request Lifespan",
+ "description": "Sets how long the verification request (for the UI interaction) is valid.",
"type": "string",
"pattern": "^[0-9]+(ns|us|ms|s|m|h)$",
- "default": "24h",
+ "default": "1h",
"examples": [
"1h",
"1m",
diff --git a/cmd/client/migrate.go b/cmd/client/migrate.go
index f0c6a701947f..80f7993258cb 100644
--- a/cmd/client/migrate.go
+++ b/cmd/client/migrate.go
@@ -28,8 +28,9 @@ func NewMigrateHandler() *MigrateHandler {
func (h *MigrateHandler) MigrateSQL(cmd *cobra.Command, args []string) {
var d driver.Driver
+ logger := logrusx.New("ORY Kratos", cmd.Version)
if flagx.MustGetBool(cmd, "read-from-env") {
- d = driver.MustNewDefaultDriver(logrusx.New(), "", "", "", true)
+ d = driver.MustNewDefaultDriver(logger, "", "", "", true)
if len(d.Configuration().DSN()) == 0 {
fmt.Println(cmd.UsageString())
fmt.Println("")
@@ -44,7 +45,7 @@ func (h *MigrateHandler) MigrateSQL(cmd *cobra.Command, args []string) {
return
}
viper.Set(configuration.ViperKeyDSN, args[0])
- d = driver.MustNewDefaultDriver(logrusx.New(), "", "", "", true)
+ d = driver.MustNewDefaultDriver(logger, "", "", "", true)
}
var plan bytes.Buffer
diff --git a/cmd/daemon/middleware.go b/cmd/daemon/middleware.go
index 20c934440d4a..329014c3f9f6 100644
--- a/cmd/daemon/middleware.go
+++ b/cmd/daemon/middleware.go
@@ -7,13 +7,15 @@ import (
"github.com/sirupsen/logrus"
"github.com/urfave/negroni"
+ "github.com/ory/x/logrusx"
+
"github.com/ory/x/healthx"
"github.com/ory/x/reqlog"
)
-func NewNegroniLoggerMiddleware(l logrus.FieldLogger, name string) *reqlog.Middleware {
- n := reqlog.NewMiddlewareFromLogger(l.(*logrus.Logger), name).ExcludePaths(healthx.AliveCheckPath, healthx.ReadyCheckPath)
- n.Before = func(entry *logrus.Entry, req *http.Request, remoteAddr string) *logrus.Entry {
+func NewNegroniLoggerMiddleware(l *logrusx.Logger, name string) *reqlog.Middleware {
+ n := reqlog.NewMiddlewareFromLogger(l, name).ExcludePaths(healthx.AliveCheckPath, healthx.ReadyCheckPath)
+ n.Before = func(entry *logrusx.Logger, req *http.Request, remoteAddr string) *logrusx.Logger {
return entry.WithFields(logrus.Fields{
"name": name,
"request": req.RequestURI,
@@ -22,7 +24,7 @@ func NewNegroniLoggerMiddleware(l logrus.FieldLogger, name string) *reqlog.Middl
})
}
- n.After = func(entry *logrus.Entry, res negroni.ResponseWriter, latency time.Duration, name string) *logrus.Entry {
+ n.After = func(entry *logrusx.Logger, req *http.Request, res negroni.ResponseWriter, latency time.Duration, name string) *logrusx.Logger {
return entry.WithFields(logrus.Fields{
"name": name,
"status": res.Status(),
diff --git a/cmd/daemon/serve.go b/cmd/daemon/serve.go
index 8aecef01c361..8d66c69a40ff 100644
--- a/cmd/daemon/serve.go
+++ b/cmd/daemon/serve.go
@@ -5,8 +5,6 @@ import (
"strings"
"sync"
- "github.com/sirupsen/logrus"
-
"github.com/ory/analytics-go/v4"
"github.com/ory/x/flagx"
@@ -43,7 +41,7 @@ func servePublic(d driver.Driver, wg *sync.WaitGroup, cmd *cobra.Command, args [
router := x.NewRouterPublic()
r.RegisterPublicRoutes(router)
- n.Use(NewNegroniLoggerMiddleware(l.(*logrus.Logger), "public#"+c.SelfPublicURL().String()))
+ n.Use(NewNegroniLoggerMiddleware(l, "public#"+c.SelfPublicURL().String()))
n.Use(sqa(cmd, d))
csrf := x.NewCSRFHandler(
@@ -79,7 +77,7 @@ func serveAdmin(d driver.Driver, wg *sync.WaitGroup, cmd *cobra.Command, args []
router := x.NewRouterAdmin()
r.RegisterAdminRoutes(router)
- n.Use(NewNegroniLoggerMiddleware(l.(*logrus.Logger), "admin#"+c.SelfAdminURL().String()))
+ n.Use(NewNegroniLoggerMiddleware(l, "admin#"+c.SelfAdminURL().String()))
n.Use(sqa(cmd, d))
n.UseHandler(router)
diff --git a/cmd/root.go b/cmd/root.go
index c46a03fecb49..af6fe29edf6e 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -4,14 +4,13 @@ import (
"fmt"
"os"
- "github.com/sirupsen/logrus"
-
+ "github.com/ory/x/logrusx"
"github.com/ory/x/viperx"
"github.com/spf13/cobra"
)
-var logger logrus.FieldLogger
+var logger *logrusx.Logger
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
diff --git a/continuity/manager_test.go b/continuity/manager_test.go
index 3e9961310120..3f0f71f1a351 100644
--- a/continuity/manager_test.go
+++ b/continuity/manager_test.go
@@ -45,7 +45,7 @@ func TestManager(t *testing.T) {
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))
var newServer = func(t *testing.T, p continuity.Manager, tc *persisterTestCase) *httptest.Server {
- writer := herodot.NewJSONWriter(logrusx.New())
+ writer := herodot.NewJSONWriter(logrusx.New("", ""))
router := httprouter.New()
router.PUT("/:name", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if err := p.Pause(r.Context(), w, r, ps.ByName("name"), tc.ro...); err != nil {
diff --git a/courier/persistence.go b/courier/persistence.go
index 95b9f94c6b8a..339c3ac091d2 100644
--- a/courier/persistence.go
+++ b/courier/persistence.go
@@ -6,7 +6,7 @@ import (
"testing"
"time"
- "github.com/bxcodec/faker"
+ "github.com/bxcodec/faker/v3"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/courier/template/recovery_invalid.go b/courier/template/recovery_invalid.go
new file mode 100644
index 000000000000..7a8ebdce846a
--- /dev/null
+++ b/courier/template/recovery_invalid.go
@@ -0,0 +1,33 @@
+package template
+
+import (
+ "path/filepath"
+
+ "github.com/ory/kratos/driver/configuration"
+)
+
+type (
+ RecoveryInvalid struct {
+ c configuration.Provider
+ m *RecoveryInvalidModel
+ }
+ RecoveryInvalidModel struct {
+ To string
+ }
+)
+
+func NewRecoveryInvalid(c configuration.Provider, m *RecoveryInvalidModel) *RecoveryInvalid {
+ return &RecoveryInvalid{c: c, m: m}
+}
+
+func (t *RecoveryInvalid) EmailRecipient() (string, error) {
+ return t.m.To, nil
+}
+
+func (t *RecoveryInvalid) EmailSubject() (string, error) {
+ return loadTextTemplate(filepath.Join(t.c.CourierTemplatesRoot(), "recovery/invalid/email.subject.gotmpl"), t.m)
+}
+
+func (t *RecoveryInvalid) EmailBody() (string, error) {
+ return loadTextTemplate(filepath.Join(t.c.CourierTemplatesRoot(), "recovery/invalid/email.body.gotmpl"), t.m)
+}
diff --git a/courier/template/recovery_invalid_test.go b/courier/template/recovery_invalid_test.go
new file mode 100644
index 000000000000..021efc100a8e
--- /dev/null
+++ b/courier/template/recovery_invalid_test.go
@@ -0,0 +1,24 @@
+package template_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/ory/kratos/courier/template"
+ "github.com/ory/kratos/internal"
+)
+
+func TestRecoverInvalid(t *testing.T) {
+ conf, _ := internal.NewFastRegistryWithMocks(t)
+ tpl := template.NewRecoveryInvalid(conf, &template.RecoveryInvalidModel{})
+
+ rendered, err := tpl.EmailBody()
+ require.NoError(t, err)
+ assert.NotEmpty(t, rendered)
+
+ rendered, err = tpl.EmailSubject()
+ require.NoError(t, err)
+ assert.NotEmpty(t, rendered)
+}
diff --git a/courier/template/recovery_valid.go b/courier/template/recovery_valid.go
new file mode 100644
index 000000000000..c17ff8e5825e
--- /dev/null
+++ b/courier/template/recovery_valid.go
@@ -0,0 +1,34 @@
+package template
+
+import (
+ "path/filepath"
+
+ "github.com/ory/kratos/driver/configuration"
+)
+
+type (
+ RecoveryValid struct {
+ c configuration.Provider
+ m *RecoveryValidModel
+ }
+ RecoveryValidModel struct {
+ To string
+ RecoveryURL string
+ }
+)
+
+func NewRecoveryValid(c configuration.Provider, m *RecoveryValidModel) *RecoveryValid {
+ return &RecoveryValid{c: c, m: m}
+}
+
+func (t *RecoveryValid) EmailRecipient() (string, error) {
+ return t.m.To, nil
+}
+
+func (t *RecoveryValid) EmailSubject() (string, error) {
+ return loadTextTemplate(filepath.Join(t.c.CourierTemplatesRoot(), "recovery/valid/email.subject.gotmpl"), t.m)
+}
+
+func (t *RecoveryValid) EmailBody() (string, error) {
+ return loadTextTemplate(filepath.Join(t.c.CourierTemplatesRoot(), "recovery/valid/email.body.gotmpl"), t.m)
+}
diff --git a/courier/template/recovery_valid_test.go b/courier/template/recovery_valid_test.go
new file mode 100644
index 000000000000..09d355e14555
--- /dev/null
+++ b/courier/template/recovery_valid_test.go
@@ -0,0 +1,24 @@
+package template_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/ory/kratos/courier/template"
+ "github.com/ory/kratos/internal"
+)
+
+func TestRecoverValid(t *testing.T) {
+ conf, _ := internal.NewFastRegistryWithMocks(t)
+ tpl := template.NewRecoveryValid(conf, &template.RecoveryValidModel{})
+
+ rendered, err := tpl.EmailBody()
+ require.NoError(t, err)
+ assert.NotEmpty(t, rendered)
+
+ rendered, err = tpl.EmailSubject()
+ require.NoError(t, err)
+ assert.NotEmpty(t, rendered)
+}
diff --git a/courier/template/templates/recovery/invalid/email.body.gotmpl b/courier/template/templates/recovery/invalid/email.body.gotmpl
new file mode 100644
index 000000000000..b8d9188c5975
--- /dev/null
+++ b/courier/template/templates/recovery/invalid/email.body.gotmpl
@@ -0,0 +1,9 @@
+Hi,
+
+you (or someone else) entered this email address when trying to recover access to an account.
+
+However, this email address is not on our database of registered users and therefore the attempt has failed.
+
+If this was you, check if you signed up using a different address.
+
+If this was not you, please ignore this email.
diff --git a/courier/template/templates/recovery/invalid/email.subject.gotmpl b/courier/template/templates/recovery/invalid/email.subject.gotmpl
new file mode 100644
index 000000000000..403d0dd4a883
--- /dev/null
+++ b/courier/template/templates/recovery/invalid/email.subject.gotmpl
@@ -0,0 +1 @@
+Account access attempted
diff --git a/courier/template/templates/recovery/valid/email.body.gotmpl b/courier/template/templates/recovery/valid/email.body.gotmpl
new file mode 100644
index 000000000000..a03e25b9e65d
--- /dev/null
+++ b/courier/template/templates/recovery/valid/email.body.gotmpl
@@ -0,0 +1,5 @@
+Hi,
+
+please recover your account by clicking the following link:
+
+{{ .RecoveryURL }}
diff --git a/courier/template/templates/recovery/valid/email.subject.gotmpl b/courier/template/templates/recovery/valid/email.subject.gotmpl
new file mode 100644
index 000000000000..6b34ad1b58aa
--- /dev/null
+++ b/courier/template/templates/recovery/valid/email.subject.gotmpl
@@ -0,0 +1 @@
+Recover your account
diff --git a/courier/template/templates/verify/invalid/email.body.gotmpl b/courier/template/templates/verification/invalid/email.body.gotmpl
similarity index 100%
rename from courier/template/templates/verify/invalid/email.body.gotmpl
rename to courier/template/templates/verification/invalid/email.body.gotmpl
diff --git a/courier/template/templates/verify/invalid/email.subject.gotmpl b/courier/template/templates/verification/invalid/email.subject.gotmpl
similarity index 100%
rename from courier/template/templates/verify/invalid/email.subject.gotmpl
rename to courier/template/templates/verification/invalid/email.subject.gotmpl
diff --git a/courier/template/templates/verify/valid/email.body.gotmpl b/courier/template/templates/verification/valid/email.body.gotmpl
similarity index 51%
rename from courier/template/templates/verify/valid/email.body.gotmpl
rename to courier/template/templates/verification/valid/email.body.gotmpl
index 76d6bc6965d2..d8e3168e5a78 100644
--- a/courier/template/templates/verify/valid/email.body.gotmpl
+++ b/courier/template/templates/verification/valid/email.body.gotmpl
@@ -1,3 +1,3 @@
Hi, please verify your account by clicking the following link:
-{{ .VerifyURL }}
+{{ .VerificationURL }}
diff --git a/courier/template/templates/verify/valid/email.subject.gotmpl b/courier/template/templates/verification/valid/email.subject.gotmpl
similarity index 100%
rename from courier/template/templates/verify/valid/email.subject.gotmpl
rename to courier/template/templates/verification/valid/email.subject.gotmpl
diff --git a/courier/template/verification_invalid.go b/courier/template/verification_invalid.go
new file mode 100644
index 000000000000..62271cdf9932
--- /dev/null
+++ b/courier/template/verification_invalid.go
@@ -0,0 +1,33 @@
+package template
+
+import (
+ "path/filepath"
+
+ "github.com/ory/kratos/driver/configuration"
+)
+
+type (
+ VerificationInvalid struct {
+ c configuration.Provider
+ m *VerificationInvalidModel
+ }
+ VerificationInvalidModel struct {
+ To string
+ }
+)
+
+func NewVerificationInvalid(c configuration.Provider, m *VerificationInvalidModel) *VerificationInvalid {
+ return &VerificationInvalid{c: c, m: m}
+}
+
+func (t *VerificationInvalid) EmailRecipient() (string, error) {
+ return t.m.To, nil
+}
+
+func (t *VerificationInvalid) EmailSubject() (string, error) {
+ return loadTextTemplate(filepath.Join(t.c.CourierTemplatesRoot(), "verification/invalid/email.subject.gotmpl"), t.m)
+}
+
+func (t *VerificationInvalid) EmailBody() (string, error) {
+ return loadTextTemplate(filepath.Join(t.c.CourierTemplatesRoot(), "verification/invalid/email.body.gotmpl"), t.m)
+}
diff --git a/courier/template/verify_invalid_test.go b/courier/template/verification_invalid_test.go
similarity index 84%
rename from courier/template/verify_invalid_test.go
rename to courier/template/verification_invalid_test.go
index 01a9491a9390..5d77cae21fb1 100644
--- a/courier/template/verify_invalid_test.go
+++ b/courier/template/verification_invalid_test.go
@@ -12,7 +12,7 @@ import (
func TestVerifyInvalid(t *testing.T) {
conf, _ := internal.NewFastRegistryWithMocks(t)
- tpl := template.NewVerifyInvalid(conf, &template.VerifyInvalidModel{})
+ tpl := template.NewVerificationInvalid(conf, &template.VerificationInvalidModel{})
rendered, err := tpl.EmailBody()
require.NoError(t, err)
diff --git a/courier/template/verification_valid.go b/courier/template/verification_valid.go
new file mode 100644
index 000000000000..d73a47504dc4
--- /dev/null
+++ b/courier/template/verification_valid.go
@@ -0,0 +1,34 @@
+package template
+
+import (
+ "path/filepath"
+
+ "github.com/ory/kratos/driver/configuration"
+)
+
+type (
+ VerificationValid struct {
+ c configuration.Provider
+ m *VerificationValidModel
+ }
+ VerificationValidModel struct {
+ To string
+ VerificationURL string
+ }
+)
+
+func NewVerificationValid(c configuration.Provider, m *VerificationValidModel) *VerificationValid {
+ return &VerificationValid{c: c, m: m}
+}
+
+func (t *VerificationValid) EmailRecipient() (string, error) {
+ return t.m.To, nil
+}
+
+func (t *VerificationValid) EmailSubject() (string, error) {
+ return loadTextTemplate(filepath.Join(t.c.CourierTemplatesRoot(), "verification/valid/email.subject.gotmpl"), t.m)
+}
+
+func (t *VerificationValid) EmailBody() (string, error) {
+ return loadTextTemplate(filepath.Join(t.c.CourierTemplatesRoot(), "verification/valid/email.body.gotmpl"), t.m)
+}
diff --git a/courier/template/verify_valid_test.go b/courier/template/verification_valid_test.go
similarity index 85%
rename from courier/template/verify_valid_test.go
rename to courier/template/verification_valid_test.go
index 705768474d87..f80fbf4cec45 100644
--- a/courier/template/verify_valid_test.go
+++ b/courier/template/verification_valid_test.go
@@ -12,7 +12,7 @@ import (
func TestVerifyValid(t *testing.T) {
conf, _ := internal.NewFastRegistryWithMocks(t)
- tpl := template.NewVerifyValid(conf, &template.VerifyValidModel{})
+ tpl := template.NewVerificationValid(conf, &template.VerificationValidModel{})
rendered, err := tpl.EmailBody()
require.NoError(t, err)
diff --git a/courier/template/verify_invalid.go b/courier/template/verify_invalid.go
deleted file mode 100644
index 56d0faf936bf..000000000000
--- a/courier/template/verify_invalid.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package template
-
-import (
- "path/filepath"
-
- "github.com/ory/kratos/driver/configuration"
-)
-
-type (
- VerifyInvalid struct {
- c configuration.Provider
- m *VerifyInvalidModel
- }
- VerifyInvalidModel struct {
- To string
- }
-)
-
-func NewVerifyInvalid(c configuration.Provider, m *VerifyInvalidModel) *VerifyInvalid {
- return &VerifyInvalid{c: c, m: m}
-}
-
-func (t *VerifyInvalid) EmailRecipient() (string, error) {
- return t.m.To, nil
-}
-
-func (t *VerifyInvalid) EmailSubject() (string, error) {
- return loadTextTemplate(filepath.Join(t.c.CourierTemplatesRoot(), "verify/invalid/email.subject.gotmpl"), t.m)
-}
-
-func (t *VerifyInvalid) EmailBody() (string, error) {
- return loadTextTemplate(filepath.Join(t.c.CourierTemplatesRoot(), "verify/invalid/email.body.gotmpl"), t.m)
-}
diff --git a/courier/template/verify_valid.go b/courier/template/verify_valid.go
deleted file mode 100644
index 5e782f9f6991..000000000000
--- a/courier/template/verify_valid.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package template
-
-import (
- "path/filepath"
-
- "github.com/ory/kratos/driver/configuration"
-)
-
-type (
- VerifyValid struct {
- c configuration.Provider
- m *VerifyValidModel
- }
- VerifyValidModel struct {
- To string
- VerifyURL string
- }
-)
-
-func NewVerifyValid(c configuration.Provider, m *VerifyValidModel) *VerifyValid {
- return &VerifyValid{c: c, m: m}
-}
-
-func (t *VerifyValid) EmailRecipient() (string, error) {
- return t.m.To, nil
-}
-
-func (t *VerifyValid) EmailSubject() (string, error) {
- return loadTextTemplate(filepath.Join(t.c.CourierTemplatesRoot(), "verify/valid/email.subject.gotmpl"), t.m)
-}
-
-func (t *VerifyValid) EmailBody() (string, error) {
- return loadTextTemplate(filepath.Join(t.c.CourierTemplatesRoot(), "verify/valid/email.body.gotmpl"), t.m)
-}
diff --git a/docs/config.js b/docs/config.js
index bff31c047b92..4857a172c3f0 100644
--- a/docs/config.js
+++ b/docs/config.js
@@ -10,7 +10,7 @@ module.exports = {
{
replacer: ({content, next}) => content.replace(/git checkout (v[0-9a-zA-Z\\.\\-]+)/gi, `git checkout ${next}`),
files: [
- 'docs/docs/guides/zero-trust-iap-proxy-identity-access-proxy.md',
+ 'docs/docs/guides/zero-trust-iap-proxy-identity-access-proxy.mdx',
'docs/docs/quickstart.mdx',
]
},
diff --git a/docs/docs/concepts/ui-user-interface.md b/docs/docs/concepts/ui-user-interface.md
index b2cd4715ee75..37dfd851c3db 100644
--- a/docs/docs/concepts/ui-user-interface.md
+++ b/docs/docs/concepts/ui-user-interface.md
@@ -33,3 +33,7 @@ preventive measures built in.
Chapter [Self-Service Flows](../self-service/flows/index) contains further
information on APIs and flows related to the SSUI, and build self service
applications.
+
+## Messages
+
+This section is a work-in-progress.
diff --git a/docs/docs/guides/zero-trust-iap-proxy-identity-access-proxy.mdx b/docs/docs/guides/zero-trust-iap-proxy-identity-access-proxy.mdx
index 0f62c13e6231..77dfc452ed6c 100644
--- a/docs/docs/guides/zero-trust-iap-proxy-identity-access-proxy.mdx
+++ b/docs/docs/guides/zero-trust-iap-proxy-identity-access-proxy.mdx
@@ -4,6 +4,7 @@ title: Zero Trust with IAP Proxy
---
import useBaseUrl from '@docusaurus/useBaseUrl'
+import Mermaid from '@theme/Mermaid'
The [Quickstart](../quickstart.mdx) covers a basic set up that uses client-side
routing in `SecureApp` to forward requests to ORY Kratos.
@@ -90,13 +91,77 @@ To better understand the application architecture, let's take a look at the netw
configuration. This assumes that you have at least some understanding of how
Docker networks work:
-[![User Login and Registration Network Topology](https://mermaid.ink/img/eyJjb2RlIjoiZ3JhcGggVERcblxuc3ViZ3JhcGggaG5bSG9zdCBOZXR3b3JrXVxuICAgIEJbQnJvd3Nlcl1cbiAgICBCLS0-fENhbiBhY2Nlc3MgVVJMcyB2aWEgMTI3LjAuMC4xOjQ0NTV8T0tQSE5cbiAgICBCLS0-fENhbiBhY2Nlc3MgVUkgdmlhIDEyNy4wLjAuMTo0NDM2fFNNVFBVSVxuICAgIE9LUEhOKFtSZXZlcnNlIFByb3h5IGV4cG9zZWQgYXQgOjQ0NTVdKVxuICAgIFNNVFBVSShbTWFpbFNsdXJwZXIgVUkgZXhwb3NlZCBhdCA6NDQzNl0pXG5lbmRcblxuc3ViZ3JhcGggZG5bXCJJbnRlcm5hbCBEb2NrZXIgTmV0d29yayAoaW50cmFuZXQpXCJdXG4gICAgT0tQSE4tLT5PT1xuICAgIFNNVFBVSS0tPlNNVFBcbiAgICBPTy0tPnxQcm94aWVzIFVSTHNzIC8ub3J5L2tyYXRvcy9wdWJsaWMvKiB0b3xPS1xuICAgIE9PLS0-fFwiUHJveGllcyAvYXV0aC9sb2dpbiwgL2F1dGgvcmVnaXN0cmF0aW9uLCAvZGFzaGJvYXJkLCAuLi4gdG9cInxTQVxuICAgIFNBLS0-fFRhbGtzIHRvfE9LXG4gICAgT0stLT58U2VuZHMgbWFpbCB2aWF8U01UUFxuICAgIE9PLS0-fFZhbGlkYXRlcyBhdXRoIHNlc3Npb25zIHVzaW5nfE9LXG5cbiAgICBPS1tPUlkgS3JhdG9zXVxuICAgIE9PW1wiUmV2ZXJzZSBQcm94eSAoT1JZIE9hdGhrZWVwZXIpXCJdXG4gICAgU0FbXCJTZWN1cmVBcHAgKE9SWSBLcmF0b3MgU2VsZlNlcnZpY2UgVUkgTm9kZSBFeGFtcGxlKVwiXVxuICAgIFNNVFBbXCJTTVRQIFNlcnZlciAoTWFpbFNsdXJwZXIpXCJdXG5lbmRcbiIsIm1lcm1haWQiOnsidGhlbWUiOiJuZXV0cmFsIiwiZmxvd2NoYXJ0Ijp7InJhbmtTcGFjaW5nIjo2NSwibm9kZVNwYWNpbmciOjMwLCJjdXJ2ZSI6ImJhc2lzIn19fQ)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggVERcblxuc3ViZ3JhcGggaG5bSG9zdCBOZXR3b3JrXVxuICAgIEJbQnJvd3Nlcl1cbiAgICBCLS0-fENhbiBhY2Nlc3MgVVJMcyB2aWEgMTI3LjAuMC4xOjQ0NTV8T0tQSE5cbiAgICBCLS0-fENhbiBhY2Nlc3MgVUkgdmlhIDEyNy4wLjAuMTo0NDM2fFNNVFBVSVxuICAgIE9LUEhOKFtSZXZlcnNlIFByb3h5IGV4cG9zZWQgYXQgOjQ0NTVdKVxuICAgIFNNVFBVSShbTWFpbFNsdXJwZXIgVUkgZXhwb3NlZCBhdCA6NDQzNl0pXG5lbmRcblxuc3ViZ3JhcGggZG5bXCJJbnRlcm5hbCBEb2NrZXIgTmV0d29yayAoaW50cmFuZXQpXCJdXG4gICAgT0tQSE4tLT5PT1xuICAgIFNNVFBVSS0tPlNNVFBcbiAgICBPTy0tPnxQcm94aWVzIFVSTHNzIC8ub3J5L2tyYXRvcy9wdWJsaWMvKiB0b3xPS1xuICAgIE9PLS0-fFwiUHJveGllcyAvYXV0aC9sb2dpbiwgL2F1dGgvcmVnaXN0cmF0aW9uLCAvZGFzaGJvYXJkLCAuLi4gdG9cInxTQVxuICAgIFNBLS0-fFRhbGtzIHRvfE9LXG4gICAgT0stLT58U2VuZHMgbWFpbCB2aWF8U01UUFxuICAgIE9PLS0-fFZhbGlkYXRlcyBhdXRoIHNlc3Npb25zIHVzaW5nfE9LXG5cbiAgICBPS1tPUlkgS3JhdG9zXVxuICAgIE9PW1wiUmV2ZXJzZSBQcm94eSAoT1JZIE9hdGhrZWVwZXIpXCJdXG4gICAgU0FbXCJTZWN1cmVBcHAgKE9SWSBLcmF0b3MgU2VsZlNlcnZpY2UgVUkgTm9kZSBFeGFtcGxlKVwiXVxuICAgIFNNVFBbXCJTTVRQIFNlcnZlciAoTWFpbFNsdXJwZXIpXCJdXG5lbmRcbiIsIm1lcm1haWQiOnsidGhlbWUiOiJuZXV0cmFsIiwiZmxvd2NoYXJ0Ijp7InJhbmtTcGFjaW5nIjo2NSwibm9kZVNwYWNpbmciOjMwLCJjdXJ2ZSI6ImJhc2lzIn19fQ)
+|Can access URLs via 127.0.0.1:4455|OKPHN
+ B-->|Can access UI via 127.0.0.1:4436|SMTPUI
+ OKPHN([Reverse Proxy exposed at :4455])
+ SMTPUI([MailSlurper UI exposed at :4436])
+end
+subgraph dn["Internal Docker Network (intranet)"]
+ OKPHN-->OO
+ SMTPUI-->SMTP
+ OO-->|Proxies URLss /.ory/kratos/public/* to|OK
+ OO-->|"Proxies /auth/login, /auth/registration, /dashboard, ... to"|SA
+ SA-->|Talks to|OK
+ OK-->|Sends mail via|SMTP
+ OO-->|Validates auth sessions using|OK
+ OK[ORY Kratos]
+ OO["Reverse Proxy (ORY Oathkeeper)"]
+ SA["SecureApp (ORY Kratos SelfService UI Node Example)"]
+ SMTP["SMTP Server (MailSlurper)"]
+end
+`}>
As you can see, all requests except for our demo mail server are proxied through ORY Oathkeeper.
The next diagram shows how we've configured the routes in ORY Oathkeeper:
-[![User Login and Registration Routes](https://mermaid.ink/img/eyJjb2RlIjoiZ3JhcGggVERcblxuc3ViZ3JhcGggcGlbUHVibGljIEludGVybmV0XVxuICAgIEJbQnJvd3Nlcl1cbmVuZFxuXG5zdWJncmFwaCB2cGNbVlBDIC8gQ2xvdWQgLyBEb2NrZXIgTmV0d29ya11cbnN1YmdyYXBoIFwiRGVtaWxpdGFyaXplZCBab25lIC8gRE1aXCJcbiAgICBPS1tPUlkgT2F0aGtlZXBlciA6NDQ1NV1cbiAgICBCIC0tPiBPS1xuZW5kXG5cbiAgICBPSyAtLT58XCJGb3J3YXJkcyB7LywvZGFzaGJvYXJkfSB0b1wifCBTQURcbiAgICBPSyAtLT58XCJGb3J3YXJkcyAvYXV0aC9sb2dvdXQgdG9cInwgU0FMVVxuICAgIE9LIC0tPnxcIkZvcndhcmRzIC9hdXRoL2xvZ2luIHRvXCJ8IFNBTElcbiAgICBPSyAtLT58XCJGb3J3YXJkcyAvYXV0aC9yZWdpc3RyYXRpb24gdG9cInwgU0FSXG4gICAgT0sgLS0-fFwiRm9yd2FyZHMgL2F1dGgvKiB0b1wifCBTQUFcbiAgICBPSyAtLT58XCJGb3J3YXJkcyAvLm9yeS9rcmF0b3MvcHVibGljLyogdG9cInwgS1BcblxuICAgIHN1YmdyYXBoIFwiUHJpdmF0ZSBTdWJuZXQgLyBJbnRyYW5ldFwiXG4gICAgS1sgT1JZIEtyYXRvcyBdXG5cbiAgICBLUChbIE9SWSBLcmF0b3MgUHVibGljIEFQSSBdKVxuICAgIEtBKFsgT1JZIEtyYXRvcyBBZG1pbiBBUEkgXSlcbiAgICBTQSAtLT4gS0FcbiAgICBLQSAtLmJlbG9uZ3MgdG8uLT4gS1xuICAgIEtQIC0uYmVsb25ncyB0by4tPiBLXG5cbiAgICBzdWJncmFwaCBzYVtcIlNlY3VyZUFwcCAvIGtyYXRvcy1zZXJsZnNlcnZpY2UtdWktbm9kZSBFeGFtcGxlXCJdXG5cbiAgICAgICAgU0FbU2VjdXJlQXBwXVxuICAgICAgICBTQUQgLS5iZWxvbmdzIHRvLi0-IFNBXG4gICAgICAgIFNBTFUgLS5iZWxvbmdzIHRvLi0-IFNBXG4gICAgICAgIFNBTEkgLS5iZWxvbmdzIHRvLi0-IFNBXG4gICAgICAgIFNBUiAtLmJlbG9uZ3MgdG8uLT4gU0FcbiAgICAgICAgU0FBIC0uYmVsb25ncyB0by4tPiBTQVxuXG4gICAgICAgIHN1YmdyYXBoIFwiSGFzIGFjdGl2ZSBsb2dpbiBzZXNzaW9uXCJcbiAgICAgICAgICAgIFNBRChbUm91dGUgL2Rhc2hib2FyZF0pXG4gICAgICAgICAgICBTQUxVKFtSb3V0ZSAvYXV0aC9sb2dvdXRdKVxuICAgICAgICBlbmRcblxuICAgICAgICBzdWJncmFwaCBcIk5vIGFjdGl2ZSBsb2dpbiBzZXNzaW9uXCJcbiAgICAgICAgICAgIFNBTEkoW1JvdXRlIC9hdXRoL2xvZ2luXSkgXG4gICAgICAgICAgICBTQVIoW1JvdXRlIC9hdXRoL3JlZ2lzdHJhdGlvbl0pIFxuICAgICAgICAgICAgU0FBKFtSb3V0ZSAvYXV0aC8uLi5dKVxuICAgICAgICBlbmRcbiAgICBlbmRcbiAgICBlbmRcblxuZW5kXG4iLCJtZXJtYWlkIjp7InRoZW1lIjoibmV1dHJhbCIsImZsb3djaGFydCI6eyJyYW5rU3BhY2luZyI6NzAsIm5vZGVTcGFjaW5nIjozMCwiY3VydmUiOiJiYXNpcyJ9fX0)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggVERcblxuc3ViZ3JhcGggcGlbUHVibGljIEludGVybmV0XVxuICAgIEJbQnJvd3Nlcl1cbmVuZFxuXG5zdWJncmFwaCB2cGNbVlBDIC8gQ2xvdWQgLyBEb2NrZXIgTmV0d29ya11cbnN1YmdyYXBoIFwiRGVtaWxpdGFyaXplZCBab25lIC8gRE1aXCJcbiAgICBPS1tPUlkgT2F0aGtlZXBlciA6NDQ1NV1cbiAgICBCIC0tPiBPS1xuZW5kXG5cbiAgICBPSyAtLT58XCJGb3J3YXJkcyB7LywvZGFzaGJvYXJkfSB0b1wifCBTQURcbiAgICBPSyAtLT58XCJGb3J3YXJkcyAvYXV0aC9sb2dvdXQgdG9cInwgU0FMVVxuICAgIE9LIC0tPnxcIkZvcndhcmRzIC9hdXRoL2xvZ2luIHRvXCJ8IFNBTElcbiAgICBPSyAtLT58XCJGb3J3YXJkcyAvYXV0aC9yZWdpc3RyYXRpb24gdG9cInwgU0FSXG4gICAgT0sgLS0-fFwiRm9yd2FyZHMgL2F1dGgvKiB0b1wifCBTQUFcbiAgICBPSyAtLT58XCJGb3J3YXJkcyAvLm9yeS9rcmF0b3MvcHVibGljLyogdG9cInwgS1BcblxuICAgIHN1YmdyYXBoIFwiUHJpdmF0ZSBTdWJuZXQgLyBJbnRyYW5ldFwiXG4gICAgS1sgT1JZIEtyYXRvcyBdXG5cbiAgICBLUChbIE9SWSBLcmF0b3MgUHVibGljIEFQSSBdKVxuICAgIEtBKFsgT1JZIEtyYXRvcyBBZG1pbiBBUEkgXSlcbiAgICBTQSAtLT4gS0FcbiAgICBLQSAtLmJlbG9uZ3MgdG8uLT4gS1xuICAgIEtQIC0uYmVsb25ncyB0by4tPiBLXG5cbiAgICBzdWJncmFwaCBzYVtcIlNlY3VyZUFwcCAvIGtyYXRvcy1zZXJsZnNlcnZpY2UtdWktbm9kZSBFeGFtcGxlXCJdXG5cbiAgICAgICAgU0FbU2VjdXJlQXBwXVxuICAgICAgICBTQUQgLS5iZWxvbmdzIHRvLi0-IFNBXG4gICAgICAgIFNBTFUgLS5iZWxvbmdzIHRvLi0-IFNBXG4gICAgICAgIFNBTEkgLS5iZWxvbmdzIHRvLi0-IFNBXG4gICAgICAgIFNBUiAtLmJlbG9uZ3MgdG8uLT4gU0FcbiAgICAgICAgU0FBIC0uYmVsb25ncyB0by4tPiBTQVxuXG4gICAgICAgIHN1YmdyYXBoIFwiSGFzIGFjdGl2ZSBsb2dpbiBzZXNzaW9uXCJcbiAgICAgICAgICAgIFNBRChbUm91dGUgL2Rhc2hib2FyZF0pXG4gICAgICAgICAgICBTQUxVKFtSb3V0ZSAvYXV0aC9sb2dvdXRdKVxuICAgICAgICBlbmRcblxuICAgICAgICBzdWJncmFwaCBcIk5vIGFjdGl2ZSBsb2dpbiBzZXNzaW9uXCJcbiAgICAgICAgICAgIFNBTEkoW1JvdXRlIC9hdXRoL2xvZ2luXSkgXG4gICAgICAgICAgICBTQVIoW1JvdXRlIC9hdXRoL3JlZ2lzdHJhdGlvbl0pIFxuICAgICAgICAgICAgU0FBKFtSb3V0ZSAvYXV0aC8uLi5dKVxuICAgICAgICBlbmRcbiAgICBlbmRcbiAgICBlbmRcblxuZW5kXG4iLCJtZXJtYWlkIjp7InRoZW1lIjoibmV1dHJhbCIsImZsb3djaGFydCI6eyJyYW5rU3BhY2luZyI6NzAsIm5vZGVTcGFjaW5nIjozMCwiY3VydmUiOiJiYXNpcyJ9fX0)
+ OK
+end
+ OK -->|"Forwards {/,/dashboard} to"| SAD
+ OK -->|"Forwards /auth/logout to"| SALU
+ OK -->|"Forwards /auth/login to"| SALI
+ OK -->|"Forwards /auth/registration to"| SAR
+ OK -->|"Forwards /auth/* to"| SAA
+ OK -->|"Forwards /.ory/kratos/public/* to"| KP
+ subgraph "Private Subnet / Intranet"
+ K[ ORY Kratos ]
+ KP([ ORY Kratos Public API ])
+ KA([ ORY Kratos Admin API ])
+ SA --> KA
+ KA -.belongs to.-> K
+ KP -.belongs to.-> K
+ subgraph sa["SecureApp / kratos-serlfservice-ui-node Example"]
+ SA[SecureApp]
+ SAD -.belongs to.-> SA
+ SALU -.belongs to.-> SA
+ SALI -.belongs to.-> SA
+ SAR -.belongs to.-> SA
+ SAA -.belongs to.-> SA
+ subgraph "Has active login session"
+ SAD([Route /dashboard])
+ SALU([Route /auth/logout])
+ end
+ subgraph "No active login session"
+ SALI([Route /auth/login])
+ SAR([Route /auth/registration])
+ SAA([Route /auth/...])
+ end
+ end
+ end
+end
+`}/>
In order to avoid common cross-domain issues with cookies, we're using
ORY Oathkeeper to proxy requests to ORY Kratos' Public API so that all requests
diff --git a/docs/docs/quickstart.mdx b/docs/docs/quickstart.mdx
index 45571b763753..12ddff343a6c 100644
--- a/docs/docs/quickstart.mdx
+++ b/docs/docs/quickstart.mdx
@@ -3,7 +3,8 @@ id: quickstart
title: Quickstart
---
-import useBaseUrl from '@docusaurus/useBaseUrl';
+import useBaseUrl from '@docusaurus/useBaseUrl'
+import Mermaid from '@theme/Mermaid'
ORY Kratos has several moving parts and getting everything right from the
beginning can be challenging. This getting started guide will help you install
@@ -73,7 +74,7 @@ const needsLogin = (req, res, next) => {
});
};
-// You can use `needsLogin` as a middleware for Express or any other web framework:
+// You can use `needsLogin` as a middleware for Express or any other web framework:
// import express from 'express'
// const app = express()
//
@@ -220,7 +221,26 @@ To better understand the application architecture, let's take a look at the netw
configuration. This assumes that you have at least some understanding of how
Docker networks work:
-[![User Login and Registration Network Topology](https://mermaid.ink/img/eyJjb2RlIjoiZ3JhcGggVERcblxuc3ViZ3JhcGggaG5bSG9zdCBOZXR3b3JrXVxuICAgIEJbQnJvd3Nlcl1cbiAgICBCLS0-fENhbiBhY2Nlc3MgVVJMcyB2aWEgMTI3LjAuMC4xOjQ0NTV8T0tQSE5cbiAgICBCLS0-fENhbiBhY2Nlc3MgVUkgdmlhIDEyNy4wLjAuMTo0NDM2fFNNVFBVSVxuICAgIE9LUEhOKFtTZWN1cmVBcHAgZXhwb3NlZCBhdCA6NDQ1NV0pXG4gICAgU01UUFVJKFtNYWlsU2x1cnBlciBVSSBleHBvc2VkIGF0IDo0NDM2XSlcbmVuZFxuXG5zdWJncmFwaCBkbltcIkludGVybmFsIERvY2tlciBOZXR3b3JrIChpbnRyYW5ldClcIl1cbiAgICBPS1BITi0uLT5TQVxuICAgIFNNVFBVSS0uLT5TTVRQXG4gICAgU0EtLT58UHJveGllcyBVUkxzIC8ub3J5L2tyYXRvcy9wdWJsaWMvKiB0b3xPS1xuICAgIFNBLS0-fFRhbGtzIHRvIGFuZCB2YWxpZGF0ZXMgbG9naW4gc2Vzc2lvbnMgdXNpbmd8T0tcbiAgICBPSy0tPnxTZW5kcyBtYWlsIHZpYXxTTVRQXG5cbiAgICBPS1tPUlkgS3JhdG9zXVxuICAgIFNBW1wiU2VjdXJlQXBwIChPUlkgS3JhdG9zIFNlbGZTZXJ2aWNlIFVJIE5vZGUgRXhhbXBsZSlcIl1cbiAgICBTTVRQW1wiU01UUCBTZXJ2ZXIgKE1haWxTbHVycGVyKVwiXVxuZW5kXG4iLCJtZXJtYWlkIjp7InRoZW1lIjoibmV1dHJhbCIsImZsb3djaGFydCI6eyJyYW5rU3BhY2luZyI6NjUsIm5vZGVTcGFjaW5nIjozMCwiY3VydmUiOiJiYXNpcyJ9fSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggVERcblxuc3ViZ3JhcGggaG5bSG9zdCBOZXR3b3JrXVxuICAgIEJbQnJvd3Nlcl1cbiAgICBCLS0-fENhbiBhY2Nlc3MgVVJMcyB2aWEgMTI3LjAuMC4xOjQ0NTV8T0tQSE5cbiAgICBCLS0-fENhbiBhY2Nlc3MgVUkgdmlhIDEyNy4wLjAuMTo0NDM2fFNNVFBVSVxuICAgIE9LUEhOKFtTZWN1cmVBcHAgZXhwb3NlZCBhdCA6NDQ1NV0pXG4gICAgU01UUFVJKFtNYWlsU2x1cnBlciBVSSBleHBvc2VkIGF0IDo0NDM2XSlcbmVuZFxuXG5zdWJncmFwaCBkbltcIkludGVybmFsIERvY2tlciBOZXR3b3JrIChpbnRyYW5ldClcIl1cbiAgICBPS1BITi0uLT5TQVxuICAgIFNNVFBVSS0uLT5TTVRQXG4gICAgU0EtLT58UHJveGllcyBVUkxzIC8ub3J5L2tyYXRvcy9wdWJsaWMvKiB0b3xPS1xuICAgIFNBLS0-fFRhbGtzIHRvIGFuZCB2YWxpZGF0ZXMgbG9naW4gc2Vzc2lvbnMgdXNpbmd8T0tcbiAgICBPSy0tPnxTZW5kcyBtYWlsIHZpYXxTTVRQXG5cbiAgICBPS1tPUlkgS3JhdG9zXVxuICAgIFNBW1wiU2VjdXJlQXBwIChPUlkgS3JhdG9zIFNlbGZTZXJ2aWNlIFVJIE5vZGUgRXhhbXBsZSlcIl1cbiAgICBTTVRQW1wiU01UUCBTZXJ2ZXIgKE1haWxTbHVycGVyKVwiXVxuZW5kXG4iLCJtZXJtYWlkIjp7InRoZW1lIjoibmV1dHJhbCIsImZsb3djaGFydCI6eyJyYW5rU3BhY2luZyI6NjUsIm5vZGVTcGFjaW5nIjozMCwiY3VydmUiOiJiYXNpcyJ9fSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)
+|Can access URLs via 127.0.0.1:4455|OKPHN
+ B-->|Can access UI via 127.0.0.1:4436|SMTPUI
+ OKPHN([SecureApp exposed at :4455])
+ SMTPUI([MailSlurper UI exposed at :4436])
+end
+subgraph dn["Internal Docker Network (intranet)"]
+ OKPHN-.->SA
+ SMTPUI-.->SMTP
+ SA-->|Proxies URLs /.ory/kratos/public/* to|OK
+ SA-->|Talks to and validates login sessions using|OK
+ OK-->|Sends mail via|SMTP
+ OK[ORY Kratos]
+ SA["SecureApp (ORY Kratos SelfService UI Node Example)"]
+ SMTP["SMTP Server (MailSlurper)"]
+end
+`}/>
In order to avoid common cross-domain issues with cookies, we're proxying requests to ORY Kratos' Public API so that all requests come from the same
hostname.
diff --git a/docs/docs/self-service.mdx b/docs/docs/self-service.mdx
index 796a040a40d7..e5884ffa1a62 100644
--- a/docs/docs/self-service.mdx
+++ b/docs/docs/self-service.mdx
@@ -29,7 +29,7 @@ Research, Troy Hunt, ...) and implements the following flows:
- [Login and Registration](self-service/flows/user-login-user-registration.mdx)
- [Logout](self-service/flows/user-logout.md)
- [User Settings](self-service/flows/user-settings.mdx)
-- [Account Recovery](self-service/flows/password-reset-account-recovery.md)
+- [Account Recovery](self-service/flows/account-recovery.mdx)
- [Address Verification](self-service/flows/verify-email-account-activation.mdx)
- [User-Facing Error](self-service/flows/user-facing-errors.md)
- [2FA / MFA](self-service/flows/2fa-mfa-multi-factor-authentication.md)
diff --git a/docs/docs/self-service/flows/password-reset-account-recovery.md b/docs/docs/self-service/flows/account-recovery.mdx
similarity index 94%
rename from docs/docs/self-service/flows/password-reset-account-recovery.md
rename to docs/docs/self-service/flows/account-recovery.mdx
index 05e7de435d02..661bef2d4abe 100644
--- a/docs/docs/self-service/flows/password-reset-account-recovery.md
+++ b/docs/docs/self-service/flows/account-recovery.mdx
@@ -3,6 +3,8 @@ id: password-reset-account-recovery
title: Account Recovery
---
+import Mermaid from '@theme/Mermaid'
+
Account Recovery must be performed if access to an account needs to be
recovered. Common use cases include:
@@ -15,6 +17,8 @@ recovered. Common use cases include:
The forgot password flow is a work in progress and will be implemented in a
future release of ORY Kratos.
+https://cheatsheetseries.owasp.org/cheatsheets/Choosing_and_Using_Security_Questions_Cheat_Sheet.html
+
## Questions
> One option is to allow the user to self-construct their own questions. The problem with this though is that you end up with either painfully obvious questions:
diff --git a/docs/docs/self-service/flows/account-recovery/password-reset.mdx b/docs/docs/self-service/flows/account-recovery/password-reset.mdx
new file mode 100644
index 000000000000..21b75b8e885f
--- /dev/null
+++ b/docs/docs/self-service/flows/account-recovery/password-reset.mdx
@@ -0,0 +1,16 @@
+---
+id: password-reset
+title: Password Reset
+---
+
+import Mermaid from '@theme/Mermaid'
+
+ choose_method
+ recovered --> [*]
+ choose_method --> sent_email
+ sent_email --> sent_email
+ sent_email --> passed_challenge
+ passed_challenge --> recovered
+`}/>
diff --git a/docs/docs/self-service/flows/user-login-user-registration.mdx b/docs/docs/self-service/flows/user-login-user-registration.mdx
index 37cbbae94ca0..310a9151a5ce 100644
--- a/docs/docs/self-service/flows/user-login-user-registration.mdx
+++ b/docs/docs/self-service/flows/user-login-user-registration.mdx
@@ -4,6 +4,8 @@ title: User Login And Registration
sidebar_label: Overview
---
+import Mermaid from '@theme/Mermaid'
+
ORY Kratos supports two type of login and registration flows:
- Browser-based (easy): This flow works for all applications running on top of a
@@ -104,7 +106,29 @@ Each Login and Registration Strategy (e.g.
Passwordless, ...) works a bit different but they all boil down to the same
abstract sequence:
-[![Abstract Login and Registration User Flow](https://mermaid.ink/img/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEIgYXMgQnJvd3NlclxuICBwYXJ0aWNpcGFudCBLIGFzIE9SWSBLcmF0b3NcbiAgcGFydGljaXBhbnQgQSBhcyBZb3VyIEFwcGxpY2F0aW9uXG5cblxuICBCLT4-SzogSW5pdGlhdGUgTG9naW5cbiAgSy0-PkI6IFJlZGlyZWN0cyB0byB5b3VyIEFwcGxpY2F0aW9uJ3MgL2xvZ2luIGVuZHBvaW50XG4gIEItPj5BOiBDYWxscyAvbG9naW5cbiAgQS0tPj5LOiBGZXRjaGVzIGRhdGEgdG8gcmVuZGVyIGZvcm1zIGV0Y1xuICBCLS0-PkE6IEZpbGxzIG91dCBmb3JtcywgY2xpY2tzIGUuZy4gXCJTdWJtaXQgTG9naW5cIlxuICBCLT4-SzogUE9TVHMgZGF0YSB0b1xuICBLLS0-Pks6IFByb2Nlc3NlcyBMb2dpbiBJbmZvXG5cbiAgYWx0IExvZ2luIGRhdGEgdmFsaWRcbiAgICBLLS0-PkI6IFNldHMgc2Vzc2lvbiBjb29raWVcbiAgICBLLT4-QjogUmVkaXJlY3RzIHRvIGUuZy4gRGFzaGJvYXJkXG4gIGVsc2UgTG9naW4gZGF0YSBpbnZhbGlkXG4gICAgSy0tPj5COiBSZWRpcmVjdHMgdG8geW91ciBBcHBsaWNhaXRvbidzIC9sb2dpbiBlbmRwb2ludFxuICAgIEItPj5BOiBDYWxscyAvbG9naW5cbiAgICBBLS0-Pks6IEZldGNoZXMgZGF0YSB0byByZW5kZXIgZm9ybSBmaWVsZHMgYW5kIGVycm9yc1xuICAgIEItLT4-QTogRmlsbHMgb3V0IGZvcm1zIGFnYWluLCBjb3JyZWN0cyBlcnJvcnNcbiAgICBCLT4-SzogUE9TVHMgZGF0YSBhZ2FpbiAtIGFuZCBzbyBvbi4uLlxuICBlbmRcbiIsIm1lcm1haWQiOnsidGhlbWUiOiJuZXV0cmFsIiwic2VxdWVuY2VEaWFncmFtIjp7ImRpYWdyYW1NYXJnaW5YIjoxNSwiZGlhZ3JhbU1hcmdpblkiOjE1LCJib3hUZXh0TWFyZ2luIjowLCJub3RlTWFyZ2luIjoxNSwibWVzc2FnZU1hcmdpbiI6NDUsIm1pcnJvckFjdG9ycyI6dHJ1ZX19fQ)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEIgYXMgQnJvd3NlclxuICBwYXJ0aWNpcGFudCBLIGFzIE9SWSBLcmF0b3NcbiAgcGFydGljaXBhbnQgQSBhcyBZb3VyIEFwcGxpY2F0aW9uXG5cblxuICBCLT4-SzogSW5pdGlhdGUgTG9naW5cbiAgSy0-PkI6IFJlZGlyZWN0cyB0byB5b3VyIEFwcGxpY2F0aW9uJ3MgL2xvZ2luIGVuZHBvaW50XG4gIEItPj5BOiBDYWxscyAvbG9naW5cbiAgQS0tPj5LOiBGZXRjaGVzIGRhdGEgdG8gcmVuZGVyIGZvcm1zIGV0Y1xuICBCLS0-PkE6IEZpbGxzIG91dCBmb3JtcywgY2xpY2tzIGUuZy4gXCJTdWJtaXQgTG9naW5cIlxuICBCLT4-SzogUE9TVHMgZGF0YSB0b1xuICBLLS0-Pks6IFByb2Nlc3NlcyBMb2dpbiBJbmZvXG5cbiAgYWx0IExvZ2luIGRhdGEgdmFsaWRcbiAgICBLLS0-PkI6IFNldHMgc2Vzc2lvbiBjb29raWVcbiAgICBLLT4-QjogUmVkaXJlY3RzIHRvIGUuZy4gRGFzaGJvYXJkXG4gIGVsc2UgTG9naW4gZGF0YSBpbnZhbGlkXG4gICAgSy0tPj5COiBSZWRpcmVjdHMgdG8geW91ciBBcHBsaWNhaXRvbidzIC9sb2dpbiBlbmRwb2ludFxuICAgIEItPj5BOiBDYWxscyAvbG9naW5cbiAgICBBLS0-Pks6IEZldGNoZXMgZGF0YSB0byByZW5kZXIgZm9ybSBmaWVsZHMgYW5kIGVycm9yc1xuICAgIEItLT4-QTogRmlsbHMgb3V0IGZvcm1zIGFnYWluLCBjb3JyZWN0cyBlcnJvcnNcbiAgICBCLT4-SzogUE9TVHMgZGF0YSBhZ2FpbiAtIGFuZCBzbyBvbi4uLlxuICBlbmRcbiIsIm1lcm1haWQiOnsidGhlbWUiOiJuZXV0cmFsIiwic2VxdWVuY2VEaWFncmFtIjp7ImRpYWdyYW1NYXJnaW5YIjoxNSwiZGlhZ3JhbU1hcmdpblkiOjE1LCJib3hUZXh0TWFyZ2luIjowLCJub3RlTWFyZ2luIjoxNSwibWVzc2FnZU1hcmdpbiI6NDUsIm1pcnJvckFjdG9ycyI6dHJ1ZX19fQ)
+>K: Initiate Login
+ K->>B: Redirects to your Application's /login endpoint
+ B->>A: Calls /login
+ A-->>K: Fetches data to render forms etc
+ B-->>A: Fills out forms, clicks e.g. "Submit Login"
+ B->>K: POSTs data to
+ K-->>K: Processes Login Info
+ alt Login data valid
+ K-->>B: Sets session cookie
+ K->>B: Redirects to e.g. Dashboard
+ else Login data invalid
+ K-->>B: Redirects to your Applicaiton's /login endpoint
+ B->>A: Calls /login
+ A-->>K: Fetches data to render form fields and errors
+ B-->>A: Fills out forms again, corrects errors
+ B->>K: POSTs data again - and so on...
+ end
+`}>
The exact data being fetched and the step _"Processes Login / Registration
Info"_ depend, of course, on the actual Strategy being used. But it is important
@@ -301,7 +325,27 @@ Nginx, Kong, Envoy, ORY Oathkeeper, or others.
The Login and Registration User Flow is composed of several high-level steps
summarized in this state diagram:
-[![User Login and Registration State Machine](https://mermaid.ink/img/eyJjb2RlIjoic3RhdGVEaWFncmFtXG4gIHMxOiBVc2VyIGJyb3dzZXMgYXBwXG4gIHMyOiBFeGVjdXRlIFwiQmVmb3JlIExvZ2luL1JlZ2lzdHJhdGlvbiBIb29rKHMpXCJcbiAgczM6IFVzZXIgSW50ZXJmYWNlIEFwcGxpY2F0aW9uIHJlbmRlcnMgXCJMb2dpbi9SZWdpc3RyYXRpb24gUmVxdWVzdFwiXG4gIHM0OiBFeGVjdXRlIFwiQWZ0ZXIgTG9naW4vUmVnaXN0cmF0aW9uIEhvb2socylcIlxuICBzNTogVXBkYXRlIFwiTG9naW4vUmVnaXN0cmF0aW9uIFJlcXVlc3RcIiB3aXRoIEVycm9yIENvbnRleHQocylcbiAgczY6IExvZ2luL1JlZ2lzdHJhdGlvbiBzdWNjZXNzZnVsXG5cblxuXG5cdFsqXSAtLT4gczFcbiAgczEgLS0-IHMyIDogVXNlciBjbGlja3MgXCJMb2cgaW4gLyBTaWduIHVwXCJcbiAgczIgLS0-IEVycm9yIDogQSBob29rIGZhaWxzXG4gIHMyIC0tPiBzMyA6IFVzZXIgaXMgcmVkaXJlY3RlZCB0byBMb2dpbi9SZWdpc3RyYXRpb24gVUkgVVJMXG4gIHMzIC0tPiBzNCA6IFVzZXIgcHJvdmlkZXMgdmFsaWQgY3JlZGVudGlhbHMvcmVnaXN0cmF0aW9uIGRhdGFcbiAgczMgLS0-IHM1IDogVXNlciBwcm92aWRlcyBpbnZhbGlkIGNyZWRlbnRpYWxzL3JlZ2lzdHJhdGlvbiBkYXRhXG4gIHM1IC0tPiBzMyA6IFVzZXIgaXMgcmVkaXJlY3RlZCB0byBMb2dpbi9SZWdpc3RyYXRpb24gVUkgVVJMXG4gIHM0IC0tPiBFcnJvciA6IEEgSG9vayBmYWlsc1xuICBzNCAtLT4gczZcbiAgczYgLS0-IFsqXVxuXG4gIEVycm9yIC0tPiBbKl1cblxuXG4iLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic3RhdGVEaWFncmFtXG4gIHMxOiBVc2VyIGJyb3dzZXMgYXBwXG4gIHMyOiBFeGVjdXRlIFwiQmVmb3JlIExvZ2luL1JlZ2lzdHJhdGlvbiBIb29rKHMpXCJcbiAgczM6IFVzZXIgSW50ZXJmYWNlIEFwcGxpY2F0aW9uIHJlbmRlcnMgXCJMb2dpbi9SZWdpc3RyYXRpb24gUmVxdWVzdFwiXG4gIHM0OiBFeGVjdXRlIFwiQWZ0ZXIgTG9naW4vUmVnaXN0cmF0aW9uIEhvb2socylcIlxuICBzNTogVXBkYXRlIFwiTG9naW4vUmVnaXN0cmF0aW9uIFJlcXVlc3RcIiB3aXRoIEVycm9yIENvbnRleHQocylcbiAgczY6IExvZ2luL1JlZ2lzdHJhdGlvbiBzdWNjZXNzZnVsXG5cblxuXG5cdFsqXSAtLT4gczFcbiAgczEgLS0-IHMyIDogVXNlciBjbGlja3MgXCJMb2cgaW4gLyBTaWduIHVwXCJcbiAgczIgLS0-IEVycm9yIDogQSBob29rIGZhaWxzXG4gIHMyIC0tPiBzMyA6IFVzZXIgaXMgcmVkaXJlY3RlZCB0byBMb2dpbi9SZWdpc3RyYXRpb24gVUkgVVJMXG4gIHMzIC0tPiBzNCA6IFVzZXIgcHJvdmlkZXMgdmFsaWQgY3JlZGVudGlhbHMvcmVnaXN0cmF0aW9uIGRhdGFcbiAgczMgLS0-IHM1IDogVXNlciBwcm92aWRlcyBpbnZhbGlkIGNyZWRlbnRpYWxzL3JlZ2lzdHJhdGlvbiBkYXRhXG4gIHM1IC0tPiBzMyA6IFVzZXIgaXMgcmVkaXJlY3RlZCB0byBMb2dpbi9SZWdpc3RyYXRpb24gVUkgVVJMXG4gIHM0IC0tPiBFcnJvciA6IEEgSG9vayBmYWlsc1xuICBzNCAtLT4gczZcbiAgczYgLS0-IFsqXVxuXG4gIEVycm9yIC0tPiBbKl1cblxuXG4iLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ)
+ s1
+ s1 --> s2 : User clicks "Log in / Sign up"
+ s2 --> Error : A hook fails
+ s2 --> s3 : User is redirected to Login/Registration UI URL
+ s3 --> s4 : User provides valid credentials/registration data
+ s3 --> s5 : User provides invalid credentials/registration data
+ s5 --> s3 : User is redirected to Login/Registration UI URL
+ s4 --> Error : A Hook fails
+ s4 --> s6
+ s6 --> [*]
+ Error --> [*]
+`}>
+
1. The **Login/Registration User Flow** is initiated because a link was clicked
or an action was performed that requires an active user session.
@@ -384,7 +428,38 @@ summarized in this state diagram:
session and/or identity. For more information on this topic check
[Self-Service Flow Completion](../../concepts/selfservice-flow-completion.md).
-[![User Login Sequence Diagram for Server-Side Applications](https://mermaid.ink/img/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEIgYXMgQnJvd3NlclxuICBwYXJ0aWNpcGFudCBBIGFzIFlvdXIgU2VydmVyLVNpZGUgQXBwbGljYXRpb25cbiAgcGFydGljaXBhbnQgS1AgYXMgT1JZIEtyYXRvcyBQdWJsaWMgQVBJXG4gIHBhcnRpY2lwYW50IEtBIGFzIE9SWSBLcmF0b3MgQWRtaW4gQVBJXG5cbiAgQi0-PitBOiBHRVQgLy5vcnkva3JhdG9zL3B1YmxpYy9zZWxmLXNlcnZpY2UvYnJvd3Nlci9mbG93cy8obG9naW58cmVnaXN0cmF0aW9uKVxuICBBLT4-K0tQOiBHRVQgL3NlbGYtc2VydmljZS9icm93c2VyL2Zsb3dzL2xvZ2luKGxvZ2lufHJlZ2lzdHJhdGlvbilcbiAgS1AtLT4-S1A6IEV4ZWN1dGUgSG9va3MgZGVmaW5lZCBpbiBcIkJlZm9yZSBMb2dpbi9SZWdpc3RyYXRpb25cIlxuICBLUC0tPj4tQTogSFRUUCAzMDIgRm91bmQgL2F1dGgvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBBLS0-Pi1COiBIVFRQIDMwMiBGb3VuZCAvYXV0aC8obG9naW58cmVnaXN0cmF0aW9uKT9yZXF1ZXN0PWFiY2RlXG5cbiAgQi0-PitBOiBHRVQgL2F1dGgvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBBLT4-K0tBOiBHRVQvc2VsZi1zZXJ2aWNlL2Jyb3dzZXIvZmxvd3MvcmVxdWVzdHMvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBLQS0-Pi1BOiBTZW5kcyBMb2dpbi9SZWdpc3RyYXRpb24gUmVxdWVzdCBKU09OIFBheWxvYWRcbiAgTm90ZSBvdmVyIEEsS0E6ICB7XCJtZXRob2RzXCI6e1wicGFzc3dvcmRcIjouLi4sXCJvaWRjXCI6Li59fVxuICBBLS0-PkE6IEdlbmVyYXRlIGFuZCByZW5kZXIgSFRNTFxuICBBLS0-Pi1COiBSZXR1cm4gSFRNTCAoRm9ybSwgLi4uKVxuXG4gIEItLT4-QjogRmlsbCBvdXQgSFRNTFxuXG4gIEItPj4rS1A6IFBPU1QgSFRNTCBGb3JtXG4gIEtQLS0-PktQOiBDaGVja3MgbG9naW4gLyByZWdpc3RyYXRpb24gZGF0YVxuXG5cbiAgYWx0IExvZ2luIGRhdGEgaXMgdmFsaWRcbiAgICBLUC0tPj4tS1A6IEV4ZWN1dGUgSm9icyBkZWZpbmVkIGluIFwiQWZ0ZXIgTG9naW4gV29ya2Zsb3cocylcIlxuICAgIEtQLS0-PkE6IEhUVFAgMzAyIEZvdW5kIC9kYXNoYm9hcmRcbiAgICBOb3RlIG92ZXIgS1AsQjogU2V0LUNvb2tpZTogYXV0aF9zZXNzaW9uPS4uLlxuICAgIEItPj4rQTogR0VUIC9kYXNoYm9hcmRcbiAgICBBLS0-S0E6IFZhbGlkYXRlcyBTZXNzaW9uIENvb2tpZVxuICAgIEEtPj4tQjogU2VuZCBEYXNoYm9hcmQgUmVzcG9uc2VcbiAgZWxzZSBMb2dpbiBkYXRhIGlzIGludmFsaWRcbiAgICBOb3RlIG92ZXIgS1AsQjogVXNlciByZXRyaWVzIGxvZ2luIC8gcmVnaXN0cmF0aW9uXG4gICAgS1AtLT4-QjogSFRUUCAzMDIgRm91bmQgL2F1dGgvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBlbmRcbiAgIiwibWVybWFpZCI6eyJ0aGVtZSI6Im5ldXRyYWwiLCJzZXF1ZW5jZURpYWdyYW0iOnsiZGlhZ3JhbU1hcmdpblgiOjE1LCJkaWFncmFtTWFyZ2luWSI6MTUsImJveFRleHRNYXJnaW4iOjEsIm5vdGVNYXJnaW4iOjEwLCJtZXNzYWdlTWFyZ2luIjo1NSwibWlycm9yQWN0b3JzIjp0cnVlfX0sInVwZGF0ZUVkaXRvciI6ZmFsc2V9)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEIgYXMgQnJvd3NlclxuICBwYXJ0aWNpcGFudCBBIGFzIFlvdXIgU2VydmVyLVNpZGUgQXBwbGljYXRpb25cbiAgcGFydGljaXBhbnQgS1AgYXMgT1JZIEtyYXRvcyBQdWJsaWMgQVBJXG4gIHBhcnRpY2lwYW50IEtBIGFzIE9SWSBLcmF0b3MgQWRtaW4gQVBJXG5cbiAgQi0-PitBOiBHRVQgLy5vcnkva3JhdG9zL3B1YmxpYy9zZWxmLXNlcnZpY2UvYnJvd3Nlci9mbG93cy8obG9naW58cmVnaXN0cmF0aW9uKVxuICBBLT4-K0tQOiBHRVQgL3NlbGYtc2VydmljZS9icm93c2VyL2Zsb3dzL2xvZ2luKGxvZ2lufHJlZ2lzdHJhdGlvbilcbiAgS1AtLT4-S1A6IEV4ZWN1dGUgSG9va3MgZGVmaW5lZCBpbiBcIkJlZm9yZSBMb2dpbi9SZWdpc3RyYXRpb25cIlxuICBLUC0tPj4tQTogSFRUUCAzMDIgRm91bmQgL2F1dGgvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBBLS0-Pi1COiBIVFRQIDMwMiBGb3VuZCAvYXV0aC8obG9naW58cmVnaXN0cmF0aW9uKT9yZXF1ZXN0PWFiY2RlXG5cbiAgQi0-PitBOiBHRVQgL2F1dGgvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBBLT4-K0tBOiBHRVQvc2VsZi1zZXJ2aWNlL2Jyb3dzZXIvZmxvd3MvcmVxdWVzdHMvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBLQS0-Pi1BOiBTZW5kcyBMb2dpbi9SZWdpc3RyYXRpb24gUmVxdWVzdCBKU09OIFBheWxvYWRcbiAgTm90ZSBvdmVyIEEsS0E6ICB7XCJtZXRob2RzXCI6e1wicGFzc3dvcmRcIjouLi4sXCJvaWRjXCI6Li59fVxuICBBLS0-PkE6IEdlbmVyYXRlIGFuZCByZW5kZXIgSFRNTFxuICBBLS0-Pi1COiBSZXR1cm4gSFRNTCAoRm9ybSwgLi4uKVxuXG4gIEItLT4-QjogRmlsbCBvdXQgSFRNTFxuXG4gIEItPj4rS1A6IFBPU1QgSFRNTCBGb3JtXG4gIEtQLS0-PktQOiBDaGVja3MgbG9naW4gLyByZWdpc3RyYXRpb24gZGF0YVxuXG5cbiAgYWx0IExvZ2luIGRhdGEgaXMgdmFsaWRcbiAgICBLUC0tPj4tS1A6IEV4ZWN1dGUgSm9icyBkZWZpbmVkIGluIFwiQWZ0ZXIgTG9naW4gV29ya2Zsb3cocylcIlxuICAgIEtQLS0-PkE6IEhUVFAgMzAyIEZvdW5kIC9kYXNoYm9hcmRcbiAgICBOb3RlIG92ZXIgS1AsQjogU2V0LUNvb2tpZTogYXV0aF9zZXNzaW9uPS4uLlxuICAgIEItPj4rQTogR0VUIC9kYXNoYm9hcmRcbiAgICBBLS0-S0E6IFZhbGlkYXRlcyBTZXNzaW9uIENvb2tpZVxuICAgIEEtPj4tQjogU2VuZCBEYXNoYm9hcmQgUmVzcG9uc2VcbiAgZWxzZSBMb2dpbiBkYXRhIGlzIGludmFsaWRcbiAgICBOb3RlIG92ZXIgS1AsQjogVXNlciByZXRyaWVzIGxvZ2luIC8gcmVnaXN0cmF0aW9uXG4gICAgS1AtLT4-QjogSFRUUCAzMDIgRm91bmQgL2F1dGgvKGxvZ2lufHJlZ2lzdHJhdGlvbik_cmVxdWVzdD1hYmNkZVxuICBlbmRcbiAgIiwibWVybWFpZCI6eyJ0aGVtZSI6Im5ldXRyYWwiLCJzZXF1ZW5jZURpYWdyYW0iOnsiZGlhZ3JhbU1hcmdpblgiOjE1LCJkaWFncmFtTWFyZ2luWSI6MTUsImJveFRleHRNYXJnaW4iOjEsIm5vdGVNYXJnaW4iOjEwLCJtZXNzYWdlTWFyZ2luIjo1NSwibWlycm9yQWN0b3JzIjp0cnVlfX0sInVwZGF0ZUVkaXRvciI6ZmFsc2V9)
+>+A: GET /.ory/kratos/public/self-service/browser/flows/(login|registration)
+ A->>+KP: GET /self-service/browser/flows/login(login|registration)
+ KP-->>KP: Execute Hooks defined in "Before Login/Registration"
+ KP-->>-A: HTTP 302 Found /auth/(login|registration)?request=abcde
+ A-->>-B: HTTP 302 Found /auth/(login|registration)?request=abcde
+ B->>+A: GET /auth/(login|registration)?request=abcde
+ A->>+KA: GET/self-service/browser/flows/requests/(login|registration)?request=abcde
+ KA->>-A: Sends Login/Registration Request JSON Payload
+ Note over A,KA: {"methods":{"password":...,"oidc":..}}
+ A-->>A: Generate and render HTML
+ A-->>-B: Return HTML (Form, ...)
+ B-->>B: Fill out HTML
+ B->>+KP: POST HTML Form
+ KP-->>KP: Checks login / registration data
+ alt Login data is valid
+ KP-->>-KP: Execute Jobs defined in "After Login Workflow(s)"
+ KP-->>A: HTTP 302 Found /dashboard
+ Note over KP,B: Set-Cookie: auth_session=...
+ B->>+A: GET /dashboard
+ A-->KA: Validates Session Cookie
+ A->>-B: Send Dashboard Response
+ else Login data is invalid
+ Note over KP,B: User retries login / registration
+ KP-->>B: HTTP 302 Found /auth/(login|registration)?request=abcde
+ end
+`}>
### Client-Side Browser Applications
diff --git a/docs/docs/self-service/flows/user-settings.mdx b/docs/docs/self-service/flows/user-settings.mdx
index be5b6b7c212e..a45cb67d4b77 100644
--- a/docs/docs/self-service/flows/user-settings.mdx
+++ b/docs/docs/self-service/flows/user-settings.mdx
@@ -4,6 +4,8 @@ title: User Settings
sidebar_label: Overview
---
+import Mermaid from '@theme/Mermaid'
+
ORY Kratos allows users to update their own settings and profile information
using two principal flows:
@@ -89,7 +91,29 @@ Each Settings Strategy ([Profile](user-settings/user-profile-management.mdx),
Passwordless, ...) works a bit different but they all boil down to the same
abstract sequence:
-[![Abstract Settings Flow](https://mermaid.ink/img/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEIgYXMgQnJvd3NlclxuICBwYXJ0aWNpcGFudCBLIGFzIE9SWSBLcmF0b3NcbiAgcGFydGljaXBhbnQgQSBhcyBZb3VyIEFwcGxpY2F0aW9uXG5cblxuICBCLT4-SzogSW5pdGlhdGUgTG9naW5cbiAgSy0-PkI6IFJlZGlyZWN0cyB0byB5b3VyIEFwcGxpY2F0aW9uJ3MgL3NldHRpbmdzX3VpIGVuZHBvaW50XG4gIEItPj5BOiBDYWxscyAvc2V0dGluZ3NfdWlcbiAgQS0tPj5LOiBGZXRjaGVzIGRhdGEgdG8gcmVuZGVyIGZvcm1zIGV0Y1xuICBCLS0-PkE6IEZpbGxzIG91dCBmb3JtcywgY2xpY2tzIGUuZy4gXCJTYXZlIENoYW5nZXNcIlxuICBCLT4-SzogUE9TVHMgZGF0YSB0b1xuICBLLS0-Pks6IFByb2Nlc3NlcyBTZXR0aW5ncyBJbmZvXG5cbiAgYWx0IFNldHRpbmdzIGRhdGEgdmFsaWRcbiAgICBLLS0-PkI6IFVwZGF0ZXMgSWRlbnRpdHkgU2V0dGluZ3NcbiAgICBLLT4-QjogUmVkaXJlY3RzIHRvIGUuZy4gRGFzaGJvYXJkXG4gIGVsc2UgU2V0aW5ncyBkYXRhIGludmFsaWRcbiAgICBLLS0-PkI6IFJlZGlyZWN0cyB0byB5b3VyIEFwcGxpY2FpdG9uJ3MgL3NldHRpbmdzX3VpIGVuZHBvaW50XG4gICAgQi0-PkE6IENhbGxzIC9zZXR0aW5nc191aVxuICAgIEEtLT4-SzogRmV0Y2hlcyBkYXRhIHRvIHJlbmRlciBmb3JtIGZpZWxkcyBhbmQgZXJyb3JzXG4gICAgQi0tPj5BOiBGaWxscyBvdXQgZm9ybXMgYWdhaW4sIGNvcnJlY3RzIGVycm9yc1xuICAgIEItPj5LOiBQT1NUcyBkYXRhIGFnYWluIC0gYW5kIHNvIG9uLi4uXG4gIGVuZFxuIiwibWVybWFpZCI6eyJ0aGVtZSI6Im5ldXRyYWwiLCJzZXF1ZW5jZURpYWdyYW0iOnsiZGlhZ3JhbU1hcmdpblgiOjE1LCJkaWFncmFtTWFyZ2luWSI6MTUsImJveFRleHRNYXJnaW4iOjAsIm5vdGVNYXJnaW4iOjE1LCJtZXNzYWdlTWFyZ2luIjo0NSwibWlycm9yQWN0b3JzIjp0cnVlfX0sInVwZGF0ZUVkaXRvciI6ZmFsc2V9)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEIgYXMgQnJvd3NlclxuICBwYXJ0aWNpcGFudCBLIGFzIE9SWSBLcmF0b3NcbiAgcGFydGljaXBhbnQgQSBhcyBZb3VyIEFwcGxpY2F0aW9uXG5cblxuICBCLT4-SzogSW5pdGlhdGUgTG9naW5cbiAgSy0-PkI6IFJlZGlyZWN0cyB0byB5b3VyIEFwcGxpY2F0aW9uJ3MgL3NldHRpbmdzX3VpIGVuZHBvaW50XG4gIEItPj5BOiBDYWxscyAvc2V0dGluZ3NfdWlcbiAgQS0tPj5LOiBGZXRjaGVzIGRhdGEgdG8gcmVuZGVyIGZvcm1zIGV0Y1xuICBCLS0-PkE6IEZpbGxzIG91dCBmb3JtcywgY2xpY2tzIGUuZy4gXCJTYXZlIENoYW5nZXNcIlxuICBCLT4-SzogUE9TVHMgZGF0YSB0b1xuICBLLS0-Pks6IFByb2Nlc3NlcyBTZXR0aW5ncyBJbmZvXG5cbiAgYWx0IFNldHRpbmdzIGRhdGEgdmFsaWRcbiAgICBLLS0-PkI6IFVwZGF0ZXMgSWRlbnRpdHkgU2V0dGluZ3NcbiAgICBLLT4-QjogUmVkaXJlY3RzIHRvIGUuZy4gRGFzaGJvYXJkXG4gIGVsc2UgU2V0aW5ncyBkYXRhIGludmFsaWRcbiAgICBLLS0-PkI6IFJlZGlyZWN0cyB0byB5b3VyIEFwcGxpY2FpdG9uJ3MgL3NldHRpbmdzX3VpIGVuZHBvaW50XG4gICAgQi0-PkE6IENhbGxzIC9zZXR0aW5nc191aVxuICAgIEEtLT4-SzogRmV0Y2hlcyBkYXRhIHRvIHJlbmRlciBmb3JtIGZpZWxkcyBhbmQgZXJyb3JzXG4gICAgQi0tPj5BOiBGaWxscyBvdXQgZm9ybXMgYWdhaW4sIGNvcnJlY3RzIGVycm9yc1xuICAgIEItPj5LOiBQT1NUcyBkYXRhIGFnYWluIC0gYW5kIHNvIG9uLi4uXG4gIGVuZFxuIiwibWVybWFpZCI6eyJ0aGVtZSI6Im5ldXRyYWwiLCJzZXF1ZW5jZURpYWdyYW0iOnsiZGlhZ3JhbU1hcmdpblgiOjE1LCJkaWFncmFtTWFyZ2luWSI6MTUsImJveFRleHRNYXJnaW4iOjAsIm5vdGVNYXJnaW4iOjE1LCJtZXNzYWdlTWFyZ2luIjo0NSwibWlycm9yQWN0b3JzIjp0cnVlfX0sInVwZGF0ZUVkaXRvciI6ZmFsc2V9)
+>K: Initiate Login
+ K->>B: Redirects to your Application's /settings_ui endpoint
+ B->>A: Calls /settings_ui
+ A-->>K: Fetches data to render forms etc
+ B-->>A: Fills out forms, clicks e.g. "Save Changes"
+ B->>K: POSTs data to
+ K-->>K: Processes Settings Info
+ alt Settings data valid
+ K-->>B: Updates Identity Settings
+ K->>B: Redirects to e.g. Dashboard
+ else Setings data invalid
+ K-->>B: Redirects to your Applicaiton's /settings_ui endpoint
+ B->>A: Calls /settings_ui
+ A-->>K: Fetches data to render form fields and errors
+ B-->>A: Fills out forms again, corrects errors
+ B->>K: POSTs data again - and so on...
+ end
+`}>
### Code
@@ -239,7 +263,23 @@ Nginx, Kong, Envoy, ORY Oathkeeper, or others.
The User Settings Flow is composed of several high-level steps summarized in
this state diagram:
-[![User Settings State Machine](https://mermaid.ink/img/eyJjb2RlIjoic3RhdGVEaWFncmFtXG4gIHMxOiBVc2VyIGJyb3dzZXMgYXBwXG4gIHMzOiBVc2VyIEludGVyZmFjZSBBcHBsaWNhdGlvbiByZW5kZXJzIFwiU2V0dGluZ3MgUmVxdWVzdFwiXG4gIHM0OiBFeGVjdXRlIFwiQWZ0ZXIgU2V0dGluZ3MgSG9vayhzKVwiXG4gIHM1OiBVcGRhdGUgXCJTZXR0aW5ncyBSZXF1ZXN0XCIgd2l0aCBFcnJvciBDb250ZXh0KHMpXG4gIHM2OiBTZXR0aW5ncyB1cGRhdGUgc3VjY2Vzc2Z1bFxuXG5cdFsqXSAtLT4gczFcbiAgczEgLS0-IHMzIDogVXNlciBjbGlja3MgXCJNYW5hZ2UgQWNjb3VudFwiIGFuZCBpcyByZWRpcmVjdGVkIHRvIFNldHRpbmdzIEluaXQgRW5kcG9pbnRcbiAgczMgLS0-IHM0IDogVXNlciBwcm92aWRlcyB2YWxpZCBwcm9maWxlIGRhdGFcbiAgczMgLS0-IHM1IDogVXNlciBwcm92aWRlcyBpbnZhbGlkIHByb2ZpbGUgZGF0YVxuICBzNSAtLT4gczMgOiBVc2VyIGlzIHJlZGlyZWN0ZWQgdG8gU2V0dGluZ3MgVUkgVVJMXG4gIHM0IC0tPiBFcnJvciA6IEEgSG9vayBmYWlsc1xuICBzNCAtLT4gczZcbiAgczYgLS0-IFsqXVxuXG4gIEVycm9yIC0tPiBbKl1cblxuXG4iLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic3RhdGVEaWFncmFtXG4gIHMxOiBVc2VyIGJyb3dzZXMgYXBwXG4gIHMzOiBVc2VyIEludGVyZmFjZSBBcHBsaWNhdGlvbiByZW5kZXJzIFwiU2V0dGluZ3MgUmVxdWVzdFwiXG4gIHM0OiBFeGVjdXRlIFwiQWZ0ZXIgU2V0dGluZ3MgSG9vayhzKVwiXG4gIHM1OiBVcGRhdGUgXCJTZXR0aW5ncyBSZXF1ZXN0XCIgd2l0aCBFcnJvciBDb250ZXh0KHMpXG4gIHM2OiBTZXR0aW5ncyB1cGRhdGUgc3VjY2Vzc2Z1bFxuXG5cdFsqXSAtLT4gczFcbiAgczEgLS0-IHMzIDogVXNlciBjbGlja3MgXCJNYW5hZ2UgQWNjb3VudFwiIGFuZCBpcyByZWRpcmVjdGVkIHRvIFNldHRpbmdzIEluaXQgRW5kcG9pbnRcbiAgczMgLS0-IHM0IDogVXNlciBwcm92aWRlcyB2YWxpZCBwcm9maWxlIGRhdGFcbiAgczMgLS0-IHM1IDogVXNlciBwcm92aWRlcyBpbnZhbGlkIHByb2ZpbGUgZGF0YVxuICBzNSAtLT4gczMgOiBVc2VyIGlzIHJlZGlyZWN0ZWQgdG8gU2V0dGluZ3MgVUkgVVJMXG4gIHM0IC0tPiBFcnJvciA6IEEgSG9vayBmYWlsc1xuICBzNCAtLT4gczZcbiAgczYgLS0-IFsqXVxuXG4gIEVycm9yIC0tPiBbKl1cblxuXG4iLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ)
+ s1
+ s1 --> s3 : User clicks "Manage Account" and is redirected to Settings Init Endpoint
+ s3 --> s4 : User provides valid profile data
+ s3 --> s5 : User provides invalid profile data
+ s5 --> s3 : User is redirected to Settings UI URL
+ s4 --> Error : A Hook fails
+ s4 --> s6
+ s6 --> [*]
+ Error --> [*]
+`}>
1. The flow is initiated by directing the user's browser to
`http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/settings`.
@@ -314,7 +354,36 @@ this state diagram:
session and/or identity. For more information on this topic check
[Self-Service Flow Completion](../../concepts/selfservice-flow-completion.md).
-[![User Settings Sequence Diagram for Server-Side Applications](https://mermaid.ink/img/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEIgYXMgQnJvd3NlclxuICBwYXJ0aWNpcGFudCBBIGFzIFlvdXIgU2VydmVyLVNpZGUgQXBwbGljYXRpb25cbiAgcGFydGljaXBhbnQgS1AgYXMgT1JZIEtyYXRvcyBQdWJsaWMgQVBJXG4gIHBhcnRpY2lwYW50IEtBIGFzIE9SWSBLcmF0b3MgQWRtaW4gQVBJXG5cbiAgQi0-PitBOiBHRVQgLy5vcnkva3JhdG9zL3B1YmxpYy9zZWxmLXNlcnZpY2UvYnJvd3Nlci9mbG93cy9zZXR0aW5nc1xuICBBLT4-K0tQOiBHRVQgL3NlbGYtc2VydmljZS9icm93c2VyL2Zsb3dzL3NldHRpbmdzXG4gIEtQLS0-Pi1BOiBIVFRQIDMwMiBGb3VuZCAvc2V0dGluZ3M_cmVxdWVzdD1hYmNkZVxuICBBLS0-Pi1COiBIVFRQIDMwMiBGb3VuZCAvc2V0dGluZ3M_cmVxdWVzdD1hYmNkZVxuXG4gIEItPj4rQTogR0VUIC9zZXR0aW5ncz9yZXF1ZXN0PWFiY2RlXG4gIEEtPj4rS0E6IEdFVCAvc2VsZi1zZXJ2aWNlL2Jyb3dzZXIvZmxvd3MvcmVxdWVzdHMvc2V0dGluZ3M_cmVxdWVzdD1hYmNkZVxuICBLQS0-Pi1BOiBTZW5kcyBTZXR0aW5ncyBSZXF1ZXN0IEpTT04gUGF5bG9hZFxuICBOb3RlIG92ZXIgQSxLQTogIHtcIm1ldGhvZHNcIjp7XCJwYXNzd29yZFwiOi4uLixcIm9pZGNcIjouLn19XG4gIEEtLT4-QTogR2VuZXJhdGUgYW5kIHJlbmRlciBIVE1MXG4gIEEtLT4-LUI6IFJldHVybiBIVE1MIChGb3JtLCAuLi4pXG5cbiAgQi0tPj5COiBGaWxsIG91dCBIVE1MXG5cbiAgQi0-PitLUDogUE9TVCBIVE1MIEZvcm1cbiAgS1AtLT4-S1A6IENoZWNrcyBwcm9maWxlIGRhdGFcblxuXG4gIGFsdCBTZXR0aW5nIHVwZGF0ZXMgYXJlIHZhbGlkXG4gICAgS1AtLT4-LUtQOiBFeGVjdXRlIEpvYnMgZGVmaW5lZCBpbiBcIkFmdGVyIFNldHRpbmdzIFdvcmtmbG93KHMpXCJcbiAgICBLUC0tPj5BOiBIVFRQIDMwMiBGb3VuZCAvZGFzaGJvYXJkXG4gIGVsc2UgU2V0dGluZyB1cGRhdGVzIHJlcXVpcmUgcmUtYXV0aGVudGljYXRpb25cbiAgICBOb3RlIG92ZXIgS1AsQjogVXNlciBpcyBhc2tlZCB0byBsb2dpbiBpbiBhZ2Fpbi4gSWYgdGhlIGxvZ2luIGlzIHZhbGlkLCB0aGUgZGF0YSBpcyB1cGRhdGVkLlxuICAgIEtQLS0-PkI6IEhUVFAgMzAyIEZvdW5kIC9zZXR0aW5ncz9yZXF1ZXN0PWFiY2RlXG4gIGVsc2UgU2V0dGluZyB1cGRhdGVzIGFyZSBpbnZhbGlkXG4gICAgTm90ZSBvdmVyIEtQLEI6IFVzZXIgcmV0cmllcyBzZXR0aW5ncyBmbG93XG4gICAgS1AtLT4-QjogSFRUUCAzMDIgRm91bmQgL3NldHRpbmdzP3JlcXVlc3Q9YWJjZGVcbiAgZW5kXG4gICIsIm1lcm1haWQiOnsidGhlbWUiOiJuZXV0cmFsIiwic2VxdWVuY2VEaWFncmFtIjp7ImRpYWdyYW1NYXJnaW5YIjoxNSwiZGlhZ3JhbU1hcmdpblkiOjE1LCJib3hUZXh0TWFyZ2luIjoxLCJub3RlTWFyZ2luIjoxMCwibWVzc2FnZU1hcmdpbiI6NTUsIm1pcnJvckFjdG9ycyI6dHJ1ZX19LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEIgYXMgQnJvd3NlclxuICBwYXJ0aWNpcGFudCBBIGFzIFlvdXIgU2VydmVyLVNpZGUgQXBwbGljYXRpb25cbiAgcGFydGljaXBhbnQgS1AgYXMgT1JZIEtyYXRvcyBQdWJsaWMgQVBJXG4gIHBhcnRpY2lwYW50IEtBIGFzIE9SWSBLcmF0b3MgQWRtaW4gQVBJXG5cbiAgQi0-PitBOiBHRVQgLy5vcnkva3JhdG9zL3B1YmxpYy9zZWxmLXNlcnZpY2UvYnJvd3Nlci9mbG93cy9zZXR0aW5nc1xuICBBLT4-K0tQOiBHRVQgL3NlbGYtc2VydmljZS9icm93c2VyL2Zsb3dzL3NldHRpbmdzXG4gIEtQLS0-Pi1BOiBIVFRQIDMwMiBGb3VuZCAvc2V0dGluZ3M_cmVxdWVzdD1hYmNkZVxuICBBLS0-Pi1COiBIVFRQIDMwMiBGb3VuZCAvc2V0dGluZ3M_cmVxdWVzdD1hYmNkZVxuXG4gIEItPj4rQTogR0VUIC9zZXR0aW5ncz9yZXF1ZXN0PWFiY2RlXG4gIEEtPj4rS0E6IEdFVCAvc2VsZi1zZXJ2aWNlL2Jyb3dzZXIvZmxvd3MvcmVxdWVzdHMvc2V0dGluZ3M_cmVxdWVzdD1hYmNkZVxuICBLQS0-Pi1BOiBTZW5kcyBTZXR0aW5ncyBSZXF1ZXN0IEpTT04gUGF5bG9hZFxuICBOb3RlIG92ZXIgQSxLQTogIHtcIm1ldGhvZHNcIjp7XCJwYXNzd29yZFwiOi4uLixcIm9pZGNcIjouLn19XG4gIEEtLT4-QTogR2VuZXJhdGUgYW5kIHJlbmRlciBIVE1MXG4gIEEtLT4-LUI6IFJldHVybiBIVE1MIChGb3JtLCAuLi4pXG5cbiAgQi0tPj5COiBGaWxsIG91dCBIVE1MXG5cbiAgQi0-PitLUDogUE9TVCBIVE1MIEZvcm1cbiAgS1AtLT4-S1A6IENoZWNrcyBwcm9maWxlIGRhdGFcblxuXG4gIGFsdCBTZXR0aW5nIHVwZGF0ZXMgYXJlIHZhbGlkXG4gICAgS1AtLT4-LUtQOiBFeGVjdXRlIEpvYnMgZGVmaW5lZCBpbiBcIkFmdGVyIFNldHRpbmdzIFdvcmtmbG93KHMpXCJcbiAgICBLUC0tPj5BOiBIVFRQIDMwMiBGb3VuZCAvZGFzaGJvYXJkXG4gIGVsc2UgU2V0dGluZyB1cGRhdGVzIHJlcXVpcmUgcmUtYXV0aGVudGljYXRpb25cbiAgICBOb3RlIG92ZXIgS1AsQjogVXNlciBpcyBhc2tlZCB0byBsb2dpbiBpbiBhZ2Fpbi4gSWYgdGhlIGxvZ2luIGlzIHZhbGlkLCB0aGUgZGF0YSBpcyB1cGRhdGVkLlxuICAgIEtQLS0-PkI6IEhUVFAgMzAyIEZvdW5kIC9zZXR0aW5ncz9yZXF1ZXN0PWFiY2RlXG4gIGVsc2UgU2V0dGluZyB1cGRhdGVzIGFyZSBpbnZhbGlkXG4gICAgTm90ZSBvdmVyIEtQLEI6IFVzZXIgcmV0cmllcyBzZXR0aW5ncyBmbG93XG4gICAgS1AtLT4-QjogSFRUUCAzMDIgRm91bmQgL3NldHRpbmdzP3JlcXVlc3Q9YWJjZGVcbiAgZW5kXG4gICIsIm1lcm1haWQiOnsidGhlbWUiOiJuZXV0cmFsIiwic2VxdWVuY2VEaWFncmFtIjp7ImRpYWdyYW1NYXJnaW5YIjoxNSwiZGlhZ3JhbU1hcmdpblkiOjE1LCJib3hUZXh0TWFyZ2luIjoxLCJub3RlTWFyZ2luIjoxMCwibWVzc2FnZU1hcmdpbiI6NTUsIm1pcnJvckFjdG9ycyI6dHJ1ZX19LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ)
+>+A: GET /.ory/kratos/public/self-service/browser/flows/settings
+ A->>+KP: GET /self-service/browser/flows/settings
+ KP-->>-A: HTTP 302 Found /settings?request=abcde
+ A-->>-B: HTTP 302 Found /settings?request=abcde
+ B->>+A: GET /settings?request=abcde
+ A->>+KA: GET /self-service/browser/flows/requests/settings?request=abcde
+ KA->>-A: Sends Settings Request JSON Payload
+ Note over A,KA: {"methods":{"password":...,"oidc":..}}
+ A-->>A: Generate and render HTML
+ A-->>-B: Return HTML (Form, ...)
+ B-->>B: Fill out HTML
+ B->>+KP: POST HTML Form
+ KP-->>KP: Checks profile data
+ alt Setting updates are valid
+ KP-->>-KP: Execute Jobs defined in "After Settings Workflow(s)"
+ KP-->>A: HTTP 302 Found /dashboard
+ else Setting updates require re-authentication
+ Note over KP,B: User is asked to login in again. If the login is valid, the data is updated.
+ KP-->>B: HTTP 302 Found /settings?request=abcde
+ else Setting updates are invalid
+ Note over KP,B: User retries settings flow
+ KP-->>B: HTTP 302 Found /settings?request=abcde
+ end
+`}>
### Client-Side Browser Applications
diff --git a/driver/configuration/provider.go b/driver/configuration/provider.go
index af3b047bf0c6..75239bb4a395 100644
--- a/driver/configuration/provider.go
+++ b/driver/configuration/provider.go
@@ -99,7 +99,7 @@ type Provider interface {
RegisterURL() *url.URL
- HashersArgon2() *HasherArgon2Config
+ HasherArgon2() *HasherArgon2Config
TracingServiceName() string
TracingProvider() string
diff --git a/driver/configuration/provider_viper.go b/driver/configuration/provider_viper.go
index 19b7fc3dee10..50d190ae6c8e 100644
--- a/driver/configuration/provider_viper.go
+++ b/driver/configuration/provider_viper.go
@@ -11,8 +11,8 @@ import (
"github.com/google/uuid"
"github.com/pkg/errors"
- "github.com/sirupsen/logrus"
+ "github.com/ory/x/logrusx"
"github.com/ory/x/stringsx"
"github.com/ory/x/tracing"
@@ -25,7 +25,7 @@ import (
)
type ViperProvider struct {
- l logrus.FieldLogger
+ l *logrusx.Logger
ss [][]byte
dev bool
}
@@ -76,8 +76,8 @@ const (
ViperKeySelfServiceLifespanRecoveryRequest = "selfservice.recovery.request_lifespan"
- ViperKeySelfServiceLifespanVerificationRequest = "selfservice.verification.request_lifespan"
- ViperKeySelfServiceVerifyReturnTo = "selfservice.verification.return_to"
+ ViperKeySelfServiceLifespanVerificationRequest = "selfservice.verify.request_lifespan"
+ ViperKeySelfServiceVerifyReturnTo = "selfservice.verify.return_to"
ViperKeyDefaultIdentityTraitsSchemaURL = "identity.traits.default_schema_url"
ViperKeyIdentityTraitsSchemas = "identity.traits.schemas"
@@ -93,14 +93,11 @@ func HookStrategyKey(key, strategy string) string {
return fmt.Sprintf("%s.%s.hooks", key, strategy)
}
-func NewViperProvider(l logrus.FieldLogger, dev bool) *ViperProvider {
- return &ViperProvider{
- l: l,
- dev: dev,
- }
+func NewViperProvider(l *logrusx.Logger, dev bool) *ViperProvider {
+ return &ViperProvider{l: l, dev: dev}
}
-func (p *ViperProvider) HashersArgon2() *HasherArgon2Config {
+func (p *ViperProvider) HasherArgon2() *HasherArgon2Config {
return &HasherArgon2Config{
Memory: uint32(viperx.GetInt(p.l, ViperKeyHasherArgon2ConfigMemory, 4*1024*1024)),
Iterations: uint32(viperx.GetInt(p.l, ViperKeyHasherArgon2ConfigIterations, 4)),
@@ -344,10 +341,10 @@ func (p *ViperProvider) CourierTemplatesRoot() string {
return viperx.GetString(p.l, ViperKeyCourierTemplatesPath, "")
}
-func mustParseURLFromViper(l logrus.FieldLogger, key string) *url.URL {
+func mustParseURLFromViper(l *logrusx.Logger, key string) *url.URL {
u, err := url.ParseRequestURI(viper.GetString(key))
if err != nil {
- l.WithError(err).WithField("stack", fmt.Sprintf("%+v", errors.WithStack(err))).Fatalf("Configuration value from key %s is not a valid URL: %s", key, viper.GetString(key))
+ l.WithError(err).Fatalf("Configuration value from key %s is not a valid URL: %s", key, viper.GetString(key))
}
return u
}
diff --git a/driver/configuration/provider_viper_test.go b/driver/configuration/provider_viper_test.go
index 9a0189110cc9..8f7a809db663 100644
--- a/driver/configuration/provider_viper_test.go
+++ b/driver/configuration/provider_viper_test.go
@@ -5,6 +5,8 @@ import (
"testing"
"time"
+ "github.com/ory/x/logrusx"
+
"github.com/ory/kratos/driver/configuration"
_ "github.com/ory/jsonschema/v3/fileloader"
@@ -24,11 +26,11 @@ func TestViperProvider(t *testing.T) {
viperx.InitializeConfig(
"kratos",
"./../../internal/",
- logrus.New(),
+ logrusx.New("", ""),
)
require.NoError(t, viperx.ValidateFromURL("file://../../.schema/config.schema.json"))
- p := configuration.NewViperProvider(logrus.New(), true)
+ p := configuration.NewViperProvider(logrusx.New("", ""), true)
t.Run("group=urls", func(t *testing.T) {
assert.Equal(t, "http://test.kratos.ory.sh/login", p.LoginURL().String())
@@ -217,7 +219,7 @@ func TestViperProvider(t *testing.T) {
Parallelism: 4,
SaltLength: 16,
KeyLength: 32,
- }, p.HashersArgon2())
+ }, p.HasherArgon2())
})
})
}
@@ -240,7 +242,7 @@ func TestViperProvider_DSN(t *testing.T) {
viper.Reset()
viper.Set(configuration.ViperKeyDSN, "memory")
- l := logrus.New()
+ l := logrusx.New("", "")
p := configuration.NewViperProvider(l, false)
assert.Equal(t, "sqlite://mem.db?mode=memory&_fk=true&cache=shared", p.DSN())
@@ -251,7 +253,7 @@ func TestViperProvider_DSN(t *testing.T) {
viper.Reset()
viper.Set(configuration.ViperKeyDSN, dsn)
- l := logrus.New()
+ l := logrusx.New("", "")
p := configuration.NewViperProvider(l, false)
assert.Equal(t, dsn, p.DSN())
@@ -262,15 +264,12 @@ func TestViperProvider_DSN(t *testing.T) {
viper.Reset()
viper.Set(configuration.ViperKeyDSN, dsn)
- l := logrus.New()
- p := configuration.NewViperProvider(l, false)
-
var exitCode int
- l.ExitFunc = func(i int) {
+ l := logrusx.New("", "", logrusx.WithExitFunc(func(i int) {
exitCode = i
- }
- h := InterceptHook{}
- l.AddHook(h)
+ }), logrusx.WithHook(InterceptHook{}))
+ p := configuration.NewViperProvider(l, false)
+
assert.Equal(t, dsn, p.DSN())
assert.NotEqual(t, 0, exitCode)
})
diff --git a/driver/driver.go b/driver/driver.go
index 223fe4b0cfc6..9cfbc0a5e673 100644
--- a/driver/driver.go
+++ b/driver/driver.go
@@ -1,7 +1,7 @@
package driver
import (
- "github.com/sirupsen/logrus"
+ "github.com/ory/x/logrusx"
"github.com/ory/kratos/driver/configuration"
)
@@ -13,7 +13,7 @@ type BuildInfo struct {
}
type Driver interface {
- Logger() logrus.FieldLogger
+ Logger() *logrusx.Logger
Configuration() configuration.Provider
Registry() Registry
}
diff --git a/driver/driver_default.go b/driver/driver_default.go
index 2705bb73e907..fe9731144a37 100644
--- a/driver/driver_default.go
+++ b/driver/driver_default.go
@@ -2,7 +2,6 @@ package driver
import (
"github.com/pkg/errors"
- "github.com/sirupsen/logrus"
"github.com/ory/x/logrusx"
@@ -14,9 +13,9 @@ type DefaultDriver struct {
r Registry
}
-func NewDefaultDriver(l logrus.FieldLogger, version, build, date string, dev bool) (Driver, error) {
+func NewDefaultDriver(l *logrusx.Logger, version, build, date string, dev bool) (Driver, error) {
if l == nil {
- l = logrusx.New()
+ l = logrusx.New("ORY Kratos", version)
}
c := configuration.NewViperProvider(l, dev)
@@ -39,7 +38,7 @@ func NewDefaultDriver(l logrus.FieldLogger, version, build, date string, dev boo
return &DefaultDriver{r: r, c: c}, nil
}
-func MustNewDefaultDriver(l logrus.FieldLogger, version, build, date string, dev bool) Driver {
+func MustNewDefaultDriver(l *logrusx.Logger, version, build, date string, dev bool) Driver {
d, err := NewDefaultDriver(l, version, build, date, dev)
if err != nil {
l.WithError(err).Fatal("Unable to initialize driver.")
@@ -51,9 +50,9 @@ func (r *DefaultDriver) BuildInfo() *BuildInfo {
return &BuildInfo{}
}
-func (r *DefaultDriver) Logger() logrus.FieldLogger {
+func (r *DefaultDriver) Logger() *logrusx.Logger {
if r.r == nil {
- return logrusx.New()
+ return logrusx.New("ORY Kratos", r.BuildInfo().Version)
}
return r.r.Logger()
}
diff --git a/driver/registry.go b/driver/registry.go
index 2674bf7ef8ad..5967bfc1eed6 100644
--- a/driver/registry.go
+++ b/driver/registry.go
@@ -7,10 +7,12 @@ import (
"github.com/gorilla/sessions"
"github.com/pkg/errors"
- "github.com/sirupsen/logrus"
+
+ "github.com/ory/x/logrusx"
"github.com/ory/kratos/continuity"
"github.com/ory/kratos/courier"
+ "github.com/ory/kratos/hash"
"github.com/ory/kratos/schema"
"github.com/ory/kratos/selfservice/flow/recovery"
"github.com/ory/kratos/selfservice/flow/settings"
@@ -40,7 +42,7 @@ type Registry interface {
Init() error
WithConfig(c configuration.Provider) Registry
- WithLogger(l logrus.FieldLogger) Registry
+ WithLogger(l *logrusx.Logger) Registry
BuildVersion() string
BuildDate() string
@@ -72,6 +74,8 @@ type Registry interface {
errorx.HandlerProvider
errorx.PersistenceProvider
+ hash.HashProvider
+
identity.HandlerProvider
identity.ValidationProvider
identity.PoolProvider
@@ -82,7 +86,6 @@ type Registry interface {
schema.HandlerProvider
password2.ValidationProvider
- password2.HashProvider
session.HandlerProvider
session.ManagementProvider
diff --git a/driver/registry_default.go b/driver/registry_default.go
index 00265c9118d4..d9b87e5badac 100644
--- a/driver/registry_default.go
+++ b/driver/registry_default.go
@@ -7,18 +7,19 @@ import (
"time"
"github.com/ory/kratos/continuity"
+ "github.com/ory/kratos/hash"
"github.com/ory/kratos/schema"
"github.com/ory/kratos/selfservice/flow/recovery"
"github.com/ory/kratos/selfservice/flow/settings"
"github.com/ory/kratos/selfservice/flow/verify"
"github.com/ory/kratos/selfservice/hook"
+ "github.com/ory/kratos/selfservice/strategy/link"
"github.com/ory/kratos/x"
"github.com/cenkalti/backoff"
"github.com/gobuffalo/pop/v5"
"github.com/gorilla/sessions"
"github.com/pkg/errors"
- "github.com/sirupsen/logrus"
"github.com/ory/x/dbal"
"github.com/ory/x/healthx"
@@ -54,7 +55,8 @@ func init() {
}
type RegistryDefault struct {
- l logrus.FieldLogger
+ l *logrusx.Logger
+ a *logrusx.Logger
c configuration.Provider
injectedSelfserviceHooks map[string]func(configuration.SelfServiceHook) interface{}
@@ -83,7 +85,7 @@ type RegistryDefault struct {
sessionsStore *sessions.CookieStore
sessionManager session.Manager
- passwordHasher password2.Hasher
+ passwordHasher hash.Hasher
passwordValidator password2.Validator
errorHandler *errorx.Handler
@@ -109,7 +111,6 @@ type RegistryDefault struct {
selfserviceRecoveryErrorHandler *recovery.ErrorHandler
selfserviceRecoveryHandler *recovery.Handler
- selfserviceRecoverySender *recovery.Sender
selfserviceLogoutHandler *logout.Handler
@@ -127,6 +128,13 @@ type RegistryDefault struct {
csrfTokenGenerator x.CSRFToken
}
+func (m *RegistryDefault) Audit() *logrusx.Logger {
+ if m.a == nil {
+ m.a = logrusx.NewAudit("ORY Kratos", m.BuildVersion())
+ }
+ return m.a
+}
+
func (m *RegistryDefault) RegisterPublicRoutes(router *x.RouterPublic) {
m.LoginHandler().RegisterPublicRoutes(router)
m.RegistrationHandler().RegisterPublicRoutes(router)
@@ -135,6 +143,7 @@ func (m *RegistryDefault) RegisterPublicRoutes(router *x.RouterPublic) {
m.LoginStrategies().RegisterPublicRoutes(router)
m.SettingsStrategies().RegisterPublicRoutes(router)
m.RegistrationStrategies().RegisterPublicRoutes(router)
+ m.RecoveryStrategies().RegisterPublicRoutes(router)
m.SessionHandler().RegisterPublicRoutes(router)
m.SelfServiceErrorHandler().RegisterPublicRoutes(router)
m.SchemaHandler().RegisterPublicRoutes(router)
@@ -184,7 +193,7 @@ func (m *RegistryDefault) BuildHash() string {
return m.buildHash
}
-func (m *RegistryDefault) WithLogger(l logrus.FieldLogger) Registry {
+func (m *RegistryDefault) WithLogger(l *logrusx.Logger) Registry {
m.l = l
return m
}
@@ -198,9 +207,8 @@ func (m *RegistryDefault) LogoutHandler() *logout.Handler {
func (m *RegistryDefault) HealthHandler() *healthx.Handler {
if m.healthxHandler == nil {
- m.healthxHandler = healthx.NewHandler(m.Writer(), m.BuildVersion(), healthx.ReadyCheckers{
- "database": m.Ping,
- })
+ m.healthxHandler = healthx.NewHandler(m.Writer(), m.BuildVersion(),
+ healthx.ReadyCheckers{"database": m.Ping})
}
return m.healthxHandler
@@ -223,6 +231,7 @@ func (m *RegistryDefault) selfServiceStrategies() []interface{} {
password2.NewStrategy(m, m.c),
oidc.NewStrategy(m, m.c),
settings.NewStrategyTraits(m, m.c),
+ link.NewStrategyLink(m, m.c),
}
}
@@ -282,9 +291,9 @@ func (m *RegistryDefault) Writer() herodot.Writer {
return m.writer
}
-func (m *RegistryDefault) Logger() logrus.FieldLogger {
+func (m *RegistryDefault) Logger() *logrusx.Logger {
if m.l == nil {
- m.l = logrusx.New()
+ m.l = logrusx.New("ORY Kratos", m.BuildVersion())
}
return m.l
}
@@ -310,9 +319,9 @@ func (m *RegistryDefault) SessionHandler() *session.Handler {
return m.sessionHandler
}
-func (m *RegistryDefault) PasswordHasher() password2.Hasher {
+func (m *RegistryDefault) Hasher() hash.Hasher {
if m.passwordHasher == nil {
- m.passwordHasher = password2.NewHasherArgon2(m.c)
+ m.passwordHasher = hash.NewHasherArgon2(m.c)
}
return m.passwordHasher
}
@@ -479,6 +488,10 @@ func (m *RegistryDefault) CourierPersister() courier.Persister {
return m.persister
}
+func (m *RegistryDefault) RecoveryTokenPersister() link.Persister {
+ return m.Persister()
+}
+
func (m *RegistryDefault) Persister() persistence.Persister {
return m.persister
}
diff --git a/driver/registry_default_recovery.go b/driver/registry_default_recovery.go
index 3f3b8b8d0ac0..50ff4424a053 100644
--- a/driver/registry_default_recovery.go
+++ b/driver/registry_default_recovery.go
@@ -20,14 +20,6 @@ func (m *RegistryDefault) RecoveryHandler() *recovery.Handler {
return m.selfserviceRecoveryHandler
}
-func (m *RegistryDefault) RecoverySender() *recovery.Sender {
- if m.selfserviceRecoverySender == nil {
- m.selfserviceRecoverySender = recovery.NewSender(m, m.c)
- }
-
- return m.selfserviceRecoverySender
-}
-
func (m *RegistryDefault) RecoveryStrategies() recovery.Strategies {
if len(m.recoveryStrategies) == 0 {
for _, strategy := range m.selfServiceStrategies() {
diff --git a/driver/registry_default_settings.go b/driver/registry_default_settings.go
index ce25dbfa7e92..dcf935fa4fb9 100644
--- a/driver/registry_default_settings.go
+++ b/driver/registry_default_settings.go
@@ -10,6 +10,7 @@ func (m *RegistryDefault) PostSettingsPrePersistHooks(settingsType string) (b []
}
return
}
+
func (m *RegistryDefault) PostSettingsPostPersistHooks(settingsType string) (b []settings.PostHookPostPersistExecutor) {
for _, v := range m.getHooks(settingsType, m.c.SelfServiceSettingsAfterHooks(settingsType)) {
if hook, ok := v.(settings.PostHookPostPersistExecutor); ok {
diff --git a/go.mod b/go.mod
index 606d50c86bf8..9a40f2a45c9c 100644
--- a/go.mod
+++ b/go.mod
@@ -2,16 +2,13 @@ module github.com/ory/kratos
go 1.14
-replace github.com/ory/x => ../x
-
require (
github.com/Masterminds/sprig/v3 v3.0.0
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
- github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869
- github.com/bxcodec/faker v2.0.1+incompatible
+ github.com/bxcodec/faker/v3 v3.3.1
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6
@@ -56,23 +53,22 @@ require (
github.com/ory/go-acc v0.1.0
github.com/ory/go-convenience v0.1.0
github.com/ory/graceful v0.1.1
- github.com/ory/herodot v0.8.2
+ github.com/ory/herodot v0.9.0
github.com/ory/jsonschema/v3 v3.0.1
github.com/ory/mail/v3 v3.0.0
github.com/ory/sdk/swagutil v0.0.0-20200528141631-666b43d19aa3
github.com/ory/viper v1.7.5
- github.com/ory/x v0.0.122
+ github.com/ory/x v0.0.127
github.com/pelletier/go-toml v1.7.0 // indirect
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/pkg/errors v0.9.1
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e
github.com/sirupsen/logrus v1.6.0
- github.com/spf13/cobra v0.0.7
+ github.com/spf13/cobra v1.0.0
github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518
github.com/stretchr/testify v1.5.1
github.com/tidwall/gjson v1.3.5
github.com/tidwall/sjson v1.0.4
- github.com/uber/jaeger-lib v2.2.0+incompatible // indirect
github.com/urfave/negroni v1.0.0
go.mongodb.org/mongo-driver v1.3.3 // indirect
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
@@ -82,7 +78,6 @@ require (
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 // indirect
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d
gopkg.in/go-playground/validator.v9 v9.28.0
- gopkg.in/gorp.v1 v1.7.2 // indirect
gopkg.in/ini.v1 v1.56.0 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
)
diff --git a/go.sum b/go.sum
index 4625ab158d5a..5ec4a33dee3f 100644
--- a/go.sum
+++ b/go.sum
@@ -67,8 +67,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
-github.com/bxcodec/faker v2.0.1+incompatible h1:P0KUpUw5w6WJXwrPfv35oc91i4d8nf40Nwln+M/+faA=
-github.com/bxcodec/faker v2.0.1+incompatible/go.mod h1:BNzfpVdTwnFJ6GtfYTcQu6l6rHShT+veBxNCnjCx5XM=
+github.com/bxcodec/faker/v3 v3.3.1 h1:G7uldFk+iO/ES7W4v7JlI/WU9FQ6op9VJ15YZlDEhGQ=
+github.com/bxcodec/faker/v3 v3.3.1/go.mod h1:gF31YgnMSMKgkvl+fyEo1xuSMbEuieyqfeslGYFjneM=
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
@@ -631,6 +631,7 @@ github.com/gorilla/sessions v1.1.2/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotestyourself/gotestyourself v1.3.0 h1:9X3T0HDKAY/58/sEPpTkmyOg4wbb1ab9tZfV44mTSeE=
github.com/gotestyourself/gotestyourself v1.3.0/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI=
@@ -743,6 +744,7 @@ github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f26
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@@ -914,18 +916,16 @@ github.com/ory/gojsonschema v1.1.1-0.20190919112458-f254ca73d5e9/go.mod h1:BNZpd
github.com/ory/graceful v0.1.1 h1:zx+8tDObLPrG+7Tc8jKYlXsqWnLtOQA1IZ/FAAKHMXU=
github.com/ory/graceful v0.1.1/go.mod h1:zqu70l95WrKHF4AZ6tXHvAqAvpY6M7g6ttaAVcMm7KU=
github.com/ory/herodot v0.6.2/go.mod h1:3BOneqcyBsVybCPAJoi92KN2BpJHcmDqAMcAAaJiJow=
-github.com/ory/herodot v0.7.0 h1:DGPUyPDBZwQSaQzci4UW/edjG6OWixZTwXyfjBgEVgs=
github.com/ory/herodot v0.7.0/go.mod h1:YXKOfAXYdQojDP5sD8m0ajowq3+QXNdtxA+QiUXBwn0=
-github.com/ory/herodot v0.8.2 h1:Lq5DpT81tkcegzp1QFqVFlcDWNCcq9xIq5FQ191rI0E=
-github.com/ory/herodot v0.8.2/go.mod h1:kFWnruHnnokHH4e7tbkGyHOjHGj70sJTrdiz01Xcq4Y=
+github.com/ory/herodot v0.8.3/go.mod h1:rvLjxOAlU5omtmgjCfazQX2N82EpMfl3BytBWc1jjsk=
+github.com/ory/herodot v0.9.0 h1:p/V+68/5CWmqEmbSVaLusDBECMt9xJjLtklqU/O63HM=
+github.com/ory/herodot v0.9.0/go.mod h1:GYF7mp8/WFRYDYJBR989lipjgx3NTjjdVdUC+hpB8mc=
github.com/ory/jsonschema/v3 v3.0.1 h1:xzV7w2rt/Qn+jvh71joIXNKKOCqqNyTlaIxdxU0IQJc=
github.com/ory/jsonschema/v3 v3.0.1/go.mod h1:jgLHekkFk0uiGdEWGleC+tOm6JSSP8cbf17PnBuGXlw=
github.com/ory/mail v2.3.1+incompatible h1:vHntHDHtQXamt2T+iwTTlCoBkDvILUeujE9Ocwe9md4=
github.com/ory/mail v2.3.1+incompatible/go.mod h1:87D9/1gB6ewElQoN0lXJ0ayfqcj3cW3qCTXh+5E9mfU=
github.com/ory/mail/v3 v3.0.0 h1:8LFMRj473vGahFD/ntiotWEd4S80FKYFtiZTDfOQ+sM=
github.com/ory/mail/v3 v3.0.0/go.mod h1:JGAVeZF8YAlxbaFDUHqRZAKBCSeW2w1vuxf28hFbZAw=
-github.com/ory/sdk/swagutil v0.0.0-20200505101021-3f40b808145c h1:xxLzUYgOhUfui6LXZrzNnVc2MKV6D1+Vxj0Mx2eoKs4=
-github.com/ory/sdk/swagutil v0.0.0-20200505101021-3f40b808145c/go.mod h1:Ufg1eAyz+Zt3+oweSZVThG13ewewWCKwBmoNmK8Z0co=
github.com/ory/sdk/swagutil v0.0.0-20200508110558-16957df12672 h1:3KTFyxZt6USQGmeQliC2w9MjSrmxaMqjoPNOaQ82API=
github.com/ory/sdk/swagutil v0.0.0-20200508110558-16957df12672/go.mod h1:Ufg1eAyz+Zt3+oweSZVThG13ewewWCKwBmoNmK8Z0co=
github.com/ory/sdk/swagutil v0.0.0-20200528141631-666b43d19aa3 h1:kp8wBBpZzracz3ut3ThGaeSZ0iyGr1K7+PO23EdImQk=
@@ -940,8 +940,8 @@ github.com/ory/x v0.0.85/go.mod h1:s44V8t3xyjWZREcU+mWlp4h302rTuM4aLXcW+y5FbQ8=
github.com/ory/x v0.0.88/go.mod h1:wrnJRjIfYXFY/AUiuUlcIUpLBDxFtWc+8x6toAeLZXU=
github.com/ory/x v0.0.93/go.mod h1:lfcTaGXpTZs7IEQAW00r9EtTCOxD//SiP5uWtNiz31g=
github.com/ory/x v0.0.110/go.mod h1:DJfkE3GdakhshNhw4zlKoRaL/ozg/lcTahA9OCih2BE=
-github.com/ory/x v0.0.122 h1:CumxV2IpCh0wFyGJQYb4y/Wpoc3H4TnrLO1XCamLip4=
-github.com/ory/x v0.0.122/go.mod h1:72UkrchuWldt9oIcOME79PuqIUuJPIXMNHXCKWHMrQk=
+github.com/ory/x v0.0.127 h1:goEspwhiKRoKRMBb6T6nDiv5L2mMU4EQUGwu90WM+Ao=
+github.com/ory/x v0.0.127/go.mod h1:FwUujfFuCj5d+xgLn4fGMYPnzriR5bdAIulFXMtnK0M=
github.com/parnurzeal/gorequest v0.2.15/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE=
github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
@@ -1064,6 +1064,8 @@ github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tL
github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU=
github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
diff --git a/selfservice/strategy/password/hasher.go b/hash/hasher.go
similarity index 90%
rename from selfservice/strategy/password/hasher.go
rename to hash/hasher.go
index ded96a127363..b89acb07952d 100644
--- a/selfservice/strategy/password/hasher.go
+++ b/hash/hasher.go
@@ -1,4 +1,4 @@
-package password
+package hash
// Hasher provides methods for generating and comparing password hashes.
type Hasher interface {
@@ -10,5 +10,5 @@ type Hasher interface {
}
type HashProvider interface {
- PasswordHasher() Hasher
+ Hasher() Hasher
}
diff --git a/selfservice/strategy/password/hasher_argon2.go b/hash/hasher_argon2.go
similarity index 86%
rename from selfservice/strategy/password/hasher_argon2.go
rename to hash/hasher_argon2.go
index 7c8f7ec480a8..c00db58e46fe 100644
--- a/selfservice/strategy/password/hasher_argon2.go
+++ b/hash/hasher_argon2.go
@@ -1,4 +1,4 @@
-package password
+package hash
import (
"bytes"
@@ -20,20 +20,20 @@ var (
ErrMismatchedHashAndPassword = errors.New("passwords do not match")
)
-type HasherArgon2 struct {
- c HasherArgon2Configuration
+type Argon2 struct {
+ c Argon2Configuration
}
-type HasherArgon2Configuration interface {
- HashersArgon2() *configuration.HasherArgon2Config
+type Argon2Configuration interface {
+ HasherArgon2() *configuration.HasherArgon2Config
}
-func NewHasherArgon2(c HasherArgon2Configuration) *HasherArgon2 {
- return &HasherArgon2{c: c}
+func NewHasherArgon2(c Argon2Configuration) *Argon2 {
+ return &Argon2{c: c}
}
-func (h *HasherArgon2) Generate(password []byte) ([]byte, error) {
- p := h.c.HashersArgon2()
+func (h *Argon2) Generate(password []byte) ([]byte, error) {
+ p := h.c.HasherArgon2()
salt := make([]byte, p.SaltLength)
if _, err := rand.Read(salt); err != nil {
@@ -59,7 +59,7 @@ func (h *HasherArgon2) Generate(password []byte) ([]byte, error) {
return b.Bytes(), nil
}
-func (h *HasherArgon2) Compare(password []byte, hash []byte) error {
+func (h *Argon2) Compare(password []byte, hash []byte) error {
// Extract the parameters, salt and derived key from the encoded password
// hash.
p, salt, hash, err := decodeHash(string(hash))
diff --git a/selfservice/strategy/password/hasher_test.go b/hash/hasher_test.go
similarity index 86%
rename from selfservice/strategy/password/hasher_test.go
rename to hash/hasher_test.go
index 1613e5876b7f..9affdbf07ef4 100644
--- a/selfservice/strategy/password/hasher_test.go
+++ b/hash/hasher_test.go
@@ -1,4 +1,4 @@
-package password_test
+package hash_test
import (
"crypto/rand"
@@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/ory/kratos/hash"
"github.com/ory/kratos/internal"
- "github.com/ory/kratos/selfservice/strategy/password"
)
func mkpw(t *testing.T, length int) []byte {
@@ -29,8 +29,8 @@ func TestHasher(t *testing.T) {
} {
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
conf := internal.NewConfigurationWithDefaults()
- for kk, h := range []password.Hasher{
- password.NewHasherArgon2(conf),
+ for kk, h := range []hash.Hasher{
+ hash.NewHasherArgon2(conf),
} {
t.Run(fmt.Sprintf("hasher=%T/password=%d", h, kk), func(t *testing.T) {
hs, err := h.Generate(pw)
diff --git a/identity/extension_recovery.go b/identity/extension_recovery.go
new file mode 100644
index 000000000000..80c03e911f82
--- /dev/null
+++ b/identity/extension_recovery.go
@@ -0,0 +1,65 @@
+package identity
+
+import (
+ "fmt"
+ "sync"
+
+ "github.com/ory/jsonschema/v3"
+
+ "github.com/ory/kratos/schema"
+)
+
+type SchemaExtensionRecovery struct {
+ l sync.Mutex
+ v []RecoveryAddress
+ i *Identity
+}
+
+func NewSchemaExtensionRecovery(i *Identity) *SchemaExtensionRecovery {
+ return &SchemaExtensionRecovery{i: i}
+}
+
+func (r *SchemaExtensionRecovery) Run(ctx jsonschema.ValidationContext, s schema.ExtensionConfig, value interface{}) error {
+ r.l.Lock()
+ defer r.l.Unlock()
+
+ switch s.Recovery.Via {
+ case "email":
+ if !jsonschema.Formats["email"](value) {
+ return ctx.Error("format", "%q is not valid %q", value, "email")
+ }
+
+ address := NewRecoveryEmailAddress(fmt.Sprintf("%s", value), r.i.ID)
+
+ if has := r.has(r.i.RecoveryAddresses, address); has != nil {
+ if r.has(r.v, address) == nil {
+ r.v = append(r.v, *has)
+ }
+ return nil
+ }
+
+ if has := r.has(r.v, address); has == nil {
+ r.v = append(r.v, *address)
+ }
+
+ return nil
+ case "":
+ return nil
+ }
+
+ return ctx.Error("", "recovery.via has unknown value %q", s.Recovery.Via)
+}
+
+func (r *SchemaExtensionRecovery) has(haystack []RecoveryAddress, needle *RecoveryAddress) *RecoveryAddress {
+ for _, has := range haystack {
+ if has.Value == needle.Value && has.Via == needle.Via {
+ return &has
+ }
+ }
+ return nil
+}
+
+func (r *SchemaExtensionRecovery) Finish() error {
+ r.i.RecoveryAddresses = r.v
+ return nil
+}
diff --git a/identity/extension_recovery_test.go b/identity/extension_recovery_test.go
new file mode 100644
index 000000000000..f687a06c5bde
--- /dev/null
+++ b/identity/extension_recovery_test.go
@@ -0,0 +1,173 @@
+package identity
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "reflect"
+ "testing"
+
+ "github.com/ory/jsonschema/v3"
+ _ "github.com/ory/jsonschema/v3/fileloader"
+
+ "github.com/ory/kratos/schema"
+ "github.com/ory/kratos/x"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSchemaExtensionRecovery(t *testing.T) {
+ iid := x.NewUUID()
+ for k, tc := range []struct {
+ expectErr error
+ schema string
+ doc string
+ expect []RecoveryAddress
+ existing []RecoveryAddress
+ }{
+ {
+ doc: `{"username":"foo@ory.sh"}`,
+ schema: "file://./stub/extension/recovery/schema.json",
+ expect: []RecoveryAddress{
+ {
+ Value: "foo@ory.sh",
+ Via: RecoveryAddressTypeEmail,
+ IdentityID: iid,
+ },
+ },
+ },
+ {
+ doc: `{"username":"foo@ory.sh"}`,
+ schema: "file://./stub/extension/recovery/schema.json",
+ expect: []RecoveryAddress{
+ {
+ Value: "foo@ory.sh",
+ Via: RecoveryAddressTypeEmail,
+ IdentityID: iid,
+ },
+ },
+ existing: []RecoveryAddress{
+ {
+ Value: "bar@ory.sh",
+ Via: RecoveryAddressTypeEmail,
+ IdentityID: iid,
+ },
+ },
+ },
+ {
+ doc: `{"emails":["baz@ory.sh","foo@ory.sh"]}`,
+ schema: "file://./stub/extension/recovery/schema.json",
+ expect: []RecoveryAddress{
+ {
+ Value: "foo@ory.sh",
+ Via: RecoveryAddressTypeEmail,
+ IdentityID: iid,
+ },
+ {
+ Value: "baz@ory.sh",
+ Via: RecoveryAddressTypeEmail,
+ IdentityID: iid,
+ },
+ },
+ existing: []RecoveryAddress{
+ {
+ Value: "foo@ory.sh",
+ Via: RecoveryAddressTypeEmail,
+ IdentityID: iid,
+ },
+ {
+ Value: "bar@ory.sh",
+ Via: RecoveryAddressTypeEmail,
+ IdentityID: iid,
+ },
+ },
+ },
+ {
+ doc: `{"emails":["foo@ory.sh","foo@ory.sh","baz@ory.sh"]}`,
+ schema: "file://./stub/extension/recovery/schema.json",
+ expect: []RecoveryAddress{
+ {
+ Value: "foo@ory.sh",
+ Via: RecoveryAddressTypeEmail,
+ IdentityID: iid,
+ },
+ {
+ Value: "baz@ory.sh",
+ Via: RecoveryAddressTypeEmail,
+ IdentityID: iid,
+ },
+ },
+ existing: []RecoveryAddress{
+ {
+ Value: "foo@ory.sh",
+ Via: RecoveryAddressTypeEmail,
+ IdentityID: iid,
+ },
+ {
+ Value: "bar@ory.sh",
+ Via: RecoveryAddressTypeEmail,
+ IdentityID: iid,
+ },
+ },
+ },
+ {
+ doc: `{"emails":["foo@ory.sh","bar@ory.sh"], "username": "foobar"}`,
+ schema: "file://./stub/extension/recovery/schema.json",
+ expectErr: errors.New("I[#/username] S[#/properties/username/format] \"foobar\" is not valid \"email\""),
+ },
+ {
+ doc: `{"emails":["foo@ory.sh","bar@ory.sh","bar@ory.sh"], "username": "foobar@ory.sh"}`,
+ schema: "file://./stub/extension/recovery/schema.json",
+ expect: []RecoveryAddress{
+ {
+ Value: "foo@ory.sh",
+ Via: RecoveryAddressTypeEmail,
+ IdentityID: iid,
+ },
+ {
+ Value: "bar@ory.sh",
+ Via: RecoveryAddressTypeEmail,
+ IdentityID: iid,
+ },
+ {
+ Value: "foobar@ory.sh",
+ Via: RecoveryAddressTypeEmail,
+ IdentityID: iid,
+ },
+ },
+ },
+ } {
+ t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
+ id := &Identity{ID: iid, RecoveryAddresses: tc.existing}
+ c := jsonschema.NewCompiler()
+ runner, err := schema.NewExtensionRunner(schema.ExtensionRunnerIdentityMetaSchema)
+ require.NoError(t, err)
+
+ e := NewSchemaExtensionRecovery(id)
+ runner.AddRunner(e).Register(c)
+
+ err = c.MustCompile(tc.schema).Validate(bytes.NewBufferString(tc.doc))
+ if tc.expectErr != nil {
+ require.EqualError(t, err, tc.expectErr.Error())
+ return
+ }
+
+ require.NoError(t, e.Finish())
+
+ addresses := id.RecoveryAddresses
+ require.Len(t, addresses, len(tc.expect))
+
+ for _, actual := range addresses {
+ var found bool
+ for _, expect := range tc.expect {
+ if reflect.DeepEqual(actual, expect) {
+ found = true
+ break
+ }
+ }
+ assert.True(t, found, "%+v not in %+v", actual, tc.expect)
+ }
+ })
+ }
+}
diff --git a/identity/extension_verify.go b/identity/extension_verify.go
index 7cfe98790bd9..8176b754e8a8 100644
--- a/identity/extension_verify.go
+++ b/identity/extension_verify.go
@@ -10,18 +10,18 @@ import (
"github.com/ory/kratos/schema"
)
-type SchemaExtensionVerify struct {
+type SchemaExtensionVerification struct {
lifespan time.Duration
l sync.Mutex
v []VerifiableAddress
i *Identity
}
-func NewSchemaExtensionVerify(i *Identity, lifespan time.Duration) *SchemaExtensionVerify {
- return &SchemaExtensionVerify{i: i, lifespan: lifespan}
+func NewSchemaExtensionVerification(i *Identity, lifespan time.Duration) *SchemaExtensionVerification {
+ return &SchemaExtensionVerification{i: i, lifespan: lifespan}
}
-func (r *SchemaExtensionVerify) Run(ctx jsonschema.ValidationContext, s schema.ExtensionConfig, value interface{}) error {
+func (r *SchemaExtensionVerification) Run(ctx jsonschema.ValidationContext, s schema.ExtensionConfig, value interface{}) error {
r.l.Lock()
defer r.l.Unlock()
@@ -55,7 +55,7 @@ func (r *SchemaExtensionVerify) Run(ctx jsonschema.ValidationContext, s schema.E
return ctx.Error("", "verification.via has unknown value %q", s.Verification.Via)
}
-func (r *SchemaExtensionVerify) has(haystack []VerifiableAddress, needle *VerifiableAddress) *VerifiableAddress {
+func (r *SchemaExtensionVerification) has(haystack []VerifiableAddress, needle *VerifiableAddress) *VerifiableAddress {
for _, has := range haystack {
if has.Value == needle.Value && has.Via == needle.Via {
return &has
@@ -64,7 +64,7 @@ func (r *SchemaExtensionVerify) has(haystack []VerifiableAddress, needle *Verifi
return nil
}
-func (r *SchemaExtensionVerify) Finish() error {
+func (r *SchemaExtensionVerification) Finish() error {
r.i.VerifiableAddresses = r.v
return nil
}
diff --git a/identity/extension_verify_test.go b/identity/extension_verify_test.go
index b4715c7c9e46..3880b54365c5 100644
--- a/identity/extension_verify_test.go
+++ b/identity/extension_verify_test.go
@@ -18,7 +18,7 @@ import (
"github.com/stretchr/testify/require"
)
-func TestSchemaExtensionVerify(t *testing.T) {
+func TestSchemaExtensionVerification(t *testing.T) {
iid := x.NewUUID()
for k, tc := range []struct {
expectErr error
@@ -184,7 +184,7 @@ func TestSchemaExtensionVerify(t *testing.T) {
require.NoError(t, err)
const expiresAt = time.Minute
- e := NewSchemaExtensionVerify(id, time.Minute)
+ e := NewSchemaExtensionVerification(id, time.Minute)
runner.AddRunner(e).Register(c)
err = c.MustCompile(tc.schema).Validate(bytes.NewBufferString(tc.doc))
diff --git a/identity/identity.go b/identity/identity.go
index 49f740ea68c0..d410d0c61f7d 100644
--- a/identity/identity.go
+++ b/identity/identity.go
@@ -32,7 +32,7 @@ type (
// or the email, as this field is immutable.
//
// required: true
- ID uuid.UUID `json:"id" faker:"uuid" db:"id" rw:"r"`
+ ID uuid.UUID `json:"id" faker:"-" db:"id"`
// Credentials represents all credentials that can be used for authenticating this identity.
Credentials map[CredentialsType]Credentials `json:"-" faker:"-" db:"-"`
@@ -54,13 +54,18 @@ type (
// required: true
Traits Traits `json:"traits" faker:"-" db:"traits"`
+ // VerifiableAddresses contains all the addresses that can be verified by the user.
VerifiableAddresses []VerifiableAddress `json:"verifiable_addresses,omitempty" faker:"-" has_many:"identity_verifiable_addresses" fk_id:"identity_id"`
- RecoveryAddresses []RecoveryAddress `json:"recovery_addresses,omitempty" faker:"-" has_many:"identity_recovery_addresses" fk_id:"identity_id"`
+
+ // RecoveryAddresses contains all the addresses that can be used to recover an identity.
+ RecoveryAddresses []RecoveryAddress `json:"recovery_addresses,omitempty" faker:"-" has_many:"identity_recovery_addresses" fk_id:"identity_id"`
// CredentialsCollection is a helper struct field for gobuffalo.pop.
CredentialsCollection CredentialsCollection `json:"-" faker:"-" has_many:"identity_credentials" fk_id:"identity_id"`
+
// CreatedAt is a helper struct field for gobuffalo.pop.
CreatedAt time.Time `json:"-" db:"created_at"`
+
// UpdatedAt is a helper struct field for gobuffalo.pop.
UpdatedAt time.Time `json:"-" db:"updated_at"`
}
@@ -106,6 +111,12 @@ func (i *Identity) lock() *sync.RWMutex {
return i.l
}
+func (i *Identity) SetSecurityAnswers(answers map[string]string) {
+ i.lock().Lock()
+ defer i.lock().Unlock()
+
+}
+
func (i *Identity) SetCredentials(t CredentialsType, c Credentials) {
i.lock().Lock()
defer i.lock().Unlock()
diff --git a/identity/identity_recovery.go b/identity/identity_recovery.go
index 3883c0a41e3d..611460ccd77f 100644
--- a/identity/identity_recovery.go
+++ b/identity/identity_recovery.go
@@ -4,8 +4,6 @@ import (
"time"
"github.com/gofrs/uuid"
-
- "github.com/ory/kratos/otp"
)
const (
@@ -22,7 +20,7 @@ type (
// swagger:model recoveryIdentityAddress
RecoveryAddress struct {
// required: true
- ID uuid.UUID `json:"id" db:"id" faker:"uuid" rw:"r"`
+ ID uuid.UUID `json:"id" db:"id" faker:"-"`
// required: true
Value string `json:"value" db:"value"`
@@ -30,17 +28,12 @@ type (
// required: true
Via RecoveryAddressType `json:"via" db:"via"`
- // required: true
- ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"`
-
// IdentityID is a helper struct field for gobuffalo.pop.
IdentityID uuid.UUID `json:"-" faker:"-" db:"identity_id"`
// CreatedAt is a helper struct field for gobuffalo.pop.
CreatedAt time.Time `json:"-" faker:"-" db:"created_at"`
// UpdatedAt is a helper struct field for gobuffalo.pop.
UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"`
- // Code is the recovery code, never to be shared as JSON
- Code string `json:"-" db:"code"`
}
)
@@ -59,18 +52,10 @@ func (a RecoveryAddress) TableName() string {
func NewRecoveryEmailAddress(
value string,
identity uuid.UUID,
- expiresIn time.Duration,
-) (*RecoveryAddress, error) {
- code, err := otp.New()
- if err != nil {
- return nil, err
- }
-
+) *RecoveryAddress {
return &RecoveryAddress{
- Code: code,
Value: value,
Via: RecoveryAddressTypeEmail,
- ExpiresAt: time.Now().Add(expiresIn).UTC(),
IdentityID: identity,
- }, nil
+ }
}
diff --git a/identity/identity_recovery_test.go b/identity/identity_recovery_test.go
index 8ad83d0c1e57..8b7cb4954bdc 100644
--- a/identity/identity_recovery_test.go
+++ b/identity/identity_recovery_test.go
@@ -1,27 +1,18 @@
package identity
import (
- "encoding/json"
"testing"
- "time"
"github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
"github.com/ory/kratos/x"
)
func TestNewRecoveryEmailAddress(t *testing.T) {
iid := x.NewUUID()
- a, err := NewRecoveryEmailAddress("foo@ory.sh", iid, time.Minute)
- require.NoError(t, err)
+ a := NewRecoveryEmailAddress("foo@ory.sh", iid)
- assert.Len(t, a.Code, 32)
assert.Equal(t, a.Value, "foo@ory.sh")
assert.Equal(t, a.Via, RecoveryAddressTypeEmail)
assert.NotEmpty(t, a.ID)
-
- out, err := json.Marshal(a)
- require.NoError(t, err)
- assert.NotContains(t, out, a.Code)
}
diff --git a/identity/identity_verification.go b/identity/identity_verification.go
index d411ef19c277..77c42d804664 100644
--- a/identity/identity_verification.go
+++ b/identity/identity_verification.go
@@ -25,7 +25,7 @@ type (
// swagger:model verifiableIdentityAddress
VerifiableAddress struct {
// required: true
- ID uuid.UUID `json:"id" db:"id" faker:"uuid" rw:"r"`
+ ID uuid.UUID `json:"id" db:"id" faker:"-"`
// required: true
Value string `json:"value" db:"value"`
diff --git a/identity/manager_test.go b/identity/manager_test.go
index 285694d60325..fc4e7a7e29fa 100644
--- a/identity/manager_test.go
+++ b/identity/manager_test.go
@@ -160,7 +160,7 @@ func TestManager(t *testing.T) {
original.Traits = identity.Traits(`{"email":"verifyme@ory.sh"}`)
require.NoError(t, reg.IdentityManager().Create(context.Background(), original))
- address, err := reg.IdentityPool().FindAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "verifyme@ory.sh")
+ address, err := reg.IdentityPool().FindVerifiableAddressByValue(context.Background(), identity.VerifiableAddressTypeEmail, "verifyme@ory.sh")
require.NoError(t, err)
pc := address.Code
diff --git a/identity/pool.go b/identity/pool.go
index a8197d6ad967..9aade3700b1f 100644
--- a/identity/pool.go
+++ b/identity/pool.go
@@ -8,7 +8,7 @@ import (
"testing"
"time"
- "github.com/bxcodec/faker"
+ "github.com/bxcodec/faker/v3"
"github.com/ory/x/errorsx"
"github.com/ory/x/sqlcon"
@@ -34,11 +34,11 @@ type (
// connectivity is broken.
GetIdentity(context.Context, uuid.UUID) (*Identity, error)
- // FindAddressByCode returns a matching address or sql.ErrNoRows if no address could be found.
- FindAddressByCode(ctx context.Context, code string) (*VerifiableAddress, error)
+ // FindVerifiableAddressByValue returns a matching address or sql.ErrNoRows if no address could be found.
+ FindVerifiableAddressByValue(ctx context.Context, via VerifiableAddressType, address string) (*VerifiableAddress, error)
- // FindAddressByValue returns a matching address or sql.ErrNoRows if no address could be found.
- FindAddressByValue(ctx context.Context, via VerifiableAddressType, address string) (*VerifiableAddress, error)
+ // FindRecoveryAddressByValue returns a matching address or sql.ErrNoRows if no address could be found.
+ FindRecoveryAddressByValue(ctx context.Context, via RecoveryAddressType, address string) (*RecoveryAddress, error)
}
PoolProvider interface {
@@ -374,7 +374,7 @@ func TestPool(p PrivilegedPool) func(t *testing.T) {
assertEqual(t, expected, actual)
})
- t.Run("suite=address", func(t *testing.T) {
+ t.Run("suite=verifiable-address", func(t *testing.T) {
createIdentityWithAddresses := func(t *testing.T, expiry time.Duration, email string) VerifiableAddress {
var i Identity
require.NoError(t, faker.FakeData(&i))
@@ -390,17 +390,14 @@ func TestPool(p PrivilegedPool) func(t *testing.T) {
}
t.Run("case=not found", func(t *testing.T) {
- _, err := p.FindAddressByCode(context.Background(), "does-not-exist")
- require.Equal(t, sqlcon.ErrNoRows, errorsx.Cause(err))
-
- _, err = p.FindAddressByValue(context.Background(), VerifiableAddressTypeEmail, "does-not-exist")
+ _, err := p.FindVerifiableAddressByValue(context.Background(), VerifiableAddressTypeEmail, "does-not-exist")
require.Equal(t, sqlcon.ErrNoRows, errorsx.Cause(err))
})
t.Run("case=create and find", func(t *testing.T) {
addresses := make([]VerifiableAddress, 15)
for k := range addresses {
- addresses[k] = createIdentityWithAddresses(t, time.Minute, "verify.TestPersister.Create"+strconv.Itoa(k)+"@ory.sh")
+ addresses[k] = createIdentityWithAddresses(t, time.Minute, "recovery.TestPersister.Create"+strconv.Itoa(k)+"@ory.sh")
require.NotEmpty(t, addresses[k].ID)
}
@@ -415,17 +412,9 @@ func TestPool(p PrivilegedPool) func(t *testing.T) {
}
for k, expected := range addresses {
- t.Run("method=FindAddressByCode", func(t *testing.T) {
- t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
- actual, err := p.FindAddressByCode(context.Background(), expected.Code)
- require.NoError(t, err)
- compare(t, expected, *actual)
- })
- })
-
- t.Run("method=FindAddressByValue", func(t *testing.T) {
+ t.Run("method=FindVerifiableAddressByValue", func(t *testing.T) {
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
- actual, err := p.FindAddressByValue(context.Background(), expected.Via, expected.Value)
+ actual, err := p.FindVerifiableAddressByValue(context.Background(), expected.Via, expected.Value)
require.NoError(t, err)
compare(t, expected, *actual)
})
@@ -434,7 +423,7 @@ func TestPool(p PrivilegedPool) func(t *testing.T) {
})
t.Run("case=verify expired should not work", func(t *testing.T) {
- address := createIdentityWithAddresses(t, -time.Minute, "verify.TestPersister.VerifyAddress.expired@ory.sh")
+ address := createIdentityWithAddresses(t, -time.Minute, "verification.TestPersister.VerifyAddress.expired@ory.sh")
require.EqualError(t, errorsx.Cause(p.VerifyAddress(context.Background(), address.Code)), sqlcon.ErrNoRows.Error())
})
@@ -443,10 +432,10 @@ func TestPool(p PrivilegedPool) func(t *testing.T) {
})
t.Run("case=create and verify", func(t *testing.T) {
- address := createIdentityWithAddresses(t, time.Minute, "verify.TestPersister.VerifyAddress.valid@ory.sh")
+ address := createIdentityWithAddresses(t, time.Minute, "verification.TestPersister.VerifyAddress.valid@ory.sh")
require.NoError(t, p.VerifyAddress(context.Background(), address.Code))
- actual, err := p.FindAddressByValue(context.Background(), address.Via, address.Value)
+ actual, err := p.FindVerifiableAddressByValue(context.Background(), address.Via, address.Value)
require.NoError(t, err)
assert.NotEqual(t, address.Code, actual.Code)
assert.True(t, actual.Verified)
@@ -455,15 +444,104 @@ func TestPool(p PrivilegedPool) func(t *testing.T) {
})
t.Run("case=update", func(t *testing.T) {
- address := createIdentityWithAddresses(t, time.Minute, "verify.TestPersister.Update@ory.sh")
+ address := createIdentityWithAddresses(t, time.Minute, "verification.TestPersister.Update@ory.sh")
address.Code = "new-code"
require.NoError(t, p.UpdateVerifiableAddress(context.Background(), &address))
- actual, err := p.FindAddressByValue(context.Background(), address.Via, address.Value)
+ actual, err := p.FindVerifiableAddressByValue(context.Background(), address.Via, address.Value)
require.NoError(t, err)
assert.Equal(t, "new-code", actual.Code)
})
+
+ t.Run("case=create and update and find", func(t *testing.T) {
+ var i Identity
+ require.NoError(t, faker.FakeData(&i))
+
+ address, err := NewVerifiableEmailAddress("verification.TestPersister.Update-Identity@ory.sh", i.ID, time.Hour)
+ require.NoError(t, err)
+ i.VerifiableAddresses = append(i.VerifiableAddresses, *address)
+ require.NoError(t, p.CreateIdentity(context.Background(), &i))
+
+ _, err = p.FindVerifiableAddressByValue(context.Background(), VerifiableAddressTypeEmail, "verification.TestPersister.Update-Identity@ory.sh")
+ require.NoError(t, err)
+
+ address, err = NewVerifiableEmailAddress("verification.TestPersister.Update-Identity-next@ory.sh", i.ID, time.Hour)
+ require.NoError(t, err)
+ i.VerifiableAddresses = []VerifiableAddress{*address}
+ require.NoError(t, p.UpdateIdentity(context.Background(), &i))
+
+ _, err = p.FindVerifiableAddressByValue(context.Background(), VerifiableAddressTypeEmail, "verification.TestPersister.Update-Identity@ory.sh")
+ require.EqualError(t, err, sqlcon.ErrNoRows.Error())
+
+ actual, err := p.FindVerifiableAddressByValue(context.Background(), VerifiableAddressTypeEmail, "verification.TestPersister.Update-Identity-next@ory.sh")
+ require.NoError(t, err)
+
+ assert.Equal(t, VerifiableAddressTypeEmail, actual.Via)
+ assert.Equal(t, "verification.TestPersister.Update-Identity-next@ory.sh", actual.Value)
+ })
+ })
+
+ t.Run("suite=recovery-address", func(t *testing.T) {
+ createIdentityWithAddresses := func(t *testing.T, email string) *Identity {
+ var i Identity
+ require.NoError(t, faker.FakeData(&i))
+ i.Traits = []byte(`{"email":"` + email + `"}`)
+ address := NewRecoveryEmailAddress(email, i.ID)
+ i.RecoveryAddresses = append(i.RecoveryAddresses, *address)
+ require.NoError(t, p.CreateIdentity(context.Background(), &i))
+ return &i
+ }
+
+ t.Run("case=not found", func(t *testing.T) {
+ _, err := p.FindRecoveryAddressByValue(context.Background(), RecoveryAddressTypeEmail, "does-not-exist")
+ require.Equal(t, sqlcon.ErrNoRows, errorsx.Cause(err))
+ })
+
+ t.Run("case=create and find", func(t *testing.T) {
+ addresses := make([]RecoveryAddress, 15)
+ for k := range addresses {
+ addresses[k] = createIdentityWithAddresses(t, "recovery.TestPersister.Create"+strconv.Itoa(k)+"@ory.sh").RecoveryAddresses[0]
+ require.NotEmpty(t, addresses[k].ID)
+ }
+
+ compare := func(t *testing.T, expected, actual RecoveryAddress) {
+ actual.CreatedAt = actual.CreatedAt.UTC().Truncate(time.Hour * 24)
+ actual.UpdatedAt = actual.UpdatedAt.UTC().Truncate(time.Hour * 24)
+ expected.CreatedAt = expected.CreatedAt.UTC().Truncate(time.Hour * 24)
+ expected.UpdatedAt = expected.UpdatedAt.UTC().Truncate(time.Hour * 24)
+ assert.EqualValues(t, expected, actual)
+ }
+
+ for k, expected := range addresses {
+ t.Run("method=FindVerifiableAddressByValue", func(t *testing.T) {
+ t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
+ actual, err := p.FindRecoveryAddressByValue(context.Background(), expected.Via, expected.Value)
+ require.NoError(t, err)
+ compare(t, expected, *actual)
+ })
+ })
+ }
+ })
+
+ t.Run("case=create and update and find", func(t *testing.T) {
+ identity := createIdentityWithAddresses(t, "recovery.TestPersister.Update@ory.sh")
+
+ _, err := p.FindRecoveryAddressByValue(context.Background(), RecoveryAddressTypeEmail, "recovery.TestPersister.Update@ory.sh")
+ require.NoError(t, err)
+
+ identity.RecoveryAddresses = []RecoveryAddress{{Via: RecoveryAddressTypeEmail, Value: "recovery.TestPersister.Update-next@ory.sh"}}
+ require.NoError(t, p.UpdateIdentity(context.Background(), identity))
+
+ _, err = p.FindRecoveryAddressByValue(context.Background(), RecoveryAddressTypeEmail, "recovery.TestPersister.Update@ory.sh")
+ require.EqualError(t, err, sqlcon.ErrNoRows.Error())
+
+ actual, err := p.FindRecoveryAddressByValue(context.Background(), RecoveryAddressTypeEmail, "recovery.TestPersister.Update-next@ory.sh")
+ require.NoError(t, err)
+
+ assert.Equal(t, RecoveryAddressTypeEmail, actual.Via)
+ assert.Equal(t, "recovery.TestPersister.Update-next@ory.sh", actual.Value)
+ })
})
}
}
diff --git a/selfservice/flow/recovery/stub/extension/schema.json b/identity/stub/extension/recovery/schema.json
similarity index 86%
rename from selfservice/flow/recovery/stub/extension/schema.json
rename to identity/stub/extension/recovery/schema.json
index 9906957f32af..044aece98a9e 100644
--- a/selfservice/flow/recovery/stub/extension/schema.json
+++ b/identity/stub/extension/recovery/schema.json
@@ -6,7 +6,7 @@
"items": {
"type": "string",
"ory.sh/kratos": {
- "verification": {
+ "recovery": {
"via": "email"
}
}
@@ -15,7 +15,7 @@
"username": {
"type": "string",
"ory.sh/kratos": {
- "verification": {
+ "recovery": {
"via": "email"
}
}
diff --git a/identity/validator.go b/identity/validator.go
index 8ada65e010b3..60a7f2b6ac2a 100644
--- a/identity/validator.go
+++ b/identity/validator.go
@@ -34,10 +34,7 @@ func NewValidator(d validatorDependencies, c configuration.Provider) *Validator
}
func (v *Validator) ValidateWithRunner(i *Identity, runners ...schema.Extension) error {
- runner, err := schema.NewExtensionRunner(
- schema.ExtensionRunnerIdentityMetaSchema,
- runners...,
- )
+ runner, err := schema.NewExtensionRunner(schema.ExtensionRunnerIdentityMetaSchema, runners...)
if err != nil {
return err
}
@@ -64,6 +61,7 @@ func (v *Validator) ValidateWithRunner(i *Identity, runners ...schema.Extension)
func (v *Validator) Validate(i *Identity) error {
return v.ValidateWithRunner(i,
NewSchemaExtensionCredentials(i),
- NewSchemaExtensionVerify(i, v.c.SelfServiceVerificationRequestLifespan()),
+ NewSchemaExtensionVerification(i, v.c.SelfServiceVerificationRequestLifespan()),
+ NewSchemaExtensionRecovery(i),
)
}
diff --git a/internal/driver.go b/internal/driver.go
index 746fe3454ab3..800b260fc514 100644
--- a/internal/driver.go
+++ b/internal/driver.go
@@ -32,7 +32,7 @@ func resetConfig() {
func NewConfigurationWithDefaults() *configuration.ViperProvider {
viper.Reset()
resetConfig()
- return configuration.NewViperProvider(logrusx.New(), true)
+ return configuration.NewViperProvider(logrusx.New("", ""), true)
}
// NewFastRegistryWithMocks returns a registry with several mocks and an SQLite in memory database that make testing
@@ -61,7 +61,7 @@ func NewRegistryDefaultWithDSN(t *testing.T, dsn string) (*configuration.ViperPr
viper.Set(configuration.ViperKeyDSN, dsn)
}
- d, err := driver.NewDefaultDriver(logrusx.New(), "test", "test", "test", true)
+ d, err := driver.NewDefaultDriver(logrusx.New("", ""), "test", "test", "test", true)
require.NoError(t, err)
return d.Configuration().(*configuration.ViperProvider), d.Registry().(*driver.RegistryDefault)
}
diff --git a/internal/faker.go b/internal/faker.go
index b43e482186d2..ca8ec636db8e 100644
--- a/internal/faker.go
+++ b/internal/faker.go
@@ -6,12 +6,14 @@ import (
"reflect"
"time"
- "github.com/bxcodec/faker"
+ "github.com/bxcodec/faker/v3"
+ "github.com/pkg/errors"
"github.com/ory/x/randx"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/selfservice/flow/login"
+ "github.com/ory/kratos/selfservice/flow/recovery"
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/selfservice/flow/settings"
"github.com/ory/kratos/selfservice/form"
@@ -19,6 +21,8 @@ import (
)
func RegisterFakes() {
+ _ = faker.SetRandomMapAndSliceSize(4)
+
if err := faker.AddProvider("birthdate", func(v reflect.Value) (interface{}, error) {
return time.Now().Add(time.Duration(rand.Int())).Round(time.Second).UTC(), nil
}); err != nil {
@@ -50,6 +54,26 @@ func RegisterFakes() {
panic(err)
}
+ if err := faker.AddProvider("http_method", func(v reflect.Value) (interface{}, error) {
+ methods := []string{"POST", "PUT", "GET", "PATCH"}
+ return methods[rand.Intn(len(methods))], nil
+ }); err != nil {
+ panic(err)
+ }
+
+ if err := faker.AddProvider("identity_credentials_type", func(v reflect.Value) (interface{}, error) {
+ methods := []identity.CredentialsType{identity.CredentialsTypePassword, identity.CredentialsTypePassword}
+ return string(methods[rand.Intn(len(methods))]), nil
+ }); err != nil {
+ panic(err)
+ }
+
+ if err := faker.AddProvider("string", func(v reflect.Value) (interface{}, error) {
+ return randx.MustString(25, randx.AlphaNum), nil
+ }); err != nil {
+ panic(err)
+ }
+
if err := faker.AddProvider("time_type", func(v reflect.Value) (interface{}, error) {
return time.Now().Add(time.Duration(rand.Int())).Round(time.Second).UTC(), nil
}); err != nil {
@@ -79,7 +103,7 @@ func RegisterFakes() {
for _, ct := range []identity.CredentialsType{identity.CredentialsTypePassword, identity.CredentialsTypeOIDC} {
var f form.HTMLForm
if err := faker.FakeData(&f); err != nil {
- return nil, err
+ return nil, errors.WithStack(err)
}
methods[ct] = ®istration.RequestMethod{
Method: ct,
@@ -108,6 +132,23 @@ func RegisterFakes() {
panic(err)
}
+ if err := faker.AddProvider("recovery_request_methods", func(v reflect.Value) (interface{}, error) {
+ var methods = make(map[string]*recovery.RequestMethod)
+ for _, ct := range []string{recovery.StrategyRecoveryTokenName} {
+ var f form.HTMLForm
+ if err := faker.FakeData(&f); err != nil {
+ return nil, err
+ }
+ methods[ct] = &recovery.RequestMethod{
+ Method: ct,
+ Config: &recovery.RequestMethodConfig{RequestMethodConfigurator: &f},
+ }
+ }
+ return methods, nil
+ }); err != nil {
+ panic(err)
+ }
+
if err := faker.AddProvider("uuid", func(v reflect.Value) (interface{}, error) {
return x.NewUUID(), nil
}); err != nil {
diff --git a/internal/httpclient/client/common/common_client.go b/internal/httpclient/client/common/common_client.go
index 2026605226ef..d9ce8d4b499a 100644
--- a/internal/httpclient/client/common/common_client.go
+++ b/internal/httpclient/client/common/common_client.go
@@ -85,7 +85,7 @@ func (a *Client) GetSchema(params *GetSchemaParams) (*GetSchemaOK, error) {
other information.
When accessing this endpoint through ORY Kratos' Public API, ensure that cookies are set as they are required for CSRF to work. To prevent
-token scanning attacks, the public endpoint does not return 404 status codes to prevent scanning attacks.
+token scanning attacks, the public endpoint does not return 404 status codes.
More information can be found at [ORY Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).
*/
@@ -167,7 +167,7 @@ func (a *Client) GetSelfServiceBrowserRecoveryRequest(params *GetSelfServiceBrow
other information.
When accessing this endpoint through ORY Kratos' Public API, ensure that cookies are set as they are required for CSRF to work. To prevent
-token scanning attacks, the public endpoint does not return 404 status codes to prevent scanning attacks.
+token scanning attacks, the public endpoint does not return 404 status codes.
More information can be found at [ORY Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).
*/
@@ -248,7 +248,7 @@ func (a *Client) GetSelfServiceBrowserSettingsRequest(params *GetSelfServiceBrow
This endpoint returns the error associated with a user-facing self service errors.
When accessing this endpoint through ORY Kratos' Public API, ensure that cookies are set as they are required for CSRF to work. To prevent
-token scanning attacks, the public endpoint does not return 404 status codes to prevent scanning attacks.
+token scanning attacks, the public endpoint does not return 404 status codes.
More information can be found at [ORY Kratos User User Facing Error Documentation](https://www.ory.sh/docs/kratos/self-service/flows/user-facing-errors).
*/
diff --git a/internal/httpclient/client/public/complete_self_service_browser_recovery_link_strategy_flow_parameters.go b/internal/httpclient/client/public/complete_self_service_browser_recovery_link_strategy_flow_parameters.go
new file mode 100644
index 000000000000..a25c44325573
--- /dev/null
+++ b/internal/httpclient/client/public/complete_self_service_browser_recovery_link_strategy_flow_parameters.go
@@ -0,0 +1,112 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+package public
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "github.com/go-openapi/errors"
+ "github.com/go-openapi/runtime"
+ cr "github.com/go-openapi/runtime/client"
+ "github.com/go-openapi/strfmt"
+)
+
+// NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams creates a new CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams object
+// with the default values initialized.
+func NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams() *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams {
+
+ return &CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams{
+
+ timeout: cr.DefaultTimeout,
+ }
+}
+
+// NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParamsWithTimeout creates a new CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams object
+// with the default values initialized, and the ability to set a timeout on a request
+func NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParamsWithTimeout(timeout time.Duration) *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams {
+
+ return &CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams{
+
+ timeout: timeout,
+ }
+}
+
+// NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParamsWithContext creates a new CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams object
+// with the default values initialized, and the ability to set a context for a request
+func NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParamsWithContext(ctx context.Context) *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams {
+
+ return &CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams{
+
+ Context: ctx,
+ }
+}
+
+// NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParamsWithHTTPClient creates a new CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams object
+// with the default values initialized, and the ability to set a custom HTTPClient for a request
+func NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParamsWithHTTPClient(client *http.Client) *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams {
+
+ return &CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams{
+ HTTPClient: client,
+ }
+}
+
+/*CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams contains all the parameters to send to the API endpoint
+for the complete self service browser recovery link strategy flow operation typically these are written to a http.Request
+*/
+type CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams struct {
+ timeout time.Duration
+ Context context.Context
+ HTTPClient *http.Client
+}
+
+// WithTimeout adds the timeout to the complete self service browser recovery link strategy flow params
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) WithTimeout(timeout time.Duration) *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams {
+ o.SetTimeout(timeout)
+ return o
+}
+
+// SetTimeout adds the timeout to the complete self service browser recovery link strategy flow params
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) SetTimeout(timeout time.Duration) {
+ o.timeout = timeout
+}
+
+// WithContext adds the context to the complete self service browser recovery link strategy flow params
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) WithContext(ctx context.Context) *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams {
+ o.SetContext(ctx)
+ return o
+}
+
+// SetContext adds the context to the complete self service browser recovery link strategy flow params
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) SetContext(ctx context.Context) {
+ o.Context = ctx
+}
+
+// WithHTTPClient adds the HTTPClient to the complete self service browser recovery link strategy flow params
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) WithHTTPClient(client *http.Client) *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams {
+ o.SetHTTPClient(client)
+ return o
+}
+
+// SetHTTPClient adds the HTTPClient to the complete self service browser recovery link strategy flow params
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) SetHTTPClient(client *http.Client) {
+ o.HTTPClient = client
+}
+
+// WriteToRequest writes these params to a swagger request
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
+
+ if err := r.SetTimeout(o.timeout); err != nil {
+ return err
+ }
+ var res []error
+
+ if len(res) > 0 {
+ return errors.CompositeValidationError(res...)
+ }
+ return nil
+}
diff --git a/internal/httpclient/client/public/complete_self_service_browser_recovery_link_strategy_flow_responses.go b/internal/httpclient/client/public/complete_self_service_browser_recovery_link_strategy_flow_responses.go
new file mode 100644
index 000000000000..6a9b6d1d3457
--- /dev/null
+++ b/internal/httpclient/client/public/complete_self_service_browser_recovery_link_strategy_flow_responses.go
@@ -0,0 +1,97 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+package public
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/go-openapi/runtime"
+ "github.com/go-openapi/strfmt"
+
+ "github.com/ory/kratos/internal/httpclient/models"
+)
+
+// CompleteSelfServiceBrowserRecoveryLinkStrategyFlowReader is a Reader for the CompleteSelfServiceBrowserRecoveryLinkStrategyFlow structure.
+type CompleteSelfServiceBrowserRecoveryLinkStrategyFlowReader struct {
+ formats strfmt.Registry
+}
+
+// ReadResponse reads a server response into the received o.
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
+ switch response.Code() {
+ case 302:
+ result := NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowFound()
+ if err := result.readResponse(response, consumer, o.formats); err != nil {
+ return nil, err
+ }
+ return nil, result
+ case 500:
+ result := NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowInternalServerError()
+ if err := result.readResponse(response, consumer, o.formats); err != nil {
+ return nil, err
+ }
+ return nil, result
+
+ default:
+ return nil, runtime.NewAPIError("unknown error", response, response.Code())
+ }
+}
+
+// NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowFound creates a CompleteSelfServiceBrowserRecoveryLinkStrategyFlowFound with default headers values
+func NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowFound() *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowFound {
+ return &CompleteSelfServiceBrowserRecoveryLinkStrategyFlowFound{}
+}
+
+/*CompleteSelfServiceBrowserRecoveryLinkStrategyFlowFound handles this case with default header values.
+
+Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is
+typically 201.
+*/
+type CompleteSelfServiceBrowserRecoveryLinkStrategyFlowFound struct {
+}
+
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowFound) Error() string {
+ return fmt.Sprintf("[POST /self-service/browser/flows/recovery/link][%d] completeSelfServiceBrowserRecoveryLinkStrategyFlowFound ", 302)
+}
+
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+ return nil
+}
+
+// NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowInternalServerError creates a CompleteSelfServiceBrowserRecoveryLinkStrategyFlowInternalServerError with default headers values
+func NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowInternalServerError() *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowInternalServerError {
+ return &CompleteSelfServiceBrowserRecoveryLinkStrategyFlowInternalServerError{}
+}
+
+/*CompleteSelfServiceBrowserRecoveryLinkStrategyFlowInternalServerError handles this case with default header values.
+
+genericError
+*/
+type CompleteSelfServiceBrowserRecoveryLinkStrategyFlowInternalServerError struct {
+ Payload *models.GenericError
+}
+
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowInternalServerError) Error() string {
+ return fmt.Sprintf("[POST /self-service/browser/flows/recovery/link][%d] completeSelfServiceBrowserRecoveryLinkStrategyFlowInternalServerError %+v", 500, o.Payload)
+}
+
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowInternalServerError) GetPayload() *models.GenericError {
+ return o.Payload
+}
+
+func (o *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
+
+ o.Payload = new(models.GenericError)
+
+ // response payload
+ if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF {
+ return err
+ }
+
+ return nil
+}
diff --git a/internal/httpclient/client/public/public_client.go b/internal/httpclient/client/public/public_client.go
index c14ba55bf25e..b455cfa4228d 100644
--- a/internal/httpclient/client/public/public_client.go
+++ b/internal/httpclient/client/public/public_client.go
@@ -27,6 +27,8 @@ type Client struct {
// ClientService is the interface for Client methods
type ClientService interface {
+ CompleteSelfServiceBrowserRecoveryLinkStrategyFlow(params *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) error
+
CompleteSelfServiceBrowserSettingsOIDCSettingsFlow(params *CompleteSelfServiceBrowserSettingsOIDCSettingsFlowParams) error
CompleteSelfServiceBrowserSettingsPasswordStrategyFlow(params *CompleteSelfServiceBrowserSettingsPasswordStrategyFlowParams) error
@@ -54,6 +56,37 @@ type ClientService interface {
SetTransport(transport runtime.ClientTransport)
}
+/*
+ CompleteSelfServiceBrowserRecoveryLinkStrategyFlow completes the browser based recovery flow using a recovery link
+
+ > This endpoint is NOT INTENDED for API clients and only works with browsers (Chrome, Firefox, ...) and HTML Forms.
+
+More information can be found at [ORY Kratos Account Recovery Documentation](../self-service/flows/password-reset-account-recovery).
+*/
+func (a *Client) CompleteSelfServiceBrowserRecoveryLinkStrategyFlow(params *CompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams) error {
+ // TODO: Validate the params before sending
+ if params == nil {
+ params = NewCompleteSelfServiceBrowserRecoveryLinkStrategyFlowParams()
+ }
+
+ _, err := a.transport.Submit(&runtime.ClientOperation{
+ ID: "completeSelfServiceBrowserRecoveryLinkStrategyFlow",
+ Method: "POST",
+ PathPattern: "/self-service/browser/flows/recovery/link",
+ ProducesMediaTypes: []string{"application/json"},
+ ConsumesMediaTypes: []string{"application/json", "application/x-www-form-urlencoded"},
+ Schemes: []string{"http", "https"},
+ Params: params,
+ Reader: &CompleteSelfServiceBrowserRecoveryLinkStrategyFlowReader{formats: a.formats},
+ Context: params.Context,
+ Client: params.HTTPClient,
+ })
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
/*
CompleteSelfServiceBrowserSettingsOIDCSettingsFlow completes the browser based settings flow for the open ID connect strategy
diff --git a/internal/httpclient/models/generic_error_payload.go b/internal/httpclient/models/generic_error_payload.go
index 19ddaa5d0a57..5fe7451bff05 100644
--- a/internal/httpclient/models/generic_error_payload.go
+++ b/internal/httpclient/models/generic_error_payload.go
@@ -22,7 +22,7 @@ type GenericErrorPayload struct {
Debug string `json:"debug,omitempty"`
// details
- Details interface{} `json:"details,omitempty"`
+ Details map[string]interface{} `json:"details,omitempty"`
// message
Message string `json:"message,omitempty"`
diff --git a/internal/httpclient/models/id.go b/internal/httpclient/models/id.go
new file mode 100644
index 000000000000..0fb2bdda6dd7
--- /dev/null
+++ b/internal/httpclient/models/id.go
@@ -0,0 +1,20 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+package models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+ "github.com/go-openapi/strfmt"
+)
+
+// ID ID
+//
+// swagger:model ID
+type ID int64
+
+// Validate validates this ID
+func (m ID) Validate(formats strfmt.Registry) error {
+ return nil
+}
diff --git a/internal/httpclient/models/identity.go b/internal/httpclient/models/identity.go
index d0141d13dfa2..60c8a4ce6f53 100644
--- a/internal/httpclient/models/identity.go
+++ b/internal/httpclient/models/identity.go
@@ -24,7 +24,7 @@ type Identity struct {
// Format: uuid4
ID UUID `json:"id"`
- // recovery addresses
+ // RecoveryAddresses contains all the addresses that can be used to recover an identity.
RecoveryAddresses []*RecoveryAddress `json:"recovery_addresses"`
// traits
@@ -40,7 +40,7 @@ type Identity struct {
// format: url
TraitsSchemaURL string `json:"traits_schema_url,omitempty"`
- // verifiable addresses
+ // VerifiableAddresses contains all the addresses that can be verified by the user.
VerifiableAddresses []*VerifiableAddress `json:"verifiable_addresses"`
}
diff --git a/internal/httpclient/models/message.go b/internal/httpclient/models/message.go
new file mode 100644
index 000000000000..6972cbfaca76
--- /dev/null
+++ b/internal/httpclient/models/message.go
@@ -0,0 +1,98 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+package models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+ "github.com/go-openapi/errors"
+ "github.com/go-openapi/strfmt"
+ "github.com/go-openapi/swag"
+)
+
+// Message message
+//
+// swagger:model Message
+type Message struct {
+
+ // context
+ Context interface{} `json:"context,omitempty"`
+
+ // id
+ ID ID `json:"id,omitempty"`
+
+ // text
+ Text string `json:"text,omitempty"`
+
+ // type
+ Type Type `json:"type,omitempty"`
+}
+
+// Validate validates this message
+func (m *Message) Validate(formats strfmt.Registry) error {
+ var res []error
+
+ if err := m.validateID(formats); err != nil {
+ res = append(res, err)
+ }
+
+ if err := m.validateType(formats); err != nil {
+ res = append(res, err)
+ }
+
+ if len(res) > 0 {
+ return errors.CompositeValidationError(res...)
+ }
+ return nil
+}
+
+func (m *Message) validateID(formats strfmt.Registry) error {
+
+ if swag.IsZero(m.ID) { // not required
+ return nil
+ }
+
+ if err := m.ID.Validate(formats); err != nil {
+ if ve, ok := err.(*errors.Validation); ok {
+ return ve.ValidateName("id")
+ }
+ return err
+ }
+
+ return nil
+}
+
+func (m *Message) validateType(formats strfmt.Registry) error {
+
+ if swag.IsZero(m.Type) { // not required
+ return nil
+ }
+
+ if err := m.Type.Validate(formats); err != nil {
+ if ve, ok := err.(*errors.Validation); ok {
+ return ve.ValidateName("type")
+ }
+ return err
+ }
+
+ return nil
+}
+
+// MarshalBinary interface implementation
+func (m *Message) MarshalBinary() ([]byte, error) {
+ if m == nil {
+ return nil, nil
+ }
+ return swag.WriteJSON(m)
+}
+
+// UnmarshalBinary interface implementation
+func (m *Message) UnmarshalBinary(b []byte) error {
+ var res Message
+ if err := swag.ReadJSON(b, &res); err != nil {
+ return err
+ }
+ *m = res
+ return nil
+}
diff --git a/internal/httpclient/models/messages.go b/internal/httpclient/models/messages.go
new file mode 100644
index 000000000000..3b0893ef49be
--- /dev/null
+++ b/internal/httpclient/models/messages.go
@@ -0,0 +1,45 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+package models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+ "strconv"
+
+ "github.com/go-openapi/errors"
+ "github.com/go-openapi/strfmt"
+ "github.com/go-openapi/swag"
+)
+
+// Messages messages
+//
+// swagger:model Messages
+type Messages []*Message
+
+// Validate validates this messages
+func (m Messages) Validate(formats strfmt.Registry) error {
+ var res []error
+
+ for i := 0; i < len(m); i++ {
+ if swag.IsZero(m[i]) { // not required
+ continue
+ }
+
+ 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
+ }
+ }
+
+ }
+
+ if len(res) > 0 {
+ return errors.CompositeValidationError(res...)
+ }
+ return nil
+}
diff --git a/internal/httpclient/models/recovery_address.go b/internal/httpclient/models/recovery_address.go
index 197186ff4e98..c45960545e06 100644
--- a/internal/httpclient/models/recovery_address.go
+++ b/internal/httpclient/models/recovery_address.go
@@ -17,24 +17,11 @@ import (
// swagger:model RecoveryAddress
type RecoveryAddress struct {
- // expires at
- // Required: true
- // Format: date-time
- ExpiresAt *strfmt.DateTime `json:"expires_at"`
-
// id
// Required: true
// Format: uuid4
ID UUID `json:"id"`
- // recovered
- // Required: true
- Recovered *bool `json:"recovered"`
-
- // recovered at
- // Format: date-time
- RecoveredAt strfmt.DateTime `json:"recovered_at,omitempty"`
-
// value
// Required: true
Value *string `json:"value"`
@@ -48,22 +35,10 @@ type RecoveryAddress struct {
func (m *RecoveryAddress) Validate(formats strfmt.Registry) error {
var res []error
- if err := m.validateExpiresAt(formats); err != nil {
- res = append(res, err)
- }
-
if err := m.validateID(formats); err != nil {
res = append(res, err)
}
- if err := m.validateRecovered(formats); err != nil {
- res = append(res, err)
- }
-
- if err := m.validateRecoveredAt(formats); err != nil {
- res = append(res, err)
- }
-
if err := m.validateValue(formats); err != nil {
res = append(res, err)
}
@@ -78,19 +53,6 @@ func (m *RecoveryAddress) Validate(formats strfmt.Registry) error {
return nil
}
-func (m *RecoveryAddress) validateExpiresAt(formats strfmt.Registry) error {
-
- if err := validate.Required("expires_at", "body", m.ExpiresAt); err != nil {
- return err
- }
-
- if err := validate.FormatOf("expires_at", "body", "date-time", m.ExpiresAt.String(), formats); err != nil {
- return err
- }
-
- return nil
-}
-
func (m *RecoveryAddress) validateID(formats strfmt.Registry) error {
if err := m.ID.Validate(formats); err != nil {
@@ -103,28 +65,6 @@ func (m *RecoveryAddress) validateID(formats strfmt.Registry) error {
return nil
}
-func (m *RecoveryAddress) validateRecovered(formats strfmt.Registry) error {
-
- if err := validate.Required("recovered", "body", m.Recovered); err != nil {
- return err
- }
-
- return nil
-}
-
-func (m *RecoveryAddress) validateRecoveredAt(formats strfmt.Registry) error {
-
- if swag.IsZero(m.RecoveredAt) { // not required
- return nil
- }
-
- if err := validate.FormatOf("recovered_at", "body", "date-time", m.RecoveredAt.String(), formats); err != nil {
- return err
- }
-
- return nil
-}
-
func (m *RecoveryAddress) validateValue(formats strfmt.Registry) error {
if err := validate.Required("value", "body", m.Value); err != nil {
diff --git a/internal/httpclient/models/recovery_request.go b/internal/httpclient/models/recovery_request.go
index 72914d101024..3d58b493ebb1 100644
--- a/internal/httpclient/models/recovery_request.go
+++ b/internal/httpclient/models/recovery_request.go
@@ -41,6 +41,9 @@ type RecoveryRequest struct {
// Format: date-time
IssuedAt *strfmt.DateTime `json:"issued_at"`
+ // messages
+ Messages Messages `json:"messages,omitempty"`
+
// Methods contains context for all account recovery methods. If a registration request has been
// processed, but for example the password is incorrect, this will contain error messages.
// Required: true
@@ -72,6 +75,10 @@ func (m *RecoveryRequest) Validate(formats strfmt.Registry) error {
res = append(res, err)
}
+ if err := m.validateMessages(formats); err != nil {
+ res = append(res, err)
+ }
+
if err := m.validateMethods(formats); err != nil {
res = append(res, err)
}
@@ -128,6 +135,22 @@ func (m *RecoveryRequest) validateIssuedAt(formats strfmt.Registry) error {
return nil
}
+func (m *RecoveryRequest) validateMessages(formats strfmt.Registry) error {
+
+ if swag.IsZero(m.Messages) { // not required
+ return nil
+ }
+
+ if err := m.Messages.Validate(formats); err != nil {
+ if ve, ok := err.(*errors.Validation); ok {
+ return ve.ValidateName("messages")
+ }
+ return err
+ }
+
+ return nil
+}
+
func (m *RecoveryRequest) validateMethods(formats strfmt.Registry) error {
for k := range m.Methods {
diff --git a/internal/httpclient/models/settings_request.go b/internal/httpclient/models/settings_request.go
index 448e58ae7c82..fd4bd619603e 100644
--- a/internal/httpclient/models/settings_request.go
+++ b/internal/httpclient/models/settings_request.go
@@ -46,6 +46,9 @@ type SettingsRequest struct {
// Format: date-time
IssuedAt *strfmt.DateTime `json:"issued_at"`
+ // messages
+ Messages Messages `json:"messages,omitempty"`
+
// Methods contains context for all enabled registration methods. If a registration request has been
// processed, but for example the password is incorrect, this will contain error messages.
// Required: true
@@ -83,6 +86,10 @@ func (m *SettingsRequest) Validate(formats strfmt.Registry) error {
res = append(res, err)
}
+ if err := m.validateMessages(formats); err != nil {
+ res = append(res, err)
+ }
+
if err := m.validateMethods(formats); err != nil {
res = append(res, err)
}
@@ -157,6 +164,22 @@ func (m *SettingsRequest) validateIssuedAt(formats strfmt.Registry) error {
return nil
}
+func (m *SettingsRequest) validateMessages(formats strfmt.Registry) error {
+
+ if swag.IsZero(m.Messages) { // not required
+ return nil
+ }
+
+ if err := m.Messages.Validate(formats); err != nil {
+ if ve, ok := err.(*errors.Validation); ok {
+ return ve.ValidateName("messages")
+ }
+ return err
+ }
+
+ return nil
+}
+
func (m *SettingsRequest) validateMethods(formats strfmt.Registry) error {
for k := range m.Methods {
diff --git a/internal/httpclient/models/type.go b/internal/httpclient/models/type.go
new file mode 100644
index 000000000000..c6e6d3f0fd36
--- /dev/null
+++ b/internal/httpclient/models/type.go
@@ -0,0 +1,20 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+package models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+ "github.com/go-openapi/strfmt"
+)
+
+// Type type
+//
+// swagger:model Type
+type Type string
+
+// Validate validates this type
+func (m Type) Validate(formats strfmt.Registry) error {
+ return nil
+}
diff --git a/internal/testhelpers/courier.go b/internal/testhelpers/courier.go
new file mode 100644
index 000000000000..2e646af5a1d9
--- /dev/null
+++ b/internal/testhelpers/courier.go
@@ -0,0 +1,35 @@
+package testhelpers
+
+import (
+ "context"
+ "regexp"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/ory/kratos/courier"
+)
+
+func CourierExpectMessage(t *testing.T, reg interface {
+ courier.PersistenceProvider
+}, recipient, subject string) *courier.Message {
+ message, err := reg.CourierPersister().LatestQueuedMessage(context.Background())
+ require.NoError(t, err)
+
+ assert.EqualValues(t, subject, strings.TrimSpace(message.Subject))
+ assert.EqualValues(t, recipient, strings.TrimSpace(message.Recipient))
+
+ return message
+}
+
+func CourierExpectLinkInMessage(t *testing.T, message *courier.Message, offset int) string {
+ if offset == 0 {
+ offset++
+ }
+ match := regexp.MustCompile(``).FindStringSubmatch(message.Body)
+ require.Len(t, match, offset*2)
+
+ return match[offset]
+}
diff --git a/internal/testhelpers/errorx.go b/internal/testhelpers/errorx.go
index f5777f31d83a..99a5c252c988 100644
--- a/internal/testhelpers/errorx.go
+++ b/internal/testhelpers/errorx.go
@@ -8,6 +8,8 @@ import (
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
+ "github.com/ory/x/logrusx"
+
"github.com/ory/viper"
"github.com/ory/herodot"
@@ -18,8 +20,7 @@ import (
)
func NewErrorTestServer(t *testing.T, reg interface{ errorx.PersistenceProvider }) *httptest.Server {
- logger := logrus.New()
- logger.Level = logrus.TraceLevel
+ logger := logrusx.New("", "", logrusx.ForceLevel(logrus.TraceLevel))
writer := herodot.NewJSONWriter(logger)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
e, err := reg.SelfServiceErrorPersister().Read(r.Context(), x.ParseUUID(r.URL.Query().Get("error")))
diff --git a/internal/testhelpers/handler_mock.go b/internal/testhelpers/handler_mock.go
index 9047f77cb079..f75f59c0d76b 100644
--- a/internal/testhelpers/handler_mock.go
+++ b/internal/testhelpers/handler_mock.go
@@ -9,7 +9,7 @@ import (
"testing"
"time"
- "github.com/bxcodec/faker"
+ "github.com/bxcodec/faker/v3"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter"
"github.com/pkg/errors"
@@ -57,7 +57,7 @@ func MockMakeAuthenticatedRequest(t *testing.T, reg mockDeps, conf configuration
set := "/" + uuid.New().String() + "/set"
router.GET(set, MockSetSession(t, reg, conf))
- client := MockCookieClient(t)
+ client := NewClientWithCookies(t)
MockHydrateCookieClient(t, client, "http://"+req.URL.Host+set)
res, err := client.Do(req)
@@ -71,7 +71,7 @@ func MockMakeAuthenticatedRequest(t *testing.T, reg mockDeps, conf configuration
return body, res
}
-func MockCookieClient(t *testing.T) *http.Client {
+func NewClientWithCookies(t *testing.T) *http.Client {
cj, err := cookiejar.New(&cookiejar.Options{})
require.NoError(t, err)
return &http.Client{Jar: cj}
diff --git a/internal/testhelpers/recovery.go b/internal/testhelpers/recovery.go
deleted file mode 100644
index 9120d94e1275..000000000000
--- a/internal/testhelpers/recovery.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package testhelpers
-
-import (
- "net/http"
- "testing"
-
- "github.com/gobuffalo/httptest"
- "github.com/julienschmidt/httprouter"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/ory/viper"
-
- "github.com/ory/kratos/driver/configuration"
- "github.com/ory/kratos/internal/httpclient/client/common"
- "github.com/ory/kratos/selfservice/flow/settings"
-)
-
-func NewRecoveryTestServer(t *testing.T) *httptest.Server {
- router := httprouter.New()
- router.GET("/recovery", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
- w.WriteHeader(http.StatusNoContent)
- })
- ts := httptest.NewServer(router)
- t.Cleanup(ts.Close)
-
- viper.Set(configuration.ViperKeyURLsRecovery, ts.URL+"/recovery")
-
- return ts
-}
-
-func GetRecoveryRequest(t *testing.T, primaryUser *http.Client, ts *httptest.Server) *common.GetSelfServiceBrowserRecoveryRequestOK {
- publicClient := NewSDKClient(ts)
-
- res, err := primaryUser.Get(ts.URL + settings.PublicPath)
- require.NoError(t, err)
- require.NoError(t, res.Body.Close())
-
- rs, err := publicClient.Common.GetSelfServiceBrowserRecoveryRequest(
- common.NewGetSelfServiceBrowserRecoveryRequestParams().WithHTTPClient(primaryUser).
- WithRequest(res.Request.URL.Query().Get("request")),
- )
- require.NoError(t, err)
- assert.Empty(t, rs.Payload.Active)
-
- return rs
-}
diff --git a/internal/testhelpers/selfservice.go b/internal/testhelpers/selfservice.go
index 98ac1a644f16..34ea16df128a 100644
--- a/internal/testhelpers/selfservice.go
+++ b/internal/testhelpers/selfservice.go
@@ -8,7 +8,7 @@ import (
"net/url"
"testing"
- "github.com/bxcodec/faker"
+ "github.com/bxcodec/faker/v3"
"github.com/gobuffalo/httptest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/internal/testhelpers/selfservice_recovery.go b/internal/testhelpers/selfservice_recovery.go
new file mode 100644
index 000000000000..096ce1b7e108
--- /dev/null
+++ b/internal/testhelpers/selfservice_recovery.go
@@ -0,0 +1,85 @@
+// nolint
+package testhelpers
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "testing"
+
+ "github.com/julienschmidt/httprouter"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/ory/viper"
+ "github.com/ory/x/pointerx"
+
+ "github.com/ory/kratos/driver/configuration"
+ "github.com/ory/kratos/internal/httpclient/client/common"
+ "github.com/ory/kratos/internal/httpclient/models"
+ "github.com/ory/kratos/selfservice/flow/recovery"
+)
+
+func NewRecoveryUITestServer(t *testing.T) *httptest.Server {
+ router := httprouter.New()
+ router.GET("/recovery", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+ w.WriteHeader(http.StatusNoContent)
+ })
+ router.GET("/settings", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+ w.WriteHeader(http.StatusAccepted)
+ })
+ ts := httptest.NewServer(router)
+ t.Cleanup(ts.Close)
+
+ viper.Set(configuration.ViperKeyURLsSettings, ts.URL+"/settings")
+ viper.Set(configuration.ViperKeyURLsRecovery, ts.URL+"/recovery")
+
+ return ts
+}
+
+func GetRecoveryRequest(t *testing.T, client *http.Client, ts *httptest.Server) *common.GetSelfServiceBrowserRecoveryRequestOK {
+ publicClient := NewSDKClient(ts)
+
+ res, err := client.Get(ts.URL + recovery.PublicRecoveryInitPath)
+ require.NoError(t, err)
+ require.NoError(t, res.Body.Close())
+
+ rs, err := publicClient.Common.GetSelfServiceBrowserRecoveryRequest(
+ common.NewGetSelfServiceBrowserRecoveryRequestParams().WithHTTPClient(client).
+ WithRequest(res.Request.URL.Query().Get("request")),
+ )
+ require.NoError(t, err, "%s", res.Request.URL.String())
+ assert.Empty(t, rs.Payload.Active)
+
+ return rs
+}
+
+func RecoverySubmitForm(
+ t *testing.T,
+ f *models.RequestMethodConfig,
+ hc *http.Client,
+ values url.Values,
+) (string, *common.GetSelfServiceBrowserRecoveryRequestOK) {
+ require.NotEmpty(t, f.Action)
+
+ res, err := hc.PostForm(pointerx.StringR(f.Action), values)
+ require.NoError(t, err)
+ defer res.Body.Close()
+
+ b, err := ioutil.ReadAll(res.Body)
+ require.NoError(t, err)
+ assert.EqualValues(t, http.StatusNoContent, res.StatusCode, "%s", b)
+
+ assert.Equal(t, viper.GetString(configuration.ViperKeyURLsRecovery), res.Request.URL.Scheme+"://"+res.Request.URL.Host+res.Request.URL.Path, "should end up at the settings URL, used: %s", pointerx.StringR(f.Action))
+
+ rs, err := NewSDKClientFromURL(viper.GetString(configuration.ViperKeyURLsSelfPublic)).Common.GetSelfServiceBrowserRecoveryRequest(
+ common.NewGetSelfServiceBrowserRecoveryRequestParams().WithHTTPClient(hc).
+ WithRequest(res.Request.URL.Query().Get("request")),
+ )
+ require.NoError(t, err)
+ body, err := json.Marshal(rs.Payload)
+ require.NoError(t, err)
+ return string(body), rs
+}
diff --git a/internal/testhelpers/selfservice_settings.go b/internal/testhelpers/selfservice_settings.go
index 66b2ca2dd984..7f59542f0605 100644
--- a/internal/testhelpers/selfservice_settings.go
+++ b/internal/testhelpers/selfservice_settings.go
@@ -1,3 +1,4 @@
+// nolint
package testhelpers
import (
diff --git a/internal/testhelpers/server.go b/internal/testhelpers/server.go
index c1844912992a..a52f701b4bf0 100644
--- a/internal/testhelpers/server.go
+++ b/internal/testhelpers/server.go
@@ -4,6 +4,7 @@ import (
"testing"
"github.com/gobuffalo/httptest"
+
"github.com/ory/viper"
"github.com/ory/kratos/driver"
diff --git a/internal/testhelpers/session.go b/internal/testhelpers/session.go
index bd44d248ffd6..60a66c1dee4c 100644
--- a/internal/testhelpers/session.go
+++ b/internal/testhelpers/session.go
@@ -6,7 +6,7 @@ import (
)
func NewSessionClient(t *testing.T, u string) *http.Client {
- c := MockCookieClient(t)
+ c := NewClientWithCookies(t)
MockHydrateCookieClient(t, c, u)
return c
}
diff --git a/persistence/reference.go b/persistence/reference.go
index 642d26c82a7d..bf9084334a58 100644
--- a/persistence/reference.go
+++ b/persistence/reference.go
@@ -15,6 +15,7 @@ import (
"github.com/ory/kratos/selfservice/flow/registration"
"github.com/ory/kratos/selfservice/flow/settings"
"github.com/ory/kratos/selfservice/flow/verify"
+ "github.com/ory/kratos/selfservice/strategy/link"
"github.com/ory/kratos/session"
)
@@ -33,6 +34,7 @@ type Persister interface {
errorx.Persister
verify.Persister
recovery.RequestPersister
+ link.Persister
Close(context.Context) error
Ping(context.Context) error
diff --git a/persistence/sql/migrations/20191100000005_identities.mysql.down.sql b/persistence/sql/migrations/20191100000005_identities.mysql.down.sql
index 27137c86aeb6..139e50a971e1 100644
--- a/persistence/sql/migrations/20191100000005_identities.mysql.down.sql
+++ b/persistence/sql/migrations/20191100000005_identities.mysql.down.sql
@@ -1 +1 @@
-/* ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255); */
\ No newline at end of file
+ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255);
diff --git a/persistence/sql/migrations/20191100000009_verification.mysql.down.sql b/persistence/sql/migrations/20191100000009_verification.mysql.down.sql
index bbf9ef9f36ed..f8a7e0f3c3a1 100644
--- a/persistence/sql/migrations/20191100000009_verification.mysql.down.sql
+++ b/persistence/sql/migrations/20191100000009_verification.mysql.down.sql
@@ -1 +1 @@
-/* ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255); */
\ No newline at end of file
+ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255);
diff --git a/persistence/sql/migrations/20200519101057_create_recovery_addresses.down.fizz b/persistence/sql/migrations/20200519101057_create_recovery_addresses.down.fizz
index be0249931463..105186b2a1be 100644
--- a/persistence/sql/migrations/20200519101057_create_recovery_addresses.down.fizz
+++ b/persistence/sql/migrations/20200519101057_create_recovery_addresses.down.fizz
@@ -1 +1,4 @@
drop_table("recovery_addresses")
+drop_table("identity_recovery_tokens")
+drop_table("selfservice_recovery_requests")
+drop_table("selfservice_recovery_requests_methods")
diff --git a/persistence/sql/migrations/20200519101057_create_recovery_addresses.up.fizz b/persistence/sql/migrations/20200519101057_create_recovery_addresses.up.fizz
index dbb10b9c625d..1adc2e74361e 100644
--- a/persistence/sql/migrations/20200519101057_create_recovery_addresses.up.fizz
+++ b/persistence/sql/migrations/20200519101057_create_recovery_addresses.up.fizz
@@ -1,21 +1,13 @@
create_table("identity_recovery_addresses") {
t.Column("id", "uuid", {primary: true})
- t.Column("code", "string", {"size": 32})
t.Column("via", "string", {"size": 16})
-
t.Column("value", "string", {"size": 400})
- t.Column("recovered_at", "timestamp", {"null": true})
- t.Column("expires_at", "timestamp", { default_raw: "CURRENT_TIMESTAMP" })
-
t.Column("identity_id", "uuid")
t.ForeignKey("identity_id", {"identities": ["id"]}, {"on_delete": "cascade"})
}
-add_index("identity_recovery_addresses", ["code"], { "unique": true, "name": "identity_recovery_addresses_code_uq_idx" })
-add_index("identity_recovery_addresses", ["code"], { "name": "identity_recovery_addresses_code_idx" })
-
add_index("identity_recovery_addresses", ["via", "value"], { "unique": true, "name": "identity_recovery_addresses_status_via_uq_idx" })
add_index("identity_recovery_addresses", ["via", "value"], { "name": "identity_recovery_addresses_status_via_idx" })
@@ -24,19 +16,37 @@ create_table("selfservice_recovery_requests") {
t.Column("request_url", "string", {"size": 2048})
t.Column("issued_at", "timestamp", { default_raw: "CURRENT_TIMESTAMP" })
t.Column("expires_at", "timestamp")
+ t.Column("messages", "json", {"null": true})
t.Column("active_method", "string", {"size": 32, "null": true})
t.Column("csrf_token", "string")
- t.Column("state", "string", {"size": 16})
+ t.Column("state", "string", {"size": 32})
- t.Column("identity_recovery_address_id", "uuid")
- t.ForeignKey("identity_recovery_address_id", {"identity_recovery_addresses": ["id"]}, {"on_delete": "cascade"})
+ t.Column("recovered_identity_id", "uuid", { "null": true })
+ t.ForeignKey("recovered_identity_id", {"identities": ["id"]}, {"on_delete": "cascade"})
}
-create_table("selfservice_recovery_requests_methods") {
+create_table("selfservice_recovery_request_methods") {
t.Column("id", "uuid", {primary: true})
t.Column("method", "string", {"size": 32})
- t.Column("selfservice_recovery_request_id", "uuid")
t.Column("config", "json")
+ t.Column("selfservice_recovery_request_id", "uuid")
t.ForeignKey("selfservice_recovery_request_id", {"selfservice_recovery_requests": ["id"]}, {"on_delete": "cascade"})
}
+
+create_table("identity_recovery_tokens") {
+ t.Column("id", "uuid", {primary: true})
+
+ t.Column("token", "string", {"size": 64})
+ t.Column("used", "bool", {"default": false})
+ t.Column("used_at", "timestamp", {"null": true})
+
+ t.Column("identity_recovery_address_id", "uuid")
+ t.ForeignKey("identity_recovery_address_id", {"identity_recovery_addresses": ["id"]}, {"on_delete": "cascade"})
+
+ t.Column("identity_recovery_request_id", "uuid")
+ t.ForeignKey("identity_recovery_request_id", {"selfservice_recovery_requests": ["id"]}, {"on_delete": "cascade"})
+}
+
+add_index("identity_recovery_tokens", ["token"], { "unique": true, "name": "identity_recovery_addresses_code_uq_idx" })
+add_index("identity_recovery_tokens", ["token"], { "name": "identity_recovery_addresses_code_idx" })
diff --git a/persistence/sql/migrations/20200519101058_create_recovery_addresses.mysql.down.sql b/persistence/sql/migrations/20200519101058_create_recovery_addresses.mysql.down.sql
index a581fc46eb41..54c99e1acb35 100644
--- a/persistence/sql/migrations/20200519101058_create_recovery_addresses.mysql.down.sql
+++ b/persistence/sql/migrations/20200519101058_create_recovery_addresses.mysql.down.sql
@@ -1 +1 @@
-/* ALTER TABLE identity_recovery_addresses MODIFY COLUMN code VARCHAR(32); */
+ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64);
diff --git a/persistence/sql/migrations/20200519101058_create_recovery_addresses.mysql.up.sql b/persistence/sql/migrations/20200519101058_create_recovery_addresses.mysql.up.sql
index 0bc5a4b03ca2..7972b3405fb5 100644
--- a/persistence/sql/migrations/20200519101058_create_recovery_addresses.mysql.up.sql
+++ b/persistence/sql/migrations/20200519101058_create_recovery_addresses.mysql.up.sql
@@ -1 +1 @@
-ALTER TABLE identity_recovery_addresses MODIFY COLUMN code VARCHAR(32) BINARY;
+ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64) BINARY;
diff --git a/persistence/sql/migrations/20200601101000_create_messages.down.fizz b/persistence/sql/migrations/20200601101000_create_messages.down.fizz
new file mode 100644
index 000000000000..73b215238b0f
--- /dev/null
+++ b/persistence/sql/migrations/20200601101000_create_messages.down.fizz
@@ -0,0 +1 @@
+drop_column("selfservice_settings_requests", "messages", "json", {"null": true})
diff --git a/persistence/sql/migrations/20200601101000_create_messages.up.fizz b/persistence/sql/migrations/20200601101000_create_messages.up.fizz
new file mode 100644
index 000000000000..a4e0d5f3c1dd
--- /dev/null
+++ b/persistence/sql/migrations/20200601101000_create_messages.up.fizz
@@ -0,0 +1 @@
+add_column("selfservice_settings_requests", "messages", "json", {"null": true})
diff --git a/persistence/sql/migrations/20200601101001_verification.mysql.down.sql b/persistence/sql/migrations/20200601101001_verification.mysql.down.sql
new file mode 100644
index 000000000000..d16bc788e883
--- /dev/null
+++ b/persistence/sql/migrations/20200601101001_verification.mysql.down.sql
@@ -0,0 +1 @@
+ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY;
diff --git a/persistence/sql/migrations/20200601101001_verification.mysql.up.sql b/persistence/sql/migrations/20200601101001_verification.mysql.up.sql
new file mode 100644
index 000000000000..3bf20defb8c5
--- /dev/null
+++ b/persistence/sql/migrations/20200601101001_verification.mysql.up.sql
@@ -0,0 +1 @@
+ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(32) BINARY;
diff --git a/persistence/sql/persister_hmac.go b/persistence/sql/persister_hmac.go
new file mode 100644
index 000000000000..8d8e81404e62
--- /dev/null
+++ b/persistence/sql/persister_hmac.go
@@ -0,0 +1,27 @@
+package sql
+
+import (
+ "crypto/hmac"
+ "crypto/sha512"
+ "crypto/subtle"
+ "fmt"
+)
+
+func (p *Persister) hmacValue(value string) string {
+ return p.hmacValueWithSecret(value, p.cf.SessionSecrets()[0])
+}
+
+func (p *Persister) hmacValueWithSecret(value string, secret []byte) string {
+ h := hmac.New(sha512.New512_256, secret)
+ _, _ = h.Write([]byte(value))
+ return fmt.Sprintf("%x", h.Sum(nil))
+}
+
+func (p *Persister) hmacConstantCompare(value, hash string) bool {
+ for _, secret := range p.cf.SessionSecrets() {
+ if subtle.ConstantTimeCompare([]byte(p.hmacValueWithSecret(value, secret)), []byte(hash)) == 1 {
+ return true
+ }
+ }
+ return false
+}
diff --git a/persistence/sql/persister_hmac_test.go b/persistence/sql/persister_hmac_test.go
new file mode 100644
index 000000000000..8d40cdb93cc1
--- /dev/null
+++ b/persistence/sql/persister_hmac_test.go
@@ -0,0 +1,33 @@
+package sql
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/ory/viper"
+ "github.com/ory/x/logrusx"
+
+ "github.com/ory/kratos/driver/configuration"
+)
+
+func TestPersisterHMAC(t *testing.T) {
+ viper.Set(configuration.ViperKeySecretsSession, []string{"foobarbaz"})
+ p, err := NewPersister(nil, configuration.NewViperProvider(logrusx.New("", ""), false), nil)
+ require.NoError(t, err)
+
+ assert.True(t, p.hmacConstantCompare("hashme", p.hmacValue("hashme")))
+ assert.False(t, p.hmacConstantCompare("notme", p.hmacValue("hashme")))
+ assert.False(t, p.hmacConstantCompare("hashme", p.hmacValue("notme")))
+
+ hash := p.hmacValue("hashme")
+ viper.Set(configuration.ViperKeySecretsSession, []string{"notfoobarbaz"})
+ assert.False(t, p.hmacConstantCompare("hashme", hash))
+ assert.True(t, p.hmacConstantCompare("hashme", p.hmacValue("hashme")))
+
+ viper.Set(configuration.ViperKeySecretsSession, []string{"notfoobarbaz", "foobarbaz"})
+ assert.True(t, p.hmacConstantCompare("hashme", hash))
+ assert.True(t, p.hmacConstantCompare("hashme", p.hmacValue("hashme")))
+ assert.NotEqual(t, hash, p.hmacValue("hashme"))
+}
diff --git a/persistence/sql/persister_identity.go b/persistence/sql/persister_identity.go
index f629cca78c1d..456f59777f36 100644
--- a/persistence/sql/persister_identity.go
+++ b/persistence/sql/persister_identity.go
@@ -184,7 +184,7 @@ func (p *Persister) ListIdentities(ctx context.Context, limit, offset int) ([]id
/* #nosec G201 TableName is static */
if err := sqlcon.HandleError(p.GetConnection(ctx).
RawQuery(fmt.Sprintf("SELECT * FROM %s LIMIT ? OFFSET ?", new(identity.Identity).TableName()), limit, offset).
- Eager("VerifiableAddresses","RecoveryAddresses").All(&is)); err != nil {
+ Eager("VerifiableAddresses", "RecoveryAddresses").All(&is)); err != nil {
return nil, err
}
@@ -210,22 +210,18 @@ func (p *Persister) UpdateIdentity(ctx context.Context, i *identity.Identity) er
return sql.ErrNoRows
}
- /* #nosec G201 TableName is static */
- if err := tx.RawQuery(fmt.Sprintf(`DELETE FROM %s WHERE identity_id = ?`, new(identity.Credentials).TableName()), i.ID).Exec(); err != nil {
- return err
- }
-
- /* #nosec G201 TableName is static */
- if err := tx.RawQuery(fmt.Sprintf(`DELETE FROM %s WHERE identity_id = ?`, new(identity.VerifiableAddress).TableName()), i.ID).Exec(); err != nil {
- return err
+ for _, tn := range []string{
+ new(identity.Credentials).TableName(),
+ new(identity.VerifiableAddress).TableName(),
+ new(identity.RecoveryAddress).TableName(),
+ } {
+ /* #nosec G201 TableName is static */
+ if err := tx.RawQuery(fmt.Sprintf(
+ `DELETE FROM %s WHERE identity_id = ?`, tn), i.ID).Exec(); err != nil {
+ return err
+ }
}
- // This is not required because it's cascading "ON DELETE":
- //
- // if err := tx.RawQuery(fmt.Sprintf(`DELETE FROM %s WHERE ...`, new(identity.RecoveryAddress).TableName()), i.ID).Exec(); err != nil {
- // return err
- // }
-
if err := tx.Update(i); err != nil {
return err
}
@@ -256,7 +252,7 @@ func (p *Persister) DeleteIdentity(ctx context.Context, id uuid.UUID) error {
func (p *Persister) GetIdentity(ctx context.Context, id uuid.UUID) (*identity.Identity, error) {
var i identity.Identity
- if err := p.GetConnection(ctx).Eager("VerifiableAddresses","RecoveryAddresses").Find(&i, id); err != nil {
+ if err := p.GetConnection(ctx).Eager("VerifiableAddresses", "RecoveryAddresses").Find(&i, id); err != nil {
return nil, sqlcon.HandleError(err)
}
i.Credentials = nil
@@ -305,17 +301,17 @@ func (p *Persister) GetIdentityConfidential(ctx context.Context, id uuid.UUID) (
return &i, nil
}
-func (p *Persister) FindAddressByCode(ctx context.Context, code string) (*identity.VerifiableAddress, error) {
+func (p *Persister) FindVerifiableAddressByValue(ctx context.Context, via identity.VerifiableAddressType, value string) (*identity.VerifiableAddress, error) {
var address identity.VerifiableAddress
- if err := p.GetConnection(ctx).Where("code = ?", code).First(&address); err != nil {
+ if err := p.GetConnection(ctx).Where("via = ? AND value = ?", via, value).First(&address); err != nil {
return nil, sqlcon.HandleError(err)
}
return &address, nil
}
-func (p *Persister) FindAddressByValue(ctx context.Context, via identity.VerifiableAddressType, value string) (*identity.VerifiableAddress, error) {
- var address identity.VerifiableAddress
+func (p *Persister) FindRecoveryAddressByValue(ctx context.Context, via identity.RecoveryAddressType, value string) (*identity.RecoveryAddress, error) {
+ var address identity.RecoveryAddress
if err := p.GetConnection(ctx).Where("via = ? AND value = ?", via, value).First(&address); err != nil {
return nil, sqlcon.HandleError(err)
}
diff --git a/persistence/sql/persister_recovery.go b/persistence/sql/persister_recovery.go
index 875e0da8acf0..21b3b3e0fc52 100644
--- a/persistence/sql/persister_recovery.go
+++ b/persistence/sql/persister_recovery.go
@@ -2,30 +2,109 @@ package sql
import (
"context"
+ "errors"
+ "fmt"
+ "time"
+ "github.com/gobuffalo/pop/v5"
"github.com/gofrs/uuid"
"github.com/ory/x/sqlcon"
"github.com/ory/kratos/selfservice/flow/recovery"
+ "github.com/ory/kratos/selfservice/strategy/link"
)
var _ recovery.RequestPersister = new(Persister)
+var _ link.Persister = new(Persister)
func (p Persister) CreateRecoveryRequest(ctx context.Context, r *recovery.Request) error {
- // This should not create the request eagerly because otherwise we might accidentally create an address
- // that isn't supposed to be in the database.
- return p.GetConnection(ctx).Create(r)
+ return p.GetConnection(ctx).Eager("MethodsRaw").Create(r)
}
func (p Persister) GetRecoveryRequest(ctx context.Context, id uuid.UUID) (*recovery.Request, error) {
var r recovery.Request
- if err := p.GetConnection(ctx).Find(&r, id); err != nil {
+ if err := p.GetConnection(ctx).Eager().Find(&r, id); err != nil {
return nil, sqlcon.HandleError(err)
}
+
+ if err := (&r).AfterFind(p.GetConnection(ctx)); err != nil {
+ return nil, err
+ }
+
return &r, nil
}
func (p Persister) UpdateRecoveryRequest(ctx context.Context, r *recovery.Request) error {
- return sqlcon.HandleError(p.GetConnection(ctx).Update(r))
+ return p.Transaction(ctx, func(tx *pop.Connection) error {
+ ctx := WithTransaction(ctx, tx)
+ rr, err := p.GetRecoveryRequest(ctx, r.ID)
+ if err != nil {
+ return err
+ }
+
+ for id, form := range r.Methods {
+ var found bool
+ for oid := range rr.Methods {
+ if oid == id {
+ rr.Methods[id].Config = form.Config
+ found = true
+ break
+ }
+ }
+ if !found {
+ rr.Methods[id] = form
+ }
+ }
+
+ for _, of := range rr.Methods {
+ if err := tx.Save(of); err != nil {
+ return sqlcon.HandleError(err)
+ }
+ }
+
+ return tx.Save(r)
+ })
+}
+
+func (p *Persister) CreateRecoveryToken(ctx context.Context, token *link.Token) error {
+ t := token.Token
+ token.Token = p.hmacValue(t)
+
+ // This should not create the request eagerly because otherwise we might accidentally create an address that isn't
+ // supposed to be in the database.
+ if err := p.GetConnection(ctx).Create(token); err != nil {
+ return err
+ }
+ token.Token = t
+ return nil
+}
+
+func (p *Persister) UseRecoveryToken(ctx context.Context, token string) (*link.Token, error) {
+ rt := new(link.Token)
+ if err := sqlcon.HandleError(p.Transaction(ctx, func(tx *pop.Connection) (err error) {
+ _ = WithTransaction(ctx, tx)
+
+ for _, secret := range p.cf.SessionSecrets() {
+ if err = tx.Eager().Where("token = ? AND NOT used", p.hmacValueWithSecret(token, secret)).First(rt); err != nil {
+ if !errors.Is(err, sqlcon.ErrNoRows) {
+ return err
+ }
+ }
+ }
+ if err != nil {
+ return err
+ }
+
+ /* #nosec G201 TableName is static */
+ return tx.RawQuery(fmt.Sprintf("UPDATE %s SET used=true, used_at=?", rt.TableName()), time.Now().UTC()).Exec()
+ })); err != nil {
+ return nil, err
+ }
+ return rt, nil
+}
+
+func (p *Persister) DeleteRecoveryToken(ctx context.Context, token string) error {
+ /* #nosec G201 TableName is static */
+ return p.GetConnection(ctx).RawQuery(fmt.Sprintf("DELETE FROM %s WHERE token=?", new(link.Token).TableName()), token).Exec()
}
diff --git a/persistence/sql/persister_profile.go b/persistence/sql/persister_settings.go
similarity index 100%
rename from persistence/sql/persister_profile.go
rename to persistence/sql/persister_settings.go
diff --git a/persistence/sql/persister_test.go b/persistence/sql/persister_test.go
index 73b180ac62e4..46ee35c0f102 100644
--- a/persistence/sql/persister_test.go
+++ b/persistence/sql/persister_test.go
@@ -17,6 +17,8 @@ import (
"github.com/ory/kratos/continuity"
"github.com/ory/kratos/persistence/sql"
"github.com/ory/kratos/selfservice/errorx"
+ "github.com/ory/kratos/selfservice/flow/recovery"
+ "github.com/ory/kratos/selfservice/strategy/link"
"github.com/ory/kratos/x"
"github.com/gobuffalo/pop/v5"
@@ -151,10 +153,18 @@ func TestPersister(t *testing.T) {
pop.SetLogger(pl(t))
courier.TestPersister(p)(t)
})
- t.Run("contract=verify.TestPersister", func(t *testing.T) {
+ t.Run("contract=verification.TestPersister", func(t *testing.T) {
pop.SetLogger(pl(t))
verify.TestPersister(p)(t)
})
+ t.Run("contract=recovery.TestRequestPersister", func(t *testing.T) {
+ pop.SetLogger(pl(t))
+ recovery.TestRequestPersister(p)(t)
+ })
+ t.Run("contract=recovery.TestPersister", func(t *testing.T) {
+ pop.SetLogger(pl(t))
+ link.TestPersister(p)(t)
+ })
t.Run("contract=continuity.TestPersister", func(t *testing.T) {
pop.SetLogger(pl(t))
continuity.TestPersister(p)(t)
diff --git a/schema/contrib/extension/identity.schema.json b/schema/contrib/extension/identity.schema.json
index 7ff57442cb13..43fc61dcb9cc 100644
--- a/schema/contrib/extension/identity.schema.json
+++ b/schema/contrib/extension/identity.schema.json
@@ -28,6 +28,16 @@
"enum": ["email"]
}
}
+ },
+ "recovery": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "via": {
+ "type": "string",
+ "enum": ["email"]
+ }
+ }
}
}
}
diff --git a/schema/contrib/extension/oidc.schema.json b/schema/contrib/extension/oidc.schema.json
deleted file mode 100644
index 9170bb5a8bb0..000000000000
--- a/schema/contrib/extension/oidc.schema.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "properties": {
- "ory.sh/kratos": {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "traits": {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "mappings": {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "identity": {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "traits": {
- "type": "object",
- "additionalProperties": false,
- "properties": {
- "path": {
- "type": "string"
- }
- }
- }
- }
- }
- }
- }
- }
- }
- }
- }
- }
-}
diff --git a/schema/errors.go b/schema/errors.go
index d569b66eff1f..247043f7912c 100644
--- a/schema/errors.go
+++ b/schema/errors.go
@@ -8,6 +8,13 @@ import (
"github.com/ory/jsonschema/v3"
)
+func NewMinLengthError(instancePtr string, expected, actual int) error {
+ return errors.WithStack(&jsonschema.ValidationError{
+ Message: fmt.Sprintf("length must be >= %d, but got %d", expected, actual),
+ InstancePtr: instancePtr,
+ })
+}
+
func NewRequiredError(instancePtr, missing string) error {
return errors.WithStack(&jsonschema.ValidationError{
Message: fmt.Sprintf("missing properties: %s", missing),
diff --git a/schema/extension.go b/schema/extension.go
index ef1c0266f887..cff8500f2709 100644
--- a/schema/extension.go
+++ b/schema/extension.go
@@ -14,8 +14,7 @@ var box = packr.New("contrib", "contrib")
const (
ExtensionRunnerIdentityMetaSchema ExtensionRunnerMetaSchema = "extension/identity.schema.json"
- ExtensionRunnerOIDCMetaSchema ExtensionRunnerMetaSchema = "extension/oidc.schema.json"
- extensionName = "ory.sh/kratos"
+ extensionName string = "ory.sh/kratos"
)
type (
@@ -29,6 +28,9 @@ type (
Verification struct {
Via string `json:"via"`
} `json:"verification"`
+ Recovery struct {
+ Via string `json:"via"`
+ } `json:"recovery"`
Mappings struct {
Identity struct {
Traits []struct {
diff --git a/schema/handler.go b/schema/handler.go
index c916e2f047b7..b681bb3b7f27 100644
--- a/schema/handler.go
+++ b/schema/handler.go
@@ -6,20 +6,19 @@ import (
"net/http"
"os"
- "github.com/pkg/errors"
- "github.com/sirupsen/logrus"
-
"github.com/julienschmidt/httprouter"
+ "github.com/pkg/errors"
"github.com/ory/herodot"
+
"github.com/ory/kratos/x"
)
type (
handlerDependencies interface {
x.WriterProvider
+ x.LoggingProvider
IdentityTraitsSchemas() Schemas
- Logger() logrus.FieldLogger
}
Handler struct {
r handlerDependencies
diff --git a/selfservice/errorx/error.go b/selfservice/errorx/error.go
index 18d7995adeb5..8d93409817b4 100644
--- a/selfservice/errorx/error.go
+++ b/selfservice/errorx/error.go
@@ -10,7 +10,7 @@ import (
// swagger:model errorContainer
type ErrorContainer struct {
- ID uuid.UUID `db:"id" rw:"r" json:"id"`
+ ID uuid.UUID `db:"id" json:"id"`
CSRFToken string `db:"csrf_token" json:"-"`
diff --git a/selfservice/errorx/manager.go b/selfservice/errorx/manager.go
index defa49a826cb..cafc8d4d0a78 100644
--- a/selfservice/errorx/manager.go
+++ b/selfservice/errorx/manager.go
@@ -5,7 +5,6 @@ import (
"net/http"
"net/url"
- "github.com/ory/herodot"
"github.com/ory/x/urlx"
"github.com/ory/kratos/x"
@@ -42,7 +41,7 @@ func NewManager(d managerDependencies, c baseManagerConfiguration) *Manager {
// error url, appending the error ID.
func (m *Manager) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, errs ...error) (string, error) {
for _, err := range errs {
- herodot.DefaultErrorLogger(m.d.Logger(), err).Errorf("An error occurred and is being forwarded to the error user interface.")
+ m.d.Logger().WithError(err).WithRequest(r).Errorf("An error occurred and is being forwarded to the error user interface.")
}
id, emerr := m.d.SelfServiceErrorPersister().Add(ctx, m.d.GenerateCSRFToken(r), errs...)
diff --git a/selfservice/flow/login/error.go b/selfservice/flow/login/error.go
index ec48083c25f9..a054f97329ec 100644
--- a/selfservice/flow/login/error.go
+++ b/selfservice/flow/login/error.go
@@ -1,7 +1,6 @@
package login
import (
- "fmt"
"net/http"
"net/url"
"time"
@@ -70,11 +69,11 @@ func (s *ErrorHandler) HandleLoginError(
rr *Request,
err error,
) {
- s.d.Logger().WithError(err).
- WithField("details", fmt.Sprintf("%+v", err)).
- WithField("credentials_type", ct).
+ s.d.Audit().
+ WithError(err).
+ WithRequest(r).
WithField("login_request", rr).
- Warn("Encountered login error.")
+ Info("Encountered self-service login error.")
if _, ok := errorsx.Cause(err).(requestExpiredError); ok {
// create new request because the old one is not valid
diff --git a/selfservice/flow/login/handler_test.go b/selfservice/flow/login/handler_test.go
index c22b70e9be7e..02127f7f626a 100644
--- a/selfservice/flow/login/handler_test.go
+++ b/selfservice/flow/login/handler_test.go
@@ -115,7 +115,7 @@ func TestHandlerSettingForced(t *testing.T) {
func TestLoginHandler(t *testing.T) {
_, reg := internal.NewFastRegistryWithMocks(t)
- public, admin := testhelpers.NewKratosServer(t, reg)
+ public, admin := testhelpers.NewKratosServerWithCSRF(t, reg)
_ = testhelpers.NewErrorTestServer(t, reg)
_ = testhelpers.NewRedirTS(t, "")
diff --git a/selfservice/flow/login/persistence.go b/selfservice/flow/login/persistence.go
index b4ece380e661..544ea711f264 100644
--- a/selfservice/flow/login/persistence.go
+++ b/selfservice/flow/login/persistence.go
@@ -4,7 +4,7 @@ import (
"context"
"testing"
- "github.com/bxcodec/faker"
+ "github.com/bxcodec/faker/v3"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/selfservice/flow/login/request.go b/selfservice/flow/login/request.go
index 00e99401da79..ede758778046 100644
--- a/selfservice/flow/login/request.go
+++ b/selfservice/flow/login/request.go
@@ -20,7 +20,7 @@ type Request struct {
// represents the id in the login ui's query parameter: http:///?request=
//
// required: true
- ID uuid.UUID `json:"id" faker:"uuid" rw:"r" db:"id"`
+ ID uuid.UUID `json:"id" faker:"-" db:"id"`
// ExpiresAt is the time (UTC) when the request expires. If the user still wishes to log in,
// a new request has to be initiated.
diff --git a/selfservice/flow/login/request_method.go b/selfservice/flow/login/request_method.go
index c400fc5cb8a0..6334daf1d148 100644
--- a/selfservice/flow/login/request_method.go
+++ b/selfservice/flow/login/request_method.go
@@ -26,7 +26,7 @@ type RequestMethod struct {
Config *RequestMethodConfig `json:"config" db:"config"`
// ID is a helper struct field for gobuffalo.pop.
- ID uuid.UUID `json:"-" db:"id" rw:"r"`
+ ID uuid.UUID `json:"-" db:"id"`
// RequestID is a helper struct field for gobuffalo.pop.
RequestID uuid.UUID `json:"-" db:"selfservice_login_request_id"`
diff --git a/selfservice/flow/login/request_test.go b/selfservice/flow/login/request_test.go
index b36c76218ff5..ea4f92057b04 100644
--- a/selfservice/flow/login/request_test.go
+++ b/selfservice/flow/login/request_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"time"
- "github.com/bxcodec/faker"
+ "github.com/bxcodec/faker/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/selfservice/flow/logout/handler_test.go b/selfservice/flow/logout/handler_test.go
index 3ed7c30ab56a..2d746f8276c4 100644
--- a/selfservice/flow/logout/handler_test.go
+++ b/selfservice/flow/logout/handler_test.go
@@ -9,10 +9,11 @@ import (
"github.com/gobuffalo/httptest"
"github.com/julienschmidt/httprouter"
"github.com/justinas/nosurf"
- "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/ory/x/logrusx"
+
"github.com/ory/viper"
"github.com/ory/kratos/driver/configuration"
@@ -33,7 +34,7 @@ func TestLogoutHandler(t *testing.T) {
router := x.NewRouterPublic()
handler.RegisterPublicRoutes(router)
- reg.WithCSRFHandler(x.NewCSRFHandler(router, reg.Writer(), logrus.New(), "/", "", false))
+ reg.WithCSRFHandler(x.NewCSRFHandler(router, reg.Writer(), logrusx.New("", ""), "/", "", false))
ts := httptest.NewServer(reg.CSRFHandler())
defer ts.Close()
@@ -57,7 +58,7 @@ func TestLogoutHandler(t *testing.T) {
viper.Set(configuration.ViperKeySelfServiceLogoutRedirectURL, redirTS.URL)
viper.Set(configuration.ViperKeyURLsSelfPublic, ts.URL)
- client := testhelpers.MockCookieClient(t)
+ client := testhelpers.NewClientWithCookies(t)
t.Run("case=set initial session", func(t *testing.T) {
testhelpers.MockHydrateCookieClient(t, client, ts.URL+"/set")
diff --git a/selfservice/flow/recovery/error.go b/selfservice/flow/recovery/error.go
index 52ca123fe057..721f3191e3f4 100644
--- a/selfservice/flow/recovery/error.go
+++ b/selfservice/flow/recovery/error.go
@@ -1,7 +1,6 @@
package recovery
import (
- "fmt"
"net/http"
"net/url"
@@ -14,7 +13,6 @@ import (
"github.com/ory/kratos/driver/configuration"
"github.com/ory/kratos/selfservice/errorx"
- "github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/kratos/x"
)
@@ -50,26 +48,6 @@ func NewErrorHandler(d errorHandlerDependencies, c configuration.Provider) *Erro
}
}
-func (s *ErrorHandler) reauthenticate(
- w http.ResponseWriter,
- r *http.Request,
- rr *Request) {
- if err := s.d.RecoveryRequestPersister().UpdateRecoveryRequest(r.Context(), rr); err != nil {
- s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
- return
- }
-
- returnTo := urlx.CopyWithQuery(urlx.AppendPaths(s.c.SelfPublicURL(), r.URL.Path), r.URL.Query())
- s.c.SelfPublicURL()
- u := urlx.AppendPaths(
- urlx.CopyWithQuery(s.c.SelfPublicURL(), url.Values{
- "prompt": {"login"},
- "return_to": {returnTo.String()},
- }), login.BrowserLoginPath)
-
- http.Redirect(w, r, u.String(), http.StatusFound)
-}
-
func (s *ErrorHandler) HandleRecoveryError(
w http.ResponseWriter,
r *http.Request,
@@ -77,10 +55,11 @@ func (s *ErrorHandler) HandleRecoveryError(
err error,
method string,
) {
- s.d.Logger().WithError(err).
- WithField("details", fmt.Sprintf("%+v", err)).
+ s.d.Audit().
+ WithError(err).
+ WithRequest(r).
WithField("recovery_request", rr).
- Warn("Encountered recovery error.")
+ Info("Encountered self-service recovery error.")
if rr == nil {
s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
@@ -90,18 +69,12 @@ func (s *ErrorHandler) HandleRecoveryError(
return
}
- if errors.Is(err, ErrRequestNeedsReAuthentication) {
- s.reauthenticate(w, r, rr)
- return
- }
-
if _, ok := rr.Methods[method]; !ok {
- s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Expected recovery method %s to exist.", method)))
+ s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(x.PseudoPanic.WithReasonf("Expected recovery method %s to exist.", method)))
return
}
rr.Active = sqlxx.NullString(method)
-
if err := rr.Methods[method].Config.ParseError(err); err != nil {
s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
return
diff --git a/selfservice/flow/recovery/handler.go b/selfservice/flow/recovery/handler.go
index 332125a146b3..f35430ac99ca 100644
--- a/selfservice/flow/recovery/handler.go
+++ b/selfservice/flow/recovery/handler.go
@@ -21,7 +21,6 @@ import (
const (
PublicRecoveryInitPath = "/self-service/browser/flows/recovery"
PublicRecoveryRequestPath = "/self-service/browser/flows/requests/recovery"
- PublicRecoveryConfirmPath = "/self-service/browser/flows/recovery/:via/recover/:code"
)
type (
@@ -35,7 +34,6 @@ type (
session.HandlerProvider
StrategyProvider
RequestPersistenceProvider
- SenderProvider
x.CSRFTokenGeneratorProvider
x.WriterProvider
}
@@ -78,21 +76,19 @@ func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) {
// 302: emptyResponse
// 500: genericError
func (h *Handler) init(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
- a := NewRequest(h.c.SelfServiceRecoveryRequestLifespan(), h.d.GenerateCSRFToken(r), r)
- for _, strategy := range h.d.RecoveryStrategies() {
- if err := strategy.PopulateRecoveryMethod(r, a); err != nil {
- h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
- return
- }
+ req, err := NewRequest(h.c.SelfServiceRecoveryRequestLifespan(), h.d.GenerateCSRFToken(r), r, h.d.RecoveryStrategies())
+ if err != nil {
+ h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
+ return
}
- if err := h.d.RecoveryRequestPersister().CreateRecoveryRequest(r.Context(), a); err != nil {
+ if err := h.d.RecoveryRequestPersister().CreateRecoveryRequest(r.Context(), req); err != nil {
h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
return
}
http.Redirect(w, r,
- urlx.CopyWithQuery(h.c.RecoveryURL(), url.Values{"request": {a.ID.String()}}).String(),
+ urlx.CopyWithQuery(h.c.RecoveryURL(), url.Values{"request": {req.ID.String()}}).String(),
http.StatusFound,
)
}
diff --git a/selfservice/flow/recovery/handler_test.go b/selfservice/flow/recovery/handler_test.go
index 186ded8fcf7a..16248a8836f1 100644
--- a/selfservice/flow/recovery/handler_test.go
+++ b/selfservice/flow/recovery/handler_test.go
@@ -29,7 +29,7 @@ func init() {
func TestHandlerRedirectOnAuthenticated(t *testing.T) {
conf, reg := internal.NewFastRegistryWithMocks(t)
- testhelpers.NewRecoveryTestServer(t)
+ testhelpers.NewRecoveryUITestServer(t)
redirTS := testhelpers.NewRedirTS(t, "already authenticated")
viper.Set(configuration.ViperKeyURLsLogin, redirTS.URL)
@@ -48,11 +48,11 @@ func TestHandlerRedirectOnAuthenticated(t *testing.T) {
func TestRecoveryHandler(t *testing.T) {
_, reg := internal.NewFastRegistryWithMocks(t)
- testhelpers.NewRedirTS(t,"")
- testhelpers.NewLoginUIRequestEchoServer(t,reg)
+ testhelpers.NewRedirTS(t, "")
+ testhelpers.NewLoginUIRequestEchoServer(t, reg)
testhelpers.NewErrorTestServer(t, reg)
- public, admin := testhelpers.NewKratosServerWithCSRF(t,reg)
+ public, admin := testhelpers.NewKratosServerWithCSRF(t, reg)
newRecoveryTS := func(t *testing.T, upstream string, c *http.Client) *httptest.Server {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if c == nil {
diff --git a/selfservice/flow/recovery/persistence.go b/selfservice/flow/recovery/persistence.go
index 871b660d959e..291ca102e8e1 100644
--- a/selfservice/flow/recovery/persistence.go
+++ b/selfservice/flow/recovery/persistence.go
@@ -5,7 +5,7 @@ import (
"encoding/json"
"testing"
- "github.com/bxcodec/faker"
+ "github.com/bxcodec/faker/v3"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -15,6 +15,7 @@ import (
"github.com/ory/kratos/driver/configuration"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/selfservice/form"
+ "github.com/ory/kratos/selfservice/text"
"github.com/ory/kratos/x"
)
@@ -72,10 +73,10 @@ func TestRequestPersister(p interface {
actual, err := p.GetRecoveryRequest(context.Background(), expected.ID)
require.NoError(t, err)
- factual, _ := json.Marshal(actual.Methods[StrategyEmail].Config)
- fexpected, _ := json.Marshal(expected.Methods[StrategyEmail].Config)
+ fexpected, _ := json.Marshal(expected.Methods[StrategyRecoveryTokenName].Config)
+ factual, _ := json.Marshal(actual.Methods[StrategyRecoveryTokenName].Config)
- require.NotEmpty(t, actual.Methods[StrategyEmail].Config.RequestMethodConfigurator.(*form.HTMLForm).Action)
+ require.NotEmpty(t, actual.Methods[StrategyRecoveryTokenName].Config.RequestMethodConfigurator.(*form.HTMLForm).Action)
assert.EqualValues(t, expected.ID, actual.ID)
assert.JSONEq(t, string(fexpected), string(factual))
x.AssertEqualTime(t, expected.IssuedAt, actual.IssuedAt)
@@ -83,18 +84,10 @@ func TestRequestPersister(p interface {
assert.EqualValues(t, expected.RequestURL, actual.RequestURL)
})
- t.Run("case=should fail to create if identity does not exist", func(t *testing.T) {
- var expected Request
- require.NoError(t, faker.FakeData(&expected))
- clearids(&expected)
- err := p.CreateRecoveryRequest(context.Background(), &expected)
- require.Error(t, err)
- })
-
t.Run("case=should create and update a recovery request", func(t *testing.T) {
expected := newRequest(t)
- expected.Methods["oidc"] = &RequestMethod{
- Method: "oidc", Config: &RequestMethodConfig{RequestMethodConfigurator: &form.HTMLForm{Fields: []form.Field{{
+ expected.Methods[StrategyRecoveryTokenName] = &RequestMethod{
+ Method: StrategyRecoveryTokenName, Config: &RequestMethodConfig{RequestMethodConfigurator: &form.HTMLForm{Fields: []form.Field{{
Name: "zab", Type: "bar", Pattern: "baz"}}}}}
expected.Methods["password"] = &RequestMethod{
Method: "password", Config: &RequestMethodConfig{RequestMethodConfigurator: &form.HTMLForm{Fields: []form.Field{{
@@ -102,21 +95,25 @@ func TestRequestPersister(p interface {
err := p.CreateRecoveryRequest(context.Background(), expected)
require.NoError(t, err)
- expected.Methods[StrategyEmail].Config.RequestMethodConfigurator.(*form.HTMLForm).Action = "/new-action"
+ expected.Methods[StrategyRecoveryTokenName].Config.RequestMethodConfigurator.(*form.HTMLForm).Action = "/new-action"
expected.Methods["password"].Config.RequestMethodConfigurator.(*form.HTMLForm).Fields = []form.Field{{
Name: "zab", Type: "zab", Pattern: "zab"}}
expected.RequestURL = "/new-request-url"
+ expected.Active = StrategyRecoveryTokenName
+ expected.Messages.Add(text.NewRecoveryEmailSent())
require.NoError(t, p.UpdateRecoveryRequest(context.Background(), expected))
actual, err := p.GetRecoveryRequest(context.Background(), expected.ID)
require.NoError(t, err)
- assert.Equal(t, "/new-action", actual.Methods[StrategyEmail].Config.RequestMethodConfigurator.(*form.HTMLForm).Action)
+ assert.Equal(t, "/new-action", actual.Methods[StrategyRecoveryTokenName].Config.RequestMethodConfigurator.(*form.HTMLForm).Action)
assert.Equal(t, "/new-request-url", actual.RequestURL)
+ assert.Equal(t, StrategyRecoveryTokenName, actual.Active.String())
+ assert.Equal(t, expected.Messages, actual.Messages)
assert.EqualValues(t, []form.Field{{Name: "zab", Type: "zab", Pattern: "zab"}}, actual.
Methods["password"].Config.RequestMethodConfigurator.(*form.HTMLForm).Fields)
assert.EqualValues(t, []form.Field{{Name: "zab", Type: "bar", Pattern: "baz"}}, actual.
- Methods["oidc"].Config.RequestMethodConfigurator.(*form.HTMLForm).Fields)
+ Methods[StrategyRecoveryTokenName].Config.RequestMethodConfigurator.(*form.HTMLForm).Fields)
})
}
}
diff --git a/selfservice/flow/recovery/request.go b/selfservice/flow/recovery/request.go
index 3ab1bc7cc154..d32c08182988 100644
--- a/selfservice/flow/recovery/request.go
+++ b/selfservice/flow/recovery/request.go
@@ -2,43 +2,22 @@ package recovery
import (
"net/http"
+ "net/url"
"time"
"github.com/gobuffalo/pop/v5"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
+ "github.com/ory/x/urlx"
+
"github.com/ory/x/sqlxx"
- "github.com/ory/kratos/identity"
- "github.com/ory/kratos/session"
+ "github.com/ory/kratos/selfservice/form"
+ "github.com/ory/kratos/selfservice/text"
"github.com/ory/kratos/x"
)
-type State string
-
-const (
- StateBlank = ""
- StatePending = "pending"
- StateSent = "sent"
- StateConfirmed = "confirmed"
- StateSuccess = "success"
-)
-
-func NextState(current State) State {
- switch current {
- case StateBlank:
- return StatePending
- case StatePending:
- return StateSent
- case StateSent:
- return StateConfirmed
- case StateConfirmed:
- return StateSuccess
- }
- return StateBlank
-}
-
// Request presents a recovery request
//
// This request is used when an identity wants to recover their account.
@@ -53,7 +32,7 @@ type Request struct {
// required: true
// type: string
// format: uuid
- ID uuid.UUID `json:"id" db:"id" faker:"uuid" rw:"r"`
+ ID uuid.UUID `json:"id" db:"id" faker:"-"`
// ExpiresAt is the time (UTC) when the request expires. If the user still wishes to update the setting,
// a new request has to be initiated.
@@ -74,7 +53,13 @@ type Request struct {
// Active, if set, contains the registration method that is being used. It is initially
// not set.
- Active sqlxx.NullString `json:"active,omitempty" db:"active_method"`
+ Active sqlxx.NullString `json:"active,omitempty" faker:"-" db:"active_method"`
+
+ // Messages contains a list of messages to be displayed in the Recovery UI. Omitting these
+ // messages makes it significantly harder for users to figure out what is going on.
+ //
+ // More documentation on messages can be found in the [User Interface Documentation](https://www.ory.sh/kratos/docs/concepts/ui-user-interface/).
+ Messages text.Messages `json:"messages" faker:"-" db:"messages"`
// Methods contains context for all account recovery methods. If a registration request has been
// processed, but for example the password is incorrect, this will contain error messages.
@@ -85,15 +70,7 @@ type Request struct {
// MethodsRaw is a helper struct field for gobuffalo.pop.
MethodsRaw RequestMethodsRaw `json:"-" faker:"-" has_many:"selfservice_recovery_request_methods" fk_id:"selfservice_recovery_request_id"`
- // RecoveryAddress links this request to a recovery address.
- RecoveryAddress *identity.RecoveryAddress `json:"-" belongs_to:"identity_recovery_addresses" fk_id:"RecoveryAddressID"`
-
- // State represents the state of this request. Can be one of:
- //
- // - pending
- // - sent
- // - confirmed
- // - success
+ // State represents the state of this request.
//
// required: true
State State `json:"state" faker:"-" db:"state"`
@@ -103,33 +80,47 @@ type Request struct {
// CreatedAt is a helper struct field for gobuffalo.pop.
CreatedAt time.Time `json:"-" faker:"-" db:"created_at"`
+
// UpdatedAt is a helper struct field for gobuffalo.pop.
UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"`
- // RecoveryAddressID is a helper struct field for gobuffalo.pop.
- RecoveryAddressID uuid.UUID `json:"-" faker:"-" db:"identity_recovery_address_id"`
+
+ // RecoveredIdentityID is a helper struct field for gobuffalo.pop.
+ RecoveredIdentityID uuid.NullUUID `json:"-" faker:"-" db:"recovered_identity_id"`
}
-func NewRequest(exp time.Duration,csrf string, r *http.Request) *Request {
- return &Request{
+func NewRequest(exp time.Duration, csrf string, r *http.Request, strategies Strategies) (*Request, error) {
+ req := &Request{
ID: x.NewUUID(),
ExpiresAt: time.Now().UTC().Add(exp),
IssuedAt: time.Now().UTC(),
RequestURL: x.RequestURL(r).String(),
Methods: map[string]*RequestMethod{},
- State: NextState(StateBlank),
+ State: NextState(StateChooseMethod),
CSRFToken: csrf,
}
+
+ for _, strategy := range strategies {
+ if err := strategy.PopulateRecoveryMethod(r, req); err != nil {
+ return nil, err
+ }
+ }
+
+ return req, nil
}
func (r *Request) TableName() string {
return "selfservice_recovery_requests"
}
+func (r *Request) URL(recoveryURL *url.URL) *url.URL {
+ return urlx.CopyWithQuery(recoveryURL, url.Values{"request": {r.ID.String()}})
+}
+
func (r *Request) GetID() uuid.UUID {
return r.ID
}
-func (r *Request) Valid(s *session.Session) error {
+func (r *Request) Valid() error {
if r.ExpiresAt.Before(time.Now().UTC()) {
return errors.WithStack(ErrRequestExpired.
WithReasonf("The recovery request expired %.2f minutes ago, please try again.",
@@ -160,3 +151,19 @@ func (r *Request) AfterFind(_ *pop.Connection) error {
r.MethodsRaw = nil
return nil
}
+
+func (r *Request) MethodToForm(id string) (form.Form, error) {
+ method, ok := r.Methods[id]
+ if !ok {
+ return nil, errors.WithStack(x.PseudoPanic.WithReasonf("Expected method %s to exist.", id))
+ }
+
+ config, ok := method.Config.RequestMethodConfigurator.(form.Form)
+ if !ok {
+ return nil, errors.WithStack(x.PseudoPanic.WithReasonf(
+ "Expected method config %s to be of type *form.HTMLForm but got: %T", id,
+ method.Config.RequestMethodConfigurator))
+ }
+
+ return config, nil
+}
diff --git a/selfservice/flow/recovery/request_method.go b/selfservice/flow/recovery/request_method.go
index 899817d46548..fae7dfc6b57a 100644
--- a/selfservice/flow/recovery/request_method.go
+++ b/selfservice/flow/recovery/request_method.go
@@ -21,7 +21,7 @@ type RequestMethod struct {
Config *RequestMethodConfig `json:"config" db:"config"`
// ID is a helper struct field for gobuffalo.pop.
- ID uuid.UUID `json:"-" db:"id" rw:"r"`
+ ID uuid.UUID `json:"-" db:"id"`
// RequestID is a helper struct field for gobuffalo.pop.
RequestID uuid.UUID `json:"-" db:"selfservice_recovery_request_id"`
diff --git a/selfservice/flow/recovery/request_test.go b/selfservice/flow/recovery/request_test.go
index c5c6527b81a5..2bce521379bc 100644
--- a/selfservice/flow/recovery/request_test.go
+++ b/selfservice/flow/recovery/request_test.go
@@ -11,26 +11,24 @@ import (
"github.com/ory/x/urlx"
"github.com/ory/kratos/selfservice/flow/recovery"
- "github.com/ory/kratos/session"
)
func TestRequest(t *testing.T) {
+ must := func(r *recovery.Request, err error) *recovery.Request {
+ require.NoError(t, err)
+ return r
+ }
+
u := &http.Request{URL: urlx.ParseOrPanic("http://foo/bar/baz"), Host: "foo"}
for k, tc := range []struct {
r *recovery.Request
- s *session.Session
expectErr bool
}{
- {
- r: recovery.NewRequest(time.Hour, "", u),
- },
- {
- r: recovery.NewRequest(-time.Hour, "", u),
- expectErr: true,
- },
+ {r: must(recovery.NewRequest(time.Hour, "", u, nil))},
+ {r: must(recovery.NewRequest(-time.Hour, "", u, nil)), expectErr: true},
} {
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
- err := tc.r.Valid(tc.s)
+ err := tc.r.Valid()
if tc.expectErr {
require.Error(t, err)
return
diff --git a/selfservice/flow/recovery/sender.go b/selfservice/flow/recovery/sender.go
deleted file mode 100644
index 40a1eabcf61c..000000000000
--- a/selfservice/flow/recovery/sender.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package recovery
-
-import (
- "context"
- "strings"
-
- "github.com/pkg/errors"
-
- "github.com/ory/go-convenience/urlx"
- "github.com/ory/x/errorsx"
- "github.com/ory/x/sqlcon"
-
- "github.com/ory/kratos/courier"
- templates "github.com/ory/kratos/courier/template"
- "github.com/ory/kratos/driver/configuration"
- "github.com/ory/kratos/identity"
- "github.com/ory/kratos/x"
-)
-
-var ErrUnknownAddress = errors.New("recovery requested for unknown address")
-
-type (
- senderDependencies interface {
- courier.Provider
- identity.PoolProvider
- identity.ManagementProvider
- x.LoggingProvider
- }
- SenderProvider interface {
- RecoverySender() *Sender
- }
- Sender struct {
- r senderDependencies
- c configuration.Provider
- }
-)
-
-func NewSender(r senderDependencies, c configuration.Provider) *Sender {
- return &Sender{r: r, c: c}
-}
-
-func (m *Sender) IssueAndSendRecoveryToken(ctx context.Context, address, via string) (*identity.VerifiableAddress, error) {
- m.r.Logger().WithField("via", via).Debug("Sending out verification code.")
-
- a, err := m.r.IdentityPool().FindAddressByValue(ctx, identity.VerifiableAddressTypeEmail, address)
- if err != nil {
- if errorsx.Cause(err) == sqlcon.ErrNoRows {
- if err := m.sendToUnknownAddress(ctx, identity.VerifiableAddressTypeEmail, address); err != nil {
- return nil, err
- }
- return nil, errors.Cause(ErrUnknownAddress)
- }
- return nil, err
- }
-
- if err := m.r.IdentityManager().RefreshVerifyAddress(ctx, a); err != nil {
- return nil, err
- }
-
- if err := m.sendCodeToKnownAddress(ctx, a); err != nil {
- return nil, err
- }
- return a, nil
-}
-
-func (m *Sender) sendToUnknownAddress(ctx context.Context, via identity.VerifiableAddressType, address string) error {
- m.r.Logger().WithField("via", via).Debug("Sending out unsuccessful recovery message because address is unknown.")
- return m.run(via, func() error {
- _, err := m.r.Courier().QueueEmail(ctx,
- templates.NewVerifyInvalid(m.c, &templates.VerifyInvalidModel{To: address}))
- return err
- })
-}
-
-func (m *Sender) sendCodeToKnownAddress(ctx context.Context, address *identity.VerifiableAddress) error {
- m.r.Logger().WithField("via", address.Via).Debug("Sending out recovery message.")
- return m.run(address.Via, func() error {
- _, err := m.r.Courier().QueueEmail(ctx, templates.NewVerifyValid(m.c,
- &templates.VerifyValidModel{
- To: address.Value,
- VerifyURL: urlx.AppendPaths(
- m.c.SelfPublicURL(),
- strings.ReplaceAll(
- strings.ReplaceAll(PublicRecoveryConfirmPath, ":via", string(address.Via)),
- ":code", address.Code)).
- String(),
- },
- ))
- return err
- })
-}
-
-func (m *Sender) run(via identity.VerifiableAddressType, emailFunc func() error) error {
- switch via {
- case identity.VerifiableAddressTypeEmail:
- return emailFunc()
- default:
- return errors.Errorf("received unexpected via type: %s", via)
- }
-}
diff --git a/selfservice/flow/recovery/sender_test.go b/selfservice/flow/recovery/sender_test.go
deleted file mode 100644
index 009129e95581..000000000000
--- a/selfservice/flow/recovery/sender_test.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package recovery_test
-
-import (
- "context"
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-
- "github.com/ory/viper"
-
- "github.com/ory/kratos/driver/configuration"
- "github.com/ory/kratos/identity"
- "github.com/ory/kratos/internal"
- "github.com/ory/kratos/selfservice/flow/verify"
-)
-
-func TestManager(t *testing.T) {
- _, reg := internal.NewFastRegistryWithMocks(t)
- viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/extension/schema.json")
- viper.Set(configuration.ViperKeyURLsSelfPublic, "https://www.ory.sh/")
- viper.Set(configuration.ViperKeyCourierSMTPURL, "smtp://foo@bar@dev.null/")
-
- t.Run("method=SendCode", func(t *testing.T) {
- i := identity.NewIdentity(configuration.DefaultIdentityTraitsSchemaID)
-
- address, err := identity.NewVerifiableEmailAddress("tracked@ory.sh", i.ID, time.Minute)
- require.NoError(t, err)
-
- i.VerifiableAddresses = []identity.VerifiableAddress{*address}
- i.Traits = identity.Traits("{}")
- require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))
-
- address, err = reg.VerificationSender().SendCode(context.Background(), address.Via, address.Value)
- require.NoError(t, err)
-
- _, err = reg.VerificationSender().SendCode(context.Background(), address.Via, "not-tracked@ory.sh")
- require.EqualError(t, err, verify.ErrUnknownAddress.Error())
-
- messages, err := reg.CourierPersister().NextMessages(context.Background(), 12)
- require.NoError(t, err)
- require.Len(t, messages, 2)
-
- assert.EqualValues(t, address.Value, messages[0].Recipient)
- assert.Contains(t, messages[0].Subject, "Please verify")
-
- assert.Contains(t, messages[0].Body, address.Code)
- fromStore, err := reg.Persister().GetIdentity(context.Background(), i.ID)
- require.NoError(t, err)
- require.Len(t, fromStore.RecoveryAddresses, 1)
- assert.Contains(t, messages[0].Body, fromStore.RecoveryAddresses[0].Code)
-
- assert.EqualValues(t, "not-tracked@ory.sh", messages[1].Recipient)
- assert.Contains(t, messages[1].Subject, "tried to verify")
- })
-}
diff --git a/selfservice/flow/recovery/state.go b/selfservice/flow/recovery/state.go
new file mode 100644
index 000000000000..a4203f510eef
--- /dev/null
+++ b/selfservice/flow/recovery/state.go
@@ -0,0 +1,32 @@
+package recovery
+
+type State string
+
+const (
+ StateChooseMethod State = "choose_method"
+ StateEmailSent State = "sent_email"
+ StatePassedChallenge State = "passed_challenge"
+)
+
+var states = []State{StateChooseMethod, StateEmailSent, StatePassedChallenge}
+
+func indexOf(current State) int {
+ for k, s := range states {
+ if s == current {
+ return k
+ }
+ }
+ return 0
+}
+
+func HasReachedState(expected, actual State) bool {
+ return indexOf(actual) >= indexOf(expected)
+}
+
+func NextState(current State) State {
+ if current == StatePassedChallenge {
+ return StatePassedChallenge
+ }
+
+ return states[indexOf(current)+1]
+}
diff --git a/selfservice/flow/recovery/state_test.go b/selfservice/flow/recovery/state_test.go
new file mode 100644
index 000000000000..4ec1f8174388
--- /dev/null
+++ b/selfservice/flow/recovery/state_test.go
@@ -0,0 +1,17 @@
+package recovery
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestState(t *testing.T) {
+ assert.EqualValues(t, StateEmailSent, NextState(StateChooseMethod))
+ assert.EqualValues(t, StatePassedChallenge, NextState(StateEmailSent))
+ assert.EqualValues(t, StatePassedChallenge, NextState(StatePassedChallenge))
+
+ assert.True(t, HasReachedState(StatePassedChallenge, StatePassedChallenge))
+ assert.False(t, HasReachedState(StatePassedChallenge, StateEmailSent))
+ assert.False(t, HasReachedState(StateEmailSent, StateChooseMethod))
+}
diff --git a/selfservice/flow/recovery/strategy.go b/selfservice/flow/recovery/strategy.go
index 5e606c247a74..cbee2edded1e 100644
--- a/selfservice/flow/recovery/strategy.go
+++ b/selfservice/flow/recovery/strategy.go
@@ -8,13 +8,21 @@ import (
"github.com/ory/kratos/x"
)
-type Strategy interface {
- RecoveryStrategyID() string
- RegisterRecoveryRoutes(*x.RouterPublic)
- PopulateRecoveryMethod(*http.Request, *Request) error
-}
+const (
+ StrategyRecoveryTokenName = "token"
+)
-type Strategies []Strategy
+type (
+ Strategy interface {
+ RecoveryStrategyID() string
+ RegisterRecoveryRoutes(*x.RouterPublic)
+ PopulateRecoveryMethod(*http.Request, *Request) error
+ }
+ Strategies []Strategy
+ StrategyProvider interface {
+ RecoveryStrategies() Strategies
+ }
+)
func (s Strategies) Strategy(id string) (Strategy, error) {
ids := make([]string, len(s))
@@ -41,7 +49,3 @@ func (s Strategies) RegisterPublicRoutes(r *x.RouterPublic) {
ss.RegisterRecoveryRoutes(r)
}
}
-
-type StrategyProvider interface {
- RecoveryStrategies() Strategies
-}
diff --git a/selfservice/flow/recovery/strategy_email.go b/selfservice/flow/recovery/strategy_email.go
deleted file mode 100644
index 80cc43ad2b0d..000000000000
--- a/selfservice/flow/recovery/strategy_email.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package recovery
-
-const (
- StrategyEmail = "profile"
-)
diff --git a/selfservice/flow/recovery/stub/identity.schema.json b/selfservice/flow/recovery/stub/identity.schema.json
index 22965676cdc0..04ed04e224a0 100644
--- a/selfservice/flow/recovery/stub/identity.schema.json
+++ b/selfservice/flow/recovery/stub/identity.schema.json
@@ -14,6 +14,9 @@
},
"verification": {
"via": "email"
+ },
+ "recovery": {
+ "via": "email"
}
}
},
diff --git a/selfservice/flow/registration/error.go b/selfservice/flow/registration/error.go
index 99c426657a54..09c5a6f714e1 100644
--- a/selfservice/flow/registration/error.go
+++ b/selfservice/flow/registration/error.go
@@ -2,7 +2,6 @@ package registration
import (
"context"
- "fmt"
"net/http"
"net/url"
"time"
@@ -71,11 +70,11 @@ func (s *ErrorHandler) HandleRegistrationError(
rr *Request,
err error,
) {
- s.d.Logger().WithError(err).
- WithField("details", fmt.Sprintf("%+v", err)).
- WithField("credentials_type", ct).
- WithField("login_request", rr).
- Warn("Encountered registration error.")
+ s.d.Audit().
+ WithError(err).
+ WithRequest(r).
+ WithField("registration_request", rr).
+ Info("Encountered self-service request error.")
if _, ok := errorsx.Cause(err).(requestExpiredError); ok {
// create new request because the old one is not valid
@@ -85,6 +84,7 @@ func (s *ErrorHandler) HandleRegistrationError(
s.HandleRegistrationError(w, r, ct, rr, err)
return
}
+
for name, method := range a.Methods {
method.Config.AddError(&form.Error{Message: "Your session expired, please try again."})
if err := s.d.RegistrationRequestPersister().UpdateRegistrationRequestMethod(context.TODO(), a.ID, name, method); err != nil {
diff --git a/selfservice/flow/registration/persistence.go b/selfservice/flow/registration/persistence.go
index 63f3dcee954d..cdc3a541218a 100644
--- a/selfservice/flow/registration/persistence.go
+++ b/selfservice/flow/registration/persistence.go
@@ -5,7 +5,7 @@ import (
"encoding/json"
"testing"
- "github.com/bxcodec/faker"
+ "github.com/bxcodec/faker/v3"
"github.com/gobuffalo/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/selfservice/flow/registration/request.go b/selfservice/flow/registration/request.go
index 58b2c2cc633a..4833c2215dfb 100644
--- a/selfservice/flow/registration/request.go
+++ b/selfservice/flow/registration/request.go
@@ -20,7 +20,7 @@ type Request struct {
// represents the id in the registration ui's query parameter: http:///?request=
//
// required: true
- ID uuid.UUID `json:"id" faker:"uuid" db:"id" rw:"r"`
+ ID uuid.UUID `json:"id" faker:"-" db:"id"`
// ExpiresAt is the time (UTC) when the request expires. If the user still wishes to log in,
// a new request has to be initiated.
@@ -37,11 +37,11 @@ type Request struct {
// to forward information contained in the URL's path or query for example.
//
// required: true
- RequestURL string `json:"request_url" db:"request_url"`
+ RequestURL string `json:"request_url" faker:"url" db:"request_url"`
// Active, if set, contains the registration method that is being used. It is initially
// not set.
- Active identity.CredentialsType `json:"active,omitempty" db:"active_method"`
+ Active identity.CredentialsType `json:"active,omitempty" faker:"identity_credentials_type" db:"active_method"`
// Methods contains context for all enabled registration methods. If a registration request has been
// processed, but for example the password is incorrect, this will contain error messages.
@@ -53,10 +53,10 @@ type Request struct {
MethodsRaw RequestMethodsRaw `json:"-" faker:"-" has_many:"selfservice_registration_request_methods" fk_id:"selfservice_registration_request_id"`
// CreatedAt is a helper struct field for gobuffalo.pop.
- CreatedAt time.Time `json:"-" db:"created_at"`
+ CreatedAt time.Time `json:"-" faker:"-" db:"created_at"`
// UpdatedAt is a helper struct field for gobuffalo.pop.
- UpdatedAt time.Time `json:"-" db:"updated_at"`
+ UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"`
// CSRFToken contains the anti-csrf token associated with this request.
CSRFToken string `json:"-" db:"csrf_token"`
diff --git a/selfservice/flow/registration/request_method.go b/selfservice/flow/registration/request_method.go
index 5483ef7b1bf2..f4e8e0d6a774 100644
--- a/selfservice/flow/registration/request_method.go
+++ b/selfservice/flow/registration/request_method.go
@@ -16,25 +16,25 @@ import (
// swagger:model registrationRequestMethod
type RequestMethod struct {
// Method contains the request credentials type.
- Method identity.CredentialsType `json:"method" db:"method"`
+ Method identity.CredentialsType `json:"method" faker:"string" db:"method"`
// Config is the credential type's config.
Config *RequestMethodConfig `json:"config" db:"config"`
// ID is a helper struct field for gobuffalo.pop.
- ID uuid.UUID `json:"-" db:"id" rw:"r"`
+ ID uuid.UUID `json:"-" faker:"-" db:"id"`
// RequestID is a helper struct field for gobuffalo.pop.
- RequestID uuid.UUID `json:"-" db:"selfservice_registration_request_id"`
+ RequestID uuid.UUID `json:"-" faker:"-" db:"selfservice_registration_request_id"`
// Request is a helper struct field for gobuffalo.pop.
- Request *Request `json:"-" belongs_to:"selfservice_registration_request" fk_id:"RequestID"`
+ Request *Request `json:"-" faker:"-" belongs_to:"selfservice_registration_request" fk_id:"RequestID"`
// CreatedAt is a helper struct field for gobuffalo.pop.
- CreatedAt time.Time `json:"-" db:"created_at"`
+ CreatedAt time.Time `json:"-" faker:"-" db:"created_at"`
// UpdatedAt is a helper struct field for gobuffalo.pop.
- UpdatedAt time.Time `json:"-" db:"updated_at"`
+ UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"`
}
func (u RequestMethod) TableName() string {
@@ -80,7 +80,7 @@ type requestMethodConfigMock struct {
*form.HTMLForm
// Providers is set for the "oidc" request method.
- Providers []form.Field `json:"providers"`
+ Providers []form.Field `json:"providers" faker:"len=3"`
}
func (c *RequestMethodConfig) Scan(value interface{}) error {
diff --git a/selfservice/flow/registration/request_test.go b/selfservice/flow/registration/request_test.go
index 7053b4939db2..ca160b186ccd 100644
--- a/selfservice/flow/registration/request_test.go
+++ b/selfservice/flow/registration/request_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"time"
- "github.com/bxcodec/faker"
+ "github.com/bxcodec/faker/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/selfservice/flow/settings/error.go b/selfservice/flow/settings/error.go
index 64cd0eb3aef7..2b1a28b232d7 100644
--- a/selfservice/flow/settings/error.go
+++ b/selfservice/flow/settings/error.go
@@ -1,7 +1,6 @@
package settings
import (
- "fmt"
"net/http"
"net/url"
@@ -77,10 +76,11 @@ func (s *ErrorHandler) HandleSettingsError(
err error,
method string,
) {
- s.d.Logger().WithError(err).
- WithField("details", fmt.Sprintf("%+v", err)).
+ s.d.Audit().
+ WithError(err).
+ WithRequest(r).
WithField("settings_request", rr).
- Warn("Encountered settings error.")
+ Info("Encountered self-service settings error.")
if rr == nil {
s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
@@ -101,7 +101,6 @@ func (s *ErrorHandler) HandleSettingsError(
}
rr.Active = sqlxx.NullString(method)
-
if err := rr.Methods[method].Config.ParseError(err); err != nil {
s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
return
diff --git a/selfservice/flow/settings/handler.go b/selfservice/flow/settings/handler.go
index acbd942127fc..5e160413c32b 100644
--- a/selfservice/flow/settings/handler.go
+++ b/selfservice/flow/settings/handler.go
@@ -2,7 +2,6 @@ package settings
import (
"net/http"
- "net/url"
"time"
"github.com/ory/kratos/continuity"
@@ -99,28 +98,32 @@ func (h *Handler) initUpdateSettings(w http.ResponseWriter, r *http.Request, ps
return
}
- a := NewRequest(h.c.SelfServiceSettingsRequestLifespan(), r, s)
+ req := NewRequest(h.c.SelfServiceSettingsRequestLifespan(), r, s)
+
+ if err := h.CreateRequest(w, r, s, req); err != nil {
+ h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
+ return
+ }
+
+ http.Redirect(w, r, req.URL(h.c.SettingsURL()).String(), http.StatusFound)
+}
+
+func (h *Handler) CreateRequest(w http.ResponseWriter, r *http.Request, sess *session.Session, req *Request) error {
for _, strategy := range h.d.SettingsStrategies() {
if err := h.d.ContinuityManager().Abort(r.Context(), w, r, ContinuityKey(strategy.SettingsStrategyID())); err != nil {
- h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
- return
+ return err
}
- if err := strategy.PopulateSettingsMethod(r, s, a); err != nil {
- h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
- return
+ if err := strategy.PopulateSettingsMethod(r, sess, req); err != nil {
+ return err
}
}
- if err := h.d.SettingsRequestPersister().CreateSettingsRequest(r.Context(), a); err != nil {
- h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
- return
+ if err := h.d.SettingsRequestPersister().CreateSettingsRequest(r.Context(), req); err != nil {
+ return err
}
- http.Redirect(w, r,
- urlx.CopyWithQuery(h.c.SettingsURL(), url.Values{"request": {a.ID.String()}}).String(),
- http.StatusFound,
- )
+ return nil
}
// nolint:deadcode,unused
diff --git a/selfservice/flow/settings/persistence.go b/selfservice/flow/settings/persistence.go
index b77b17e07f77..b9fbf6bc2e6e 100644
--- a/selfservice/flow/settings/persistence.go
+++ b/selfservice/flow/settings/persistence.go
@@ -5,7 +5,7 @@ import (
"encoding/json"
"testing"
- "github.com/bxcodec/faker"
+ "github.com/bxcodec/faker/v3"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/selfservice/flow/settings/request.go b/selfservice/flow/settings/request.go
index 37e0cd078598..3690dd66f3bd 100644
--- a/selfservice/flow/settings/request.go
+++ b/selfservice/flow/settings/request.go
@@ -2,17 +2,21 @@ package settings
import (
"net/http"
+ "net/url"
"time"
"github.com/gobuffalo/pop/v5"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
+ "github.com/ory/x/urlx"
+
"github.com/ory/x/sqlxx"
"github.com/ory/herodot"
"github.com/ory/kratos/identity"
+ "github.com/ory/kratos/selfservice/text"
"github.com/ory/kratos/session"
"github.com/ory/kratos/x"
)
@@ -32,7 +36,7 @@ type Request struct {
// required: true
// type: string
// format: uuid
- ID uuid.UUID `json:"id" db:"id" faker:"uuid" rw:"r"`
+ ID uuid.UUID `json:"id" db:"id" faker:"-"`
// ExpiresAt is the time (UTC) when the request expires. If the user still wishes to update the setting,
// a new request has to be initiated.
@@ -55,6 +59,12 @@ type Request struct {
// not set.
Active sqlxx.NullString `json:"active,omitempty" db:"active_method"`
+ // Messages contains a list of messages to be displayed in the Settings UI. Omitting these
+ // messages makes it significantly harder for users to figure out what is going on.
+ //
+ // More documentation on messages can be found in the [User Interface Documentation](https://www.ory.sh/kratos/docs/concepts/ui-user-interface/).
+ Messages text.Messages `json:"messages" db:"messages" faker:"-"`
+
// Methods contains context for all enabled registration methods. If a registration request has been
// processed, but for example the password is incorrect, this will contain error messages.
//
@@ -104,6 +114,10 @@ func (r *Request) GetID() uuid.UUID {
return r.ID
}
+func (r *Request) URL(settingsURL *url.URL) *url.URL {
+ return urlx.CopyWithQuery(settingsURL, url.Values{"request": {r.ID.String()}})
+}
+
func (r *Request) Valid(s *session.Session) error {
if r.ExpiresAt.Before(time.Now().UTC()) {
return errors.WithStack(ErrRequestExpired.
diff --git a/selfservice/flow/settings/request_method.go b/selfservice/flow/settings/request_method.go
index 7e49a9cc4823..14671a6a325f 100644
--- a/selfservice/flow/settings/request_method.go
+++ b/selfservice/flow/settings/request_method.go
@@ -21,7 +21,7 @@ type RequestMethod struct {
Config *RequestMethodConfig `json:"config" db:"config"`
// ID is a helper struct field for gobuffalo.pop.
- ID uuid.UUID `json:"-" db:"id" rw:"r"`
+ ID uuid.UUID `json:"-" db:"id"`
// RequestID is a helper struct field for gobuffalo.pop.
RequestID uuid.UUID `json:"-" db:"selfservice_settings_request_id"`
diff --git a/selfservice/flow/verify/error.go b/selfservice/flow/verify/error.go
index 1c5933d47dcc..c89f9cb3c474 100644
--- a/selfservice/flow/verify/error.go
+++ b/selfservice/flow/verify/error.go
@@ -1,7 +1,6 @@
package verify
import (
- "fmt"
"net/http"
"net/url"
@@ -57,10 +56,11 @@ func (s *ErrorHandler) HandleVerificationError(
rr *Request,
err error,
) {
- s.d.Logger().WithError(err).
- WithField("details", fmt.Sprintf("%+v", err)).
+ s.d.Audit().
+ WithError(err).
+ WithRequest(r).
WithField("verify_request", rr).
- Warn("Encountered self-service verification error.")
+ Info("Encountered self-service verification error.")
if rr == nil {
s.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
diff --git a/selfservice/flow/verify/handler_test.go b/selfservice/flow/verify/handler_test.go
index 475e08751e39..6ef3042d89f1 100644
--- a/selfservice/flow/verify/handler_test.go
+++ b/selfservice/flow/verify/handler_test.go
@@ -11,7 +11,7 @@ import (
"testing"
"time"
- "github.com/bxcodec/faker"
+ "github.com/bxcodec/faker/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
diff --git a/selfservice/flow/verify/persistence.go b/selfservice/flow/verify/persistence.go
index 999f11896f54..80cdefacc9de 100644
--- a/selfservice/flow/verify/persistence.go
+++ b/selfservice/flow/verify/persistence.go
@@ -5,7 +5,7 @@ import (
"encoding/json"
"testing"
- "github.com/bxcodec/faker"
+ "github.com/bxcodec/faker/v3"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/selfservice/flow/verify/request.go b/selfservice/flow/verify/request.go
index f35a2f62864b..13f401880f7f 100644
--- a/selfservice/flow/verify/request.go
+++ b/selfservice/flow/verify/request.go
@@ -28,7 +28,7 @@ type Request struct {
//
// type: string
// format: uuid
- ID uuid.UUID `json:"id" db:"id" faker:"uuid" rw:"r"`
+ ID uuid.UUID `json:"id" db:"id" faker:"-"`
// ExpiresAt is the time (UTC) when the request expires. If the user still wishes to verify the address,
// a new request has to be initiated.
diff --git a/selfservice/flow/verify/sender.go b/selfservice/flow/verify/sender.go
index 27863fd422a5..e7d2e4c67268 100644
--- a/selfservice/flow/verify/sender.go
+++ b/selfservice/flow/verify/sender.go
@@ -43,9 +43,12 @@ func NewSender(r senderDependencies, c configuration.Provider) *Sender {
// still being sent to prevent account enumeration attacks. In that case, this function returns the ErrUnknownAddress
// error.
func (m *Sender) SendCode(ctx context.Context, via identity.VerifiableAddressType, value string) (*identity.VerifiableAddress, error) {
- m.r.Logger().WithField("via", via).Debug("Sending out verification code.")
+ m.r.Logger().
+ WithField("via", via).
+ WithSensitiveField("address", value).
+ Debug("Preparing verification code.")
- address, err := m.r.IdentityPool().FindAddressByValue(ctx, via, value)
+ address, err := m.r.IdentityPool().FindVerifiableAddressByValue(ctx, via, value)
if err != nil {
if errorsx.Cause(err) == sqlcon.ErrNoRows {
if err := m.sendToUnknownAddress(ctx, identity.VerifiableAddressTypeEmail, value); err != nil {
@@ -67,21 +70,28 @@ func (m *Sender) SendCode(ctx context.Context, via identity.VerifiableAddressTyp
}
func (m *Sender) sendToUnknownAddress(ctx context.Context, via identity.VerifiableAddressType, address string) error {
- m.r.Logger().WithField("via", via).Debug("Sending out invalid verification email because address is unknown.")
+ m.r.Audit().
+ WithSensitiveField("email_address", address).
+ Info("Sending out invalid verification email because address is unknown.")
+
return m.run(via, func() error {
_, err := m.r.Courier().QueueEmail(ctx,
- templates.NewVerifyInvalid(m.c, &templates.VerifyInvalidModel{To: address}))
+ templates.NewVerificationInvalid(m.c, &templates.VerificationInvalidModel{To: address}))
return err
})
}
func (m *Sender) sendCodeToKnownAddress(ctx context.Context, address *identity.VerifiableAddress) error {
- m.r.Logger().WithField("via", address.Via).Debug("Sending out verification email.")
+ m.r.Audit().
+ WithField("identity_id", address.IdentityID).
+ WithSensitiveField("email_address", address.Value).
+ Info("Sending out verification code via email.")
+
return m.run(address.Via, func() error {
- _, err := m.r.Courier().QueueEmail(ctx, templates.NewVerifyValid(m.c,
- &templates.VerifyValidModel{
+ _, err := m.r.Courier().QueueEmail(ctx, templates.NewVerificationValid(m.c,
+ &templates.VerificationValidModel{
To: address.Value,
- VerifyURL: urlx.AppendPaths(
+ VerificationURL: urlx.AppendPaths(
m.c.SelfPublicURL(),
strings.ReplaceAll(
strings.ReplaceAll(PublicVerificationConfirmPath, ":via", string(address.Via)),
diff --git a/selfservice/form/container.go b/selfservice/form/container.go
index 3f04ed93c0e4..74493de83dea 100644
--- a/selfservice/form/container.go
+++ b/selfservice/form/container.go
@@ -1,5 +1,16 @@
package form
+type Form interface {
+ ErrorParser
+ FieldSetter
+ ValueSetter
+ FieldUnsetter
+ ErrorAdder
+ CSRFSetter
+ Resetter
+ FieldSorter
+}
+
// ErrorParser is capable of parsing and processing errors.
type ErrorParser interface {
// ParseError type asserts the given error and sets the forms's errors or a
diff --git a/selfservice/form/fields.go b/selfservice/form/fields.go
index 1f1edf1a188e..9c5f8dff9b70 100644
--- a/selfservice/form/fields.go
+++ b/selfservice/form/fields.go
@@ -41,7 +41,7 @@ type Field struct {
Required bool `json:"required,omitempty"`
// Value is the equivalent of ``
- Value interface{} `json:"value,omitempty" faker:"name"`
+ Value interface{} `json:"value,omitempty" faker:"string"`
// Errors contains all validation errors this particular field has caused.
Errors Errors `json:"errors,omitempty"`
diff --git a/selfservice/form/html_form.go b/selfservice/form/html_form.go
index f74ef7844cae..498aff52348b 100644
--- a/selfservice/form/html_form.go
+++ b/selfservice/form/html_form.go
@@ -31,17 +31,17 @@ var (
//
// swagger:model form
type HTMLForm struct {
- sync.RWMutex
+ sync.RWMutex `faker:"-"`
// Action should be used as the form action URL `