signalapp/Signal-Server · error · ClientErrorException
403 Forbidden
Error message
403 Forbidden
What it means
updateSession throws this ClientErrorException with HTTP 403 FORBIDDEN when handlePushChallenge or handleCaptcha raises ForbiddenException, meaning a required verification challenge (push challenge or captcha) was absent, invalid, or not satisfied. The response body still includes the session state so the client can see what information remains required. This is an authorization-of-flow error, not an auth-token problem.
Solutions
- Inspect the 403 response body (session JSON) for requestedInformation to see which challenge is still required.
- Complete the required challenge first (obtain captcha token via the captcha endpoint or deliver the push challenge) and resubmit.
- Ensure push challenge tokens are submitted before their remoteExpirationSeconds window lapses.
- Verify the captcha token is generated for the correct site key/session and passed in the captcha field.
Defensive patterns
Strategy: validation
Validate before calling
const s = await getSession(sessionId);
if (s.requestedInformation.length > 0) { completeRequiredChallenges(s.requestedInformation); } Try / catch
try { await updateSession(...); } catch (e) {
if (e.status === 403) { const session = e.body; /* inspect requestedInformation */ }
throw e;
} Prevention
- Always read session.requestedInformation before updating
- Complete captcha/push challenges before resubmitting
- Don't submit challenge tokens for a different session
When it happens
Trigger: Updating a verification session where the push challenge token is present but invalid/expired (handlePushChallenge), or the captcha assessment did not pass and no alternative challenge was satisfied (handleCaptcha).
Common situations: Client skipping the captcha step; expired push challenge token delivered late; captcha solved for the wrong session/region; calling updateSession before completing prerequisite steps (requestedInformation not yet submitted).
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- must not use authenticated connection for one-time donation…
- recovery password could not be verified
- 429 Too Many Requests (rate limit exceeded)
- 429 Too Many Requests or 409 Conflict (not allowed to…
- too few parts
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/cdf66f360ff440b0.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java:315
verificationSession = verificationCheck.updatedSession().orElse(verificationSession);
verificationSession = handlePushToken(pushTokenAndType, verificationSession);
verificationSession = handlePushChallenge(updateVerificationSessionRequest, registrationServiceSession,
verificationSession);
verificationSession = handleCaptcha(sourceHost, updateVerificationSessionRequest, registrationServiceSession,
verificationSession, userAgent, verificationCheck.scoreThreshold());
} catch (final RateLimitExceededException e) {
final Response response = buildResponseForRateLimitExceeded(verificationSession, registrationServiceSession,
e.getRetryDuration());
throw new ClientErrorException(response);
} catch (final ForbiddenException e) {
throw new ClientErrorException(Response.status(Response.Status.FORBIDDEN)
.entity(buildResponse(registrationServiceSession, verificationSession))
.build());
} finally {
// Each of the handle* methods may update requestedInformation, submittedInformation, and allowedToRequestCode,
// and we want to be sure to store a changes, even if a later method throws
verificationSessionManager.update(verificationSession);
}
return buildResponse(registrationServiceSession, verificationSession);
}
/**
* If {@code pushTokenAndType} values are not {@code null}, sends a push challenge. If there is no existing push
* challenge in the session, one will be created, set on the returned session record, and
* {@link VerificationSession#requestedInformation()} will be updated.
*/
private VerificationSession handlePushToken(View on GitHub (pinned to 100ab61c82)