3
0

Extends user add cli command

- Adds flag `make-password-link` to generate temp token to create users password
- Adds route and handler for create user password
- Updates few auth label translation reference
- Fixes reset-password issue with existing login session
- Updates tests
This commit is contained in:
Vivek Patel
2021-09-01 22:51:27 +05:30
parent 40ddb9db58
commit 3ac2a1f3fa
23 changed files with 626 additions and 36 deletions
+1
View File
@@ -543,6 +543,7 @@ func updateAuthSettings(svc authServicer, current *types.AppSettings) {
SignupEnabled: current.Auth.Internal.Signup.Enabled,
EmailConfirmationRequired: current.Auth.Internal.Signup.EmailConfirmationRequired,
PasswordResetEnabled: current.Auth.Internal.PasswordReset.Enabled,
PasswordCreateEnabled: current.Auth.Internal.PasswordCreate.Enabled,
SplitCredentialsCheck: current.Auth.Internal.SplitCredentialsCheck,
ExternalEnabled: current.Auth.External.Enabled,
MultiFactor: authSettings.MultiFactor{
@@ -23,7 +23,7 @@
readonly
placeholder="{{ tr "change-password.template.form.email.placeholder" }}"
value="{{ .user.Email }}"
aria-label="{{ tr "change-password.template.form.email.aria-label" }}">
aria-label="{{ tr "change-password.template.form.email.label" }}">
</div>
<div class="mb-3">
<label>
@@ -36,7 +36,7 @@
name="oldPassword"
autocomplete="current-password"
placeholder="{{ tr "change-password.template.form.old-password.placeholder" }}"
aria-label="{{ tr "change-password.template.form.old-password.aria-label" }}">
aria-label="{{ tr "change-password.template.form.old-password.label" }}">
</div>
<div class="mb-3">
<label>
@@ -49,7 +49,7 @@
name="newPassword"
autocomplete="new-password"
placeholder="{{ tr "change-password.template.form.new-password.placeholder" }}"
aria-label="{{ tr "change-password.template.form.new-password.aria-label" }}">
aria-label="{{ tr "change-password.template.form.new-password.label" }}">
</div>
<div class="text-right">
<button class="btn btn-primary btn-block btn-lg" type="submit">{{ tr "change-password.template.form.button.change-password" }}</button>
@@ -0,0 +1,45 @@
{{ template "inc_header.html.tpl" set . "activeNav" "security" }}
<div class="card-body p-0">
<h1 class="h4 card-title p-3 border-bottom">{{ tr "create-password.template.title" }}</h1>
<form
method="POST"
action="{{ links.CreatePassword }}"
class="p-3"
>
{{ .csrfField }}
{{ if .form.error }}
<div class="text-danger font-weight-bold mb-3" role="alert">
{{ .form.error }}
</div>
{{ end }}
<div class="mb-3">
<label>
{{ tr "create-password.template.form.email.label" }}
</label>
<input
type="email"
class="form-control"
name="email"
readonly
value="{{ .user.Email }}"
aria-label="{{ tr "create-password.template.form.email.label" }}">
</div>
<div class="mb-3">
<label>
{{ tr "create-password.template.form.password.label" }}
</label>
<input
type="password"
required
class="form-control"
name="password"
autocomplete="password"
placeholder="{{ tr "create-password.template.form.password.placeholder" }}"
aria-label="{{ tr "create-password.template.form.password.label" }}">
</div>
<div class="text-right">
<button class="btn btn-primary btn-block btn-lg" type="submit">{{ tr "create-password.template.form.button.create-password" }}</button>
</div>
</form>
</div>
{{ template "inc_footer.html.tpl" . }}
+6
View File
@@ -185,6 +185,12 @@ change-password:
form:
error: "There was an error..."
create-password:
Default: {}
With error:
form:
error: "There was an error..."
authorized-clients:
Empty: {}
Full:
+1 -1
View File
@@ -27,7 +27,7 @@ func (h *AuthHandlers) changePasswordProc(req *request.AuthReq) (err error) {
if err == nil {
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "primary",
Text: t("change-password.alerts.text"),
Text: t("change-password.alerts.password-change-success"),
})
req.RedirectTo = GetLinks().Profile
+1 -1
View File
@@ -63,7 +63,7 @@ func Test_changePasswordProc(t *testing.T) {
{
name: "successful password change",
payload: map[string]string(nil),
alerts: []request.Alert{{Type: "primary", Text: "change-password.alerts.text", Html: ""}},
alerts: []request.Alert{{Type: "primary", Text: "change-password.alerts.password-change-success", Html: ""}},
link: GetLinks().Profile,
fn: func(_ *settings.Settings) {
authService = &authServiceMocked{
+117
View File
@@ -0,0 +1,117 @@
package handlers
import (
"github.com/cortezaproject/corteza-server/auth/request"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/system/service"
"github.com/cortezaproject/corteza-server/system/types"
"go.uber.org/zap"
)
var (
loginLink = GetLinks().Login
profileLink = GetLinks().Profile
createPasswordLink = GetLinks().CreatePassword
)
func (h *AuthHandlers) createPasswordForm(req *request.AuthReq) (err error) {
h.Log.Debug("password create form")
req.Template = TmplCreatePassword
// user not set, expecting valid token in URL
if token := req.Request.URL.Query().Get("token"); len(token) > 0 {
var (
user *types.User
// If user has password or not
passwordSet bool
)
user, err = h.AuthService.ValidatePasswordCreateToken(req.Context(), token)
if err == nil {
// If user does not have password (or in case there is no user with such email)
passwordSet = h.AuthService.PasswordSet(req.Context(), user.Email)
if !passwordSet {
// login user
req.AuthUser = request.NewAuthUser(h.Settings, user, false, h.Opt.SessionLifetime)
// redirect back to self (but without token and with user in session)
h.Log.Debug("valid password create token found, refreshing page with stored user")
req.RedirectTo = createPasswordLink
req.AuthUser.Save(req.Session)
return nil
}
}
}
if req.AuthUser == nil || err != nil {
h.Log.Warn("invalid password create token request", zap.Error(err))
h.invalidPasswordCreateAlert(req, loginLink)
return nil
}
req.Data["form"] = req.PopKV()
return nil
}
func (h *AuthHandlers) createPasswordProc(req *request.AuthReq) (err error) {
h.Log.Debug("password create proc")
err = h.AuthService.SetPassword(
auth.SetIdentityToContext(req.Context(), req.AuthUser.User),
req.AuthUser.User.ID,
req.Request.PostFormValue("password"),
)
if err == nil {
t := translator(req, "auth")
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "primary",
Text: t("create-password.alerts.password-create-success"),
})
req.RedirectTo = profileLink
return nil
}
switch {
case service.AuthErrPasswordCreateDisabledByConfig().Is(err):
h.passwordCreateDisabledAlert(req)
h.Log.Warn("handled error", zap.Error(err))
return nil
default:
h.Log.Error("unhandled error", zap.Error(err))
return err
}
}
func (h *AuthHandlers) onlyIfPasswordCreateEnabled(fn handlerFn) handlerFn {
return func(req *request.AuthReq) error {
if !h.Settings.PasswordCreateEnabled || !h.Settings.LocalEnabled {
h.passwordCreateDisabledAlert(req)
return nil
}
return fn(req)
}
}
func (h *AuthHandlers) passwordCreateDisabledAlert(req *request.AuthReq) {
req.RedirectTo = loginLink
t := translator(req, "auth")
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "danger",
Text: t("create-password.alert.password-create-disabled"),
})
}
func (h *AuthHandlers) invalidPasswordCreateAlert(req *request.AuthReq, redirectTo string) {
req.RedirectTo = redirectTo
t := translator(req, "auth")
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "warning",
Text: t("create-password.alerts.invalid-expired-password-token"),
})
}
@@ -0,0 +1,182 @@
package handlers
import (
"context"
"errors"
"github.com/cortezaproject/corteza-server/system/service"
"net/http"
"net/url"
"testing"
"github.com/cortezaproject/corteza-server/auth/request"
"github.com/cortezaproject/corteza-server/auth/settings"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/stretchr/testify/require"
)
func Test_createPasswordForm(t *testing.T) {
var (
ctx = context.Background()
req = &http.Request{
URL: &url.URL{},
}
user *types.User
authService authService
authHandlers *AuthHandlers
authReq *request.AuthReq
)
tcc := []testingExpect{
{
name: "password create success",
payload: map[string]string(nil),
alerts: []request.Alert(nil),
link: GetLinks().CreatePassword,
template: TmplCreatePassword,
fn: func(_ *settings.Settings) {
req.URL = &url.URL{RawQuery: "token=NOT_EMPTY"}
authService = &authServiceMocked{
passwordSet: func(ctx context.Context, s string) bool {
return false
},
validatePasswordCreateToken: func(ctx context.Context, token string) (user *types.User, err error) {
u := makeMockUser(ctx)
u.SetRoles()
return u, nil
},
}
},
},
{
name: "invalid password create token",
payload: map[string]string(nil),
alerts: []request.Alert{{Type: "warning", Text: "create-password.alerts.invalid-expired-password-token"}},
link: GetLinks().Login,
template: TmplCreatePassword,
fn: func(_ *settings.Settings) {
req.URL = &url.URL{RawQuery: "token=NOT_EMPTY"}
authService = &authServiceMocked{
passwordSet: func(ctx context.Context, s string) bool {
return false
},
validatePasswordCreateToken: func(ctx context.Context, token string) (user *types.User, err error) {
return nil, errors.New("invalid token")
},
}
},
},
{
name: "invalid password create request",
payload: map[string]string(nil),
alerts: []request.Alert{{Type: "warning", Text: "create-password.alerts.invalid-expired-password-token"}},
link: GetLinks().Login,
template: TmplCreatePassword,
fn: func(_ *settings.Settings) {
req.URL = &url.URL{RawQuery: "token=NOT_EMPTY"}
user = makeMockUser(ctx)
authService = &authServiceMocked{
passwordSet: func(ctx context.Context, s string) bool {
return false
},
validatePasswordCreateToken: func(ctx context.Context, token string) (user *types.User, err error) {
return nil, errors.New("invalid token")
},
}
},
},
}
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
rq := require.New(t)
// reset from previous
req.Form = url.Values{}
req.PostForm = url.Values{}
authSettings := &settings.Settings{}
tc.fn(authSettings)
authReq = prepareClientAuthReq(ctx, req, user)
authHandlers = prepareClientAuthHandlers(ctx, authService, authSettings)
// unset so we get to the main functionality
authReq.AuthUser = nil
err := authHandlers.createPasswordForm(authReq)
rq.NoError(err)
rq.Equal(tc.template, authReq.Template)
rq.Equal(tc.payload, authReq.GetKV())
rq.Equal(tc.alerts, authReq.NewAlerts)
rq.Equal(tc.link, authReq.RedirectTo)
})
}
}
func Test_createPasswordProc(t *testing.T) {
var (
ctx = context.Background()
user = makeMockUser(ctx)
req = &http.Request{}
authService authService
authHandlers *AuthHandlers
authReq *request.AuthReq
)
tcc := []testingExpect{
{
name: "create password success",
payload: map[string]string(nil),
alerts: []request.Alert{{Type: "primary", Text: "create-password.alerts.password-create-success", Html: ""}},
link: GetLinks().Profile,
fn: func(_ *settings.Settings) {
authService = &authServiceMocked{
setPassword: func(ctx context.Context, userID uint64, password string) (err error) {
return nil
},
}
},
},
{
name: "create password disabled",
payload: map[string]string(nil),
alerts: []request.Alert{{Type: "danger", Text: "create-password.alert.password-create-disabled", Html: ""}},
link: GetLinks().Login,
fn: func(_ *settings.Settings) {
authService = &authServiceMocked{
setPassword: func(ctx context.Context, userID uint64, password string) (err error) {
return service.AuthErrPasswordCreateDisabledByConfig()
},
}
},
},
}
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
rq := require.New(t)
authSettings := &settings.Settings{}
tc.fn(authSettings)
authReq = prepareClientAuthReq(ctx, req, user)
authHandlers = prepareClientAuthHandlers(ctx, authService, authSettings)
err := authHandlers.createPasswordProc(authReq)
rq.NoError(err)
rq.Equal(tc.payload, authReq.GetKV())
rq.Equal(tc.alerts, authReq.NewAlerts)
rq.Equal(tc.link, authReq.RedirectTo)
})
}
}
+17 -17
View File
@@ -47,30 +47,30 @@ func (h *AuthHandlers) resetPasswordForm(req *request.AuthReq) (err error) {
req.Template = TmplResetPassword
if req.AuthUser == nil {
// user not set, expecting valid token in URL
if token := req.Request.URL.Query().Get("token"); len(token) > 0 {
var user *types.User
// user not set, expecting valid token in URL
if token := req.Request.URL.Query().Get("token"); len(token) > 0 {
var user *types.User
user, err = h.AuthService.ValidatePasswordResetToken(req.Context(), token)
if err == nil {
// login user
req.AuthUser = request.NewAuthUser(h.Settings, user, false, h.Opt.SessionLifetime)
user, err = h.AuthService.ValidatePasswordResetToken(req.Context(), token)
if err == nil {
// login user
req.AuthUser = request.NewAuthUser(h.Settings, user, false, h.Opt.SessionLifetime)
// redirect back to self (but without token and with user in session
h.Log.Debug("valid password reset token found, refreshing page with stored user")
req.RedirectTo = GetLinks().ResetPassword
req.AuthUser.Save(req.Session)
return nil
}
// redirect back to self (but without token and with user in session
h.Log.Debug("valid password reset token found, refreshing page with stored user")
req.RedirectTo = GetLinks().ResetPassword
req.AuthUser.Save(req.Session)
return nil
}
}
if req.AuthUser == nil || err != nil {
h.Log.Warn("invalid password reset token used", zap.Error(err))
req.RedirectTo = GetLinks().RequestPasswordReset
t := translator(req, "auth")
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "warning",
Text: t("password_reset_requested.alert.inv-exp-passw-token"),
Text: t("password-reset-requested.alerts.invalid-expired-password-token"),
})
}
@@ -87,7 +87,7 @@ func (h *AuthHandlers) resetPasswordProc(req *request.AuthReq) (err error) {
t := translator(req, "auth")
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "primary",
Text: t("password_reset_requested.alert.pass-reset-success"),
Text: t("password-reset-requested.alerts.password-reset-success"),
})
req.RedirectTo = GetLinks().Profile
@@ -121,6 +121,6 @@ func (h *AuthHandlers) passwordResetDisabledAlert(req *request.AuthReq) {
t := translator(req, "auth")
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "danger",
Text: t("password_reset_requested.alert.pass-reset-disabled"),
Text: t("password-reset-requested.alerts.password-reset-disabled"),
})
}
+4 -4
View File
@@ -80,7 +80,7 @@ func Test_resetPasswordForm(t *testing.T) {
{
name: "invalid password reset token",
payload: map[string]string(nil),
alerts: []request.Alert{{Type: "warning", Text: "password_reset_requested.alert.inv-exp-passw-token"}},
alerts: []request.Alert{{Type: "warning", Text: "password-reset-requested.alerts.invalid-expired-password-token"}},
link: GetLinks().RequestPasswordReset,
template: TmplResetPassword,
fn: func(_ *settings.Settings) {
@@ -153,7 +153,7 @@ func Test_requestPasswordReset(t *testing.T) {
{
name: "request reset disabled",
payload: map[string]string(nil),
alerts: []request.Alert{{Type: "danger", Text: "password_reset_requested.alert.pass-reset-disabled"}},
alerts: []request.Alert{{Type: "danger", Text: "password-reset-requested.alerts.password-reset-disabled"}},
link: GetLinks().Login,
fn: func(_ *settings.Settings) {
authService = &authServiceMocked{
@@ -207,7 +207,7 @@ func Test_requestPasswordProc(t *testing.T) {
{
name: "reset password success",
payload: map[string]string(nil),
alerts: []request.Alert{{Type: "primary", Text: "password_reset_requested.alert.pass-reset-success", Html: ""}},
alerts: []request.Alert{{Type: "primary", Text: "password-reset-requested.alerts.password-reset-success", Html: ""}},
link: GetLinks().Profile,
fn: func(_ *settings.Settings) {
authService = &authServiceMocked{
@@ -220,7 +220,7 @@ func Test_requestPasswordProc(t *testing.T) {
{
name: "reset password disabled",
payload: map[string]string(nil),
alerts: []request.Alert{{Type: "danger", Text: "password_reset_requested.alert.pass-reset-disabled", Html: ""}},
alerts: []request.Alert{{Type: "danger", Text: "password-reset-requested.alerts.password-reset-disabled", Html: ""}},
link: GetLinks().Login,
fn: func(_ *settings.Settings) {
authService = &authServiceMocked{
+1 -1
View File
@@ -135,7 +135,7 @@ func (h *AuthHandlers) confirmEmail(req *request.AuthReq) (err error) {
t := translator(req, "auth")
req.NewAlerts = append(req.NewAlerts, request.Alert{
Type: "warning",
Text: t("signup.alerts.inv-or-exp-token"),
Text: t("signup.alerts.invalid-expired-token"),
})
return nil
+2
View File
@@ -35,6 +35,7 @@ type (
ChangePassword(ctx context.Context, userID uint64, oldPassword, newPassword string) (err error)
ValidateEmailConfirmationToken(ctx context.Context, token string) (user *types.User, err error)
ValidatePasswordResetToken(ctx context.Context, token string) (user *types.User, err error)
ValidatePasswordCreateToken(ctx context.Context, token string) (user *types.User, err error)
SendEmailAddressConfirmationToken(ctx context.Context, u *types.User) (err error)
SendPasswordResetToken(ctx context.Context, email string) (err error)
GetProviders() types.ExternalAuthProviderSet
@@ -109,6 +110,7 @@ type (
const (
TmplAuthorizedClients = "authorized-clients.html.tpl"
TmplChangePassword = "change-password.html.tpl"
TmplCreatePassword = "create-password.html.tpl"
TmplLogin = "login.html.tpl"
TmplLogout = "logout.html.tpl"
TmplOAuth2AuthorizeClient = "oauth2-authorize-client.html.tpl"
+2
View File
@@ -12,6 +12,7 @@ type (
Security,
ChangePassword,
CreatePassword,
RequestPasswordReset,
PasswordResetRequested,
@@ -58,6 +59,7 @@ func GetLinks() Links {
Login: b + "auth/login",
Security: b + "auth/security",
ChangePassword: b + "auth/change-password",
CreatePassword: b + "auth/create-password",
RequestPasswordReset: b + "auth/request-password-reset",
PasswordResetRequested: b + "auth/password-reset-requested",
ResetPassword: b + "auth/reset-password",
+17
View File
@@ -85,8 +85,10 @@ type (
internalLogin func(context.Context, string, string) (u *types.User, err error)
setPassword func(context.Context, uint64, string) (err error)
changePassword func(context.Context, uint64, string, string) (err error)
createPassword func(context.Context, uint64, string) (err error)
validateEmailConfirmationToken func(context.Context, string) (user *types.User, err error)
validatePasswordResetToken func(context.Context, string) (user *types.User, err error)
validatePasswordCreateToken func(context.Context, string) (user *types.User, err error)
sendEmailAddressConfirmationToken func(context.Context, *types.User) (err error)
sendPasswordResetToken func(context.Context, string) (err error)
passwordSet func(context.Context, string) bool
@@ -138,6 +140,10 @@ func (s authServiceMocked) ValidatePasswordResetToken(ctx context.Context, token
return s.validatePasswordResetToken(ctx, token)
}
func (s authServiceMocked) ValidatePasswordCreateToken(ctx context.Context, token string) (user *types.User, err error) {
return s.validatePasswordCreateToken(ctx, token)
}
func (s authServiceMocked) PasswordSet(ctx context.Context, email string) (is bool) {
return s.passwordSet(ctx, email)
}
@@ -248,6 +254,13 @@ func (ma mockAuthService) ValidatePasswordResetToken(ctx context.Context, token
return &types.User{ID: 123}, nil
}
//
// Mocking authService
//
func (ma mockAuthService) ValidatePasswordCreateToken(ctx context.Context, token string) (*types.User, error) {
return &types.User{ID: 123}, nil
}
func (ma mockAuthService) SendEmailOTP(ctx context.Context) error {
return nil
}
@@ -268,6 +281,10 @@ func (m mockNotificationService) PasswordReset(ctx context.Context, emailAddress
return nil
}
func (m mockNotificationService) PasswordCreate(ctx context.Context, emailAddress string, token string) (string, error) {
return "", nil
}
func (m mockNotificationService) EmailOTP(ctx context.Context, emailAddress string, code string) error {
return nil
}
+2
View File
@@ -80,6 +80,8 @@ func (h *AuthHandlers) MountHttpRoutes(r chi.Router) {
r.Post(tbp(l.Security), h.handle(authOnly(h.securityProc)))
r.Get(tbp(l.ChangePassword), h.handle(h.onlyIfLocalEnabled(authOnly(h.changePasswordForm))))
r.Post(tbp(l.ChangePassword), h.handle(h.onlyIfLocalEnabled(authOnly(h.changePasswordProc))))
r.Get(tbp(l.CreatePassword), h.handle(h.onlyIfPasswordCreateEnabled(h.createPasswordForm)))
r.Post(tbp(l.CreatePassword), h.handle(h.onlyIfPasswordCreateEnabled(h.createPasswordProc)))
r.Get(tbp(l.MfaTotpNewSecret), h.handle(partAuthOnly(h.mfaTotpConfigForm)))
r.Post(tbp(l.MfaTotpNewSecret), h.handle(partAuthOnly(h.mfaTotpConfigProc)))
+1
View File
@@ -6,6 +6,7 @@ type (
SignupEnabled bool
EmailConfirmationRequired bool
PasswordResetEnabled bool
PasswordCreateEnabled bool
ExternalEnabled bool
SplitCredentialsCheck bool
Providers []Provider
+30 -4
View File
@@ -9,10 +9,12 @@ import (
// credits: https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang
const (
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
letterSpecials = "~=+%^*/()[]{}/!@#$?|"
letterDigits = "0123456789"
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + letterDigits
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
var randSrc = rand.NewSource(time.Now().UnixNano())
@@ -39,3 +41,27 @@ func Bytes(n int) []byte {
return b
}
// Password generates a random ASCII string with at least one digit and one special character
func Password(n int) string {
mu.Lock()
defer mu.Unlock()
b := make([]byte, n)
for i := 0; i < n; i++ {
var s string
if i == 0 {
s = letterDigits
} else if i == 1 {
s = letterSpecials
} else {
s = letterBytes
}
b[i] = s[rand.Intn(len(s))]
}
rand.Shuffle(len(b), func(i, j int) {
b[i], b[j] = b[j], b[i]
})
return string(b) // E.g. "3i[g0|)z"
}
+26 -5
View File
@@ -18,9 +18,10 @@ import (
func Users(ctx context.Context, app serviceInitializer) *cobra.Command {
var (
flagNoPassword bool
flagPassword string
flagRoles []string
flagNoPassword bool
flagPassword string
flagMakePasswordLink bool
flagRoles []string
)
// User management commands.
@@ -101,6 +102,9 @@ func Users(ctx context.Context, app serviceInitializer) *cobra.Command {
err error
// url for to create password(make-password-link)
url string
// Use provided password...
password = []byte(flagPassword)
@@ -108,12 +112,11 @@ func Users(ctx context.Context, app serviceInitializer) *cobra.Command {
mm types.RoleMemberSet
)
if !flagNoPassword && len(password) == 0 {
if !flagNoPassword && !flagMakePasswordLink && len(password) == 0 {
cmd.Print("Set password: ")
if password, err = terminal.ReadPassword(syscall.Stdin); err != nil {
cli.HandleError(err)
}
}
if len(password) > 0 && !authSvc.CheckPasswordStrength(string(password)) {
@@ -134,7 +137,19 @@ func Users(ctx context.Context, app serviceInitializer) *cobra.Command {
cli.HandleError(err)
}
// generate the create password link of user
if !flagNoPassword && len(password) == 0 && flagMakePasswordLink {
url, err = authSvc.GeneratePasswordCreateToken(ctx, user.Email)
if err != nil {
cli.HandleError(err)
}
}
cmd.Printf("User created [%d].\n", user.ID)
if flagMakePasswordLink && len(url) > 0 {
cmd.Println("Set password by clicking here:")
cmd.Printf("%s\n", url)
}
if len(mm) > 0 {
_ = mm.Walk(func(m *types.RoleMember) error {
@@ -165,6 +180,12 @@ func Users(ctx context.Context, app serviceInitializer) *cobra.Command {
"",
"Provide password (as alternative to interactive way)")
addCmd.Flags().BoolVar(
&flagMakePasswordLink,
"make-password-link",
false,
"Provide link to create password")
addCmd.Flags().StringSliceVar(
&flagRoles,
"role",
+57
View File
@@ -48,6 +48,7 @@ const (
credentialsTypeEmailAuthToken = "email-authentication-token"
credentialsTypeResetPasswordToken = "password-reset-token"
credentialsTypeResetPasswordTokenExchanged = "password-reset-token-exchanged"
credentialsTypeCreatePasswordToken = "password-create-token"
credentialsTypeMfaTotpSecret = "mfa-totp-secret"
credentialsTypeMFAEmailOTP = "mfa-email-otp"
@@ -676,6 +677,11 @@ func (svc auth) ValidatePasswordResetToken(ctx context.Context, token string) (u
return svc.loadFromTokenAndConfirmEmail(ctx, token, credentialsTypeResetPasswordToken)
}
// ValidatePasswordCreateToken validates password create token
func (svc auth) ValidatePasswordCreateToken(ctx context.Context, token string) (user *types.User, err error) {
return svc.loadFromTokenAndConfirmEmail(ctx, token, credentialsTypeCreatePasswordToken)
}
// PasswordSet checks and returns true if user's password is set
// False is also returned in case user does not exist.
func (svc *auth) PasswordSet(ctx context.Context, email string) (is bool) {
@@ -840,6 +846,47 @@ func (svc auth) sendPasswordResetToken(ctx context.Context, u *types.User) (err
return svc.notifications.PasswordReset(ctx, u.Email, token)
}
// GeneratePasswordCreateToken generates password create token
func (svc auth) GeneratePasswordCreateToken(ctx context.Context, email string) (url string, err error) {
var (
u *types.User
aam = &authActionProps{
user: u,
email: email,
}
)
err = func() error {
if !svc.settings.Auth.Internal.Enabled || !svc.settings.Auth.Internal.PasswordCreate.Enabled {
return AuthErrPasswordCreateDisabledByConfig(aam)
}
if u, err = store.LookupUserByEmail(ctx, svc.store, email); err != nil {
return err
}
ctx = internalAuth.SetIdentityToContext(ctx, u)
if url, err = svc.sendPasswordCreateToken(ctx, u); err != nil {
return err
}
return nil
}()
return url, svc.recordAction(ctx, aam, AuthActionGeneratePasswordCreateToken, err)
}
func (svc auth) sendPasswordCreateToken(ctx context.Context, u *types.User) (url string, err error) {
token, err := svc.createUserToken(ctx, u, credentialsTypeCreatePasswordToken)
if err != nil {
return
}
return svc.notifications.PasswordCreate(token)
}
// procLogin fn performs standard validation, credentials-update tasks and triggers events
func (svc auth) procLogin(ctx context.Context, s store.Storer, u *types.User, c *types.Credentials, p *types.AuthProvider) (err error) {
if err = svc.eventbus.WaitFor(ctx, event.AuthBeforeLogin(u, p)); err != nil {
@@ -960,6 +1007,16 @@ func (svc auth) createUserToken(ctx context.Context, u *types.User, kind string)
// random number, 6 chars
token = fmt.Sprintf("%06d", rand2.Int())[0:6]
case credentialsTypeCreatePasswordToken:
expSec := svc.settings.Auth.Internal.PasswordCreate.Expires
if expSec == 0 {
expSec = 24
}
expiresAt = now().Add(time.Hour * time.Duration(expSec))
// random password string, "3i[g0|)z"
token = fmt.Sprintf("%s", rand.Password(credentialsTokenLength))
default:
// 1h expiration for all tokens send via email
expiresAt = now().Add(time.Minute * 60)
+88
View File
@@ -456,6 +456,26 @@ func AuthActionExchangePasswordResetToken(props ...*authActionProps) *authAction
return a
}
// AuthActionGeneratePasswordCreateToken returns "system:auth.generatePasswordCreateToken" action
//
// This function is auto-generated.
//
func AuthActionGeneratePasswordCreateToken(props ...*authActionProps) *authAction {
a := &authAction{
timestamp: time.Now(),
resource: "system:auth",
action: "generatePasswordCreateToken",
log: "password create token generated for {{email}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// AuthActionAutoPromote returns "system:auth.autoPromote" action
//
// This function is auto-generated.
@@ -1092,6 +1112,40 @@ func AuthErrPasswodResetFailedOldPasswordCheckFailed(mm ...*authActionProps) *er
return e
}
// AuthErrPasswordCreateFailedForUnknownUser returns "system:auth.passwordCreateFailedForUnknownUser" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrPasswordCreateFailedForUnknownUser(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("failed to create password for the unknown user", nil),
errors.Meta("type", "passwordCreateFailedForUnknownUser"),
errors.Meta("resource", "system:auth"),
errors.Meta(authPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "system"),
errors.Meta(locale.ErrorMetaKey{}, "auth.errors.passwordCreateFailedForUnknownUser"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// AuthErrPasswordResetDisabledByConfig returns "system:auth.passwordResetDisabledByConfig" as *errors.Error
//
//
@@ -1126,6 +1180,40 @@ func AuthErrPasswordResetDisabledByConfig(mm ...*authActionProps) *errors.Error
return e
}
// AuthErrPasswordCreateDisabledByConfig returns "system:auth.passwordCreateDisabledByConfig" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrPasswordCreateDisabledByConfig(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("password create is disabled", nil),
errors.Meta("type", "passwordCreateDisabledByConfig"),
errors.Meta("resource", "system:auth"),
errors.Meta(authPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "system"),
errors.Meta(locale.ErrorMetaKey{}, "auth.errors.passwordCreateDisabledByConfig"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// AuthErrPasswordNotSecure returns "system:auth.passwordNotSecure" as *errors.Error
//
//
+9
View File
@@ -56,6 +56,9 @@ actions:
- action: exchangePasswordResetToken
log: "password reset token exchanged"
- action: generatePasswordCreateToken
log: "password create token generated for {{email}}"
- action: autoPromote
log: "auto-promoted to {{role}}"
@@ -128,9 +131,15 @@ errors:
- error: passwodResetFailedOldPasswordCheckFailed
message: "failed to change password, old password does not match"
- error: passwordCreateFailedForUnknownUser
message: "failed to create password for the unknown user"
- error: passwordResetDisabledByConfig
message: "password reset is disabled"
- error: passwordCreateDisabledByConfig
message: "password create is disabled"
- error: passwordNotSecure
message: "provided password is not secure; use longer password with more non-alphanumeric character"
+5
View File
@@ -29,6 +29,7 @@ type (
EmailOTP(ctx context.Context, emailAddress string, otp string) error
EmailConfirmation(ctx context.Context, emailAddress string, url string) error
PasswordReset(ctx context.Context, emailAddress string, url string) error
PasswordCreate(url string) (string, error)
}
)
@@ -63,6 +64,10 @@ func (svc authNotification) PasswordReset(ctx context.Context, emailAddress stri
})
}
func (svc authNotification) PasswordCreate(token string) (string, error) {
return fmt.Sprintf("%s/create-password?token=%s", svc.opt.BaseURL, url.QueryEscape(token)), nil
}
func (svc authNotification) newMail() *gomail.Message {
var (
m = mail.New()
+9
View File
@@ -50,6 +50,15 @@ type (
// Can users reset their passwords
PasswordReset struct{ Enabled bool } `kv:"password-reset"`
// PasswordCreate setting for create password for user via generated link with token
// If user has no password then link redirects to create password page
// Otherwise it redirects to profile page of that user
// link can be generated through useradd cli command with `make-password-link` flag
PasswordCreate struct {
Enabled bool
Expires uint
} `kv:"password-create"`
// Splits credentials check into 2 parts
// If user has password credentials it offers him to enter the password
// Otherwise we offer the user to choose among the enabled external providers