Blogg
Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn
Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn
In the first part, we implemented access control for a sample API using Kong plugins for OIDC and OAuth 2.0, and in the second part, we implemented access control using Mutual TLS. In this short post, we will combine OAuth 2.0 Client Credentials with Mutual TLS, which is a frequently used pattern.
When using OAuth 2.0 Client Credentials in IoT scenarios, client authentication towards the OAuth 2.0 token service based on x509 certificate using Mutual TLS is often required. For extra security, to mitigate the risk that a leaked access token is used to give access to an API to an attacker, an additional mechanism can be used to bind the access token to the client certificate to whom it was originally issued. The protected API thus requires a mutual TLS session to be used, alongside the use of an access token. If the token contains information about the intended client’s certificate, the API can verify that the token is not being misused by a bogus client.
Photo by Bryson Hammer on Unsplash
RFC8705 specifies how mutual TLS can be used as an authentication mechanism when requesting access tokens. It also specifies how the client certificate “fingerprint” can be encoded in the access token, thereby binding the token to the specific client. The cnf.x5t#S256
property contains a sha256
digest of the client certificate:
{
...
"sub": "service-account-system-client-mtls",
"typ": "Bearer",
"azp": "system-client-mtls",
"cnf": {
"x5t#S256": "4itBP0qzu-ZJAswWgV0dGyofr-Xqz8tfrhbxdecgev4"
},
...
}
Neither the Kong Enterprise oauth2-introspection plugin nor the Open Source kong-token-introspection that we used in part 1 supports validating a certificate-bound access token. They also are not flexible enough to meet a couple of common requirements: limit access to an API based on a set of required scopes that must be present in the access token, and making access token information such as username, client-id and scope easily available to the upstream API via custom http headers. All the actual x509 functionality is however provided by OpenResty, a Lua interface to Nginx on which Kong is based. Hence it is quite easy to build a custom Kong plugin which besides doing token introspection also can verify a client certificate digest against a certificate-bound access token.
I therefore wrote a simple token-introspection plugin in Lua, available as Open Source under the Apache license. If configured with the name of a header holding a client certificate and the access token is bound to a certificate as of RFC8705, the plugin will verify that the client certificate digest matches the digest from the access token. If configured with one or more scopes, the plugin will verify that the access token contains the configured scopes. If authorization is successful, the plugin will make a set of standard claims and optionally a set of custom claims from the access token available as http headers, for the upstream service to consume.
Let’s see how we can expose our sample API using mTLS and a certificate-bound access token! As before, a fully working, minimalistic example can be found in the Github repository.
We first update the Keycloak docker container to use https, using our self-signed PKI for both server certificate and trust store:
services:
...
keycloak:
image: quay.io/keycloak/keycloak
ports:
- "9080:9080"
- "9443:9443"
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
command:
- start-dev
- --db=dev-mem
- --hostname=host.docker.internal
- --http-port=9080
- --https-port=9443
- --https-certificate-file=/opt/keycloak/ssl/server/localhost.crt
- --https-certificate-key-file=/opt/keycloak/ssl/server/localhost.key
- --https-trust-store-file=/opt/keycloak/ssl/CA/localCA.p12
- --https-trust-store-password=secret
- --https-client-auth=request
- --import-realm
volumes:
- ./keycloak-test-realm.json:/opt/keycloak/data/import/realm.json
- ./certs:/opt/keycloak/ssl
Next, we add an OAuth 2.0 client that requires authentication using mTLS, again with the custom scope system-access
:
"users": [
...
{
"id": "service-account-system-client-mtls",
"username": "service-account-system-client-mtls",
"enabled": true,
"serviceAccountClientId": "system-client-mtls"
},
...
],
"clients": [
...
{
"clientId": "system-client-mtls",
"enabled": true,
"clientAuthenticatorType": "client-x509",
"standardFlowEnabled": false,
"serviceAccountsEnabled": true,
"authorizationServicesEnabled": true,
"protocol": "openid-connect",
"attributes": {
"x509.subjectdn": ".*O=Callista Enterprise AB,.*",
"x509.allow.regex.pattern.comparison": "true",
"tls.client.certificate.bound.access.tokens": "true"
},
"defaultClientScopes": [
"system-access"
]
},
...
],
"clientScopes": [
{
"name": "system-access",
"protocol": "openid-connect"
},
...
]
In the attributes
section, we specify the client certificate details for the client. By using a regular expression for the x509.subjectdn
value, we can again allow any system with a certificate issued for the Callista Enterprise AB
organization to be able to use this client. We also specify that access tokens for this client should be bound to the client certificate.
As before, we need to update the Dockerfile
to bundle the token-introspection plugin:
...
COPY ./plugins/kong-plugin-token-introspection /custom-plugins/kong-plugin-token-introspection
WORKDIR /custom-plugins/kong-plugin-token-introspection
RUN luarocks make
...
and update kong.conf
to load it:
plugins = bundled, oidc, mtls-auth, token-introspection
We finally add configuration in kong.yml
to expose our sample API beneath the path /token-mtls/
, with the mtls-auth and token-introspection plugins applied:
services:
...
- name: access-token-mtls
host: upstream
port: 80
protocol: http
plugins:
- name: mtls-auth
config:
upstream_cert_header: "X-Client-Cert"
- name: token-introspection
config:
introspection_endpoint: https://host.docker.internal:9443/realms/test/protocol/openid-connect/token/introspect
client_id: introspection-client
client_secret: secret
certificate_header: "X-Client-Cert"
scope:
- system-access
routes:
- name: access-token-mtls-route
paths:
- /token-mtls/
strip_path: true
We use the mtls-auth
plugin to extract the client certificate into the X-Client-Cert
header, which is then used by the token-introspection
plugin to validate against the digest in the bounded access token. We also require that the access token carries the system-access
custom scope.
We are now ready to test the flow. Rebuild the docker image, and start up the containers using docker compose in a terminal window:
> docker compose build
> docker compose up -d
Retrieve an access token from Keycloak with the Client Credentials flow, using the client1
client certificate as authentication:
> ACCESS_TOKEN=`curl --cacert certs/CA/localCA.crt --key certs/clients/client1.key --cert certs/clients/client1.crt \
--location --request POST 'https://host.docker.internal:9443/realms/test/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=system-client-mtls' \
--data-urlencode 'grant_type=client_credentials' | jq -j '.access_token'`
Then call the api, again providing the client certificate to the TLS handshake and the access token as a header:
> curl -H "Authorization: Bearer $ACCESS_TOKEN" --cacert certs/CA/localCA.crt \
--key certs/clients/client1.key --cert certs/clients/client1.crt \
https://host.docker.internal:8443/token-mtls/api/ | jq .request.headers
In the echo’ed result, we can see the headers retrieved from the access token:
{
"host": "upstream",
...
"x-credential-scope": "system-access",
"x-credential-client-id": "system-client-mtls",
"x-credential-token-type": "Bearer",
"x-credential-exp": "1683188977",
"x-credential-iat": "1683188677",
"x-credential-sub": "service-account-system-client-mtls",
"x-credential-iss": "https://host.docker.internal:9443/realms/test",
"x-credential-jti": "7f01ef9e-ce54-4221-a7ae-5868e9e2b8fc"
}
If we try to call the api using the same access token but another client certificate (e.g. client2
), the mTLS handshake still succeeds, but since the client2
certificate’s digest differs from the digest bound to access token, access is denied by the token-introspection plugin:
> curl -v -H "Authorization: Bearer $ACCESS_TOKEN" --cacert certs/CA/localCA.crt \
--key certs/clients/client2.key --cert certs/clients/client2.crt \
https://host.docker.internal:8443/token-mtls/api/ | jq .request.headers
...
< HTTP/1.1 401 Unauthorized
...
{
"data": [],
"error": {
"code": 401,
"message": "The resource owner or authorization server denied the request."
}
}
Finally, shut down the containers:
> docker compose down
In this post, we have extended the OAuth 2.0 Client Credentials flow with autentication using mutual TLS. By binding the access token to the client certificate to which the token was issued, we prevent the risk that a compromised access token can be misused by a bogus client. Just as in the previous examples, the result is simple, elegant and well encapsuled.
A fully working, minimalistic example can be found in the Github repository.
This post concludes these Access Control examples. In a forthcoming post (if time permits …), we will have a look at Rate Limiting using Kong Gateway. Stay tuned!
The following Kong plugin has been a very useful inspiration when writing the token-introspection plugin:
https://github.com/VentaApps/kong-token-introspection