Skip to content

Refresh Tokens - #1278

Open
Scullyon wants to merge 21 commits into
mainfrom
feat/DF-1399-refresh-tokens
Open

Scullyon wants to merge 21 commits into
mainfrom
feat/DF-1399-refresh-tokens

Conversation

@Scullyon

Copy link
Copy Markdown
Contributor

Implementation of refresh tokens.

Tokens are no longer stored in the session, but instead separately in Redis and the key stored in the session. This is so that:

  • Session isn't overwritten between simultaneous requests with an old (used) refresh token
  • So that the token store item can be 'locked' whilst a refresh token is exchanged for a new set of tokens (including a new refresh token)

alexluckett and others added 21 commits September 14, 2026 14:29
The identity provider now accepts RS256 alone, so the assertion this
service proves itself with is signed by an RSA key rather than an
elliptic curve one.

The keypair must be rotated with the provider in step: it holds the
public half as OIDC_RUNNER_JWKS, and sign-in fails while the two are
different kinds of key. Generate a matched pair with the
generate-client-keypair script in forms-identity-ui.
The sign in now names forms-submission-api as the resource it wants a
token for, on the authorization request and the token request alike.
Naming it at the token endpoint alone returns an opaque token and no
error, so both carry it.

The token is kept in the session, where it is read to call that API and
never reaches the browser.

A token bound to a resource cannot reach the userinfo endpoint, so the
provider puts the scope's claims in the ID token instead and the email
is read from there.
The homepage now lists the forms the signed-in citizen saved and has not
yet submitted, soonest to expire first, with the reference number, an in
progress tag, when it was last saved and when it expires. A citizen who
has saved nothing is told so rather than shown an empty table.

The access token in the session names the citizen to
forms-submission-api, so the request carries no identifier of its own.

Dates are formatted in the route rather than the template, so they are
covered by a test.
Both dates in the table now read as GOV.UK writes them, for example
"10 September 2026 at 14:00".

They are read in UK time. A timestamp is stored in UTC, so the day it
falls on depends on where it is read: half past eleven at night in UTC
is already the next day in British Summer Time. Naming the zone keeps
every server showing a citizen the same day, which matters most for the
date their saved form expires.
The saved forms table needs more than the default 960px, so the homepage
carries a body class that widens every container on the page: the
header, the service navigation, the cookie banner, the main content and
the footer. Widening the main content alone leaves the header and footer
at the old width, and the seam shows. The service navigation and the
cookie banner hardcode their container, so there is no parameter to pass
them.

The width is set on the homepage rather than the layout, which is shared
with every form page rendered by forms-engine-plugin, where a shorter
line length is easier to read.

The class is built with the govuk-width-container mixin, which brings
the responsive margins and safe area insets with it.
The forms service reads form metadata and definitions from the manager.
The save-and-exit calls and the saved forms list read from the submission
API, so they sit together in their own service.
The submission API sends timestamps as strings, so the helper takes a
string rather than a string or a date.
Drops the elliptic curve key case and the userinfo expectation in the
sign-out test, since the client signs with RS256 and reads the email from
the ID token. Reads the token endpoint parameters by index, as the other
mock call assertions do.
It calls the submission API, so it sits with the other submission API
calls. The forms service now reads only from the manager.
The Identity typedef gained an access token, so the session value and the
request credentials declare it as well, and the fixtures carry one.
The citizen-session scheme always puts a full identity on the credentials,
and every route that reads them requires sign in. The hapi typings now say
so, so that routes read the fields directly rather than cast each one.
The homepage table shows a red "Expired" tag for a saved form whose
expiry time has passed, and the teal "In progress" tag for the others.

The route chooses each form's status, so the choice is tested with the
route and more statuses can be added in one place. Each status is also
the translation key of its tag text, and the template maps it to the
tag class.
cy.json had two top-level "signIn" keys. JSON keeps only the last one, so
the Welsh caption, start button, preview banner and account menu fell back
to English. The table strings now sit in the first signIn.homepage block,
the same layout as en-GB.json.
formatDateTime takes the page language, and Intl supplies the month name
and the word that joins the date to the time, for example "10 Medi 2026
am 14:00". The route passes the language getFormTranslator resolves, so a
form with no Welsh translation keeps English dates.

The language is a required argument rather than a default, so that each
caller states which language it formats for.
…ssion, but instead in a separate Redis record to avoid an expired refresh token accidentally being written back to the session (if two simultaneous requests) and so that the record can be locked whilst tokens are exchanged during a refresh.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
2 New Critical Issues (required ≤ 0)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants