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
-
https://www.keycloak.org/docs/latest/server_admin/index.html#_offline-access
This section should be considered as the penultimate reference for offline tokens.
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)
-
after user active session has expired
-
(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 »
-
URI, like
- (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.)
-
this means that the offline token once used
will be immediately revoked
-
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)
-
Revoke Refresh token flag should set
to ON in order to avoid offline token piling up
-
(
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.
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
- New Keycloak online training - 19 janvier 2022
- Sizing Keycloak or Redhat SSO projects - 8 juin 2021
- Keycloak.X Distribution - 28 janvier 2021