Requests a one-time passcode for a user. @Summary Request one-time passcode @ID request-one-time-passcode @Accept json @Tags Authorization @Param request body codersdk.RequestOneTimePasscodeRequest true "One-time passcode request" @Success 204 @Router /api/v2/users/otp/request [post]
(rw http.ResponseWriter, r *http.Request)
| 228 | // @Success 204 |
| 229 | // @Router /api/v2/users/otp/request [post] |
| 230 | func (api *API) postRequestOneTimePasscode(rw http.ResponseWriter, r *http.Request) { |
| 231 | var ( |
| 232 | ctx = r.Context() |
| 233 | auditor = api.Auditor.Load() |
| 234 | logger = api.Logger.Named(userAuthLoggerName) |
| 235 | aReq, commitAudit = audit.InitRequest[database.User](rw, &audit.RequestParams{ |
| 236 | Audit: *auditor, |
| 237 | Log: api.Logger, |
| 238 | Request: r, |
| 239 | Action: database.AuditActionRequestPasswordReset, |
| 240 | }) |
| 241 | ) |
| 242 | defer commitAudit() |
| 243 | |
| 244 | if api.DeploymentValues.DisablePasswordAuth { |
| 245 | httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ |
| 246 | Message: "Password authentication is disabled.", |
| 247 | }) |
| 248 | return |
| 249 | } |
| 250 | |
| 251 | var req codersdk.RequestOneTimePasscodeRequest |
| 252 | if !httpapi.Read(ctx, rw, r, &req) { |
| 253 | return |
| 254 | } |
| 255 | |
| 256 | defer func() { |
| 257 | // We always send the same response. If we give a more detailed response |
| 258 | // it would open us up to an enumeration attack. |
| 259 | rw.WriteHeader(http.StatusNoContent) |
| 260 | }() |
| 261 | |
| 262 | //nolint:gocritic // In order to request a one-time passcode, we need to get the user first - and can only do that in the system auth context. |
| 263 | user, err := api.Database.GetUserByEmailOrUsername(dbauthz.AsSystemRestricted(ctx), database.GetUserByEmailOrUsernameParams{ |
| 264 | Email: req.Email, |
| 265 | }) |
| 266 | if err != nil && !errors.Is(err, sql.ErrNoRows) { |
| 267 | logger.Error(ctx, "unable to get user by email", slog.Error(err)) |
| 268 | return |
| 269 | } |
| 270 | // We continue if err == sql.ErrNoRows to help prevent a timing-based attack. |
| 271 | aReq.Old = user |
| 272 | aReq.UserID = user.ID |
| 273 | |
| 274 | passcode := uuid.New() |
| 275 | passcodeExpiresAt := dbtime.Now().Add(api.OneTimePasscodeValidityPeriod) |
| 276 | |
| 277 | hashedPasscode, err := userpassword.Hash(passcode.String()) |
| 278 | if err != nil { |
| 279 | logger.Error(ctx, "unable to hash passcode", slog.Error(err)) |
| 280 | return |
| 281 | } |
| 282 | |
| 283 | //nolint:gocritic // We need the system auth context to be able to save the one-time passcode. |
| 284 | err = api.Database.UpdateUserHashedOneTimePasscode(dbauthz.AsSystemRestricted(ctx), database.UpdateUserHashedOneTimePasscodeParams{ |
| 285 | ID: user.ID, |
| 286 | HashedOneTimePasscode: []byte(hashedPasscode), |
| 287 | OneTimePasscodeExpiresAt: sql.NullTime{Time: passcodeExpiresAt, Valid: true}, |
nothing calls this directly
no test coverage detected