Skip to content

Commit

Permalink
[Settings v2] Manage MFA (except recovery code)
Browse files Browse the repository at this point in the history
ref DEV-1964
ref DEV-1967
ref DEV-1971
ref DEV-1961
  • Loading branch information
louischan-oursky committed Oct 14, 2024
2 parents 1668184 + 7bca2be commit 91eb433
Show file tree
Hide file tree
Showing 59 changed files with 9,449 additions and 573 deletions.
2 changes: 1 addition & 1 deletion .make-lint-translation-keys-expect
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ resources/authgear/templates/en/web/authflowv2/forgot_password.html:159:26: temp
resources/authgear/templates/en/web/authflowv2/layout.html:5:14: template translation is forbidden: `widget`
resources/authgear/templates/en/web/authflowv2/login.html:252:37: translation key not defined: "%s-icon"
resources/authgear/templates/en/web/authflowv2/settings_layout.html:3:14: template translation is forbidden: `widget`
resources/authgear/templates/en/web/authflowv2/settings_oob_otp.html:38:31: translation key not defined: "/settings/mfa/new_oob_otp_%s"
resources/authgear/templates/en/web/authflowv2/settings_oob_otp.html:68:31: translation key not defined: "/settings/mfa/create_oob_otp_%s"
resources/authgear/templates/en/web/authflowv2/settings_profile.html:138:22: translation key not defined: "custom-attribute-label-%s"
resources/authgear/templates/en/web/authflowv2/settings_profile.html:139:20: invalid translation key: "$labelKey"
resources/authgear/templates/en/web/authflowv2/settings_profile.html:143:21: invalid translation key: "$labelKey"
Expand Down
1 change: 1 addition & 0 deletions authui/src/authflowv2/components/settings-action-item.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
}

.settings-action-item__action-button-container {
@apply flex flex-none items-center;
@apply py-[var(--settings-action-item-padding-y)];
}

Expand Down
6 changes: 6 additions & 0 deletions pkg/auth/handler/webapp/authflowv2/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,16 @@ var DependencySet = wire.NewSet(
wire.Struct(new(AuthflowV2SettingsAdvancedSettingsHandler), "*"),
wire.Struct(new(AuthflowV2SettingsDeleteAccountHandler), "*"),
wire.Struct(new(AuthflowV2SettingsDeleteAccountSuccessHandler), "*"),
wire.Struct(new(AuthflowV2SettingsMFAViewRecoveryCodeHandler), "*"),
wire.Struct(new(AuthflowV2SettingsMFACreatePasswordHandler), "*"),
wire.Struct(new(AuthflowV2SettingsMFAChangePasswordHandler), "*"),
wire.Struct(new(AuthflowV2SettingsMFAPasswordHandler), "*"),
wire.Struct(new(AuthflowV2SettingsTOTPHandler), "*"),
wire.Struct(new(AuthflowV2SettingsMFACreateTOTPHandler), "*"),
wire.Struct(new(AuthflowV2SettingsMFAEnterTOTPHandler), "*"),
wire.Struct(new(AuthflowV2SettingsOOBOTPHandler), "*"),
wire.Struct(new(AuthflowV2SettingsMFACreateOOBOTPHandler), "*"),
wire.Struct(new(AuthflowV2SettingsMFAEnterOOBOTPHandler), "*"),
wire.Struct(new(AuthflowV2SettingsIdentityAddEmailHandler), "*"),
wire.Struct(new(AuthflowV2SettingsIdentityEditEmailHandler), "*"),
wire.Struct(new(AuthflowV2SettingsIdentityVerifyEmailHandler), "*"),
Expand Down
12 changes: 10 additions & 2 deletions pkg/auth/handler/webapp/authflowv2/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,20 @@ const (

AuthflowV2RouteFinishFlow = "/authflow/v2/finish"

AuthflowV2RouteSettingsProfile = "/settings/v2/profile"
AuthflowV2RouteSettingsMFA = "/settings/mfa"
AuthflowV2RouteSettingsProfile = "/settings/v2/profile"
AuthflowV2RouteSettingsMFA = "/settings/mfa"
AuthflowV2RouteSettingsMFAViewRecoveryCode = "/settings/mfa/view_recovery_code"

// nolint: gosec
AuthflowV2RouteSettingsMFACreatePassword = "/settings/mfa/create_password"
AuthflowV2RouteSettingsMFACreateOOBOTP = "/settings/mfa/create_oob_otp_:channel"
AuthflowV2RouteSettingsMFAEnterOOBOTP = "/settings/mfa/enter_oob_otp"
// nolint: gosec
AuthflowV2RouteSettingsMFAPassword = "/settings/mfa/password"
// nolint: gosec
AuthflowV2RouteSettingsMFAChangePassword = "/settings/mfa/change_password"
AuthflowV2RouteSettingsMFACreateTOTP = "/settings/mfa/create_totp"
AuthflowV2RouteSettingsMFAEnterTOTP = "/settings/mfa/enter_totp"

AuthflowV2RouteSettingsIdentityListEmail = "/settings/identity/email"
AuthflowV2RouteSettingsIdentityAddEmail = "/settings/identity/add_email"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,14 @@ func (h *AuthflowV2SettingsChangePasswordHandler) ServeHTTP(w http.ResponseWrite
redirectURI = webappSession.RedirectURI
}

input := &accountmanagement.ChangePasswordInput{
input := &accountmanagement.ChangePrimaryPasswordInput{
OAuthSessionID: oAuthSessionID,
RedirectURI: redirectURI,
OldPassword: oldPassword,
NewPassword: newPassword,
}

changePasswordOutput, err := h.AccountManagementService.ChangePassword(s, input)
changePasswordOutput, err := h.AccountManagementService.ChangePrimaryPassword(s, input)
if err != nil {
return err
}
Expand Down
120 changes: 120 additions & 0 deletions pkg/auth/handler/webapp/authflowv2/settings_mfa_change_password.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package authflowv2

import (
"net/http"

handlerwebapp "github.com/authgear/authgear-server/pkg/auth/handler/webapp"
"github.com/authgear/authgear-server/pkg/auth/handler/webapp/viewmodels"
"github.com/authgear/authgear-server/pkg/auth/webapp"
"github.com/authgear/authgear-server/pkg/lib/accountmanagement"
"github.com/authgear/authgear-server/pkg/lib/session"
"github.com/authgear/authgear-server/pkg/util/httproute"
pwd "github.com/authgear/authgear-server/pkg/util/password"
"github.com/authgear/authgear-server/pkg/util/template"
"github.com/authgear/authgear-server/pkg/util/validation"
)

var TemplateWebSettingsV2MFAChangePasswordHTML = template.RegisterHTML(
"web/authflowv2/settings_mfa_change_password.html",
handlerwebapp.SettingsComponents...,
)

var AuthflowV2SettingsMFAChangePasswordSchema = validation.NewSimpleSchema(`
{
"type": "object",
"properties": {
"x_old_password": { "type": "string" },
"x_new_password": { "type": "string" },
"x_confirm_password": { "type": "string" }
},
"required": ["x_old_password", "x_new_password", "x_confirm_password"]
}
`)

type AuthflowV2SettingsMFAChangePasswordHandler struct {
ControllerFactory handlerwebapp.ControllerFactory
BaseViewModel *viewmodels.BaseViewModeler
Renderer handlerwebapp.Renderer
AccountManagementService *accountmanagement.Service
PasswordPolicy handlerwebapp.PasswordPolicy
}

func ConfigureAuthflowV2SettingsMFAChangePassword(route httproute.Route) httproute.Route {
return route.
WithMethods("OPTIONS", "POST", "GET").
WithPathPattern(AuthflowV2RouteSettingsMFAChangePassword)
}

func (h *AuthflowV2SettingsMFAChangePasswordHandler) GetData(r *http.Request, rw http.ResponseWriter) (map[string]interface{}, error) {
data := make(map[string]interface{})

// BaseViewModel
baseViewModel := h.BaseViewModel.ViewModel(r, rw)
viewmodels.Embed(data, baseViewModel)

passwordPolicyViewModel := viewmodels.NewPasswordPolicyViewModel(
h.PasswordPolicy.PasswordPolicy(),
h.PasswordPolicy.PasswordRules(),
baseViewModel.RawError,
viewmodels.GetDefaultPasswordPolicyViewModelOptions(),
)
viewmodels.Embed(data, passwordPolicyViewModel)

viewmodels.Embed(data, handlerwebapp.ChangePasswordViewModel{
Force: false,
})

return data, nil
}

func (h *AuthflowV2SettingsMFAChangePasswordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctrl, err := h.ControllerFactory.New(r, w)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer ctrl.ServeWithoutDBTx()

ctrl.Get(func() error {
data, err := h.GetData(r, w)
if err != nil {
return err
}

h.Renderer.RenderHTML(w, r, TemplateWebSettingsV2MFAChangePasswordHTML, data)

return nil
})

ctrl.PostAction("", func() error {
err := AuthflowV2SettingsMFAChangePasswordSchema.Validator().ValidateValue(handlerwebapp.FormToJSON(r.Form))
if err != nil {
return err
}

oldPassword := r.Form.Get("x_old_password")
newPassword := r.Form.Get("x_new_password")
confirmPassword := r.Form.Get("x_confirm_password")

err = pwd.ConfirmPassword(newPassword, confirmPassword)
if err != nil {
return err
}

s := session.GetSession(r.Context())
input := &accountmanagement.ChangeSecondaryPasswordInput{
OldPassword: oldPassword,
NewPassword: newPassword,
}

_, err = h.AccountManagementService.ChangeSecondaryPassword(s, input)
if err != nil {
return err
}

result := webapp.Result{RedirectURI: AuthflowV2RouteSettingsMFAPassword}
result.WriteResponse(w, r)
return nil
})

}
125 changes: 125 additions & 0 deletions pkg/auth/handler/webapp/authflowv2/settings_mfa_create_oob_otp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package authflowv2

import (
"net/http"
"net/url"

"github.com/authgear/authgear-server/pkg/api/model"
handlerwebapp "github.com/authgear/authgear-server/pkg/auth/handler/webapp"
"github.com/authgear/authgear-server/pkg/auth/handler/webapp/viewmodels"
"github.com/authgear/authgear-server/pkg/auth/webapp"
"github.com/authgear/authgear-server/pkg/lib/accountmanagement"
"github.com/authgear/authgear-server/pkg/lib/session"
"github.com/authgear/authgear-server/pkg/util/httproute"
"github.com/authgear/authgear-server/pkg/util/template"
"github.com/authgear/authgear-server/pkg/util/validation"
)

var TemplateWebSettingsMFACreateOOBOTPHTML = template.RegisterHTML(
"web/authflowv2/settings_mfa_create_oob_otp.html",
handlerwebapp.SettingsComponents...,
)

var AuthflowV2SettingsMFACreateOOBOTPSchema = validation.NewSimpleSchema(`
{
"type": "object",
"properties": {
"x_target": { "type": "string" }
},
"required": ["x_target"]
}
`)

func ConfigureAuthflowV2SettingsMFACreateOOBOTPRoute(route httproute.Route) httproute.Route {
return route.
WithMethods("OPTIONS", "POST", "GET").
WithPathPattern(AuthflowV2RouteSettingsMFACreateOOBOTP)
}

type SettingsMFACreateOOBOTPViewModel struct {
OOBAuthenticatorType model.AuthenticatorType
Channel model.AuthenticatorOOBChannel
}

type AuthflowV2SettingsMFACreateOOBOTPHandler struct {
ControllerFactory handlerwebapp.ControllerFactory
BaseViewModel *viewmodels.BaseViewModeler
Renderer handlerwebapp.Renderer

AccountManagementService *accountmanagement.Service
}

func NewSettingsMFACreateOOBOTPViewModel(channel model.AuthenticatorOOBChannel, authenticatorType model.AuthenticatorType) SettingsMFACreateOOBOTPViewModel {
return SettingsMFACreateOOBOTPViewModel{
OOBAuthenticatorType: authenticatorType,
Channel: channel,
}
}

func (h *AuthflowV2SettingsMFACreateOOBOTPHandler) GetData(r *http.Request, w http.ResponseWriter, channel model.AuthenticatorOOBChannel) (map[string]interface{}, error) {
data := make(map[string]interface{})

authenticatorType, err := model.GetOOBAuthenticatorType(channel)
if err != nil {
return nil, err
}

baseViewModel := h.BaseViewModel.ViewModelForAuthFlow(r, w)
viewmodels.Embed(data, baseViewModel)

settingsViewModel := NewSettingsMFACreateOOBOTPViewModel(channel, authenticatorType)
viewmodels.Embed(data, settingsViewModel)

return data, nil
}

func (h *AuthflowV2SettingsMFACreateOOBOTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctrl, err := h.ControllerFactory.New(r, w)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer ctrl.ServeWithoutDBTx()

channel := model.AuthenticatorOOBChannel(httproute.GetParam(r, "channel"))

ctrl.Get(func() error {
data, err := h.GetData(r, w, channel)
if err != nil {
return err
}
h.Renderer.RenderHTML(w, r, TemplateWebSettingsMFACreateOOBOTPHTML, data)
return nil
})

ctrl.PostAction("", func() error {
err := AuthflowV2SettingsMFACreateOOBOTPSchema.Validator().ValidateValue(handlerwebapp.FormToJSON(r.Form))
if err != nil {
return err
}

target := r.Form.Get("x_target")

s := session.GetSession(r.Context())
output, err := h.AccountManagementService.StartAddOOBOTPAuthenticator(s, &accountmanagement.StartAddOOBOTPAuthenticatorInput{
Channel: channel,
Target: target,
})
if err != nil {
return err
}

redirectURI, err := url.Parse(AuthflowV2RouteSettingsMFAEnterOOBOTP)
if err != nil {
return err
}
q := redirectURI.Query()
q.Set("q_token", output.Token)
redirectURI.RawQuery = q.Encode()

result := webapp.Result{RedirectURI: redirectURI.String()}
result.WriteResponse(w, r)

return nil
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func (h *AuthflowV2SettingsMFACreatePasswordHandler) ServeHTTP(w http.ResponseWr
}

s := session.GetSession(r.Context())
err = h.AccountManagementService.CreateAdditionalPassword(s, accountmanagement.CreateAdditionalPasswordInput{
_, err = h.AccountManagementService.CreateSecondaryPassword(s, accountmanagement.CreateSecondaryPasswordInput{
PlainPassword: newPassword,
})
if err != nil {
Expand Down
Loading

0 comments on commit 91eb433

Please sign in to comment.