Skip to content

Commit

Permalink
feat: implement acocunt recovery
Browse files Browse the repository at this point in the history
This patch implements the account recovery with endpoints such as "Init Account Recovery", a new config value `urls.recovery_ui` and so on. Additionally, some refactoring was made to DRY code and make naming consistent. As part of dependency upgrades, structured logging has also improved and an audit trail prototype has been added (currently streams to stderr only).

Closes #37

BREAKING CHANGES:

* Applying this patch requires running SQL Migrations.
* The field `identity.addresses` has moved to `identity.verifiable_addresses`. A new field has been added
`identity.recovery_addresses`. Configuration key `selfservice.verify` was renamed to `selfservice.verification`. Configuration key `selfservice.verification.link_lifespan`
has been merged with `selfservice.verification.request_lifespan`.
  • Loading branch information
aeneasr committed Jun 5, 2020
1 parent b1e9183 commit cea5715
Show file tree
Hide file tree
Showing 196 changed files with 3,629 additions and 968 deletions.
265 changes: 254 additions & 11 deletions .schema/api.swagger.json

Large diffs are not rendered by default.

15 changes: 10 additions & 5 deletions .schema/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions cmd/client/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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("")
Expand All @@ -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
Expand Down
10 changes: 6 additions & 4 deletions cmd/daemon/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(),
Expand Down
6 changes: 2 additions & 4 deletions cmd/daemon/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (
"strings"
"sync"

"github.com/sirupsen/logrus"

"github.com/ory/analytics-go/v4"

"github.com/ory/x/flagx"
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 2 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
2 changes: 1 addition & 1 deletion continuity/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion courier/persistence.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
33 changes: 33 additions & 0 deletions courier/template/recovery_invalid.go
Original file line number Diff line number Diff line change
@@ -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)
}
24 changes: 24 additions & 0 deletions courier/template/recovery_invalid_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
34 changes: 34 additions & 0 deletions courier/template/recovery_valid.go
Original file line number Diff line number Diff line change
@@ -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)
}
24 changes: 24 additions & 0 deletions courier/template/recovery_valid_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
9 changes: 9 additions & 0 deletions courier/template/templates/recovery/invalid/email.body.gotmpl
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Account access attempted
5 changes: 5 additions & 0 deletions courier/template/templates/recovery/valid/email.body.gotmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Hi,

please recover your account by clicking the following link:

<a href="{{ .RecoveryURL }}">{{ .RecoveryURL }}</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Recover your account
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Hi, please verify your account by clicking the following link:

<a href="{{ .VerifyURL }}">{{ .VerifyURL }}</a>
<a href="{{ .VerificationURL }}">{{ .VerificationURL }}</a>
33 changes: 33 additions & 0 deletions courier/template/verification_invalid.go
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 34 additions & 0 deletions courier/template/verification_valid.go
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 0 additions & 33 deletions courier/template/verify_invalid.go

This file was deleted.

Loading

0 comments on commit cea5715

Please sign in to comment.