This article is dedicated to describe the behaviour and usage of offline sessions and offline tokens within Keycloak.

The behaviour of offline tokens is also illustrated through the off-line-token example of the keycloak demo template (available with version 5.0 of keycloak sources). This example has been very slightly updated also to perform REST API calls (using direct grant access) in order to manipulate directly/ introspect offline access tokens

Pointer

Doc reference

Off line example

main feature of an offline token

The main features :

  • (1) OffLine Sessions
    • An offline session is like an active session, created upon user authentication with scope=offline_access
    • An offline session activity is controlled by a an offline token, and can be indefinitely maintained as long as the offline token has not expired (offline token session timeout)
    • Offline session gets revoked:
      • upon offline token idle timeout has been superseded
      • manual revokation of the off line session
  • (2) offline token :
    • is a special kind of refresh token
    • is typed as offline token (in the payload of the signed jwt offline token)
    • has to be used as a refresh token for token endpoint queries
    • allows keycloak client apps to obtain a new access token without the need of having the user to reauthenticate against keycloak
    • offline tokens can be used
      • after user active session has expired
        (it means that the offline token can be used by the client app to obtain a an access token, even if there is no user active session)
      • are persistent across keycloak restart
        (it means that same offline tokens can be used, even if keycloak is restarted)
  • (3) client app need to request offline token at first login either through
    • URI, like
      • /offline-access-portal/app/login?scope=offline_access
    • or CLI using direct access grant (REST API call) adding offline_access to the scope
      • scope=openid info offline_access »
  • (5) offline token not subject to sessionIdleTimeout /sessionMaxLifeTimespan as usual Refresh tokens
  • (6) offline token need to be managed by the client application itself
    • It means that offline tokens have to be stored on a per client basis
  • (7) the offline token remains valid during Offline Session Idle timeout before the offline token is revoked
  • (8) an offline token when used generates as response (upon successful request)
    • an access token
    • a new offline token.
  • (9) use of Refresh token flag
    • Revoke Refresh token flag should set to ON in order to avoid offline token piling up
      • this means that the offline token once used will be immediately revoked
        (This basically means that refresh tokens have a one time use.)
    • A request with the current offline token performs:
      • generation of new access token
      • generation of a new offline token
      • revokation of the current offline (which has thus been used only once)
  • (10) Offline SessionMax/ Offline Session Max Limited
    • offline token expires after a 60 days time period (when this flag is enabled), regardless of offline refresh token actions.
    • The behaviour of this flag tends to endeavour that same token reused multiple times. When Revoke Refresh tokens flag is active, such a flag does not make much sense as current offline token get immediately revoked.

Introspection of an offline token

Offline token

A keycloak offline token is refresh token of type signed JWT.
When introspecting the content of an offline token, to be noticed :

  • payload
    • the type is Offline
  • scope
    • contains off_line access
 {
   "jti": "339752cf-14ac-4ae3-b80b-c96af40d2adf",
   "exp": 0,
   "nbf": 0,
   "iat": 1566405530,
   "iss": "https://localhost:8080/auth/realms/demo",
   "aud": "https://localhost:8080/auth/realms/demo",
   "sub": "74fded5b-292b-4cea-8771-c47550c9ea0d",
   "typ": "Offline",
   "azp": "offline-access-portal",
   "auth_time": 0,
   "session_state": "6979c960-9cf7-4172-a16d-6c21e245c222",
   "realm_access": {
     "roles": [
       "offline_access",
       "user"
     ]
   },
   "scope": "openid profile offline_access email"
 } 

Such an offline token is issued from the request

 curl \  
 -d "client_id=offline-access-portal" -d "client_secret=password" \  
 -d "username=user1" -d "password=password" \
 -d "grant_type=password" \
 -d "scope=openid info offline_access" \   
 https://localhost:8080/auth/realms/demo/protocol/openid-connect/token | jq 
Refresh token

As a matter of guidance is provided the introspection of a usual refresh token (payload only)

 curl \  
 -d "client_id=offline-access-portal" -d "client_secret=password" \  
 -d "username=user1" -d "password=password" \
 -d "grant_type=password" \
 -d "scope=openid info offline_access" \   
 https://localhost:8080/auth/realms/demo/protocol/openid-connect/token | jq 

To be noticed :

  • typ :
    • « Refresh »
  • scope
    • does not contains offline_access

The correspondingCLI requestto obtain a refresh token is

 refresh_token=`curl \
 -d "client_id=ldap-app" -d "client_secret=password" \
 -d "username=user1" -d "password=password" \
 -d "grant_type=password"  \
 -d "scope=openid info"  \
 https://localhost:8080/auth/realms/ldap-demo/protocol/openid-connect/token  | jq -r '.refresh_token'` 

Lifecycle of Offline access token

In this section is illustrated the behaviour of an offline access token, throughout the example

Within keycloak, this example has been very slightly modified with following :

  • Direct access grant mode has been added to the offline-access-app to allow REST API calls
  • User1 (user1/password) has been added to the demo-template realm
    This user has also the rôle offline_access_mode, which allows to manipulate offline access tokens.
obtaining an offline access token

An offline access token is obtained in the context of a user login session using the UI, by adding scope=offline_access

  • https://localhost:8080/offline-access-portal/app/login?scope=offline_access

It can also be obtained using directly the call on the command line for REST API

  • « scope=openid info offline_access »

Example using REST API

 
 curl \  
 -d "client_id=offline-access-portal" -d "client_secret=password" \  
 -d "username=user1" -d "password=password" \
 -d "grant_type=password" \
 -d "scope=openid info offline_access" \   
 https://localhost:8080/auth/realms/demo/protocol/openid-connect/token | jq
 {
   "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJGSjg2R2NGM2pUYk5MT2NvNE52WmtVQ0lVbWZZQ3FvcXRPUWVNZmJoTmxFIn0.eyJqdGkiOiIyOGVhNTE1Ni1jN2Y5LTQ1OGQtOWRhYy02Y2Q2M2NhM2Q4YWIiLCJleHAiOjE1NjY0MTEwNzEsIm5iZiI6MCwiaWF0IjoxNTY2NDExMDExLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGVtbyIsInN1YiI6Ijc0ZmRlZDViLTI5MmItNGNlYS04NzcxLWM0NzU1MGM5ZWEwZCIsInR5cCI6IkJlYXJlciIsImF6cCI6Im9mZmxpbmUtYWNjZXNzLXBvcnRhbCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6IjUxMzRiODQ0LWMxMjktNDE2OS1hNGNlLTI4YzI3MmVhOTIyZiIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1c2VyIl19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIG9mZmxpbmVfYWNjZXNzIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ1c2VyMSJ9.A2NwfNMlC0-Pr5COCSEhbS0e0yWf1haDEJ4ym9U_de9Bgt9_w5DXRM-0fw9d_sWohZ1Q6YpaUhwJG_dHzdquXZVTaR0baBl0oQsKlLZTqBodgEaMtNCdjtZi-HtN1sN2Gl8EtFfm-mPxrl2FgYGm-ekS0PUcdPRyUxlw3zTh8_Q",
   "expires_in": 60,
   "refresh_expires_in": 0,
   "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxZTRhYTI2Ni1iNmI4LTQ5ZGYtYTEyMC0zMzFhMzA1NDgwYjcifQ.eyJqdGkiOiIzYjdmNjA3OS1lMWQyLTQ2YWItYmQ5Ni04MDkxODI0YmFiMzgiLCJleHAiOjAsIm5iZiI6MCwiaWF0IjoxNTY2NDExMDExLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGVtbyIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9hdXRoL3JlYWxtcy9kZW1vIiwic3ViIjoiNzRmZGVkNWItMjkyYi00Y2VhLTg3NzEtYzQ3NTUwYzllYTBkIiwidHlwIjoiT2ZmbGluZSIsImF6cCI6Im9mZmxpbmUtYWNjZXNzLXBvcnRhbCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6IjUxMzRiODQ0LWMxMjktNDE2OS1hNGNlLTI4YzI3MmVhOTIyZiIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVzZXIiXX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgb2ZmbGluZV9hY2Nlc3MgZW1haWwifQ.1nB-KZmU8nSWxPuWl2rPONtrGlRaHEqtSxonPJIy1Og",
   "token_type": "bearer",
   "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJGSjg2R2NGM2pUYk5MT2NvNE52WmtVQ0lVbWZZQ3FvcXRPUWVNZmJoTmxFIn0.eyJqdGkiOiJmNzNkY2NiZi1mYjE4LTQ4NDAtYWZkYS01YzNmZjQ3YWFiOGIiLCJleHAiOjE1NjY0MTEwNzEsIm5iZiI6MCwiaWF0IjoxNTY2NDExMDExLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGVtbyIsImF1ZCI6Im9mZmxpbmUtYWNjZXNzLXBvcnRhbCIsInN1YiI6Ijc0ZmRlZDViLTI5MmItNGNlYS04NzcxLWM0NzU1MGM5ZWEwZCIsInR5cCI6IklEIiwiYXpwIjoib2ZmbGluZS1hY2Nlc3MtcG9ydGFsIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiNTEzNGI4NDQtYzEyOS00MTY5LWE0Y2UtMjhjMjcyZWE5MjJmIiwiYWNyIjoiMSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoidXNlcjEifQ.UZO2r2hq3_yQIbdBC0_6L6hBI6hXrpwcHJMKGRwxThmzbnwkWSLJ2Fc6nZrf1cSKwBFRsCQANmtclbxFsfXF8Ly9SZ9DW0wbp3ixwPfvvYS736U7rCnelaHOno4-ZVVvI-fefDJkAGxuOzJe-wi2M_2wzEX8_ZYf10ps0OOQWyM",
   "not-before-policy": 1566405102,
   "session_state": "5134b844-c129-4169-a4ce-28c272ea922f",
   "scope": "openid profile offline_access email"
 } 

A user active session and offline token are generated

It is possible to list directly the sessions through the admin UI.

Offline Sessions and Offline tokens
using the offline token

Below is indicated how to reuse an offline token (which is the one obtianed just previously).
As outcome is shown that the answer corresponds to :

  • access token
  • offline access token
 
 curl \
 -d "client_id=offline-access-portal" -d "client_secret=password" \
 -d "grant_type=refresh_token" https://localhost:8080/auth/realms/demo/protocol/openid-connect/token \
 -d "refresh_token=$refresh_token" | jq
 
 
   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                  Dload  Upload   Total   Spent    Left  Speed
 100  3463  100  2648  100   815   517k   159k --:--:-- --:--:-- --:--:--  676k
 {
   "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJGSjg2R2NGM2pUYk5MT2NvNE52WmtVQ0lVbWZZQ3FvcXRPUWVNZmJoTmxFIn0.eyJqdGkiOiIyY2FhYmJhNC0xYjJlLTQzY2QtYmM1My0xYzJlN2VlY2FhMGUiLCJleHAiOjE1NjY0MTI2NjEsIm5iZiI6MCwiaWF0IjoxNTY2NDEyNjAxLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGVtbyIsInN1YiI6Ijc0ZmRlZDViLTI5MmItNGNlYS04NzcxLWM0NzU1MGM5ZWEwZCIsInR5cCI6IkJlYXJlciIsImF6cCI6Im9mZmxpbmUtYWNjZXNzLXBvcnRhbCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6ImYwZDMxNDlmLWM1MWYtNDY4Mi04ZWMzLTIwMzEwZjk2YmFlZSIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1c2VyIl19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIG9mZmxpbmVfYWNjZXNzIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ1c2VyMSJ9.Q5MzaHIOjkJo6xpi8E8cZ5g1GwkWHUbEJOal5oExb9ffBTuYhA4Sfuondj0GU00tOeLMXLOal5KiNfT1S3wY0kKWVHVV4A83lp90wSp7ePt_ZZ9Kto5tOL3dVlmL8YQj-tC6WhKgO-Q2NJN2uGFbc0vmEhfbiFjVYx2GG0jW7_4",
   "expires_in": 60,
   "refresh_expires_in": 0,
   "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxZTRhYTI2Ni1iNmI4LTQ5ZGYtYTEyMC0zMzFhMzA1NDgwYjcifQ.eyJqdGkiOiI1NzhmYzU5Ny04MDY0LTRlZTItYjQyZi1mMzUyMjRkNmNhMDAiLCJleHAiOjAsIm5iZiI6MCwiaWF0IjoxNTY2NDEyNjAxLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGVtbyIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9hdXRoL3JlYWxtcy9kZW1vIiwic3ViIjoiNzRmZGVkNWItMjkyYi00Y2VhLTg3NzEtYzQ3NTUwYzllYTBkIiwidHlwIjoiT2ZmbGluZSIsImF6cCI6Im9mZmxpbmUtYWNjZXNzLXBvcnRhbCIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6ImYwZDMxNDlmLWM1MWYtNDY4Mi04ZWMzLTIwMzEwZjk2YmFlZSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVzZXIiXX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgb2ZmbGluZV9hY2Nlc3MgZW1haWwifQ.FSJ6bcu9pc_mSpN1cqE3CpQ3tbAKds1v3WO5DAOigRM",
   "token_type": "bearer",
   "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJGSjg2R2NGM2pUYk5MT2NvNE52WmtVQ0lVbWZZQ3FvcXRPUWVNZmJoTmxFIn0.eyJqdGkiOiJhNmFkMjkwOC02Y2Q4LTQyYmUtOTA4OC01ZmEwN2ZmZjZjYzgiLCJleHAiOjE1NjY0MTI2NjEsIm5iZiI6MCwiaWF0IjoxNTY2NDEyNjAxLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvZGVtbyIsImF1ZCI6Im9mZmxpbmUtYWNjZXNzLXBvcnRhbCIsInN1YiI6Ijc0ZmRlZDViLTI5MmItNGNlYS04NzcxLWM0NzU1MGM5ZWEwZCIsInR5cCI6IklEIiwiYXpwIjoib2ZmbGluZS1hY2Nlc3MtcG9ydGFsIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiZjBkMzE0OWYtYzUxZi00NjgyLThlYzMtMjAzMTBmOTZiYWVlIiwiYWNyIjoiMSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoidXNlcjEifQ.iOxxVFMrjFyjROKZLByAmRbyB8znzPr_IOexQIkuA0571FgA7cIjlVYcih1bnECeM8lRAvlCZPGlDBdEmznRqU8eEhEGIFM1jhxgFZPV8J6Ww0XHZNPDcv_z5dLVbqen9XxQkRLGYZKw23_LCCMaQ-SNFJo_PnQzjuEsQYUpeOk",
   "not-before-policy": 1566411963,
   "session_state": "f0d3149f-c51f-4682-8ec3-20310f96baee",
   "scope": "openid profile offline_access email"
 } 

It would be possible to reuse the same offline token for further calls, but this would induce offline tokens piling up. Keycloak implements a mechanism (Revoke Refresh token flag) to avoid refresh token explosion and hence offline (as offline tokens are a special form of refresh token).
Whenever this flag, the current offline token is replaced by the new one.

As a consequence, it means that the application has to update/store after each offline token invokation the new offline token.

Below is showcased how to set the Revoke Refresh token flag.
if on, will revoke that refresh token and issue another with the request that the client has to use. This basically means that refresh tokens have a one time use

Note :
The different token time values provided should not be be accounted for, as they have provided only for test purpose.

Using the offline app example using the UI

The goal of this esction section is to showcase another aspect of offline tokens, where it is possible to use offline token after the user has disconnected to access to another microservice.

Here the scenario displayed is :

  • (1) the user is connecting/authenticating to the app using the UI (using offline mode)
    • This entails the creation of an offline token which is stored/managed by the app
    • creation of an user active session and also offline session
  • (2) the user disconnects (logout)
    • disappearance of the user active session
    • only offline session remains
  • (3) application is using the offline token
    • allows to connect to the microservice (which is a database) using the new access token obtained.

Note :

  • This example is quite limited, and does not handle/take into account the new offline token which has been generated.
Step1 – Authenticating using the UI using offline URL access

Before authentication

after authentication  (offline token has been generated)

The admin UI shows :

step2 – Logout from the app

It remains only the offline token

Step3 – login with the offline token

The user is no longer logged.
The offline token is used to obtain another access token, which allows to access to the database.

Note :

  • The application has been able to use the offline access token to access to the database (declared in mode confidential)
  • This application does not handle the new offline token

Offline token revocation

Offline tokens can be revoked either from :

  • From the admin UI (or admin rest API) OR
  • from the user directly
Revocation of access token from the user

The user should go to his account management console.

For the corresponding application (offline-access-portal), by clicking on the Revoke Grant button, offline tokens will get revoked

Revoking the offline access token from the admin console

The administator should go to the user panel, and select the Consent TAB. Thus he has to click on « Revoke » button to revoke offline tokens for this user

Offline session Misc

OffLine Session Object

Keycloak maintains 2 types of sessions :

  • Active sessions and OffLine Sessions
  • Active Session and OffLine Sessions are created once a user authenticates (using scope=offline_access for offline session)
    • As a consquence, it means that an offLine session is created each time a user authenticates(assuming that it contains scope=offline_access)
  • The LifeSpan of Active Session is controlled by SSO SessionMax Time (10Hours) and SSO Session MaxIdleTime (30mn)
  • An OffLine Session is bound to offline token. It only expires only if idle timeout (Offline Session Idle) of the offline token has been elapsed, or manually revoked.
  • Assuming that OffLine token are used each time before the offLine SessionIdle Timeout, it means that the OffLine Session coudl last indefinitely.
  • A possible way to control offLine Session duration is to set a limit to offLine Session. (OffLine Session Max Time).

REST command to list the number of offLines sessions

OffLine sessions can be monitored with kcadm

 

 kcadm.sh get realms/demo/clients/65965596-3a25-4e65-b3e9-458d5ed870a4/offline-session-count
 {
   "count" : 3
 } 
 
 kcadm.sh get realms/demo/clients/65965596-3a25-4e65-b3e9-458d5ed870a4/offline-sessions
 [ {
   "id" : "dd3b4c98-3740-4468-8654-6babb3a3b1d9",
   "username" : "user1",
   "userId" : "74fded5b-292b-4cea-8771-c47550c9ea0d",
   "ipAddress" : "127.0.0.1",
   "start" : 1566479025000,
   "lastAccess" : 1566479117000,
   "clients" : {
     "65965596-3a25-4e65-b3e9-458d5ed870a4" : "offline-access-portal"
   }
 }, {
   "id" : "c11423d0-f7b3-45de-a49d-049ce516d960",
   "username" : "user1",
   "userId" : "74fded5b-292b-4cea-8771-c47550c9ea0d",
   "ipAddress" : "127.0.0.1",
   "start" : 1566479271000,
   "lastAccess" : 1566479271000,
   "clients" : {
     "65965596-3a25-4e65-b3e9-458d5ed870a4" : "offline-access-portal"
   }
 } 

Synthesis / Best practices with offline tokens

Below are synthesized the best practices to be used with offline access token

  • Offline access tokens is a kind of special refresh token
  • offline tokens allows the app to access to microservice, even if the user is disconnected
  • offline tokens are persistent across keycloak restart
  • an offline is valid during the offline idle timeout
  • offline token once invoked entails the creation of new access and new offline token
  • in order to limit the number of offline and to avoid combinatory explosion, best practice is to set the Revoke Refresh flag to on, so that the token that has been used get revoked automatically and is superseded by the new one.

    Such a measure allows to control the number of offline. A user should not have more than tan a handful of offline tokens (one per device type).
  • apps have to take care/manage and store their offline token. Upon usage of the offline token, they have to store the new offline token which has been generated.
  • If the user is working with multiple devices (workstation, mobile …) he will have to manage one access token per device type
janua
Les derniers articles par janua (tout voir)