Account Recovery deprecated

Introduction

Account Recovery lets users reset a forgotten password themselves.

  • The user registers a recovery email address and/or a phone number while logged in.
  • On the login screen the user requests a reset and receives a single-use, time-limited link by mail or SMS.
  • The link opens a reset page where the user sets a new password; all of the user's sessions end.
  • A recovery PIN can be stored as well. It is a shared secret for support staff and plays no part in the self-service flow.

The user-initiated password change in the settings is a different feature, see Change Passwords External.

Quick Start

  1. Enable the package (features.status.account-recovery: enabled in the core-mw chart), set com.openexchange.accountrecovery.enabled=true and restart the middleware.
  2. Pick the password change backend: database authentication works as-is, every other setup needs the webhook.
  3. Set the recovery password policy under com.openexchange.passwordchange.admin.*, see Password Policy.
  4. Configure com.openexchange.hostname (or set com.openexchange.accountrecovery.api.resetUrl). This is a MUST: the host of a link is never taken from the request, and without either no reset link is sent.

Optional next steps: contact verification, SMS, provisioning recovery details for existing users.

How the Recovery Flow Works

  1. Request. The user clicks Forgot password? on the login screen and enters a recovery email address or their own mail address. The UI calls POST /account-recovery/v1/forgot-password.
  2. Lookup. The middleware answers right away and looks the address up in the background: every user with that recovery email, found through an index in ConfigDB, plus the user who owns that address (primary address or alias) when com.openexchange.mailmapping.lookUpByDomain=true. Each match that has a channel to deliver to and passes the abuse checks gets a single-use token.
  3. Notification. The token is sent as a link via primary mail, recovery mail and/or SMS, see Notifications.
  4. Reset page. The page checks the token with GET /account-recovery/v1/reset?token=…, which also returns the account's primary address in masked form (p***@example.com), and loads the policy from GET /account-recovery/v1/password-requirements?token=… (with the token the policy of that user is reported, without it the server-wide policy).
  5. Reset. POST /account-recovery/v1/reset consumes the token, re-checks the policy, changes the password through the backend, ends all sessions of the user and sends a confirmation.

Behavior worth knowing:

  • The forgot-password response is identical whether the address matched or not. The endpoint cannot be used to find registered addresses.
  • With com.openexchange.mailmapping.lookUpByDomain=false (the default) only recovery addresses lead to a token: resolving a primary address in that mode queries every database schema per request, also when a custom mail resolver answers first and leaves the address unresolved. To keep the primary-address match, set the property to true and restart the middleware (it is read at bundle start); the default resolver then needs a unique domain per context in the login mappings.
  • A token lives 60 minutes (com.openexchange.accountrecovery.api.tokenLifetimeMinutes).
  • Tokens are stored in Redis only as a hash, so access to Redis does not yield usable links.
  • An unknown, expired or used token answers 410 Gone.
  • The legacy paths /account-recovery/{request,waypoint,reset,requirements} behave the same. New integrations should use the /v1/ paths.

Enabling the Feature

One switch on the middleware enables the whole feature, including the Settings pane in the UI, once the package is installed.

Package

  • The core-mw chart disables open-xchange-account-recovery by default. It MUST be enabled with features.status.account-recovery: enabled in addition to the property below.
  • The feature also switches open-xchange-cloudplugins-account-recovery and open-xchange-tet-extension when they are installed (middleware-cloud image), so one features.status.account-recovery: enabled starts all three packages.

Middleware

com.openexchange.accountrecovery.enabled=true
  • Default is false. While off, the middleware installs no endpoints, no AJAX module and no recovery services, and denies the account_recovery capability. The tables (user_recovery_details through an update task, the ConfigDB index through a changelog), the cleanup on user and context deletion and the revocation of open links on a password change are installed regardless.
  • When on, the account_recovery capability is announced.
  • The value is read at bundle start. Changing it requires a middleware restart.
  • The property is the off switch. Packages that extend account recovery import its bundles, so disabling open-xchange-account-recovery while such a package stays enabled leaves their bundles unresolved and the middleware never becomes ready. Disable both packages together, or keep them and set the property to false.

To switch recovery off for single contexts or users, set com.openexchange.capability.account_recovery=false through the config cascade:

  • The settings pane is hidden and the forgot-password flow sends those users no link.
  • A link sent earlier answers 403 Forbidden. It works again once recovery is switched back on.

The reset link opens the App Suite set-password page (<host>/appsuite/?login_type=set_password). To use a different frontend, set:

com.openexchange.accountrecovery.api.resetUrl=https://<recovery-frontend>/reset

UI

The Settings → Account Recovery pane appears for every user with the account_recovery capability. No UI setting is needed.

To hide the pane while the recovery flow stays active, e.g. for a brand that manages recovery contacts in its own portal, set the flag to false through the config cascade:

io.ox/core//features/accountRecovery: "false"
  • The middleware computes the flag per user: true only with the capability and the cascade value not set to false.
  • Setting the flag to true cannot show the pane without the capability.
Setting Default Description
io.ox/core//features/accountRecovery true false hides the Settings pane.
io.ox/core//features/accountRecoveryMandatory false Makes the shown fields required in the Settings pane and the first-start wizard.
io.ox/core//accountRecovery/showEmail true false hides the recovery email field.
io.ox/core//accountRecovery/showPhone true false hides the phone field.

The middleware computes showEmail and showPhone per user, so the UI only offers channels that can deliver:

  • showEmail is true only if notify.request.backup-email or notify.reset.backup-email is on.
  • showPhone is true only if notify.request.sms or notify.reset.sms is on and an SMS provider is registered.
  • Set both through the config cascade. Values written through the jslob API are discarded.

The link is controlled by forgotPassword in as-config.yml:

default:
  host: all
  forgotPassword: "https://<recovery-frontend>/"
  • URL set: the link opens that page. This works with or without account recovery.
  • Unset or false, recovery enabled: the link opens the built-in forgot-password form.
  • Unset or false, recovery disabled: the link is removed.

Password Change Backends

The reset changes the password through one of two backends:

  • Database (default): writes to the App Suite users table. For database authentication only.
  • Webhook: hands the new password to an external system. Required for SSO, an external IdP, a header-trusted proxy or a customer-operated mail host.

If no backend is usable, the reset answers 503 Service Unavailable and the token stays valid. The user requests a new link once the configuration is fixed.

Database

  • Active only while the database AuthenticationService is registered. Without it the backend disables itself, so it never writes a password the login does not check.
  • Writes the new password to the users table and ends the user's sessions.

Password Policy

Recovery resets use their own policy, separate from com.openexchange.passwordchange.*, so the recovery flow can be stricter. With the defaults any non-empty password is accepted.

Property Default Description
com.openexchange.passwordchange.admin.minLength 0 Minimum password length.
com.openexchange.passwordchange.admin.digitRequired false Requires a digit.
com.openexchange.passwordchange.admin.upperCaseRequired false Requires an upper-case letter.
com.openexchange.passwordchange.admin.lowerCaseRequired false Requires a lower-case letter.
com.openexchange.passwordchange.admin.specialCharRequired false Requires a special character.

Webhook

Use the webhook when App Suite does not own the user's password.

  • It needs both enabled=true and an endpoint. Enabled without an endpoint, it steps aside and logs a warning.
  • Once configured, it takes precedence over the database backend and over extension backends with a lower ranking (the webhook ranks 750).
  • It does not write to the App Suite users table.
  • Enable it only if the endpoint changes the password the login checks: a success reported by the endpoint is final.
Property Default Description
com.openexchange.passwordchange.admin.webhook.enabled false Enables the webhook.
com.openexchange.passwordchange.admin.webhook.endpoint (empty) HTTP(S) URL the new password is posted to.
com.openexchange.passwordchange.admin.webhook.username (empty) Optional basic-auth user.
com.openexchange.passwordchange.admin.webhook.password (empty) Optional basic-auth password.
com.openexchange.passwordchange.admin.webhook.httpsOnly true Refuses plain HTTP endpoints. Set to false for local testing only.

The request is a form-encoded POST. username is the login name; lang is the user's language and is left out when unknown.

POST <endpoint>
Content-Type: application/x-www-form-urlencoded

cid=1&userid=42&username=user%40example.com&newpassword=<new cleartext password>&lang=en
Endpoint answers User gets
2xx 200 OK, the reset succeeds.
4xx with a body 400 Bad Request with the body as reason, e.g. "Password contains user name". The token stays valid.
4xx without a body 400 Bad Request with a generic reason. The token stays valid.
5xx, other status, unreachable or misconfigured 503 Service Unavailable. The token stays valid.

The webhook receives the new password in cleartext. Use HTTPS, allow only the middleware to reach the endpoint, and keep com.openexchange.passwordchange.admin.webhook.password in a secret, not in a configmap. HTTPS alone does not validate the certificate: with the default com.openexchange.net.ssl.trustlevel=all any certificate is accepted; with restricted, which requires a restart, the webhook refuses an untrusted endpoint and the reset answers 503.

Recovery Contact Verification

By default a recovery contact is usable as soon as the user saves it. With verification on, the user first proves control over the contact.

  • Off by default, switched on per channel: email, phone, or both.
  • Contacts written by admins are trusted and count as verified.
  • With the channel's flag off, the verified state is tracked but not enforced.

The verify page and the SMS code entry in the Settings pane ship with the UI follow-up (BIZDEV-287). Until that UI is deployed, keep both requireVerified* flags false: users cannot complete a challenge, so their contacts would become unusable.

Configuration

Property Default Description
com.openexchange.accountrecovery.verification.requireVerifiedEmail false Uses a recovery email only once verified. Config-cascade aware.
com.openexchange.accountrecovery.verification.requireVerifiedPhone false Uses a phone number only once verified. Config-cascade aware.
com.openexchange.accountrecovery.api.verifyUrl (empty) Verify page; the token is appended as ?token=. Empty opens <host>/appsuite/?login_type=verify_email with the scheme of the request that saved the address.
com.openexchange.accountrecovery.verification.emailLinkTtlMinutes 60 Lifetime of the verification link.
com.openexchange.accountrecovery.verification.smsCodeTtlMinutes 15 Lifetime of the SMS code.
com.openexchange.accountrecovery.verification.smsCodeMaxAttempts 5 Wrong attempts before the code is invalidated.
com.openexchange.accountrecovery.verification.smsCodeLength 6 Digits in the SMS code.

The host of a verify link comes only from com.openexchange.hostname (or a HostnameService provider); its scheme follows the request that saved or re-sent the address, com.openexchange.forceHTTPS included. Configure a host, or set verifyUrl; otherwise no verify mail is sent.

Verification Flow

  • Email: a new or changed address is stored as unverified and a mail with a single-use link is sent. Opening the link calls POST /account-recovery/v1/verify-email and marks the address verified.
  • Phone: a new or changed number is stored as unverified and a numeric code is sent by SMS. The user enters the code in the Settings pane.
  • Resend: the Settings pane can send a fresh challenge. The previous one becomes invalid.
Endpoint Description
POST /account-recovery/v1/verify-email Public. Verifies the address; the token is passed in the body.
PUT ?module=accountrecovery&action=confirmPhone Authenticated. Body {"code": "..."}.
PUT ?module=accountrecovery&action=resendVerification Authenticated. Body {"channel": "email"} or {"channel": "phone"}.

Who Counts as Verified

  • Admin paths (SOAP, RMI, CLI, admin REST): stored as verified, usable at once.
  • Settings pane: stored as unverified until the user completes the challenge.
  • Changing a contact in the Settings pane resets it to unverified and sends a new challenge.

Turning a flag on after users have saved contacts in the Settings pane makes those contacts unusable until each user verifies them. Tell users to open Settings → Account Recovery and complete the verification.

Provisioning Recovery Details

A user can only recover the account with a recovery email or phone on file. Four ways to set them:

Surface Who Fields
Settings → Account Recovery The user Recovery email, phone. The pane does not show the PIN, although the accountrecovery AJAX module returns and accepts it.
Admin REST Master admin (global REST credentials while master authentication is disabled) Recovery email, phone, PIN
CLI createuser / changeuser Context admin, reseller, master admin Recovery email, phone, PIN
RMI / SOAP Context admin, reseller, master admin Recovery email, phone, PIN

Resellers use the CLI, RMI and SOAP surfaces with their own credentials, as for any other user attribute. The admin REST endpoints accept no reseller credentials: they resolve users by mail address across all contexts.

All four apply the same rules to each field:

  • Not provided: the stored value stays.
  • Empty: the value is cleared.
  • Any other value: the value is set.
  • Invalid value (no bare email address whose domain contains a dot, no valid international phone number with a leading +, a PIN with whitespace or longer than 64 characters): the call fails and nothing is stored. A failed createuser or createcontext leaves no user or context behind.
  • Phone numbers are stored in E.164 form. A number without country code is accepted only where com.openexchange.accountrecovery.phone.defaultRegion is set for the user, for example per brand; createcontext always needs the leading +.

Admin REST

Mounted under /admin/v1/account-recovery/ with HTTP basic auth. Accepted credentials are the master admin; the global REST credentials only while master authentication is disabled. An extension can register its own RecoveryRestAuthenticator; it then replaces both.

Endpoint Description
GET /details/email/{email} Reads recovery email, phone and PIN of the user whose primary address or alias is {email}.
PUT /details/email/{email} Updates the fields recoveryEmail, phone and pin of the JSON body.
PUT /password/email/{email} Resets the password to password of the JSON body. Without it a random one is generated and returned.

Example: set recovery email and PIN, clear the phone:

PUT /admin/v1/account-recovery/details/email/user@example.com
Content-Type: application/json

{"recoveryEmail": "alice.backup@example.com", "phone": "", "pin": "123456"}

The deprecated endpoints under /api/account/recovery/ keep taking these values as query parameters.

CLI

createuser and changeuser accept --recoveryemail, --recoveryphone (international, or national with phone.defaultRegion set) and --recoverypin:

/opt/open-xchange/sbin/changeuser -c 1 -u alice \
  --recoveryemail alice.backup@example.com \
  --recoveryphone '+49 170 1234567' \
  --recoverypin 123456

RMI and SOAP

  • OXUserService and the reseller OXResellerUserService accept recoveryEmail, recoveryPhone and recoveryPin on the User object.
  • OXContextService.create and the reseller OXResellerContextService.create accept the same fields for the context admin.

Storage

The details live in the user_recovery_details table and are deleted together with the user or context.

Column Holds
recovery_email Recovery email address
phone Phone number, E.164 format (+491701234567)
pin Recovery PIN, plaintext
created / updated Timestamps, epoch millis

A copy of every recovery email sits in the ConfigDB table user_recovery_email_index (cid, id, recovery_email), so the public lookup costs one query instead of one per schema.

  • The schema row is authoritative. An index entry whose user no longer has that address is skipped and deleted by the next lookup that hits it.
  • An entry whose context does not resolve on this middleware is skipped and kept: in a shared ConfigDB it may belong to another installation. Deleting a user or context removes its entries once the deletion has committed; if that write fails it is logged and the entry is dropped by the next lookup that hits it.
  • Every write goes to the schema first, then to the index. If the index write fails, the call fails although the address is stored; repeating the call repairs the index.
  • The update task RecoveryEmailIndexBackfillTask fills the index from existing rows; the table is created by a ConfigDB changelog and, if missing, on first use. A failed run is not retried by later update runs. To re-run it or rebuild the entries of one schema: forceupdatetask -t com.openexchange.account.recovery.rdb.groupware.RecoveryEmailIndexBackfillTask -n <schema>.
  • Moving a context to another schema needs nothing: the index is keyed by context and user.

Notifications

Two events send notifications: the recovery request and the reset confirmation. Each event/channel pair has its own switch, prefixed with com.openexchange.accountrecovery.notify.:

Property Default Sends
request.primary-email false Reset link to the primary address
request.backup-email true Reset link to the recovery email
request.sms false Reset link by SMS
reset.primary-email true Confirmation to the primary address
reset.backup-email true Confirmation to the recovery email
reset.sms false Confirmation by SMS
  • No token is issued to a user with no enabled channel that has an address, so by default a user without a recovery email or phone gets no reset link. An enabled SMS channel with a stored phone number counts even without a registered SMS provider; the token is then issued and the SMS fails as unavailable.
  • The primary address identifies the user but is not a delivery target by default: anyone with read access to the INBOX (deputies, shared or functional accounts, redirect rules) could otherwise take the account over with the link. A deployment that relies on the link reaching the INBOX sets request.primary-email=true.
  • SMS requires a registered SMS service.
  • Messages are rendered in the recipient's language.
  • A deployment MAY register a RecoverySmsGuard, e.g. to send SMS only with recorded consent. A withheld verification SMS shows the user "could not send"; withheld notification SMS are dropped with a log line. Core ships no guard.

Sender and Reply-To

Mails are sent from com.openexchange.noreply.address. Both properties are config-cascade aware:

Property Default Description
com.openexchange.accountrecovery.mail.senderName (empty) Display name of the sender. Overrides the name in com.openexchange.noreply.address.
com.openexchange.accountrecovery.mail.replyTo (empty) Reply-To address(es). Empty means no Reply-To.

Templates

To customize a message, edit the file of that name under /opt/open-xchange/templates/, where the package installs them. A package update overwrites the file, so keep customized templates in a configmap or mount.

Template Used for
notify.account-recovery.request.mail.{html,txt}.tmpl Reset-link mail
notify.account-recovery.reset.mail.{html,txt}.tmpl Confirmation mail
notify.account-recovery.verify.mail.{html,txt}.tmpl Verification mail
notify.account-recovery.{request,reset}.sms.tmpl Reset-link and confirmation SMS
notify.account-recovery.verify.sms.tmpl Verification SMS with the code
  • Mails use the shared notification mail layout, including the footer configured per brand.
  • Translated texts come from the i18n catalog. Mail variables: greeting, intro, instructions_lead, reset_url, validity, safety (request); greeting, reset_notice, reset_ok, reset_warning (reset).
  • SMS templates get text plus reset_url (request) or code (verify). Keep them short: one SMS segment holds 160 characters and the URL uses most of it.
  • A deployment can register its own mail service or RecoverySmsService with a higher ranking. The templates above then no longer apply; such a service documents its own file names. With cloud-plugins they are account-recovery-<event>[-sms][-<brand>[-<tier>]] with .txt.tmpl, plus .html.tmpl for mails.

Abuse Prevention

Requests above any limit are dropped silently: no notification, same response.

Mechanism Property Default
Same response for every address - always on
Requests per minute, per address and per matched user com.openexchange.accountrecovery.api.rateLimit 5 (0 or -1 disables)
Unexpired tokens per user com.openexchange.accountrecovery.api.maxOutstandingTokensPerUser 2 (-1 disables)
Requests per minute per client address com.openexchange.accountrecovery.api.rateLimitPerClient 20 (0 or less disables)
Users that get a link per request com.openexchange.accountrecovery.api.maxMatchesPerRequest 10 (0 or less disables)
  • rateLimit also limits each caller of the admin REST endpoints.
  • A deployment can register a RecoveryRequestGuard to block single users, e.g. through a block-list with its own admin endpoints. Core ships no guard.
  • The client address is the one the HTTP engine reports: the X-Forwarded-For value for requests from a proxy in com.openexchange.server.knownProxies while com.openexchange.server.considerXForwards=true (default), otherwise the peer address. Behind a proxy with considerXForwards=false all clients share one address and one limit.
  • The response is sent before the lookup runs. Each middleware node runs at most 32 lookups at a time and drops further requests silently. If the thread pool refuses a lookup, it runs on the request thread and delays that response.
  • Above maxMatchesPerRequest the remaining users get no link; which users these are is not defined.

Monitoring

Dropped requests look the same as unused ones from the outside. These meters show what happens; they appear under /metrics:

Meter Type Outcomes
appsuite.account.recovery.requests timer matched, unmatched, ratelimited (per address), throttled (per client), shed (node busy), unavailable, failed. Tag api: legacy or v1.
appsuite.account.recovery.tokens counter Per matched user: issued, disabled, undeliverable (no channel), ratelimited (per user), capped, vetoed, excess (beyond maxMatchesPerRequest), failed.
appsuite.account.recovery.resets timer completed, expired, vetoed, unavailable (no backend), failed. Tag api: legacy or v1.
appsuite.account.recovery.notifications timer sent, failed, withheld (SMS guard), unavailable. Tags event (request, reset) and channel (mail, sms).
appsuite.account.recovery.index counter written, cleared, failed (index write failed, see Storage), stale (entry purged by a lookup), pruned (entries a schema rebuild removed because their user no longer has a recovery email there; counts entries).
  • A request that matches several users counts once in requests and once per user in tokens.
  • requests times the lookup and the token issuing, which run after the response. Notifications are sent asynchronously and timed separately.
  • index with outcome failed means the index misses addresses: rebuild with forceupdatetask, see Storage. stale means a user's address changed or was removed without the index following; the lookup has repaired it.
  • No client input ends up in a tag value.

Configuration Reference

All middleware properties (com.openexchange.accountrecovery.*, com.openexchange.passwordchange.admin.*) are listed in the configuration documentation. UI settings are listed under UI.