How-to - Kong with Keycloak

Use case

Authentication is delegated to Keycloak. Clients apps are registered into Keycloak and provide the ability to an user to claim an access token. This token is a JSON Web Token. It contains user's identity (subject id, name, group, roles) and some meta data relatives to the authorization process (issuer, time to live, etc.). The access token can be claimed using several methods. Keycloak supports major OAuth2 flows. The access token can be also online or offline. An online token is a token used by client apps having a direct user interaction (GUI such as: web site, desktop apps, mobile apps, etc). It's a short-lived token, so it shall be renew before its expiration date using a refresh token. Once claimed, the access token is renewed as well as the refresh token. And the process is repeated during the whole user session life time. An offline token is a bit different. It's used for client applications that need to have access to the user's data in background. Without user interaction. This is especially useful for client apps like CLI, background services or mobile applications. The user use the client app once to claim an offline token. The access token is as usual short-lived, but not the refresh token. This token is valid as long it is used during an long period of time (a month). The refresh token is used in backrgound in order to claim a new access token. So, an offline token is very sensitive. This is why the user have the ability to see all the claimed offline tokens. Furthermore, claiming an offline token require user approval (however this can be implicit regarding the used flow). This approval can be revoked afterward.

The process is following:

  1. The user is signing in on the client app. He is redirected to the Keycloak login page. He can use his credentials or use a third party identity provider (depending the IAM configuration).
  2. Once logged, Keycloak is issuing an access token and a refresh to the user.
  3. Both tokens are saved by the client app for the next usage.
  4. Now the client application can access to the API by filling the Authorization http header with the access token. The access token is short-lived and must be refreshed before its expiration date. So the client app should verify each time that the access token is not about to expire. In this case, the client app shall use the refresh token to claim a new access token to Keycloak.
  5. Kong validates the access token. It verify the signature, the issuer and the expiration time of the token.
  6. If everything is ok, Kong transfers the request to the backend service. The access token is still carried by the the Authorization header and can be decoded by the backend services to gather information required by the fine grained authorization layer (subject id, group, roles). Note than Kong add the client app ID into the header. This can be useful to the backend service in order to identify where the user comes from.
  7. And the service response is transmitted to the client app through all layers.

And now, how can we do that?

x509 setup

Before setting up any tools we have to create a certificate. This certificate will be used by the IAM to sign the JSON Web Tokens (with the private key). And the certificate will be also used by the API Gateway to verify the tokens.

Here a quick (and dirty) method to generate a self signed certificate:

$ # Generate the private key
$ openssl genrsa -out mykey.pem 1024
$ # Extract the public key
$ openssl rsa -in mykey.pem -pubout -out mykey-pub.pem
$ # Generate the certificate request
$ openssl req -new -key mykey.pem -out mycert.csr -subj '/CN=my-common-name/'
$ # Generate the certificate
$ openssl req -x509 -sha256 -days 365 -key mykey.pem -in mycert.csr -out mycert.crt
$ # Cleaning
* rm mycert.csr

Kong setup

Let's try to configure a simple backend service. I our case the backend will be httpbin. It's a simple online service that expose endpoints for HTTP testing.

$ curl https://httpbin.org/get
{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/7.38.0"
  }, 
  "origin": "160.92.7.69", 
  "url": "https://httpbin.org/get"
}

First we have to declare this API into the API gateway:

$ curl -i -X POST \
  --url http://localhost:8001/apis/ \
  --data 'name=test' \
  --data 'upstream_url=https://httpbin.org/' \
  --data 'request_host=api.example.org' \
  --data 'request_path=/test' \
  --data 'strip_request_path=true'

Try the new API:

$ curl -X GET \   
  --url http://localhost:8000/get \
  --header 'Host: api.example.org'
{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/7.38.0", 
    "X-Forwarded-Host": "api.example.org"
  }, 
  "origin": "172.17.0.4, 160.92.141.133", 
  "url": "http://api.example.org/get"
}

Now we are able to add a plugin to the new API. Let's activate the JWT plugin:

$ jwt_plugin=`curl -X POST http://localhost:8001/apis/test/plugins --data "name=jwt"`
$ echo $jwt_plugin
{"api_id":"91652817-1dee-4dd9-94f1-d93af088977b","id":"118f39e7-6e5f-4bc3-abf5-679b1af90e33","created_at":1473675276000,"enabled":true,"name":"jwt","config":{"uri_param_names":["jwt"],"secret_is_base64":false,"key_claim_name":"iss"}}
$

Note the id attribute in the response. It's the ID of the plugin for this API.

Now we can create a consumer. The consumer is an entity consuming the API. In our case the consumer will be the client app:

$ consumer=`curl -X POST http://localhost:8001/consumers --data "username=curl"`
$ echo $consumer
{"username":"curl","created_at":1473675519000,"id":"ffb4cc8d-414d-4c34-82ba-c97a295daddc"}

Note the id attribute in the response. It's the ID of the consumer.

Once created we add RS256 JWT credentials to the consumer.

$ TOKEN_ISSUER="my-token-issuer"
$ RSA_PUB_KEY=`cat mykey-pub.pem`
$ CONSUMER_ID=`echo $consumer | jq ".id"`
$ curl -X POST http://localhost:8001/consumers/$CONSUMER_ID/jwt \
  --data "key=$TOKEN_ISSUER" \
  --data "algorithm=RS256" \
  --data-urlencode "rsa_public_key=$RSA_PUB_KEY"

Note:

  • the usage of data-urlencode in the command to properly encode the public key
  • the key parameter set to the ID of the token issuer. This key is used to validate the issuer field of the token.

Now we can test the new API:

$  curl -X GET \
  --url http://localhost:8000/get \
  --header 'Host: api.example.org'
{"message":"Unauthorized"}

The request is rejected as expected because we didn't provide the Authorization header. Let's retry with a valid header. In order to build a valid JWT we can forge one with jwt.io. Here an example:

{
  "alg": "RS256",
  "typ": "JWT"
}
{
  "sub": "1234567890",
  "name": "John Doe",
  "iss": "my-token-issuer"
}

Note the iss attribute that refer to the issuer of the token. As explain before it must match the key registered into the API gateway.

The result is:

$ ACCESS_TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaXNzIjoibXktdG9rZW4taXNzdWVyIn0.ttvC6in9I0X2xBoQJZHN_iGPq1NnYXgH0pk-_RAXSey-6Nb1z6p4ZqkWyj6VjZLvlJk5sKjkxC6DURecglz1HzGdJ028zJGkBaINmSXH64uCXPQkH2WojIORsRS4Oa9h-a0T21gA24YT1_NRBbtkl4sWthhhoXZYSpCD06eJ3F8"

Let's retry with a proper header:

$  curl -X GET \
  --url http://localhost:8000/get \
  --header "Host: api.example.org" \
  --header "Authorization: Bearer $ACCESS_TOKEN"
{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaXNzIjoibXktYXBpLWNsaWVudCJ9.L-Onae4kf7WNhoIZuEYbVhpCoQNnDSU8Sz-P-YaJ1KT49Ip72IISDCO274nRjTK1AynbyU7AK6W5VBSWMhhdP9xlZ1s-H9NW-hN0TK_Yylrbcf-xB6KAR0V64qZtQqCu9QPg7Ql7kmZ5U-iKd0YfHCJ-5vuhV_lX0OzfcT4c72U", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/7.38.0", 
    "X-Consumer-Id": "ffb4cc8d-414d-4c34-82ba-c97a295daddc", 
    "X-Consumer-Username": "curl", 
    "X-Forwarded-Host": "api.example.org"
  }, 
  "origin": "172.17.0.4, 160.92.141.133", 
  "url": "http://api.example.org/get"
}

Now the API gateway let pass the request to the backend service and add some useful request headers.

results matching ""

    No results matching ""