diff --git a/controllers/accounts.go b/controllers/accounts.go index 71d038e..98491d9 100644 --- a/controllers/accounts.go +++ b/controllers/accounts.go @@ -22,7 +22,7 @@ var ErrIncorrectVerificationIntent = errors.New("incorrect verification intent") type AccountsController struct { opaqueService *services.OpaqueService validate *validator.Validate - jwtService *services.JWTService + jwtService *services.JWTService ds *datastore.Datastore } @@ -149,23 +149,55 @@ func NewAccountsController(opaqueService *services.OpaqueService, jwtService *se return &AccountsController{ opaqueService: opaqueService, validate: validator.New(validator.WithRequiredStructEnabled()), - jwtService: jwtService, + jwtService: jwtService, ds: ds, } } -func (ac *AccountsController) Router(permissiveAuthMiddleware func(http.Handler) http.Handler, verificationAuthMiddleware func(http.Handler) http.Handler) chi.Router { +func (ac *AccountsController) Router(verificationMiddleware func(http.Handler) http.Handler) chi.Router { r := chi.NewRouter() - r.With(verificationAuthMiddleware).Post("/setup/init", ac.SetupPasswordInit) - r.With(verificationAuthMiddleware).Post("/setup/finalize", ac.SetupPasswordFinalize) - r.With(permissiveAuthMiddleware).Post("/change_pwd/init", ac.ChangePasswordInit) - r.With(permissiveAuthMiddleware).Post("/change_pwd/finalize", ac.ChangePasswordFinalize) + r.With(verificationMiddleware).Post("/password/init", ac.SetupPasswordInit) + r.With(verificationMiddleware).Post("/password/finalize", ac.SetupPasswordFinalize) return r } -func (ac *AccountsController) setupPasswordInitHelper(email string, w http.ResponseWriter, r *http.Request) { +func checkVerificationStatusAndIntent(w http.ResponseWriter, r *http.Request, verification *datastore.Verification) bool { + if !verification.Verified { + util.RenderErrorResponse(w, r, http.StatusForbidden, ErrEmailNotVerified) + return false + } + + if verification.Intent != registrationIntent && verification.Intent != resetIntent && verification.Intent != changePasswordIntent { + util.RenderErrorResponse(w, r, http.StatusForbidden, ErrIncorrectVerificationIntent) + return false + } + return true +} + +// @Summary Initialize password setup +// @Description Start the password setup process using OPAQUE protocol. +// @Description If `serializeResponse` is set to true, the `serializedResponse` field will be populated +// @Description in the response, with other fields omitted. +// @Tags Accounts +// @Accept json +// @Produce json +// @Param Authorization header string true "Bearer + verification token" +// @Param request body RegistrationRequest true "Registration request" +// @Success 200 {object} RegistrationResponse +// @Failure 400 {object} util.ErrorResponse +// @Failure 401 {object} util.ErrorResponse +// @Failure 403 {object} util.ErrorResponse +// @Failure 500 {object} util.ErrorResponse +// @Router /v2/accounts/password/init [post] +func (ac *AccountsController) SetupPasswordInit(w http.ResponseWriter, r *http.Request) { + verification := r.Context().Value(middleware.ContextVerification).(*datastore.Verification) + + if !checkVerificationStatusAndIntent(w, r, verification) { + return + } + var requestData RegistrationRequest if err := render.DecodeJSON(r.Body, &requestData); err != nil { util.RenderErrorResponse(w, r, http.StatusBadRequest, err) @@ -183,7 +215,7 @@ func (ac *AccountsController) setupPasswordInitHelper(email string, w http.Respo return } - opaqueResponse, err := ac.opaqueService.SetupPasswordInit(email, opaqueReq) + opaqueResponse, err := ac.opaqueService.SetupPasswordInit(verification.Email, opaqueReq) if err != nil { util.RenderErrorResponse(w, r, http.StatusInternalServerError, err) return @@ -199,25 +231,47 @@ func (ac *AccountsController) setupPasswordInitHelper(email string, w http.Respo render.JSON(w, r, response) } -func (ac *AccountsController) setupPasswordFinalizeHelper(email string, w http.ResponseWriter, r *http.Request) *PasswordFinalizeResponse { +// @Summary Finalize password setup +// @Description Complete the password setup process and return auth token. +// @Description Either `publicKey`, `maskingKey` and `envelope` must be provided together, +// @Description or `serializedRecord` must be provided. +// @Tags Accounts +// @Accept json +// @Produce json +// @Param Authorization header string true "Bearer + verification token" +// @Param request body RegistrationRecord true "Registration record" +// @Success 200 {object} PasswordFinalizeResponse +// @Failure 400 {object} util.ErrorResponse +// @Failure 401 {object} util.ErrorResponse +// @Failure 403 {object} util.ErrorResponse +// @Failure 404 {object} util.ErrorResponse +// @Failure 500 {object} util.ErrorResponse +// @Router /v2/accounts/password/finalize [post] +func (ac *AccountsController) SetupPasswordFinalize(w http.ResponseWriter, r *http.Request) { + verification := r.Context().Value(middleware.ContextVerification).(*datastore.Verification) + + if !checkVerificationStatusAndIntent(w, r, verification) { + return + } + var requestData RegistrationRecord if err := render.DecodeJSON(r.Body, &requestData); err != nil { util.RenderErrorResponse(w, r, http.StatusBadRequest, err) - return nil + return } if err := ac.validate.Struct(requestData); err != nil { util.RenderErrorResponse(w, r, http.StatusBadRequest, err) - return nil + return } opaqueRecord, err := requestData.ToOpaqueRecord(ac.opaqueService) if err != nil { util.RenderErrorResponse(w, r, http.StatusBadRequest, err) - return nil + return } - account, err := ac.opaqueService.SetupPasswordFinalize(email, opaqueRecord) + account, err := ac.opaqueService.SetupPasswordFinalize(verification.Email, opaqueRecord) if err != nil { switch { case errors.Is(err, datastore.ErrRegistrationStateNotFound): @@ -227,89 +281,18 @@ func (ac *AccountsController) setupPasswordFinalizeHelper(email string, w http.R default: util.RenderErrorResponse(w, r, http.StatusInternalServerError, err) } - return nil + return } session, err := ac.ds.CreateSession(account.ID, datastore.PasswordAuthSessionVersion, r.UserAgent()) if err != nil { util.RenderErrorResponse(w, r, http.StatusInternalServerError, err) - return nil + return } authToken, err := ac.jwtService.CreateAuthToken(session.ID) if err != nil { util.RenderErrorResponse(w, r, http.StatusInternalServerError, err) - return nil - } - - return &PasswordFinalizeResponse{ - AuthToken: authToken, - } -} - -func checkVerificationStatusAndIntent(w http.ResponseWriter, r *http.Request, verification *datastore.Verification) bool { - if !verification.Verified { - util.RenderErrorResponse(w, r, http.StatusForbidden, ErrEmailNotVerified) - return false - } - - if verification.Intent != registrationIntent && verification.Intent != resetIntent { - util.RenderErrorResponse(w, r, http.StatusForbidden, ErrIncorrectVerificationIntent) - return false - } - return true -} - -// @Summary Initialize password setup -// @Description Start the password setup process using OPAQUE protocol. -// @Description If `serializeResponse` is set to true, the `serializedResponse` field will be populated -// @Description in the response, with other fields omitted. -// @Tags Accounts -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer + verification token" -// @Param request body RegistrationRequest true "Registration request" -// @Success 200 {object} RegistrationResponse -// @Failure 400 {object} util.ErrorResponse -// @Failure 401 {object} util.ErrorResponse -// @Failure 403 {object} util.ErrorResponse -// @Failure 500 {object} util.ErrorResponse -// @Router /v2/accounts/setup/init [post] -func (ac *AccountsController) SetupPasswordInit(w http.ResponseWriter, r *http.Request) { - verification := r.Context().Value(middleware.ContextVerification).(*datastore.Verification) - - if !checkVerificationStatusAndIntent(w, r, verification) { - return - } - - ac.setupPasswordInitHelper(verification.Email, w, r) -} - -// @Summary Finalize password setup -// @Description Complete the password setup process and return auth token. -// @Description Either `publicKey`, `maskingKey` and `envelope` must be provided together, -// @Description or `serializedRecord` must be provided. -// @Tags Accounts -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer + verification token" -// @Param request body RegistrationRecord true "Registration record" -// @Success 200 {object} PasswordFinalizeResponse -// @Failure 400 {object} util.ErrorResponse -// @Failure 401 {object} util.ErrorResponse -// @Failure 403 {object} util.ErrorResponse -// @Failure 404 {object} util.ErrorResponse -// @Failure 500 {object} util.ErrorResponse -// @Router /v2/accounts/setup/finalize [post] -func (ac *AccountsController) SetupPasswordFinalize(w http.ResponseWriter, r *http.Request) { - verification := r.Context().Value(middleware.ContextVerification).(*datastore.Verification) - - if !checkVerificationStatusAndIntent(w, r, verification) { - return - } - - response := ac.setupPasswordFinalizeHelper(verification.Email, w, r) - if response == nil { return } @@ -319,53 +302,7 @@ func (ac *AccountsController) SetupPasswordFinalize(w http.ResponseWriter, r *ht } render.Status(r, http.StatusOK) - render.JSON(w, r, response) -} - -// @Summary Initialize password change -// @Description Start the password change process using OPAQUE protocol. -// @Description This endpoint should also be used to upgrade a Phase 1 account to a Phase 2 account. -// @Description If `serializeResponse` is set to true, the `serializedResponse` field will be populated -// @Description in the response, with other fields omitted. -// @Tags Accounts -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer + auth token" -// @Param request body RegistrationRequest true "Registration request" -// @Success 200 {object} RegistrationResponse -// @Failure 400 {object} util.ErrorResponse -// @Failure 401 {object} util.ErrorResponse -// @Failure 500 {object} util.ErrorResponse -// @Router /v2/accounts/change_pwd/init [post] -func (ac *AccountsController) ChangePasswordInit(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value(middleware.ContextSession).(*datastore.Session) - - ac.setupPasswordInitHelper(session.Account.Email, w, r) -} - -// @Summary Finalize password setup -// @Description Complete the password change process and return auth token -// @Description Either `publicKey`, `maskingKey` and `envelope` must be provided together, -// @Description or `serializedRecord` must be provided. -// @Tags Accounts -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer + auth token" -// @Param request body RegistrationRecord true "Registration record" -// @Success 200 {object} PasswordFinalizeResponse -// @Failure 400 {object} util.ErrorResponse -// @Failure 401 {object} util.ErrorResponse -// @Failure 404 {object} util.ErrorResponse -// @Failure 500 {object} util.ErrorResponse -// @Router /v2/accounts/change_pwd/finalize [post] -func (ac *AccountsController) ChangePasswordFinalize(w http.ResponseWriter, r *http.Request) { - session := r.Context().Value(middleware.ContextSession).(*datastore.Session) - - response := ac.setupPasswordFinalizeHelper(session.Account.Email, w, r) - if response == nil { - return - } - - render.Status(r, http.StatusOK) - render.JSON(w, r, response) + render.JSON(w, r, &PasswordFinalizeResponse{ + AuthToken: authToken, + }) } diff --git a/controllers/auth.go b/controllers/auth.go index d07eeea..966b14d 100644 --- a/controllers/auth.go +++ b/controllers/auth.go @@ -19,7 +19,7 @@ import ( type AuthController struct { opaqueService *services.OpaqueService validate *validator.Validate - jwtService *services.JWTService + jwtService *services.JWTService ds *datastore.Datastore } @@ -175,7 +175,7 @@ func NewAuthController(opaqueService *services.OpaqueService, jwtService *servic return &AuthController{ opaqueService: opaqueService, validate: validator.New(validator.WithRequiredStructEnabled()), - jwtService: jwtService, + jwtService: jwtService, ds: ds, } } diff --git a/controllers/verification.go b/controllers/verification.go index 4da4ec7..9ea6bed 100644 --- a/controllers/verification.go +++ b/controllers/verification.go @@ -26,6 +26,7 @@ const ( verificationIntent = "verification" registrationIntent = "registration" resetIntent = "reset" + changePasswordIntent = "change_password" premiumAuthRedirectURLEnv = "PREMIUM_AUTH_REDIRECT_URL" accountsServiceName = "accounts" @@ -53,7 +54,7 @@ type VerifyInitRequest struct { // Email address to verify Email string `json:"email" validate:"required,email,ascii" example:"test@example.com"` // Purpose of verification (e.g., get auth token, simple verification, registration) - Intent string `json:"intent" validate:"required,oneof=auth_token verification registration reset" example:"registration"` + Intent string `json:"intent" validate:"required,oneof=auth_token verification registration reset change_password" example:"registration"` // Service requesting the verification Service string `json:"service" validate:"required,oneof=accounts premium inbox-aliases" example:"accounts"` // Locale for verification email @@ -127,6 +128,7 @@ func (vc *VerificationController) Router(verificationAuthMiddleware func(http.Ha // @Description - `verification`: After verification, do not create an account, but indicate that the email was verified in the "query result" response. Do not allow registration after verification. // @Description - `registration`: After verification, indicate that the email was verified in the "query result" response. An account may be created by setting a password. // @Description - `reset`: After verification, indicate that the email was verified in the "query result" response. A password may be set for the existing account. +// @Description - `change_password`: After verification, indicate that the email was verified in the "query result" response. A password may be set for the existing account. // @Description // @Description One of the following service names must be provided with the request: `inbox-aliases`, `accounts`, `premium`. // @Tags Email verification @@ -159,7 +161,7 @@ func (vc *VerificationController) VerifyInit(w http.ResponseWriter, r *http.Requ if requestData.Service != inboxAliasesServiceName && requestData.Service != premiumServiceName { intentAllowed = false } - case registrationIntent, resetIntent: + case registrationIntent, resetIntent, changePasswordIntent: if !vc.passwordAuthEnabled || requestData.Service != accountsServiceName { intentAllowed = false } @@ -198,7 +200,9 @@ func (vc *VerificationController) VerifyInit(w http.ResponseWriter, r *http.Requ } var verificationToken *string - if requestData.Intent == authTokenIntent || requestData.Intent == verificationIntent || requestData.Intent == registrationIntent || requestData.Intent == resetIntent { + + switch requestData.Intent { + case authTokenIntent, verificationIntent, registrationIntent, resetIntent, changePasswordIntent: token, err := vc.jwtService.CreateVerificationToken(verification.ID, datastore.VerificationExpiration, requestData.Service) if err != nil { util.RenderErrorResponse(w, r, http.StatusInternalServerError, err) diff --git a/docs/docs.go b/docs/docs.go index a016416..25ed885 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -15,131 +15,7 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/v2/accounts/change_pwd/finalize": { - "post": { - "description": "Complete the password change process and return auth token\nEither ` + "`" + `publicKey` + "`" + `, ` + "`" + `maskingKey` + "`" + ` and ` + "`" + `envelope` + "`" + ` must be provided together,\nor ` + "`" + `serializedRecord` + "`" + ` must be provided.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Accounts" - ], - "summary": "Finalize password setup", - "parameters": [ - { - "type": "string", - "description": "Bearer + auth token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "Registration record", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/controllers.RegistrationRecord" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.PasswordFinalizeResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/util.ErrorResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/util.ErrorResponse" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/util.ErrorResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/util.ErrorResponse" - } - } - } - } - }, - "/v2/accounts/change_pwd/init": { - "post": { - "description": "Start the password change process using OPAQUE protocol.\nThis endpoint should also be used to upgrade a Phase 1 account to a Phase 2 account.\nIf ` + "`" + `serializeResponse` + "`" + ` is set to true, the ` + "`" + `serializedResponse` + "`" + ` field will be populated\nin the response, with other fields omitted.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Accounts" - ], - "summary": "Initialize password change", - "parameters": [ - { - "type": "string", - "description": "Bearer + auth token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "Registration request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/controllers.RegistrationRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.RegistrationResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/util.ErrorResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/util.ErrorResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/util.ErrorResponse" - } - } - } - } - }, - "/v2/accounts/setup/finalize": { + "/v2/accounts/password/finalize": { "post": { "description": "Complete the password setup process and return auth token.\nEither ` + "`" + `publicKey` + "`" + `, ` + "`" + `maskingKey` + "`" + ` and ` + "`" + `envelope` + "`" + ` must be provided together,\nor ` + "`" + `serializedRecord` + "`" + ` must be provided.", "consumes": [ @@ -210,7 +86,7 @@ const docTemplate = `{ } } }, - "/v2/accounts/setup/init": { + "/v2/accounts/password/init": { "post": { "description": "Start the password setup process using OPAQUE protocol.\nIf ` + "`" + `serializeResponse` + "`" + ` is set to true, the ` + "`" + `serializedResponse` + "`" + ` field will be populated\nin the response, with other fields omitted.", "consumes": [ @@ -602,7 +478,7 @@ const docTemplate = `{ }, "/v2/verify/init": { "post": { - "description": "Starts email verification process by sending a verification email\nOne of the following intents must be provided with the request:\n- ` + "`" + `auth_token` + "`" + `: After verification, create an account if one does not exist, and generate an auth token. The token will be available via the \"query result\" endpoint.\n- ` + "`" + `verification` + "`" + `: After verification, do not create an account, but indicate that the email was verified in the \"query result\" response. Do not allow registration after verification.\n- ` + "`" + `registration` + "`" + `: After verification, indicate that the email was verified in the \"query result\" response. An account may be created by setting a password.\n- ` + "`" + `reset` + "`" + `: After verification, indicate that the email was verified in the \"query result\" response. A password may be set for the existing account.\n\nOne of the following service names must be provided with the request: ` + "`" + `inbox-aliases` + "`" + `, ` + "`" + `accounts` + "`" + `, ` + "`" + `premium` + "`" + `.", + "description": "Starts email verification process by sending a verification email\nOne of the following intents must be provided with the request:\n- ` + "`" + `auth_token` + "`" + `: After verification, create an account if one does not exist, and generate an auth token. The token will be available via the \"query result\" endpoint.\n- ` + "`" + `verification` + "`" + `: After verification, do not create an account, but indicate that the email was verified in the \"query result\" response. Do not allow registration after verification.\n- ` + "`" + `registration` + "`" + `: After verification, indicate that the email was verified in the \"query result\" response. An account may be created by setting a password.\n- ` + "`" + `reset` + "`" + `: After verification, indicate that the email was verified in the \"query result\" response. A password may be set for the existing account.\n- ` + "`" + `change_password` + "`" + `: After verification, indicate that the email was verified in the \"query result\" response. A password may be set for the existing account.\n\nOne of the following service names must be provided with the request: ` + "`" + `inbox-aliases` + "`" + `, ` + "`" + `accounts` + "`" + `, ` + "`" + `premium` + "`" + `.", "consumes": [ "application/json" ], @@ -931,7 +807,8 @@ const docTemplate = `{ "auth_token", "verification", "registration", - "reset" + "reset", + "change_password" ], "example": "registration" }, diff --git a/docs/swagger.json b/docs/swagger.json index 29044b9..4b75bc2 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -5,131 +5,7 @@ "contact": {} }, "paths": { - "/v2/accounts/change_pwd/finalize": { - "post": { - "description": "Complete the password change process and return auth token\nEither `publicKey`, `maskingKey` and `envelope` must be provided together,\nor `serializedRecord` must be provided.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Accounts" - ], - "summary": "Finalize password setup", - "parameters": [ - { - "type": "string", - "description": "Bearer + auth token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "Registration record", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/controllers.RegistrationRecord" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.PasswordFinalizeResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/util.ErrorResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/util.ErrorResponse" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/util.ErrorResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/util.ErrorResponse" - } - } - } - } - }, - "/v2/accounts/change_pwd/init": { - "post": { - "description": "Start the password change process using OPAQUE protocol.\nThis endpoint should also be used to upgrade a Phase 1 account to a Phase 2 account.\nIf `serializeResponse` is set to true, the `serializedResponse` field will be populated\nin the response, with other fields omitted.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Accounts" - ], - "summary": "Initialize password change", - "parameters": [ - { - "type": "string", - "description": "Bearer + auth token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "Registration request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/controllers.RegistrationRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.RegistrationResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/util.ErrorResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/util.ErrorResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/util.ErrorResponse" - } - } - } - } - }, - "/v2/accounts/setup/finalize": { + "/v2/accounts/password/finalize": { "post": { "description": "Complete the password setup process and return auth token.\nEither `publicKey`, `maskingKey` and `envelope` must be provided together,\nor `serializedRecord` must be provided.", "consumes": [ @@ -200,7 +76,7 @@ } } }, - "/v2/accounts/setup/init": { + "/v2/accounts/password/init": { "post": { "description": "Start the password setup process using OPAQUE protocol.\nIf `serializeResponse` is set to true, the `serializedResponse` field will be populated\nin the response, with other fields omitted.", "consumes": [ @@ -592,7 +468,7 @@ }, "/v2/verify/init": { "post": { - "description": "Starts email verification process by sending a verification email\nOne of the following intents must be provided with the request:\n- `auth_token`: After verification, create an account if one does not exist, and generate an auth token. The token will be available via the \"query result\" endpoint.\n- `verification`: After verification, do not create an account, but indicate that the email was verified in the \"query result\" response. Do not allow registration after verification.\n- `registration`: After verification, indicate that the email was verified in the \"query result\" response. An account may be created by setting a password.\n- `reset`: After verification, indicate that the email was verified in the \"query result\" response. A password may be set for the existing account.\n\nOne of the following service names must be provided with the request: `inbox-aliases`, `accounts`, `premium`.", + "description": "Starts email verification process by sending a verification email\nOne of the following intents must be provided with the request:\n- `auth_token`: After verification, create an account if one does not exist, and generate an auth token. The token will be available via the \"query result\" endpoint.\n- `verification`: After verification, do not create an account, but indicate that the email was verified in the \"query result\" response. Do not allow registration after verification.\n- `registration`: After verification, indicate that the email was verified in the \"query result\" response. An account may be created by setting a password.\n- `reset`: After verification, indicate that the email was verified in the \"query result\" response. A password may be set for the existing account.\n- `change_password`: After verification, indicate that the email was verified in the \"query result\" response. A password may be set for the existing account.\n\nOne of the following service names must be provided with the request: `inbox-aliases`, `accounts`, `premium`.", "consumes": [ "application/json" ], @@ -921,7 +797,8 @@ "auth_token", "verification", "registration", - "reset" + "reset", + "change_password" ], "example": "registration" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index bd55faf..1b9084d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -161,6 +161,7 @@ definitions: - verification - registration - reset + - change_password example: registration type: string language: @@ -241,96 +242,7 @@ info: contact: {} title: Brave Accounts Service paths: - /v2/accounts/change_pwd/finalize: - post: - consumes: - - application/json - description: |- - Complete the password change process and return auth token - Either `publicKey`, `maskingKey` and `envelope` must be provided together, - or `serializedRecord` must be provided. - parameters: - - description: Bearer + auth token - in: header - name: Authorization - required: true - type: string - - description: Registration record - in: body - name: request - required: true - schema: - $ref: '#/definitions/controllers.RegistrationRecord' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.PasswordFinalizeResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/util.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/util.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/util.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/util.ErrorResponse' - summary: Finalize password setup - tags: - - Accounts - /v2/accounts/change_pwd/init: - post: - consumes: - - application/json - description: |- - Start the password change process using OPAQUE protocol. - This endpoint should also be used to upgrade a Phase 1 account to a Phase 2 account. - If `serializeResponse` is set to true, the `serializedResponse` field will be populated - in the response, with other fields omitted. - parameters: - - description: Bearer + auth token - in: header - name: Authorization - required: true - type: string - - description: Registration request - in: body - name: request - required: true - schema: - $ref: '#/definitions/controllers.RegistrationRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.RegistrationResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/util.ErrorResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/util.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/util.ErrorResponse' - summary: Initialize password change - tags: - - Accounts - /v2/accounts/setup/finalize: + /v2/accounts/password/finalize: post: consumes: - application/json @@ -380,7 +292,7 @@ paths: summary: Finalize password setup tags: - Accounts - /v2/accounts/setup/init: + /v2/accounts/password/init: post: consumes: - application/json @@ -656,6 +568,7 @@ paths: - `verification`: After verification, do not create an account, but indicate that the email was verified in the "query result" response. Do not allow registration after verification. - `registration`: After verification, indicate that the email was verified in the "query result" response. An account may be created by setting a password. - `reset`: After verification, indicate that the email was verified in the "query result" response. A password may be set for the existing account. + - `change_password`: After verification, indicate that the email was verified in the "query result" response. A password may be set for the existing account. One of the following service names must be provided with the request: `inbox-aliases`, `accounts`, `premium`. parameters: diff --git a/main.go b/main.go index 970c132..1c843bc 100644 --- a/main.go +++ b/main.go @@ -52,9 +52,9 @@ func main() { passwordAuthEnabled := os.Getenv(passwordAuthEnabledEnv) == "true" emailAuthDisabled := os.Getenv(emailAuthDisabledEnv) == "true" - minSessionVersion := 1 + minSessionVersion := datastore.EmailAuthSessionVersion if passwordAuthEnabled && emailAuthDisabled { - minSessionVersion = 2 + minSessionVersion = datastore.PasswordAuthSessionVersion } datastore, err := datastore.NewDatastore(minSessionVersion) @@ -82,9 +82,8 @@ func main() { log.Panic().Err(err).Msg("Failed to init OPAQUE service") } - restrictiveAuthMiddleware := middleware.AuthMiddleware(jwtService, datastore, minSessionVersion) - permissiveAuthMiddleware := middleware.AuthMiddleware(jwtService, datastore, 0) - verificationAuthMiddleware := middleware.VerificationAuthMiddleware(jwtService, datastore) + authMiddleware := middleware.AuthMiddleware(jwtService, datastore, minSessionVersion) + verificationMiddleware := middleware.VerificationAuthMiddleware(jwtService, datastore) r := chi.NewRouter() @@ -96,12 +95,12 @@ func main() { r.Use(middleware.LoggerMiddleware) r.Route("/v2", func(r chi.Router) { - r.Mount("/auth", authController.Router(restrictiveAuthMiddleware)) + r.Mount("/auth", authController.Router(authMiddleware)) if passwordAuthEnabled { - r.Mount("/accounts", accountsController.Router(permissiveAuthMiddleware, verificationAuthMiddleware)) + r.Mount("/accounts", accountsController.Router(verificationMiddleware)) } - r.Mount("/verify", verificationController.Router(verificationAuthMiddleware)) - r.Mount("/sessions", sessionsController.Router(restrictiveAuthMiddleware)) + r.Mount("/verify", verificationController.Router(verificationMiddleware)) + r.Mount("/sessions", sessionsController.Router(authMiddleware)) }) if os.Getenv(serveSwaggerEnv) == "true" { diff --git a/misc/test-client-go/main.go b/misc/test-client-go/main.go index 180435b..7de9474 100644 --- a/misc/test-client-go/main.go +++ b/misc/test-client-go/main.go @@ -100,7 +100,7 @@ func register() { "blindedMessage": hex.EncodeToString(blindedMessage), } - resp := postReq(initFields, "http://localhost:8080/v2/accounts/setup/init", &verificationToken) + resp := postReq(initFields, "http://localhost:8080/v2/accounts/password/init", &verificationToken) evalMsgBytes, err := hex.DecodeString(resp["evaluatedMessage"].(string)) if err != nil { @@ -138,7 +138,7 @@ func register() { "envelope": hex.EncodeToString(record.Envelope), } - resp = postReq(recordFields, "http://localhost:8080/v2/accounts/setup/finalize", &verificationToken) + resp = postReq(recordFields, "http://localhost:8080/v2/accounts/password/finalize", &verificationToken) log.Printf("auth token: %v", resp["authToken"]) } @@ -231,7 +231,7 @@ func main() { conf.KSF.Salt = make([]byte, 16) fmt.Println("1. Login") - fmt.Println("2. Register") + fmt.Println("2. Register/set password") fmt.Print("Choose an option (1-2): ") reader := bufio.NewReader(os.Stdin) diff --git a/misc/test-client-rust/src/main.rs b/misc/test-client-rust/src/main.rs index 9d776c7..a1c1053 100644 --- a/misc/test-client-rust/src/main.rs +++ b/misc/test-client-rust/src/main.rs @@ -68,8 +68,8 @@ pub fn prompt_credentials() -> (String, String) { (email, password) } -fn set_password(change_password: bool) { - print!("Enter verification/auth token: "); +fn set_password() { + print!("Enter verification token: "); stdout().flush().unwrap(); let mut token = String::new(); stdin().read_line(&mut token).unwrap(); @@ -88,12 +88,11 @@ fn set_password(change_password: bool) { body.insert("blindedMessage", registration_request_hex.into()); body.insert("serializeResponse", true.into()); - let init_url = if change_password { - "http://localhost:8080/v2/accounts/change_pwd/init" - } else { - "http://localhost:8080/v2/accounts/setup/init" - }; - let resp = post_request(init_url, Some(&token), body); + let resp = post_request( + "http://localhost:8080/v2/accounts/password/init", + Some(&token), + body, + ); let resp_bin = hex::decode(resp.get("serializedResponse").unwrap()).unwrap(); @@ -117,12 +116,11 @@ fn set_password(change_password: bool) { let mut body: HashMap<&str, Value> = HashMap::new(); body.insert("serializedRecord", record_hex.into()); - let finalize_url = if change_password { - "http://localhost:8080/v2/accounts/change_pwd/finalize" - } else { - "http://localhost:8080/v2/accounts/setup/finalize" - }; - let resp = post_request(finalize_url, Some(&token), body); + let resp = post_request( + "http://localhost:8080/v2/accounts/password/finalize", + Some(&token), + body, + ); println!("auth token: {}", resp.get("authToken").unwrap()) } @@ -174,7 +172,7 @@ fn login() { } fn main() { - print!("1. Login\n2. Register\n3. Change password\nEnter choice (1, 2 or 3): "); + print!("1. Login\n2. Register/set password\nEnter choice (1 or 2): "); stdout().flush().unwrap(); let mut choice = String::new(); @@ -182,8 +180,7 @@ fn main() { match choice.trim() { "1" => login(), - "2" => set_password(false), - "3" => set_password(true), + "2" => set_password(), _ => println!("Invalid choice"), } }