From 3f83a3362273f5e1250b37725de4be686c480faa Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Tue, 9 Apr 2019 22:35:39 +0200 Subject: [PATCH] Change password reset flow, add token exchange step --- api/system/spec.json | 20 ++++++++++-- api/system/spec/auth_internal.json | 20 ++++++++++-- docs/system/README.md | 23 +++++++++++--- system/internal/service/auth.go | 38 +++++++++++++++++++--- system/rest/auth_internal.go | 17 ++++++++++ system/rest/handlers/auth_internal.go | 22 +++++++++---- system/rest/request/auth_internal.go | 46 +++++++++++++++++++++++++++ 7 files changed, 168 insertions(+), 18 deletions(-) diff --git a/api/system/spec.json b/api/system/spec.json index 53ed164be..4a928d3c6 100644 --- a/api/system/spec.json +++ b/api/system/spec.json @@ -93,7 +93,7 @@ { "name": "requestPasswordReset", "method": "POST", - "title": "Request password reset token", + "title": "Request password reset token (via email)", "path": "/request-password-reset", "parameters": { "post": [ @@ -106,10 +106,26 @@ ] } }, + { + "name": "exchangePasswordResetToken", + "method": "POST", + "title": "Exchange password reset token for new token and user info", + "path": "/exchange-password-reset-token", + "parameters": { + "post": [ + { + "name": "token", + "type": "string", + "required": true, + "title": "Token" + } + ] + } + }, { "name": "resetPassword", "method": "POST", - "title": "Reset password with token", + "title": "Reset password with exchanged password reset token", "path": "/reset-password", "parameters": { "post": [ diff --git a/api/system/spec/auth_internal.json b/api/system/spec/auth_internal.json index 2cee32c69..4166dabab 100644 --- a/api/system/spec/auth_internal.json +++ b/api/system/spec/auth_internal.json @@ -72,7 +72,7 @@ { "Name": "requestPasswordReset", "Method": "POST", - "Title": "Request password reset token", + "Title": "Request password reset token (via email)", "Path": "/request-password-reset", "Parameters": { "post": [ @@ -85,10 +85,26 @@ ] } }, + { + "Name": "exchangePasswordResetToken", + "Method": "POST", + "Title": "Exchange password reset token for new token and user info", + "Path": "/exchange-password-reset-token", + "Parameters": { + "post": [ + { + "name": "token", + "required": true, + "title": "Token", + "type": "string" + } + ] + } + }, { "Name": "resetPassword", "Method": "POST", - "Title": "Reset password with token", + "Title": "Reset password with exchanged password reset token", "Path": "/reset-password", "Parameters": { "post": [ diff --git a/docs/system/README.md b/docs/system/README.md index d9ba058fe..37e5fe017 100644 --- a/docs/system/README.md +++ b/docs/system/README.md @@ -133,8 +133,9 @@ | ------ | -------- | ------- | | `POST` | `/auth/internal/login` | Login user | | `POST` | `/auth/internal/signup` | User signup/registration | -| `POST` | `/auth/internal/request-password-reset` | Request password reset token | -| `POST` | `/auth/internal/reset-password` | Reset password with token | +| `POST` | `/auth/internal/request-password-reset` | Request password reset token (via email) | +| `POST` | `/auth/internal/exchange-password-reset-token` | Exchange password reset token for new token and user info | +| `POST` | `/auth/internal/reset-password` | Reset password with exchanged password reset token | | `POST` | `/auth/internal/confirm-email` | Confirm email with token | | `POST` | `/auth/internal/change-password` | Changes password for current user, requires current password | @@ -171,7 +172,7 @@ | handle | string | POST | User handle | N/A | NO | | name | string | POST | Display name | N/A | NO | -## Request password reset token +## Request password reset token (via email) #### Method @@ -185,7 +186,21 @@ | --------- | ---- | ------ | ----------- | ------- | --------- | | email | string | POST | Email | N/A | YES | -## Reset password with token +## Exchange password reset token for new token and user info + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/auth/internal/exchange-password-reset-token` | HTTP/S | POST | | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| token | string | POST | Token | N/A | YES | + +## Reset password with exchanged password reset token #### Method diff --git a/system/internal/service/auth.go b/system/internal/service/auth.go index 817646055..6cc4a2536 100644 --- a/system/internal/service/auth.go +++ b/system/internal/service/auth.go @@ -42,6 +42,7 @@ type ( ChangePassword(userID uint64, oldPassword, newPassword string) error ValidateEmailConfirmationToken(token string) (user *types.User, err error) + ExchangePasswordResetToken(token string) (user *types.User, exchangedToken string, err error) ValidatePasswordResetToken(token string) (user *types.User, err error) SendEmailAddressConfirmationToken(email string) (err error) SendPasswordResetToken(email string) (err error) @@ -49,9 +50,10 @@ type ( ) const ( - credentialsTypePassword = "password" - credentialsTypeEmailAuthToken = "email-authentication-token" - credentialsTypeResetPasswordToken = "password-reset-token" + credentialsTypePassword = "password" + credentialsTypeEmailAuthToken = "email-authentication-token" + credentialsTypeResetPasswordToken = "password-reset-token" + credentialsTypeResetPasswordTokenExchanged = "password-reset-token-exchanged" credentialsTokenLength = 32 ) @@ -560,7 +562,35 @@ func (svc auth) ValidatePasswordResetToken(token string) (user *types.User, err return nil, errors.New("password reset disabled") } - return svc.loadUserFromToken(token, credentialsTypeResetPasswordToken) + return svc.loadUserFromToken(token, credentialsTypeResetPasswordTokenExchanged) +} + +// ExchangePasswordResetToken exchanges reset password token for a new one and returns it with user info +func (svc auth) ExchangePasswordResetToken(token string) (user *types.User, exchangedToken string, err error) { + if !svc.settings.internalEnabled { + err = errors.New("internal authentication disabled") + return + } + + if !svc.settings.internalPasswordResetEnabled { + err = errors.New("password reset disabled") + return + } + + user, err = svc.loadUserFromToken(token, credentialsTypeResetPasswordToken) + if err != nil { + user = nil + return + } + + exchangedToken, err = svc.createUserToken(user, credentialsTypeResetPasswordTokenExchanged) + if err != nil { + user = nil + exchangedToken = "" + return + } + + return } func (svc auth) SendEmailAddressConfirmationToken(email string) error { diff --git a/system/rest/auth_internal.go b/system/rest/auth_internal.go index 6a44a9ef5..4151db3b7 100644 --- a/system/rest/auth_internal.go +++ b/system/rest/auth_internal.go @@ -19,6 +19,11 @@ type ( User *types.User `json:"user"` } + authPasswordResetTokenExchangeResponse struct { + Token string `json:"token"` + User *types.User `json:"user"` + } + AuthInternal struct { tokenEncoder auth.TokenEncoder authSvc service.AuthService @@ -71,6 +76,18 @@ func (ctrl *AuthInternal) RequestPasswordReset(ctx context.Context, r *request.A return true, ctrl.authSvc.SendPasswordResetToken(r.Email) } +func (ctrl *AuthInternal) ExchangePasswordResetToken(ctx context.Context, r *request.AuthInternalExchangePasswordResetToken) (interface{}, error) { + u, token, err := ctrl.authSvc.ExchangePasswordResetToken(r.Token) + if err != nil { + return nil, err + } + + return authPasswordResetTokenExchangeResponse{ + Token: token, + User: u, + }, nil +} + func (ctrl *AuthInternal) ResetPassword(ctx context.Context, r *request.AuthInternalResetPassword) (interface{}, error) { var u, err = ctrl.authSvc.ValidatePasswordResetToken(r.Token) if err != nil { diff --git a/system/rest/handlers/auth_internal.go b/system/rest/handlers/auth_internal.go index 7b8f9b179..b27e08572 100644 --- a/system/rest/handlers/auth_internal.go +++ b/system/rest/handlers/auth_internal.go @@ -31,6 +31,7 @@ type AuthInternalAPI interface { Login(context.Context, *request.AuthInternalLogin) (interface{}, error) Signup(context.Context, *request.AuthInternalSignup) (interface{}, error) RequestPasswordReset(context.Context, *request.AuthInternalRequestPasswordReset) (interface{}, error) + ExchangePasswordResetToken(context.Context, *request.AuthInternalExchangePasswordResetToken) (interface{}, error) ResetPassword(context.Context, *request.AuthInternalResetPassword) (interface{}, error) ConfirmEmail(context.Context, *request.AuthInternalConfirmEmail) (interface{}, error) ChangePassword(context.Context, *request.AuthInternalChangePassword) (interface{}, error) @@ -38,12 +39,13 @@ type AuthInternalAPI interface { // HTTP API interface type AuthInternal struct { - Login func(http.ResponseWriter, *http.Request) - Signup func(http.ResponseWriter, *http.Request) - RequestPasswordReset func(http.ResponseWriter, *http.Request) - ResetPassword func(http.ResponseWriter, *http.Request) - ConfirmEmail func(http.ResponseWriter, *http.Request) - ChangePassword func(http.ResponseWriter, *http.Request) + Login func(http.ResponseWriter, *http.Request) + Signup func(http.ResponseWriter, *http.Request) + RequestPasswordReset func(http.ResponseWriter, *http.Request) + ExchangePasswordResetToken func(http.ResponseWriter, *http.Request) + ResetPassword func(http.ResponseWriter, *http.Request) + ConfirmEmail func(http.ResponseWriter, *http.Request) + ChangePassword func(http.ResponseWriter, *http.Request) } func NewAuthInternal(ah AuthInternalAPI) *AuthInternal { @@ -69,6 +71,13 @@ func NewAuthInternal(ah AuthInternalAPI) *AuthInternal { return ah.RequestPasswordReset(r.Context(), params) }) }, + ExchangePasswordResetToken: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAuthInternalExchangePasswordResetToken() + resputil.JSON(w, params.Fill(r), func() (interface{}, error) { + return ah.ExchangePasswordResetToken(r.Context(), params) + }) + }, ResetPassword: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewAuthInternalResetPassword() @@ -99,6 +108,7 @@ func (ah *AuthInternal) MountRoutes(r chi.Router, middlewares ...func(http.Handl r.Post("/auth/internal/login", ah.Login) r.Post("/auth/internal/signup", ah.Signup) r.Post("/auth/internal/request-password-reset", ah.RequestPasswordReset) + r.Post("/auth/internal/exchange-password-reset-token", ah.ExchangePasswordResetToken) r.Post("/auth/internal/reset-password", ah.ResetPassword) r.Post("/auth/internal/confirm-email", ah.ConfirmEmail) r.Post("/auth/internal/change-password", ah.ChangePassword) diff --git a/system/rest/request/auth_internal.go b/system/rest/request/auth_internal.go index 45b63b5e3..5e4d49902 100644 --- a/system/rest/request/auth_internal.go +++ b/system/rest/request/auth_internal.go @@ -193,6 +193,52 @@ func (auReq *AuthInternalRequestPasswordReset) Fill(r *http.Request) (err error) var _ RequestFiller = NewAuthInternalRequestPasswordReset() +// AuthInternal exchangePasswordResetToken request parameters +type AuthInternalExchangePasswordResetToken struct { + Token string +} + +func NewAuthInternalExchangePasswordResetToken() *AuthInternalExchangePasswordResetToken { + return &AuthInternalExchangePasswordResetToken{} +} + +func (auReq *AuthInternalExchangePasswordResetToken) Fill(r *http.Request) (err error) { + if strings.ToLower(r.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(r.Body).Decode(auReq) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = r.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := r.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := r.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := post["token"]; ok { + + auReq.Token = val + } + + return err +} + +var _ RequestFiller = NewAuthInternalExchangePasswordResetToken() + // AuthInternal resetPassword request parameters type AuthInternalResetPassword struct { Token string