Protected data in single-page apps
What do the websites for a bank, an online retailer, and a social media platform have in common? They serve public facing content and personalized content based on an account. After a user signs in with their bank, they can check their balance, transfer money, or request a new (debit) card. At the online retailer, a user can check their previous orders or update their wish list. Similarly, at Facebook, a user can post a story, receive notifications, or update their profile.
These front-end websites must return the data for the correct users. You can accomplish this in a single-page app that uses access tokens obtained with the OpenID Connect standard.
Sessions
When handling authentication, single-page apps have more responsibility in the frontend. With classic server-side rendered websites, the server detects an unauthenticated request, and shows a login page. The user then enters their username and password, and submits the form. If the credentials are correct, the server creates a session and returns the session identifier in a cookie. With every subsequent request, the browser sends the cookie. The server returns the personalized content.
This is still possible when using client-side rendering. The server might respond with an HTTP response status code 401 when the user has not yet authenticated. Based on this response, you need to present the login page and pass the credentials to the backend, which returns a session cookie that can be used in requests to fetch data.
This classic setup has a few drawbacks. Sessions are usually bound to a server. For larger websites, there are usually several servers that serve the content for the website to guarantee availability and handle the load. Without any extra configuration, the user is switched from one server to another, but the servers are not aware of the other servers' sessions. There are solutions for this problem: either all requests from a single user are sent to the same server (sticky sessions), or the servers need to synchronize their sessions.
Another drawback is that the server that is responsible for authentication is also responsible for serving the content. This can work for smaller web applications, but larger websites might require a technical separation based on functionality. One web app might be responsible for your savings account, while another web app is responsible for your stock portfolio. As a user, you expect a single sign-on experience. After logging in once, you do not notice that you are switching web apps that form the bank's website.
JSON web tokens
An alternative for session cookies is to use tokens that are sent with every request. The OneWelcome Identity Platform can hand out these access tokens in two formats: as an opaque token (random, unpredictable strings) or as JSON wb tokens. A JSON web token (JWT) consists of three parts: a header, the payload, and a signature.
The payload is a JSON object with claims. Claims can contain information about things such as the validity, the target audience of the token, a user identifier, an e-mail address, or subscriptions.
Example: JWT payload
The payload in the following example contains three claims: sub (user), iss (issuer) and aud (audience). When the frontend requests the profile at the server, it sends the JWT with the request. The backend server validates the JWT and sends the profile of "jane.doe" to the frontend.
{
"sub": "jane.doe",
"iss": "https://onesurance.onegini.com/oauth",
"aud": ["Onesurance-backend", "Onesurance-frontend"]
}
Validation signature
How can a JWT be validated? The issuer, OneWelcome Identity Platform, signs the header and payload with a JSON web key (JWK). By default, it uses a cryptographic algorithm with a private and a public key. The issuer uses the private key to create the signature. The audience (receiver) can use the public key to validate the signature. When the validation passes, the audience can trust that the JWT is legitimate.
To validate the signature, the audience needs to know the public key and the algorithm. OneWelcome Identity Platform exposes a list of public keys and their algorithms via the jwks_uri
on https://<your-tenant-host>/oauth/.well-known/openid-configuration
, which is https://<your-tenant-host>/oauth/v1/keys
. This list contains the current, the future, and potentially a previously used combination of the key and algorithm. This makes it possible to rotate keys and still be able to validate a JWT that was issued in the past.
Validation claims
The JWT payload contains claims that you can use to verify the validity of the JWT:
-
iss (issuer) is a unique identifier for the issuer. You should only trust the JWT when it matches the expected value. The issuer for OneWelcome Identity PLatform is listed on
https://<your-tenant-host>/oauth/.well-known/openid-configuration
. -
aud (audience) is a list of identifiers that are expected to process this JWT. This list is maintained in the configuration of your application in the OneWelcome Identity Platform. Only trust the JWT when your application is listed in the aud claim. The audience is a list, because the frontend can pass the JWT access token to the backend, which in turn might even pass it to an internal API. All of these layers can have their own identifier for the audience.
-
sub (subject) is a unique identifier for the user. It is not present in tokens that are issued for machine-to-machine authentication.
-
exp (expiration time) is a timestamp that identifies until when the JWT is valid. You should not accept the JWT when the current time has passed the expiration time.
-
nbf (not before) is a timestamp from when the JWT is valid. You should not accept the JWT when the current time is before this value.
The frontend and various backend services can decide independently which secured data the user can access by verifying both the signature and the claims inside the JWT. By using a JWT access token, they no longer need to synchronize sessions.
OpenID Connect
How do you obtain an access token (JWT)? There are multiple standards and one of them is OpenID Connect (OIDC). OpenID Connect is a layer on top of OAuth 2.0. OAuth is all about authorization (what are you allowed), while OIDC also handles authentication (who are you). For simplicity, treat them as one standard,and use the OpenID Connect terminology.
In OIDC, your (single-page) application is called a Relying Party (RP). It calls the authentication endpoint in OneWelcome Identity Platform, which is an OpenID Provider (OP). We recommend using an existing, certified library to handle this call. The certified libraries comply with the specifications of the OpenID Foundation for an RP implementation.
When the user reaches the OneWelcome Identity Platform, they might need to log in. How the user authenticates depends on the configuration. OneWelcome Identity Platform supports multiple (external) identity providers. When the user has an existing session, it can be reused. After the user has successfully authenticated, the OneWelcome Identity Platform sends a short-lived token, called an access grant. Your single-page app exchanges this access grant for, at most, three tokens: an ID token, an access token, and a refresh token.
ID token
When your application is configured for and requests at least the openid scope, it receives an ID token. The ID token is a JWT with claims for the time and method of authentication, and with profile data, such as the user's name, email address, phone number, address, or date of birth. Your frontend can directly use the information inside the claims to personalize the response.
Access token
The access token is meant to access secured data in the backend. The frontend sends the access token with every request to the backend. When the access token is a JWT, the backend can verify the validity of the access token without calling the OneWelcome Identity Platform. The validity, user identifier, and scopes of the tokens are claims of the JWT. The backend does not need to create a session, because it receives the access token with every request.
Refresh token
Access tokens have a limited validity. For single-page apps, a good practice is a validity of several minutes, which is configurable. The validity is sent with the token response to the Relying Party. Depending on the configuration, a refresh token is sent with the access token. When the access token is about to expire, or when the access token is refused by the backend, your single-page app should use the refresh token to obtain a new set of tokens. This refresh token must be kept in a secure location. The RP must not share the refresh token with backend systems or other sites. Unfortunately, a single-page app does not have a really secure location to store data in the browser. At the moment, sessionStorage is acceptable to store a refresh token. Be aware, any script with access to your page can read the sessionStorage
.
To limit the abuse of refresh tokens, it is a good practice to limit the validity of refresh tokens for single-page apps. This validity is configurable in OneWelcome Identity Platform and is calculated from the moment the first access token was issued. When this validity is exceeded, the user needs to authenticate again to obtain a new set of tokens.
Responsibilities
When you use a single-page app, some responsibilities move from the backend to the frontend. The frontend is now responsible for keeping the tokens in a secure enough location. Also, it manages the user experience in case the user has not obtained a token or tokens have expired.
An advantage of this setup is that multiple backend services can handle protected data independently of each other. They no longer need to synchronize sessions for a single sign-on experience.