hcengineering/platform · error · PlatformError
Forbidden
Forbidden
Error message
platform.status.Forbidden
What it means
Thrown by restorePassword when the decoded restore token has no restoreEmail in its extra payload. Without the email bound into the token, the service refuses to restore the password, since it cannot validate which identity is being restored.
Source
Thrown at server/account/src/operations.ts:1646
ctx: MeasureContext,
db: AccountDB,
branding: Branding | null,
token: string,
params: { password: string }
): Promise<LoginInfo> {
const { password } = params
if (password == null || password === '') {
throw new PlatformError(new Status(Severity.ERROR, platform.status.BadRequest, {}))
}
const { account, extra } = decodeTokenVerbose(ctx, token)
ctx.info('Restoring password', { account, extra })
const email = extra?.restoreEmail
if (email === undefined) {
ctx.error('Email not provided for restoration', { account, extra })
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
const emailSocialId = await getEmailSocialId(db, email)
if (emailSocialId == null) {
ctx.error('Email social id not found', { email })
throw new PlatformError(
new Status(Severity.ERROR, platform.status.SocialIdNotFound, { value: email, type: SocialIdType.EMAIL })
)
}
await setPassword(ctx, db, branding, account, password)
if (emailSocialId.verifiedOn == null) {
await db.socialId.update({ key: emailSocialId.key }, { verifiedOn: Date.now() })
}
return await login(ctx, db, branding, token, { email, password })View on GitHub (pinned to 63e28dc964)
Solutions
- Request a fresh restore token via the standard restore request flow so restoreEmail is embedded.
- Use decodeTokenVerbose locally to check extra.restoreEmail before calling the endpoint.
- Upgrade/align service versions so token issuance includes restoreEmail.
- Verify you are passing the restore token, not another token type.
Example fix
// before: reuse old token
const ok = await accountClient.restorePassword(ctx, oldToken, { password })
// after: request a new restore token containing the email
const info = await accountClient.requestRestore(ctx, email)
await accountClient.restorePassword(ctx, info.token, { password }) Defensive patterns
Strategy: validation
Validate before calling
const { extra } = decodeTokenVerbose(ctx, token)
if (extra?.restoreEmail === undefined) {
throw new Error('Token is not a restore token (missing restoreEmail)')
} Type guard
function isRestoreToken(extra: Record<string, any> | undefined): extra is { restoreEmail: string } & Record<string, any> {
return typeof extra?.restoreEmail === 'string' && extra.restoreEmail.length > 0
} Try / catch
try {
await accountClient.restorePassword(ctx, token, { password })
} catch (err) {
if (err instanceof PlatformError && err.status.code === platform.status.Forbidden) {
await accountClient.requestRestore(ctx, email) // re-issue fresh restore token
} else throw err
} Prevention
- Always issue restore tokens through the official restore request flow.
- Label token kinds and verify kind before use.
- Keep token payload schema consistent across service versions.
When it happens
Trigger: Using a restore token that was issued without restoreEmail in extra, or a token of a different kind (not a restore token) passed to restorePassword.
Common situations: Tokens generated by older service versions before restoreEmail was added to extra; reusing a changePassword or workspace token for restore; hand-crafted or corrupted tokens.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Workspace or account not found in token
- Invalid account uuid: "${accountUuid}"
- Invalid workspace uuid: "${workspaceUuid}"
- Invalid grant workspace uuid: "${grant?.workspace}"
- nbf and exp are required when sub is not provided
AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29).
Data as JSON: /api/errors/01c794533d2d2865.
Report an issue: GitHub.