The central tenet of OAuth is to limit access to only what is needed for an operation. In Keycloak, this need-to-know limitation is called “scope”. A trusted application may have full access to all of the resource belonging to a user, but that does not mean third-party applications should be granted the same access. Rather, the third-party application — needed because of its important functionality — should only get access to the resources needed for its purpose.
Keycloak 26.2.0 adds the Standard Token Exchange. Standard Token Exchange supports an Internal-Internal scenario whereby an application can take a token granted full resource access and downscope it to a token with lesser access. Downscoping refers to the removal of access as realized as scope attributes in an access token. The benefit is that the downscoped token can be transmitted to a lower-trust application, removing the potential for misuse.
Demo
This demonstration will show how to downscope a token with full resource access. In the demo, an application requests scopes for “contacts” and “images”. scope=contacts will grant access to a resource server “ContactsResource”. scope=images grant access to a resource server “ImagesResource”.
A third-party application provides special image-processing functionality. To do this, the third-party app needs to access ImagesResource. However, the third-party app does not need to access ContactsResource.
The following diagram shows the relationship between the applications and resources as well as the entities needed in Keycloak.
Step 1: Create Server
This demo uses the following Docker container command to run.
docker run --name downscoping -demo -d -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -p 8080:8080 quay.io/keycloak/keycloak:26.2.0 start-dev
Step 2: Create Realm
Create a Realm “realm_a”.
Step 3: Create User
Add a user “carlw” or your name. Check “email verified” and fill in email, first name, and last name to make sure the account is set up correctly.
Step 4: Create Client
Create a Client “myclient”. Enable Client Authentication. Select “Direct access grants” and “Standard Token Exchange”.
“Direct access grants” is soon to be deprecated. It is used in the demo to make the interactions more understandable.
Step 5: Create Client Scopes
Add a pair of Client Scopes, one for “contacts” and one for “images”. Be sure to check the “Include in token scope” option.
For contacts
For images
Step 6: Add Client Scopes to Client
Client Scopes are Realm-wide definitions. Navigate to Client “myclient” and select the Client Scopes tab. Press the Add Client Scope button and select “contacts” and “images”.
Make the email and profile scopes “Optional”. This is not required for the downscoping described in the demo. It makes the behavior of the scope processing easier to follow.
Step 7: Fetch Initial Access Token
Using the carlw account, submit a Direct Access Grants request to myclient. This simulates an interaction with “App”, the trusted application. client_secret will be different for other containers.
POST http://localhost:8080/realms/realm_a/protocol/openid-connect/token HTTP/1.1Content-Type: application/x-www-form-urlencoded
grant_type=password&username=carlw&password=abc123&client_id=myclient&client_secret=tAIWmdFmmImEXqb7zED8C6sQkxHMbrba&scope=images%20contacts
The following is a formatted and decoded body of the preceding request. App is requesting access to images and contacts.
grant_type=password
username=carlw
password=abc123
client_id=myclient
client_secret=tAIWmdFmmImEXqb7zED8C6sQkxHMbrba
scope=images contacts
The /token request will generate a response like the following. Note the returned scope attribute. (The access_token and resource_token attributes have been elided.)
{
"access_token" : "eyJhbGciO...",
"expires_in" : 300,
"refresh_expires_in" : 1800,
"refresh_token" : "eyJhbGciOiJIU...",
"token_type" : "Bearer",
"not-before-policy" : 0,
"session_state" : "a67803ac-cdf1-4b54-a41a-6be8828ba3d0",
"scope" : "images contacts"
}
Decoding the signed token, the scope values are the same
},
"scope": "images contacts"
}
This token is suitable for “App” which is allowed full access to both resources ContactsResource and ImagesResource. The next step will remove access to ContactsResource so that a lesser token can be shared with “ThirdPartyApp”.
Step 8: Downscope Token
This next step will use the new (26.2.0) Standard Token Exchange feature to convert the token generated in Step 7 into one with lesser access.
It is important to note that Step 7 could be repeated omitting scope “contacts” and get an images-only token. However, this would affect login, logout, and session management. A second session would be created which may violate a single-session-per-user policy or make logging out more complicated.
Request
The following is a token exchange request made from carlw.
POST http://localhost:8080/realms/realm_a/protocol/openid-connect/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&subject_token=ey...&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&client_id=myclient&client_secret=tAIWmdFmmImEXqb7zED8C6sQkxHMbrba&scope=images
The following is a formatted and decoded payload from the preceding request.
grant_type: urn:ietf:params:oauth:grant-type:token-exchange
subject_token: eyJhbGci...
subject_token_type: urn:ietf:params:oauth:token-type:access_token
requested_token_type: urn:ietf:params:oauth:token-type:access_token
client_id: myclient
client_secret: tAIWmdFmmImEXqb7zED8C6sQkxHMbrba
scope: images
Response
This is the response from the preceding request.
{
"access_token" : "eyJhbGciO...",
"expires_in" : 300,
"refresh_expires_in" : 0,
"token_type" : "Bearer",
"not-before-policy" : 0,
"session_state" : "5d7dc409-78b6-42b3-98b6-2a0e4cf8d7a2",
"scope" : "images",
"issued_token_type" : "urn:ietf:params:oauth:token-type:access_token"
}
The scope attribute — which was contacts and images in Step 7 — is now reduced to just “images”.
The decoded access_token also reflects this
},
"scope": "images"
}
As your app ranges over more resource and integrates with other software, it may be the focal point for security. As such, it bears are responsibility to manage access as it absorbs new functionality (coming from third-party software). With Keycloak 26.2.0, there is a mechanism for downscoping through the newly-added Standard Token Exchange feature. Rather than getting a second access token — and adversely affecting session management — use token exchange to transform an access token into a lesser capable token suitable for transmission to a third-party app.