OpenID Connect Token Reference

In this article:


Tokens are small bits of digital information (typically encoded/encrypted as JSON objects) that are used for authentication and authorization purposes.

Note. So what is the difference between authentication and authorization? Good question, and here’s a quick answer: authentication is the process of verifying that you are who you say you are. For example, when you show your passport at the airport security line, that's authentication: you’re providing proof that you are Maria Fuentes. By comparison, authorization is the process of determining what you're allowed to do now that we know who you are. When you board the airplane and show your ticket to the flight attendant, he or she might tell you to go sit in seat 39E. That's authorization: based on who you are, you're allowed to sit in seat 39E, and only in seat 39E.

Prior to token-based authentication, session information for each logged-on user had to be maintained and updated on each and every server in a cluster; this not only led to scalability issues, but also opened the door to web attacks such as cross-origin resource sharing (CORS) and cross-site request forgery (CSRF). Token-based authentication largely does away with these problems, in part because authentication and access information is not maintained on each server. Instead, this information is included in the access token, and that token must accompany each and every request for resources. Because the token indicates what the user is allowed to do, servers no longer have to maintain a list of all the logged-on users and all their access privileges.

When working with OIDC and with Hosted Login, you will encounter four primary token types:

  • Access tokens
  • Refresh tokens
  • Identity tokens
  • Anti-forgery state tokens

In this documentation, we’ll describe each token type in more detail. In addition, we’ll also discuss token management tasks, including inspecting tokens, revoking tokens, and configuring token lifetimes.


 


Access Tokens and Refresh Tokens

Access Tokens

Access tokens let applications and websites know what you're allowed to do. Do you need access to a specific user profile? In that case, the server is going to check your access token to make sure you're actually allowed access to that user profile: you’ll get access only if your token says that you’re allowed access.

Actually, there's an important clarification we need to make here: the server will check to see if the token is allowed access to the user profile. As it turns out, access tokens are "bearer tokens," which means they provide access to anyone who has possession of (i.e., anyone who bears) that token. No ID checks are performed when you access a resource; instead, possession of the token is all that matters. It's a bit like showing up at a baseball game and giving the ticket-taker your ticket. The ticket-taker isn’t going to ask for your ID; instead, he or she is simply going to verify that the ticket is valid (for example, that it's not a ticket for tomorrow's game or for yesterday's game). If the ticket is valid, they'll let you into the stadium.

Is this important to know? Yes, it is. Because no identity checks are made, it's crucial that you keep your access tokens secure. After all, if a token "leaks out," anyone who grabs that token can use it to carry out any of the actions the token is allowed to carry out. One way to help keep a token secure? Make sure that your tokens only travel on HTTPS networks, and never on HTTP networks. Another best practice? Whenever possible, delete access tokens and refresh tokens any time a user logs off from your website or application.

Because access tokens are bearer tokens, these tokens also have a very short lifespan: if a token does leak out, there’s only a limited amount of time (by default, 1 hour) in which the token could be used for malicious purposes.

Refresh Tokens

As noted, access tokens typically have a short lifespan: at best, you can stay logged on to a web site or an app for only an hour or so before your access token expires. (And what happens then? If your access token expires you won’t be allowed access to any protected resources until you obtain a new access token.)

In other words, if your access token is only good for 60 minutes (the Hosted Login default value), then, when your 60 minutes are up, you'll lose access to site resources. Or at least you wouldn’t be allowed access if it wasn't for refresh tokens. Refresh tokens provide a way for clients to stay logged on to a website or application for – well, for however long you want users to stay logged on. For example, suppose your organization configures refresh tokens to have a 7-day lifespan. That means that, by default, a user can log on to your site and stay there for 60 minutes, the lifespan of the access token. Just before those 60 minutes are up, however, the client can use a refresh token to request a new access token. By repeating this process over and over, the user can stay logged on indefinitely (each time the user redeems a refresh token not only do they get a new access token but the "refresh token clock" resets itself to 7 days).

Important. So could a user log on once and then stay logged on forever? In theory, yes. However, you can use the max_age parameter to set a time limit on user sessions. That would force a user to reauthenticate even if he or she still has valid access and refresh tokens.

If you're wondering, "Why don't we just make our access tokens last for 7 days?" well, some organizations do just that (although that’s something you can’t do in Hosted Login: Hosted Login imposes a maximum lifetime of 60 minutes for access tokens). However, using a combination of access tokens and refresh tokens offers some security advantages to using nothing but long-lived access tokens. After all, if a hacker steals an access token they'll have a limited amount of time (e.g., 60 minutes) in which to use that token. Because of that, a hacker really needs to hijack both the access token and the refresh token, which means that you’ve already made his or her efforts twice as difficult.

Access and Refresh Token Properties

Access tokens and refresh tokens are encoded strings that look similar to this:

03v-eeodppPrrHXXIx56pRLyDBaOldDxqEwI59MFCFGVuSkLRapzgmfwmEHyKWle

Admittedly, a value like that isn’t of much use to a human being, which is fine: you rarely need to know the actual contents of a token. But what if you do need to know the actual contents of an access token or a refresh token; are you just out of luck?

As it turns out, no, you’re not out of luck. In fact, you can easily decipher a token (access or refresh tokens only) by calling the /login/token/introspect endpoint. For example, here’s a Curl command that returns the information associated with the token 03v-eeodppPrrHXXIx56pRLyDBaOldDxqEwI59MFCFGVuSkLRapzgmfwmEHyKWle:

curl -X POST \
  https://v1.api.us.janrain.com/00000000-0000-0000-0000-000000000000/login/token/introspect \
  -H 'Authorization: Basic RcaWTi0woO52rqZjlbApm2lL3Aokzd1bhCZZajX51aX4IQrH1Uj1D4ks9HfJtxoRI7HCsyNVoc6Qj4oBfuplftc7tMbR26eZHwtEqaw9RLMBeIJDvqvqyD4l' \
  -d 'token=03v-eeodppPrrHXXIx56pRLyDBaOldDxqEwI59MFCFGVuSkLRapzgmfwmEHyKWle&undefined='

Important. You can only introspect tokens that were issued to a confidential client; you cannot inspect tokens issued by a public client. Why not? Well, to introspect a token you must employ Basic authentication, with the OIDC client ID used as the username and the OIDC client secret used as the password. That’s no problem with confidential clients. However, public clients don’t have client secrets. As a result, there’s no password that you can use with the introspection endpoint.

After you issue that command, the introspection endpoint sends back token properties and property values similar to these:


{
     "active": true,
     "scope": "address email openid phone profile",
     "client_id": "a39796ab-75tg-po9f-3aa5-7yh22kj03a3",
     "token_type": "Bearer",
     "exp": 1552603442,
     "iat": 1552599842,
     "sub": "2edd2f32-1e49-4bf2-b164-763781761b52",
     "aud": [
        "a39796ab-75tg-po9f-3aa5-7yh22kj03a3",
        "https://documentation.akamai.com"
     ]
 }

These properties are explained in more detail below.

active

Returns true if the access token is valid, and returns false if the access token has expired. If the token has expired then only the active value is returned:

{
    "active": false
}

No other token properties will be available. This is a security measure that helps prevent malicious actors from using expired tokens to learn the details of how your tokens are constructed. 

scope

Specifies the user profile information that the token provides access to. For example, the phone scope provides access to the phone_number and phone_number_verified claims (attributes). See the article Scopes and Claims for more information.

client_id

Unique identifier of the OIDC client that requested the token. An OIDC client is any entity  that can connect to an OIDC server and request user authentication. 

token_type

Specifies the type of token issued. A Bearer token indicates that access and permissions should be granted to whoever has possession of the token: no attempt is made to verify that the token really belongs to the person who possesses it. You can think of bearer tokens as being similar to cash. If you pay for a purchase by writing a check or by using as credit card. you will likely be asked to provide proof of your identity. If you pay with cash, however, you don’t need to prove that the cash belongs to you: possession of the money is all you need.

Incidentally, the fact that access tokens are bearer tokens is one reason why access tokens have relatively short lifespans: should an access token somehow “leak out,” anyone who gained possession of that token would have only a limited amount of time in which they could use it.

Note. We should also add that, as long as you use TLS, it’s very difficult for a malicious user to steal an access token. OAuth and OIDC are extremely secure, provided that you follow security best practices. 

exp

Specifies the date and time when the token expires. The exp claim is formatted using Unix epoch time, which represents the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC) on January 1, 1970. For example, the value 1553405263 represents Saturday, March 23, 2019 at 22:27:43 Pacific Daylight Time. See Converting Unix Epoch Time for more information.

By default, access tokens expire 1 hour after they are issued, while refresh tokens expire after 90 days.

iat

Specifies the date and time when the token was issued. The iat (issued at time) claim is formatted using Unix epoch time, which represents the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC) on January 1, 1970. For example, the value 1553405263 represents Saturday, March 23, 2019 at 22:27:43 Pacific Daylight Time. See Converting Unix Epoch Time for more information.

sub

Unique identifier for the end user (the subject). This will always be the user’s Akamai Identity Cloud UUID (Universally Unique Identifier).

aud

Audience that the token is intended for. For Hosted Login, you will always see both the client ID (e.g., a22c9604-7b27-464f-bff5-83ba229323af) and the redirect URI (e.g.,  https://documentation.akamai.com) included in the aud claim.

 


 


Issuing Access Tokens as JSON Web Tokens (JWTs)

By default, Identity Cloud OIDC access tokens are “opaque;” that means that you, regardless of whether you’re a person or a resource server, can’t validate or decode the token. If you need to determine whether an access token is valid, or if you need to know something about the token (like when it was issued or the UUID of the user it was issued to) you have to send the token to the introspection endpoint and ask that endpoint to provide you with the necessary information.

If your organization’s primary concern is simply to log users in and then provide them with access to their user profiles, the default approach works perfectly well. However, you might have additional uses for an access token that go beyond giving users access to their user profiles. For example, you might have a set of APIs that, on a video streaming website, presents users with the videos they have access to. (In this example, we’re assuming that your membership type determines the videos you can and cannot stream.) In a case like that, the API needs to know if the user requesting the video:

  1. Has a valid access token.
  2. Has access to the video in question. 

So how does a resource server answer those questions? Well, it could determine whether a token is valid by making a call to the introspection endpoint. And it could contact the user_info endpoint or query the user profile to see if the user has been granted permission to a video. Those approaches work, but in a busy environment they can also result in hundreds of thousands (or even millions) of network calls between the resource server and the introspection endpoint and the API and the user_info endpoint ….

In other words, what you could really use in a situation like this is an access token that contains the required information (and, not incidentally, can be deciphered by a resource server). That way questions like whether an access token is valid or whether a user has access to a resource can be answered on the spot, without having to make a number of extra network calls.

That’s where JWT (JSON Web Token) access tokens come into play. Unlike standard Identity Cloud access tokens, JWT access tokens are not opaque: literally hundreds of programming libraries are available that allow a resource server to decode a JWT access token and see for itself whether or not that token has expired. In addition, user profile information can be included within a JWT access token, meaning that the token can contain information such as a user’s membership type. That information can also easily be read by the resource server itself, without making additional calls to the user_info endpoint. That’s the idea behind JWT access tokens.

In this section of the documentation we’ll examine JWT access tokens in more depth, starting with a guide to configuring your access tokens as JWT access tokens, continuing with a closer look at the structure of a JWT access token, and concluding with a few things to consider before you decide to start using JWT access tokens.

Important. We should also clarify that the difference between a standard access token and a JWT access token lies in how that token is formatted (and, as a result, whether downstream systems/APIs can read the contents of that token). However, when it comes to users registering, logging on, or accessing their user profiles, there is no difference between using a standard access token and using a JWT access token. In fact, and as we’ll see momentarily, you can even use both types of tokens at the same time.


Configuring Access Tokens as JWT Access Tokens

The type of access token issued to a user – either a “standard” (opaque) access token or a JWT access token – is determined by the value of the useAccessJWT attribute found in the token policy in force when a user is authenticated: 

  • If useAccessJWT is set to false (the default value) the user is issued a standard access token.
  • If useAccessJWT is set to true the user is issued a JWT access token. 

For example, if you want to issue access tokens as JSON Web Tokens, you can use the OpenID Connect Configuration APIs to create a new token policy (a policy that sets useAccessJWT to true), then assign that policy to the appropriate OIDC login clients. For example, here’s a Curl command that creates a new token policy, one that issues JWT access tokens:


curl -X POST \
    'https://v1.api.us.janrain.com/e0a70b4f-1eef-4856-bcdb-f050fee66aae/config/tokenPolicies' \
    --H 'Authorization: Bearer FQOLoTl77LC5tfD9rbVMpZrCTnY9sp9EFKNfXYZktvZ6OwiD2dFJRxRxSa5C5FGh' \
    --H 'Content-Type: application/json' \
    --d '{
        "accessTokenLifetime": 1800,
        "allowedScopes": [
            "openid",
            "profile",
            "address",
            "email"
        ],
        "refreshTokenLifetime": 2592000,
        "title": "JWT Token Policy",
        "useAccessJWT": true
    }'

Assign this token policy to an OIDC login client, and, from that point on, any user logging on with that OIDC client receives a JWT access token instead of a standard access token.

Alternatively, you can use the /{customerId}/tokenPolicies/{token_policy} endpoint to enable an existing token policy to issue JWT tokens. 

There are at least two things to keep in mind when using JWT access tokens. First, token policies (as you know) are associated with OIDC login clients; you’re likely to have multiple login policies that are associated with different token policies. So what happens if some of your users authenticate using Login Client A, which specifies standard access tokens, while other users authenticate by using Login Client B, which issues JWT access tokens?

The answer to that question is this: nothing happens, or at least nothing bad happens. Users who log on using Login Client A get standard access tokens, and users who log on using Login Client B get JWT access tokens. That’s pretty much it. And that’s not a problem, either: Identity Cloud can handle both token types, and it doesn’t matter which type is issued to a client. For example, the introspection endpoint can decode either token type, and the revocation endpoint can revoke either token type. Identity Cloud doesn’t care that some users have standard access tokens while other users have JWT access tokens.

Note. In fact, suppose a user logs on with Login Client A and is issued a standard access token. Immediately after the user gets his or her token, the token policy is updated to issue JWT access tokens instead. What happens when the user refreshes their access token? What happens is this: the user presents a refresh token and, in return, they’re given a new access token, albeit this time in JWT format. Switching back and forth between token types won’t create any problems for your users (for example, they won’t need to reauthenticate simply because they have the “wrong” token type). 

Second, long-time users of Hosted Login might wonder if they have to replace their existing token policies; after all, the useAccessJWT attribute wasn’t introduced until April 2020. This is something else you don’t need to worry about: all existing token policies have been updated to include the useAccessJWT attribute (with the value set to false). Suppose you have a token policy that was created in January 2019, and you’d now like to modify that policy to allow for the use of JWT access tokens. That’s fine: go ahead and modify the policy. Like we said, all token policies have been updated to include the useAccessJWT attribute; there’s nothing for you to do but enable/disable that attribute as you see fit.


What Does a JWT Access Token Look Like?

Based on the name alone, it should come as no surprise that a JWT access token looks like any other JWT token. As you know, a standard access token looks like this:

m7Y97l2SnwEQ6edH46gNmHDLI2t5zlGTLvWvK5IfRehCPBLnLJDU6mg_A0JOXMae

By comparison, a JWT access token looks like the following (In this graphic we’ve used color coding to distinguish the three sections of a JWT token -- header, payload, and signature -- that we’ll discuss in a moment):

As the graphic notes, JWT access tokens are Base64UrlEncoded, which means that they can easily be decoded by any Base64Url decoding library. In fact, you can even copy the token value and decode it on a Web-based decoder:

That’s the value of a JWT access token: it’s easy to decode the token and, as a result, it’s easy to access the information contained inside.

As we just saw, JWT access tokens are made up of three parts: the header, the payload, and the signature. Let’s take a closer look at each of these sections.


The Token Header

In its original JSON format, a token header looks similar to this:

{
    "typ": "JWT",
     "alg": "RS256",
     "jku": "https://v1.api.us.janrain.com/e0a70b4f-1eef-4856-bcdb-f050fee66aae/login/jwk",
     "kid": "3af47e3f6a4bd0569782fee5c1f8623a3ff0d78f"
}

The four attributes that make up the token header are described in the following table:

Attribute

Description

typ

Type. Type of token issued. For Identity Cloud access tokens, the typeis always JWT.

alg

Algorithm. Identifier for the algorithm used to sign the token. For Identity Cloud access tokens, this is always set to RS256, an encryption method that uses private keys and public keys to sign the token.

jku

JWK Set Url. URL to the collection of the public JSON Web Keys that correspond to the private keys used to cryptographically sign JWT access tokens.

kid

Key Identifier. Specific identifier for a public JSON Web Key, something in use when verifying the validity of a JWT access token. You will always have multiple public keys:

The kid attribute simply tells you which of these keys needs to be used to decrypt and verify the token signature.

The header is initially configured by using JSON (JavaScript Object Notation), and then is Base64URLEncoded.


The Token Payload

The token payload is comprised of several standard JSON Web Token claims. For example:

{
     "aud": [
         "70a45721-c6ef-4d7c-91ff-f14e9346b8b6"
     ],
     "auth_time": 1587926305,
     "client_id": "70a45721-c6ef-4d7c-91ff-f14e9346b8b6",
     "exp": 1587928889,
     "global_sub": "capture-v1://se-demos-gstemp.us-dev.janraincapture.com/
79y4mqf2rt3bxs378kw5479xdu/GREG_DEMO/3d7a280d-2eda-4c49-a5e3-d31f8ed8f0af",
     "iat": 1587927089,
     "iss": "https://v1.api.us.janrain.com/e0a70b4f-1eef-4856-bcdb-f050fee66aae/login",
     "jti": "85f39838-7eef-4ab0-89a7-c9b03621cfe6",
     "sub": "3d7a280d-2eda-4c49-a5e3-d31f8ed8f0af"
}

The following table describes each of these claims in more detail:

Attribute

Description

aud

Audience. Specifies who the token is intended for. For OpenID Connect flows, this will always include the OIDC login client ID (e.g., a22c9604-7b27-464f-bff5-83ba229323af).
 

client_id

Client ID. Unique identifier of the OIDC login client that requested the token. 

exp

Expiration. Date and time when the token expires. The exp claim is formatted using Unix epoch time, which represents the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC) on January 1, 1970. For example, the value 1553405263 represents Saturday, March 23, 2019 at 22:27:43 Pacific Daylight Time. See Converting Unix Epoch Time for more information.

By default, JWT access tokens (like standard access tokens) expire 1 hour after they are issued.

global_sub

Global Subject. Contains all the information needed to locate a specific user. For example:

capture-v1://capture-alb-border.us.janrain.com/x3gmnnjeyzyrrt2nm5drf5nkn8/user/
2edd2f32-1e49-4bf2-b164-763781761b52

In the preceding example:

  • capture-v1://capture-alb-border.us.janrain.com. Internal path to the Hosted Login identity store.
  • x3gmnnjeyzyrrt2nm5drf5nkn8. The Identity Cloud application ID.
  • user. Name of the entity type where user profiles are stored.
  • 2edd2f32-1e49-4bf2-b164-763781761b52. The Identity Cloud UUID (sub) of the user.

This attribute value is intended for internal Identity Cloud use.

iat

Issued At Time. Date and time when the token was issued. The iat claim is formatted using Unix epoch time, which represents the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC) on January 1, 1970. For example, the value 1553405263 represents Saturday, March 23, 2019 at 22:27:43 Pacific Daylight Time. See Converting Unix Epoch Time for more information.

iss

Issuer. URL for the endpoint that issued the token.

jti

JWT ID. Unique identifier for the access token.

sub

Subject. Unique identifier for the end user (the subject). This is always the user’s Akamai Identity Cloud UUID (Universally Unique Identifier).

Like the header, the payload is initially configured by using JSON (JavaScript Object Notation), and then is Base64URLEncoded.


The Token Signature

The signature (the one part of a JSON Web Token that’s encrypted rather than encoded) helps you verify the authenticity of a token. When an access token is issued, the Identity Cloud completes the following procedure:

  1. The token header is Base64UrlEncdded. For example, your header might look like this in JSON format:
    After encoding, it will look like this:
  2. A period (.) is added to the end of the encoded header.
     
  3. The token payload is Base64UrlEncoded. The Identity Cloud then combines the encoded header, the period, and the encoded payload to create a single string. In pseudo mathematical format, that equation looks like this:
    The net result is a string that looks similar to this:
    That’s our token header and payload.
     
  4. Next, a private key and the RSA 256 hashing algorithm is employed to hash the string created in steps 1-3. The resulting hash value is the token signature, which will look something like this:
  5. Finally, the Identity Cloud takes the value created in steps 1-3, add a period, then tacks on the token signature created in step 4. That creates a JWT token that looks like this (color coding added for clarity):

So how does this help you verify the authenticity of a token? Well, when a token is received the resources server uses the information included in the token to try and perfectly recreate that token. To begin with, the server converts the header and payload back to JSON format, then repeats steps 1-3 with the newly-decoded header and payload. 

After that, the server creates a hash value of the preceding string (step 4), this time using the public key rather than the private key. This value (a recreated token signature) is then added to the reconstructed token. If the reconstruction matches the original token that means the token really was issued and signed by the Identity Cloud. If it doesn’t match, either the token wasn’t signed using the expected private key, or the token has been changed since the time it was issued. In either case, the token should be rejected.

Note. Are you on your own to figure out how to verify a token signature? No, not really; there are a large number of JWT libraries, in many different programming languages, that make it easy to verify signatures. See the OpenID Connect Foundation’s website (https://openid.net/developers/jwt/) for more information.


Authorization Request Scopes and Claims

OpenID Connect uses scopes and claims as a way to control access to profile data associated with the user who authenticated. If you include a scope in your authorization request, the claims included in that scope (for example, email and email_verified) are accessible at the user_info endpoint. If you include a claim (either a standard claim or a custom claim) in your authorization request, you can have that user information:

  • Accessible at the user_info endpoint.
  • Included in the identity token.
  • Both accessible at the user_info endpoint and included in the identity token.

Of course, you might be wondering what this has to do with access tokens. As it turns out, any scopes or claims that you direct to the user_info endpoint are also included in a JWT access token (we should clarify that these scopes and claims are only added to a JWT access token and are not included in a standard access token). For example, suppose you make an authorization request similar to the following, a request that includes an additional scope (email) as well as a custom claim (organization) that directs the primaryAddress.company attribute to the user_info endpoint:

https://v1.api.us.janrain.com/e0a70b4f-1eef-4856-bcdb-f050fee66aae/login/authorize
     ?client_id=70a45721-c6ef-4d7c-91ff-f14e9346b8b6
     &redirect_uri=https://wacky-harmonious-bike.dev.or.janrain.com/redirect_uri
     &scope=openid email
    &code_challenge=te-pnbR9xVCz8iHK41_VUitQSPa6mZjhOg3QmF2bhPI
     &code_challenge_method=S256
    &response_type=code
    &claims={"userinfo":{"organization":null}}
    &state=jenF-kWxHsuYAf2Sct7ZTa9PB91-QPHezGImiGhJNO0

If you request a standard access token, the scope information (the email and email_verified attributes) and the claim (the organization attribute) are accessible via the user_info endpoint … and nowhere else. By contrast, if you request a JWT access token not only will this user profile information be available from the user_info endpoint, it will also appear in the JWT access token payload:

{
     "aud": [
      "70a45721-c6ef-4d7c-91ff-f14e9346b8b6"
      ],
     "auth_time": 1587926305,
     "client_id": "70a45721-c6ef-4d7c-91ff-f14e9346b8b6",
     "email": "gmstemp@hotmail.com",
    
"email_verified": true,
   
 "exp": 1587928741,
     "global_sub": "capture-v1://se-demos-gstemp.us-dev.janraincapture.com
/79y4mqf2rt3bxs378kw5479xdu/GREG_DEMO/3d7a280d-2eda-4c49-a5e3-d31f8ed8f0af",
     "iat": 1587926941,
     "iss": "https://v1.api.us.janrain.com/e0a70b4f-1eef-4856-bcdb-f050fee66aae/login",
     "jti": "353063bf-f4c8-4580-8a9b-2528fca4cca4",
     "organization": "Akamai",
   
 "sub": "3d7a280d-2eda-4c49-a5e3-d31f8ed8f0af"
}

In other words, all scopes and claims targeted to the user_info endpoint also appear in the JWT access token.

Note. What about claims targeted towards the identity token? Those claims will only show up in the identity token; they are not added to the JWT access token.

If you’re wondering why scopes and claims (which might include personally-identifiable information) can be added to a JWT access token, the answer is pretty simple. As noted earlier, JWT access tokens are often passed to downstream services or APIs that need to take action based who the token was issued to. These services/APIs need a quick and efficient method for determining whether a user should be given access to a resource. Including claims and scopes in a decodable JWT access token is one way to provide that information.

As seen in our previous example, any scope/claim can be added to an access token, including scopes and claims that contain personally-identifiable information such as an email address. Because of that, and because an access token can easily be decoded by anyone who happens to get their hands on it, organizations using JWT access tokens should think carefully about which scopes and claims are exposed in the token. 

Important. Akamai strongly recommends that, in most cases, you don’t add personally-identifiable information such as email addresses and phone numbers to an access token. As a general rule, you shouldn’t add PII to identity tokens, either. However, and unlike JWT access tokens, identity tokens aren’t passed to other services and APIs. Because of that, it’s much less likely that PII contained in an identity token will be exposed. That said, the best approach is still to make a separate API call to retrieve personally-identifiable information as needed, and not to include that PII in either an access token or an identity token.

Instead of containing personally-identifiable information, access tokens should be limited to more generic attributes such as a role or membershipLevel attribute. For example, if your website streams video, you shouldn’t make access to specific videos contingent on a user’s email address; instead, make access contingent on the user’s membership level (Gold, Silver, Platinum). When you do that, no PII needs to be included in the access token; instead, the token might have claims similar to this:

"membershipLevel": "Gold"


Things to Consider Before Using JWT Access Tokens

We just mentioned one important thing to consider before using JWT access tokens: any scopes or claims that add user profile information to the user_info endpoint add that same information to the JWT access token. That could be a problem based on the fact that JWT access tokens are often passed to other services or APIs: it’s usually not a good idea to transmit personally-identifiable user information around willy-nilly. Because of that, you shouldn’t add scopes and claims to your authorization requests without having a good reason for doing so. (Which is a good rule-of-thumb even if the scopes and claims don’t contain PII.)

Fortunately, keeping PII out of your JWT access tokens is unlikely to cause you many problems. After all, claims can still be included in the identity token; that’s a safer bet because identity tokens typically don’t get passed around the network. And, of course, you can always make an API call to the user profile and retrieve whatever information you need. Using JWT access doesn’t prevent you from using other Identity Cloud tools.


Choosing Between Opaque Access Tokens and JWT Access Tokens

With two different token types available to you, an obvious question is this: which token type should I use? Often-times this decision comes down to a tradeoff between performance and date recency. For example, consider opaque tokens. Compared to JWT tokens, there’s a performance lag associated with opaque tokens; after all, each time a resource server needs to make an authorization decision it must contact the userinfo endpoint and retrieve the information needed to make that decision. Typically this doesn’t result in a noticeable difference, at least not to the end user, but it can.

On the other hand, by querying the userinfo endpoint you’re assured of getting the most up-to-date user information. Did the user change their membership level a few minutes ago? If so, the userinfo endpoint will know about it, enabling the resource server to use that up-to-the-minute information in making its decision. Performance might lag slightly, but authorization decisions are based on up-to-date user information.

By comparison, JWT tokens are faster and more efficient than opaque tokens; that’s because user claims are included in the token itself and can be retrieved by the server without contacting the userinfo endpoint. Obviously that’s a faster way of doing things.

But, again, there’s a tradeoff here. When a JWT token is issued, user claims (such as a user’s membership level) can be embedded in the token. Once embedded, however, that user data is never changed. For example, suppose you issue a JWT access token (with a standard lifetime of 60 minutes) at 1:00 PM. At 1:05 PM, the user downgrades their membership level from Platinum to Silver; that means that the user should no longer have access to Platinum-level videos. 

With JWT tokens, however, the user will still have access to Platinum-level videos, at least until 2:00 PM. That’s because the access token says they have a Platinum membership and that access token isn’t updated when the user profile is updated (nor is the token questioned when presented to a resource server). Instead, you have to wait until 2:00 PM when the token is refreshed: at that point the newly-issued token will be updated with the most-recent user information.

In other words, opaque tokens are slower, but authorization decisions are made using up-to-date user information. JWT tokens are faster, but they aren’t guaranteed to have up-to-date user info. These differences are summarized in the following table:

Token Type

Performance

Data Recency

Opaque Token

Average: Network calls must be made to retrieve user data.

Current: Because the userinfo endpoint must be contacted whenever user data is needed, that data is always up-to-date.

JSON Web Token (JWT)

Fast: Token signature verification (using cached keys) can be carried out by the resource server and user data can be retrieved without contacting the userinfo endpoint.

Up to 60 minutes old: User information is embedded in the access token when the token is issued and is not updated. Because the default token lifetime is 60 minutes, that means the user data could be as much as one hour out of date.


A Hybrid Solution to the Performance/Data Recency Tradeoff

Fortunately, the aforementioned tradeoff doesn’t have to be an either/or proposition: either you have good performance or you have up-to-date user information. Instead, you can use a hybrid approach in which you issue JWT access tokens but, as the need arises, you make calls to the userinfo endpoint and retrieve the most-recent user data. 

For example, suppose your web site has a collection of videos that are accessible to your Gold- and Platinum-level members. A user wants to view the videos and presents a JWT access token that says they have a Platinum membership. But do they have Platinum membership? To be honest, we don’t know: the user could have downgraded their membership level a few minutes ago. But, in this case, we’re facing at a low-risk scenario: the worst thing that can happen is that, for an hour (at most), a user has access to videos that maybe they shouldn’t have access to. It’s a risk, but a small one, and one unlikely to cause damage to your organization.

JWT access token are made to order for situations like this: the resource server can make authorization decisions based on the information included in the token. And if that information is outdated and the server makes a wrong decision? In this case, there’s no real harm to your organization, and the problem will resolve itself when the token is refreshed. 

Let’s now suppose your website also has a set of videos that Platinum-level members can download for free. Because each download costs you money, you want to ensure that only Platinum-level members can access these videos: in this case, making a mistake (and allowing non-Platinum members to download the videos) is a more expensive, and thus a more high-risk, proposition. The solution? For these videos, have the resource server contact the userinfo endpoint and retrieve the most up-to-date user information. That way you can be sure that the user really does have a Platinum membership.

In other words, for a low-risk transaction let your resource server process the JWT token itself; for a high-risk transaction, have the resource server contact the userinfo endpoint for up-to-date user information:

This gives you optimal performance for low-risk transactions (which are probably the lion’s share of your transactions) while ensuring that the most up-to-date user information is used for more high-risk decision-making.


 


Identity Tokens

Identity tokens are an example of a JSON web token (often abbreviated as JWT, and usually pronounced “jot”). A JSON web token is made up of three sections, with each section in the token separated by a period. For example, a JWT token might look similar to this, with different colors used to highlight the three sections:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkpzcWY1ZzVfR0s2Z3Y5Mk1HZFcxcDZUbE5RM25sTXZRM3hEeTFXZ
WQ0MlEifQ.
eyJzdWIiOiJjM2Q2NzExOC1iNWI3LTQ3YzMtOGYyNi1iOGEyZDQwMzhjNDciLCJpYXQiOjE1MzA4MjgxNzQsIm
V4cCI6MTUzMDgzMTc3NCwiYXVkIjoicHJpdmF0ZWdyb3VwcyIsImlzcyI6Imh0dHBzOi8vYXMtZXVkZW1vOXgwcHVrdC5k
ZW1vLm1pYWFndWFyZC5jb20iLCJzY3AiOlsib3BlbmlkIiwicmVhZF9ob21lIiwibWFuYWdlX2hvbWUiXSwiY2lkIjoiMjZyd2h
td3J0bWo4Zm1kdDg3cnc4NXRkNmZtamo0ZWUiLCJqdGkiOiJPRE5qTWpJeE5tSXRZamszWXkwME9EYzFMV0V4TlRjdE5X
TTRPR0ppWXpka1lUVXkifQ
.PpqrdT1b2q01EBQ1lByxKXx4JAVcuAE4Rq1gx3LE2yU

The three sections of a Hosted Login identity token include:

  • The header (shown in red)
  • The payload (shown in blue)
  • The signature (shown in green)

These three sections are discussed in more detail below. 

Identity Token Properties: Header

The header is used to describe the hashing algorithm (for example, RS256), the token type (always JWT, for JSON Web Token), and, optionally, the key identifier (kid). Key identifiers specify the JSON Web Key used to sign the token, and are usually omitted unless you use multiple keys for signing tokens.

A header section looks similar to this:

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "Jsqf5g5_GK6gv92MGdW1p6TlNQ3nlMvQ3xDy1Wed42Q"
}

These properties are described below:

alg

Identifies the cryptographic algorithm used to sign the token. For Hosted Login, this value will always be RS256, which references the hashing algorithm RSASSA-PKCS1-v1_5 using SHA-256.

kid

Key identifier, a case-sensitive string that indicates the jJSON Web Key used to sign the token. Each JSON Web Key includes a kid property that corresponds to the kid property used in the token header. 

kid is an optional property that is typically present only if you use more than one signing key.

typ

Specifies the type of token being returned. For an identity token, this value will always be JWT.

 

Identity Token Properties: Payload

The payload contains the actual information being transmitted by the token, as well as any additional metadata about the user or the token that might be needed. Payloads are made up of a series of claims, with a claim simply referring to a name/value pair (like “name”: “bob”or “color”: “red”). For example:

{
  "iss": "accounts.janrain.com",
  "sub": "8855454e-8146-11e8-adc0-fa7ae01bbebc",
  "aud": "c2a5b7bc-e329-b4e4-dd6b-eb1ae01c22aa",
  "iat": 1530897246,
  "exp": 1530900246,
  "jti": "ID.rWH0iZkhFNxAoDxR5LhLAOqNj2bQvmMaeQiqhH5BcAU"
}

Hosted Login payload claims are described below. Before we go much further, however, we should mention that payloads are encoded (using base64url encoding) but they are not encrypted. Because of that, you should avoid including confidential information (such as a user password) in a payload whenever possible.

Note. If payloads aren’t encrypted, then why bother encoding them? That's mainly done to ensure that all JWT tokens use the same character encoding, deftly sidestepping the issues that could arise if one system in the authentication chain uses UTF-8 encoding while another uses ISO 8859-1 encoding.

at_hash

Access token hash value, used to verify which access token was issued along with the identity token. The at_hash value is calculated by:

  1. Using the token’s hashing algorithm (alg) to hash the access token.
  2. Base64url encoding the leftmost half of the hash value octets.

aud

Audience that the token is intended for. For Hosted Login, you will always see both the OIDC client ID (e.g., a22c9604-7b27-464f-bff5-83ba229323af) and the redirect URI (e.g.,  https://documentation.akamai.com) included in the aud claim.

auth_time

Indicates the last time that the user was authenticated. The auth_time claim is formatted using Unix epoch time, which represents the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC) on January 1, 1970 For example, the value 1553405263 represents Saturday, March 23, 2019 at 22:27:43 Pacific Daylight Time. See Converting Unix Epoch Time for more information.

azp

Specifies the authorized party to which the token was issued. For Hosted Login, the authorized party will always be the OIDC client ID.

exp

Specifies the date and time when the token expires. The exp claim is formatted using Unix epoch time, which represents the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC) on January 1, 1970. For example, the value 1553405263 represents Saturday, March 23, 2019 at 22:27:43 Pacific Daylight Time. See Converting Unix Epoch Time for more information.

By default, identity tokens expire 1 hour after they are issued.

global_sub

Contains all the information needed to locate a specific user. For example:

capture-v1://capture-alb-border.us.janrain.com/x3gmnnjeyzyrrt2nm5drf5nkn8/user/2edd2f32-1e49-4bf2-b164-763781761b52

In the preceding example:

  • capture-v1://capture-alb-border.us.janrain.com– Internal path to the Hosted Login identity store.
  • x3gmnnjeyzyrrt2nm5drf5nkn8– The Hosted Login application ID.
  • user– The entity type where user profiles are stored.
  • 2edd2f32-1e49-4bf2-b164-763781761b52– The Hosted Login UUID (sub) of the user.

iat

Specifies the date and time when the token was issued. The iat (issued at time) claim is formatted using Unix epoch time, which represents the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC) on January 1, 1970. For example, the value 1553405263 represents Saturday, March 23, 2019 at 22:27:43 Pacific Daylight Time. See Converting Unix Epoch Time for more information.

iss

URL for the endpoint that issued the token.

sub

Unique identifier for the end user (the subject). This will always be the user’s Akamai Identity Cloud UUID (Universally Unique Identifier).

Identity Token Properties: Signature

If you buy a house or a car, you need to sign a document (or a set of documents): that signature serves as proof of your identity, and attests that it really was you who bought the house or the car. The signature in a JWT token serves a similar purpose: if the signature can be verified, it proves that the JWT has not been changed since the token was issued AND that the token was issued by Hosted Login.

To create a signature for a token you need to:

  1. Combine the header, the payload, and a secret (a private key maintained by Hosted Login).
  2. Encrypt the resulting string by using one of the allowed encrypting algorithms. The most commonly-used signature algorithm is HMAC SHA256 (aka HS256).

The fact that the header and the payload are part of the signature explains how the signature helps verify that the token contents have not been changed since the time token was issued. For example, suppose we have a sample token issued to the user Bob Jones. We then change the audience in for that token to Robert Jones instead of Bob Jones. That's a simple enough change, but notice what happens to the token:

Token for Bob Jones

Token for Robert Jones

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJCb2Ig
Sm9uZXMiLCJpc3MiOiJBa2FtYWkgSWRlbnRpdHkgQ2xvdWQ
iLCJleHAiOjE1NTUxOTY4NDYsImlhdCI6MTU1MzAxMTgyNX0

.i2_tN25AxQqYvvD-KeKSQdXLNd1V9t3AWDoelZDCAXM

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJSb2
JlcnQgSm9uZXMiLCJpc3MiOiJBa2FtYWkgSWRlbnRpdHkgQ
2xvdWQiLCJleHAiOjE1NTUxOTY4NDYsImlhdCI6MTU1MzA
xMjE1MH0
.e38aMWhGIEjDPhKLhGfhG7KYTyoZ-QJkoRKJDZ0170

As you can see, the header is the same in each token. And that makes sense: after all, the header is the same in each token. The payload is different, however, because the two payloads are different. (The difference might be minor, but it's a difference nonetheless.) And because the payloads are different, that means that the signatures should also be different. And they are. 

Anti-forgery State Token (Nonce)

Cross-site request forgery attacks occur when a user is tricked into running an unauthorized command while logged on to an app or website. (Typically this happens when a malicious link is embedded into the app of website.) One way to help guard against CSRF attacks is to use an anti-forgery state token when making authentication requests. This token is included in the initial authorization request as the value assigned to the state parameter and is returned by the authorization server along with the authorization code. The client can then compare the original value of the state parameter with the returned value and verify that they are the same. If they aren't, that suggests that some sort of CSRF attack might have occurred.

Here's what the state parameter looks like in an API call:

https://v1.api.us.janrain.com/00000000-0000-3000-8000-000000000000/login/authorize?
client_id=kbyuFDidLLm280LIwVFiazOqjO3ty8KH
&redirect_uri= https://documentation.akamai.com/callback
&scope=openid profile email phone address
&response_type=code
&state=b49135acc48faea8fa0b641456523967f766ccb4

Anti-forgery state tokens differ from other OIDC tokens in that they are not generated by the server; instead, you create the token yourself and include it in your authentication request. Although the token value can be any character string of your choice, it's recommended that you use a string of at least 32 characters generated by a random number generator. The resulting value is then used to configure the state parameter:

state = 99846266547289293014765635352342

So how do you generate a 32-character string by using a random number generator? One easy way is to use an openssl command like this:

openssl rand -hex 16

In turn, that gives you back something similar to this:

f5e32a98c0b526f317a87cfd12ebd955

Note that the state parameter is not required. Instead, it’s simply an option available to you if you want to use it.

Trivia. You will often hear the anti-forgery state token referred to as the “nonce.” Nonce is an English word referring to a word or a term coined for a specific occurrence or occasion.