Open stormmore opened 6 years ago
There is this note in the Keycloak documentation that I am not 100% of the meaning:
If your requested_token_type parameter is a refresh token type, then the response will contain both an access token, refresh token, and expiration. Here’s an example JSON response you get back from this call.
It is also worth noting that there are 4 different types of token exchange with slightly different use cases:
The last 2 I am also super interested in as this will empower support teams to be able to understand a problem. Keycloak's Token Exchange documentation has the details
OK, I have been "playing" with Keycloak's token-exchange. Given the parameters passed below, the access and refresh tokens that are returned are JWT tokens:
curl -X POST \
-d "client_id=<CLIENT ID>" \
-d "client_secret=<CLIENT SECRET> \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
-d "subject_token=<3RD PARTY ACCESS_TOKEN>" \
-d "subject_issuer=<KEYCLOAK IDP ALIAS" \
--data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
https://<KEYCLOAK SERVER>/auth/realms/<REALM>/protocol/openid-connect/token
A redacted response example would be:
{"access_token":"<REDACTED JWT>","expires_in":300,"refresh_expires_in":1800,"refresh_token":"<REDACTED JWT>","token_type":"bearer","not-before-policy":0,"session_state":"cdf4d24f-cf05-4eb3-a1af-62efc5d46cc1","scope":"profile email"}
Example decoded JWT for access_token
:
{
"jti": "9a8613d3-8fb6-4017-9f7c-8f8d578f63b6",
"exp": 1537820164,
"nbf": 0,
"iat": 1537818364,
"iss": "https://<KEYCLOAK SERVER>/auth/realms/<REALM>",
"aud": "<CLIENT ID>",
"sub": "<CLIENT SECRET>",
"typ": "Bearer",
"azp": "<CLIENT ID>",
"auth_time": 0,
"session_state": "cdf4d24f-cf05-4eb3-a1af-62efc5d46cc1",
"acr": "1",
"allowed-origins": [
"/*"
],
"realm_access": {
"roles": [
"user"
]
},
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "profile email",
"email_verified": true,
"preferred_username": "username",
"email": "email@test.com"
}
and finally a decoded refresh_token
JWT:
{
"jti": "81fcde30-d66c-4523-b338-3aa6d50e1c9d",
"exp": 1537818664,
"nbf": 0,
"iat": 1537818364,
"iss": "https://<KEYCLOAK SERVER>/auth/realms/<REALM>",
"aud": "<CLIENT ID>",
"sub": "<CLIENT SECRET>",
"typ": "Refresh",
"azp": "<CLIENT ID>",
"auth_time": 0,
"session_state": "cdf4d24f-cf05-4eb3-a1af-62efc5d46cc1",
"realm_access": {
"roles": [
"gamer"
]
},
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "profile email"
}
So knowing this now, it looks like the 2nd call to the tokenInfo / tokenIntrospection endpoints is unnecessary at least for Keycloak's implementation of token exchange.
then, i would try to go first with a single call to 'IDP2' and see if it works in general. The second call can be added later if another IDP implementation requires it.
@stormmore can you specify how the filter should be configured?
In actual fact, since this is hitting the token endpoint, there are many use cases to be able to query that endpoint beyond my current use case. https://www.keycloak.org/docs/latest/securing_apps/#_token-exchange covers them pretty well. So it probably would be better to make it a more general "token" filter that takes a bunch of params that help it form the request to get a new token.
The part I have been thinking about quite a bit is, to accomplish this, Skipper would need to have a client id
and client secret
and these should probably be passed in through a file and will need a process to make sure if the file is updated it pulls in the update.
Due to the fact that you can do a token-exchange for a 3rd party client, I believe that one client id
and client secret
should be sufficient.
ClientID and clientsecret is also required for openID connect in https://github.com/zalando/skipper/pull/743. I am not sure what we do there, now. I think I added this as part of the filter, because some people might want to integrate different token providers and if you pass it as file via flag, then you bind the configuration more hard. On the other hand it leaks the secret to people that have access to the supportListener. Not sure how to satisfy all cases....
Use Case
I am currently looking at a scenario that requires users to authenticate with 3rd party IDP and I need to trust their tokens while passing the services behind Skipper a token from an internal IDP for authz. Similar workflow to:
Background
Looking at both the RFC Draft and Keycloak's implementation, it would be good to have a filter, tokenExchange maybe, that takes the
token
from theAuthorization: Bearer <token>
header and requests an exchange with the 2nd IDP used for Authz.Requirements
According to the Keycloak documentation, the filter should do a POST call to the
token
endpoint (e.g.http://localhost:8080/auth/realms/myrealm/protocol/openid-connect/token
- can be found in the/.well-known/openid-configuration
endpoint) with the following fields:client_id
- probably should be a global setting, maybe a conf file so it can be used with a K8s secretclient_secret
- same as client_idsubject_token
- from the Authorization: Bearersubject_issuer
- this is a variable that determines the upstream IDP (Keycloak translates it to a brokered IDP alias)audience
- optional field, allows for the generated token to be associated with a different OAuth2 clientgrant_type
-urn:ietf:params:oauth:grant-type:token-exchange
(static value)subject_token_type
-subject_token_type=urn:ietf:params:oauth:token-type:access_token
(static value)The response from this call should be something like:
This access token should then replace the one currently in the
Authorization: Bear <token>
request header and passed on to the next in the chain or the backend.