Skip to content

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 22 Jul 04:55
a1457aa

Added

  • StandardSingpass::Myinfo::ApiError#status — the HTTP status of the Myinfo response that produced the error (nil for transport failures and for errors a host raises itself). Hosts need this to separate "Myinfo or one of its upstream agencies is unavailable" (502/503/504 — tell the user to retry shortly) from "we sent something wrong" (4xx — a bug worth surfacing to support). Previously the only way to recover the status was to parse it back out of the message, which is not a stable interface. Set on both the userinfo and token-exchange failure paths.
  • Bounded retry with jittered exponential backoff on the userinfo fetch for transient upstream statuses (502/503/504), 3 attempts total. Singpass returns 502 upstream_dependency_error whenever a Myinfo upstream (CPF Board, IRAS, MOM, …) is unavailable — including throughout their published maintenance windows — and a single transient one previously failed the whole flow. Because the authorization code is already spent by that point, the user's only recovery was a full Singpass re-login, so a momentary blip cost a complete re-authentication.

Notes

  • The retry is scoped deliberately:
    • Userinfo only. The token exchange is never retried — it consumes a single-use authorization code, and replaying it earns an invalid_grant. Userinfo is an idempotent GET against an access token already in hand.
    • Status-based failures only, never timeouts. A 5xx returns in well under a second; a timed-out attempt has already spent the request budget, and retrying it risks turning a slow page into a gateway error.
    • Inside network_wrapper, not outside. A host's wrapper is typically a circuit breaker; keeping retries inside means one user attempt counts as one failure against the breaker rather than three, so a single unlucky user can't trip a shared circuit alone.
  • The DPoP proof is rebuilt on every attempt — RFC 9449 proofs carry a one-shot jti, so a replayed header would be rejected.