Did you know that you can try this plugin without talking to anyone with a free trial of Kong Konnect? Get started in under 5 minutes.
OpenID Connect (1.0) plugin allows for integration with a third party identity provider (IdP) in a standardized way. This plugin can be used to implement Kong as a (proxying) OAuth 2.0 resource server (RS) and/or as an OpenID Connect relying party (RP) between the client, and the upstream service.
The plugin supports several types of credentials and grants:
- Signed JWT access tokens (JWS)
- Opaque access tokens
- Refresh tokens
- Authorization code with client secret or PKCE
- Username and password
- Client credentials
- Session cookies
The plugin has been tested with several OpenID Connect providers:
- Auth0 (Kong Integration Guide)
- Amazon AWS Cognito (Kong Integration Guide)
- Connect2id
- Curity
- Dex
- Gluu
- Google (Kong Integration Guide)
- IdentityServer
- Keycloak
- Microsoft Azure Active Directory (Kong Integration Guide)
- Microsoft Active Directory Federation Services
- Microsoft Live Connect
- Okta (Kong Integration Guide)
- OneLogin
- OpenAM
- Paypal
- PingFederate
- Salesforce
- WSO2
- Yahoo!
As long as your provider supports OpenID Connect standards, the plugin should work, even if it is not specifically tested against it. Let Kong know if you want your provider to be tested and added to the list.
Once applied, any user with a valid credential can access the Service.
This plugin can be used for authentication in conjunction with the Application Registration plugin.
Important Configuration Parameters
This plugin includes many configuration parameters that allow finely grained customization. The following steps will help you get started setting up the plugin:
-
Configure:
config.issuer
.This parameter tells the plugin where to find discovery information, and it is the only required parameter. You should set the value
realm
oriss
on this parameter if you don’t have a discovery endpoint.Note: This does not have to match the URL of the
iss
claim in the access tokens being validated. To set URLs supported in theiss
claim, useconfig.issuers_allowed
. -
Decide what authentication grants to use with this plugin and configure the
config.auth_methods
field accordingly.In order to restrict the scope of potential attacks, the parameter should only contain the grants that you want to use.
-
In many cases, you also need to specify
config.client_id
, and if your identity provider requires authentication, such as on a token endpoint, you will need to specify the client authentication credentials too, for exampleconfig.client_secret
. -
If you are using a public identity provider, such as Google, you should limit the audience with
config.audience_required
to contain only yourconfig.client_id
. You may also need to adjustconfig.audience_claim
in case your identity provider uses a non-standard claim (other thanaud
as specified in JWT standard). This is important because some identity providers, such as Google, share public keys with different clients. -
If you are using Kong in DB-less mode with a declarative configuration and session cookie authentication, you should set
config.session_secret
. Leaving this parameter unset will result in every Nginx worker across your nodes encrypting and signing the cookies with their own secrets.
In summary, start with the following parameters:
config.issuer
config.auth_methods
-
config.client_id
(and in many cases the client authentication credentials) -
config.audience_required
(if using a public identity provider) -
config.session_secret
(if using Kong in DB-less mode)
Records
In the above parameter list, two configuration settings used an array of records as a data type:
-
config.client_jwk
: array of JWK records (one for each client) -
config.session_redis_cluster_nodes
: array of host records, either as IP addresses or hostnames, and their ports.
Below are descriptions of the record types.
JWK Record
The JSON Web Key (JWK) record is specified in RFC7517. This record is used with the
config.client_jwk
when using private_key_jwk
client authentication.
Here is an example of JWK record generated by the plugin itself (see: JSON Web Key Set):
{
"kid": "B2FxBJ8G_e61tnZEfaYpaMLjswjNO3dbVEQhR7-i_9s",
"kty": "RSA",
"alg": "RS256",
"use": "sig"
"e": "AQAB",
"n": "…",
"d": "…",
"p": "…",
"q": "…",
"dp": "…",
"dq": "…",
"qi": "…"
}
The JWK private fields (k
, d
, p
, q
, dp
, dq
, qi
, oth
, r
, t
) are referenceable,
which means they can be securely stored as a
secret
in a vault. References must follow a specific format.
Host Record
The Host record used with the config.session_redis_cluster_nodes
is simple.
It contains ip
or host
, and the port
where the port
defaults to 6379
.
Here is an example of Host the record:
{
"ip": "127.0.0.1"
"port": 6379
}
Admin API
The OpenID Connect plugin extends the Kong Admin API with a few endpoints.
Discovery Cache
When configuring the plugin using config.issuer
, the plugin will store the fetched discovery
information to the Kong database, or in the worker memory with DB-less. The discovery cache does
not have an expiry or TTL, and so must be cleared manually using the DELETE
endpoints listed below.
The discovery cache will attempt to be refreshed when a token is presented with required discovery
information that is not already available, based on the config.issuer
value. Once a rediscovery attempt
has been made, a new attempt will not occur until the number of seconds defined in rediscovery_lifetime
has elapsed - this avoids excessive discovery requests to the identity provider.
If a JWT cannot be validated due to missing discovery information and an invalid status code is received from the rediscovery request (for example, non-2xx), the plugin will attempt to validate the JWT by falling back to any sufficient discovery information that is still in the discovery cache.
Discovery Cache Object
{
"id": "<uuid>",
"issuer": "<config.issuer>"
"created_at": <timestamp>,
"configuration": {
<discovery>
},
"keys": [
<keys>
]
}
List All Discovery Cache Objects
Response
HTTP 200 OK
{
"data": [{
"id": "<uuid>",
"issuer": "<config.issuer>"
"created_at": <timestamp>,
"configuration": {
<discovery>
},
"keys": [
<keys>
]
}],
"next": null
}
Retrieve Discovery Cache Object
Attributes | Description |
---|---|
issuer or id required |
The unique identifier or the value of config.issuer
|
Response
HTTP 200 OK
{
"id": "<uuid>",
"issuer": "<config.issuer>"
"created_at": <timestamp>,
"configuration": {
<discovery>
},
"keys": [
<keys>
]
}
Delete All Discovery Cache Objects
Response
HTTP 204 No Content
Note: The automatically generated session secret (that can be overridden with the
config.session_secret
) is stored with the discovery cache objects. Deleting discovery cache objects will invalidate all the sessions created with the associated secret.
Delete Discovery Cache Object
Attributes | Description |
---|---|
issuer or id required |
The unique identifier or the value of config.issuer
|
Response
HTTP 204 No Content
JSON Web Key Set
When the OpenID Connect client (the plugin) is set to communicate with the identity provider endpoints
using private_key_jwt
, the plugin needs to use public key cryptography. Thus, the plugin needs
to generate the needed keys. Identity provider on the other hand has to verify that the assertions
used for the client authentication.
The plugin will automatically generate the key pairs for the different algorithms. It will also publish the public keys with the admin api where the identity provider could fetch them.
{
"keys": [{
<keys>
}]
}
Retrieve JWKS
This endpoint will return a standard JWK Set document with the private keys stripped out.
Response
HTTP 200 OK
{
"keys": [{
<keys>
}]
}
Rotate JWKS
Deleting JWKS will also cause auto-generation of a new JWK set, so
DELETE
will actually cause a key rotation.
Response
HTTP 204 No Content
Preparations
The OpenID Connect plugin relies in most cases on a third party identity provider. In this section, we explain configuration of Keycloak and Kong.
All the *.test
domains in the following examples point to the localhost
(127.0.0.1
and/or ::1
).
Keycloak Configuration
We use Keycloak as the identity provider in the following examples, but the steps will be similar in other standard identity providers. If you encounter difficulties during this phase, please refer to the Keycloak documentation.
- Create a confidential client
kong
withprivate_key_jwt
authentication and configure Keycloak to download the public keys from the OpenID Connect Plugin JWKS endpoint:
- Create another confidential client
service
withclient_secret_basic
authentication. For this client, Keycloak will auto-generate a secret similar to the following:cf4c655a-0622-4ce6-a0de-d3353ef0b714
. Enable the client credentials grant for the client:
- Create a verified user with the name:
john
and the non-temporary password:doe
that can be used with the password grant:
Alternatively you can download the exported Keycloak configuration, and use it to configure the Keycloak. Please refer to Keycloak import documentation for more information.
You need to modify Keycloak standalone.xml
configuration file, and change the socket binding from:
<socket-binding name="https" port="${jboss.https.port:8443}"/>
to
<socket-binding name="https" port="${jboss.https.port:8440}"/>
The Keycloak default https
port conflicts with the default Kong TLS proxy port,
and that can be a problem if both are started on the same host.
Kong Configuration
Create a Service
http -f put :8001/services/openid-connect url=http://httpbin.org/anything
HTTP/1.1 200 OK
{
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd",
"name": "openid-connect",
"protocol": "http",
"host": "httpbin.org",
"port": 80,
"path": "/anything"
}
Create a Route
http -f put :8001/services/openid-connect/routes/openid-connect paths=/
HTTP/1.1 200 OK
{
"id": "ac1e86bd-4bce-4544-9b30-746667aaa74a",
"name": "openid-connect",
"paths": [ "/" ]
}
Create a Plugin
You may execute this before patching the plugin (as seen on following examples) to reset the plugin configuration.
http -f put :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
name=openid-connect \
service.name=openid-connect \
config.issuer=http://keycloak.test:8080/auth/realms/master \
config.client_id=kong \
config.client_auth=private_key_jwt
HTTP/1.1 200 OK
{
"id": "5f35b796-ced6-4c00-9b2a-90eef745f4f9",
"name": "openid-connect",
"service": {
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd"
},
"config": {
"issuer": "http://keycloak.test:8080/auth/realms/master",
"client_id": [ "kong" ],
"client_auth": [ "private_key_jwt" ]
}
}
Also, check the discovery cache:
http :8001/openid-connect/issuers
. It should contain Keycloak OpenID Connect discovery document, and the keys.
Summary
At this point we have:
- Created a Service
- Routed traffic to the service
- Enabled OpenID Connect plugin on the service
The following sections will guide you through the process of enabling the OpenID Connect plugin for specific grants or flows.
Authentication
Before you proceed, check that you have done the preparations.
We use HTTPie to execute the examples. The output is stripped for a better readability. httpbin.org is used as an upstream service.
Using Admin API is convenient when testing the plugin, but similar configs can be done in declarative format as well.
When this plugin is configured with multiple grants/flows there is a hard-coded search order for the credentials:
- Session Authentication
- JWT Access Token Authentication
- Kong OAuth Token Authentication
- Introspection Authentication
- User Info Authentication
- Refresh Token Grant
- Password Grant
- Client Credentials Grant
- Authorization Code Flow
Once credentials are found, the plugin will stop searching further. Multiple grants may
share the same credentials. For example, both the password and client credentials grants can use
basic access authentication through the Authorization
header.
The choices made in the examples below are solely aimed at simplicity. Because
httpbin.org
is used as an upstream service, it is highly recommended that you do not run these usage examples with a production identity provider as there is a great chance of leaking information. Also the examples below use the plain HTTP protocol that you should never use in production.
Authorization Code Flow
The authorization code flow is the three-legged OAuth/OpenID Connect flow. The sequence diagram below describes the participants and their interactions for this usage scenario, including the use of session cookies:
If using PKCE, the identity provider must contain the
code_challenge_methods_supported
object in the/.well-known/openid-configuration
issuer discovery endpoint response, as required by RFC 8414. If it is not included, the PKCEcode_challenge
query parameter will not be sent.
Patch the Plugin
Let’s patch the plugin that we created in the Kong configuration step:
- We want to only use the authorization code flow and the session authentication.
- We want to set the response mode to
form_post
so that authorization codes won’t get logged to the access logs. - We want to preserve the original request query arguments over the authorization code flow redirection.
- We want to redirect the client to original request url after the authorization code flow so that
the
POST
request (because ofform_post
) is turned to theGET
request, and the browser address bar is updated with the original request query arguments. - We don’t want to include any tokens in the browser address bar.
Reset the plugin configuration before patching.
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.auth_methods=authorization_code \
config.auth_methods=session \
config.response_mode=form_post \
config.preserve_query_args=true \
config.login_action=redirect \
config.login_tokens=
HTTP/1.1 200 OK
{
"id": "5f35b796-ced6-4c00-9b2a-90eef745f4f9",
"name": "openid-connect",
"service": {
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd"
},
"config": {
"auth_methods": [
"authorization_code",
"session"
],
"login_action": "redirect",
"preserve_query_args": true,
"login_tokens": null
}
}
Test the Authorization Code Flow
- Open the Service Page with some query arguments:
open http://service.test:8000/?hello=world
- See that the browser is redirected to the Keycloak login page:
You may examine the query arguments passed to Keycloak with the browser developer tools.
- And finally you will be presented a response from httpbin.org:
- Done.
It looks rather simple from the user point of view, but what really happened is described in the diagram above.
Password Grant
Password grant is a legacy authentication grant. This is a less secure way of authenticating end users than the authorization code flow, because, for example, the passwords are shared with third parties. The image below illustrates the grant:
Patch the Plugin
Let’s patch the plugin that we created in the Kong configuration step:
- We want to only use the password grant.
- We want to search credentials for password grant from the headers only.
Reset the plugin configuration before patching.
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.auth_methods=password \
config.password_param_type=header
HTTP/1.1 200 OK
{
"id": "5f35b796-ced6-4c00-9b2a-90eef745f4f9",
"name": "openid-connect",
"service": {
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd"
},
"config": {
"auth_methods": [ "password" ],
"password_param_type": [ "header" ]
}
}
Test the Password Grant
- Request the service with basic authentication credentials created in the Keycloak configuration step:
http -v -a john:doe :8000
GET / HTTP/1.1 Authorization: Basic BEkg3bHT0ERXFmKr1qelBQYrLBeHb5Hr
HTTP/1.1 200 OK
{ "headers": { "Authorization": "Bearer <access-token>" }, "method": "GET" }
- Done.
If you make another request using the same credentials, you should see that Kong adds less latency to the request as it has cached the token endpoint call to Keycloak.
Client Credentials Grant
The client credentials grant is very similar to the password grant. The most important difference in the Kong OpenID Connect plugin is that the plugin itself does not try to authenticate. It just forwards the credentials passed by the client to the identity server’s token endpoint. The client credentials grant is visualized below:
Patch the Plugin
Let’s patch the plugin that we created in the Kong configuration step:
- We want to only use the client credentials grant.
- We want to search credentials for client credentials from the headers only.
Reset the plugin configuration before patching.
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.auth_methods=client_credentials \
config.client_credentials_param_type=header
HTTP/1.1 200 OK
{
"id": "5f35b796-ced6-4c00-9b2a-90eef745f4f9",
"name": "openid-connect",
"service": {
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd"
},
"config": {
"auth_methods": [ "client_credentials" ],
"client_credentials_param_type": [ "header" ]
}
}
Test the Client Credentials Grant
- Request the service with client credentials created in the Keycloak configuration step:
http -v -a service:cf4c655a-0622-4ce6-a0de-d3353ef0b714 :8000
GET / HTTP/1.1 Authorization: Basic c2VydmljZTpjZjRjNjU1YS0wNjIyLTRjZTYtYTBkZS1kMzM1M2VmMGI3MTQ=
HTTP/1.1 200 OK
{ "headers": { "Authorization": "Bearer <access-token>" }, "method": "GET" }
- Done.
If you make another request using the same credentials, you should see that Kong adds less latency to the request as it has cached the token endpoint call to Keycloak.
Refresh Token Grant
The refresh token grant can be used when the client has a refresh token available. There is a caveat with this: identity providers in general only allow refresh token grant to be executed with the same client that originally got the refresh token, and if there is a mismatch, it may not work. The mismatch is likely when Kong OpenID Connect is configured to use one client, and the refresh token is retrieved with another. The grant itself is very similar to password grant and client credentials grant:
Patch the Plugin
Let’s patch the plugin that we created in the Kong configuration step:
- We want to only use the refresh token grant, but we also enable the password grant for demoing purposes
- We want to search the refresh token for the refresh token grant from the headers only.
- We want to pass refresh token from the client in
Refresh-Token
header. - We want to pass refresh token to upstream in
Refresh-Token
header.
Reset the plugin configuration before patching.
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.refresh_token_param_name=refresh_token \
config.refresh_token_param_type=header \
config.auth_methods=refresh_token \
config.auth_methods=password \
config.upstream_refresh_token_header=refresh_token
HTTP/1.1 200 OK
{
"id": "5f35b796-ced6-4c00-9b2a-90eef745f4f9",
"name": "openid-connect",
"service": {
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd"
},
"config": {
"auth_methods": [
"refresh_token",
"password"
],
"refresh_token_param_name": "refresh_token",
"refresh_token_param_type": [ "header" ],
"upstream_refresh_token_header": "refresh_token"
}
}
The config.auth_methods
and config.upstream_refresh_token_header
are only enabled for demoing purposes so that we can get a refresh token with:
http -a john:doe :8000 | jq -r '.headers."Refresh-Token"'
Output:
<refresh-token>
We can use the output in Refresh-Token
header.
Test the Refresh Token Grant
- Request the service with a bearer token:
http -v :8000 Refresh-Token:$(http -a john:doe :8000 | \ jq -r '.headers."Refresh-Token"')
or
http -v :8000 Refresh-Token:"<refresh-token>"
GET / HTTP/1.1 Refresh-Token: <refresh-token>
HTTP/1.1 200 OK
{ "headers": { "Authorization": "Bearer <access-token>", "Refresh-Token": "<refresh-token>" }, "method": "GET" }
- Done.
JWT Access Token Authentication
For legacy reasons, the stateless JWT Access Token
authentication is named bearer
with the Kong
OpenID Connect plugin (see: config.auth_methods
). Stateless authentication basically means
the signature verification using the identity provider published public keys and the standard
claims’ verification (such as exp
(or expiry)). The client may have received the token directly
from the identity provider or by other means. It is simple:
Patch the Plugin
Let’s patch the plugin that we created in the Kong configuration step:
- We want to only use the bearer authentication, but we also enable the password grant for demoing purposes
- We want to search the bearer token for the bearer authentication from the headers only.
Reset the plugin configuration before patching.
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.bearer_token_param_type=header \
config.auth_methods=bearer \
config.auth_methods=password # only enabled for demoing purposes
HTTP/1.1 200 OK
{
"id": "5f35b796-ced6-4c00-9b2a-90eef745f4f9",
"name": "openid-connect",
"service": {
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd"
},
"config": {
"auth_methods": [
"bearer",
"password"
],
"bearer_token_param_type": [ "header" ]
}
}
The password grant is enabled so that we can get a JWT access token that we can use to show how the JWT access token authentication works. That is: we need a token. One way to get a JWT access token is to issue the following call (we use jq to filter the response):
http -a john:doe :8000 | jq -r .headers.Authorization
Output:
Bearer <access-token>
We can use the output in Authorization
header.
Test the JWT Access Token Authentication
- Request the service with a bearer token:
http -v :8000 Authorization:"$(http -a john:doe :8000 | \ jq -r .headers.Authorization)"
or
http -v :8000 Authorization:"Bearer <access-token>"
GET / HTTP/1.1 Authorization: Bearer <access-token>
HTTP/1.1 200 OK
{ "headers": { "Authorization": "Bearer <access-token>" }, "method": "GET" }
- Done.
Introspection Authentication
As with JWT Access Token Authentication), the introspection authentication relies on a bearer token that the client has already gotten from somewhere. The difference to stateless JWT authentication is that the plugin needs to call the introspection endpoint of the identity provider to find out whether the token is valid and active. This makes it possible to issue opaque tokens to the clients.
Patch the Plugin
Let’s patch the plugin that we created in the Kong configuration step:
- We want to only use the introspection authentication, but we also enable the password grant for demoing purposes
- We want to search the bearer token for the introspection from the headers only.
Reset the plugin configuration before patching.
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.bearer_token_param_type=header \
config.auth_methods=introspection \
config.auth_methods=password # only enabled for demoing purposes
HTTP/1.1 200 OK
{
"id": "5f35b796-ced6-4c00-9b2a-90eef745f4f9",
"name": "openid-connect",
"service": {
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd"
},
"config": {
"auth_methods": [
"introspection",
"password"
],
"bearer_token_param_type": [ "header" ]
}
}
Test the Introspection Authentication
- Request the service with a bearer token:
http -v :8000 Authorization:"$(http -a john:doe :8000 | \ jq -r .headers.Authorization)"
or
http -v :8000 Authorization:"Bearer <access-token>"
GET / HTTP/1.1 Authorization: Bearer <access-token>
HTTP/1.1 200 OK
{ "headers": { "Authorization": "Bearer <access-token>" }, "method": "GET" }
- Done.
User Info Authentication
The user info authentication uses OpenID Connect standard user info endpoint to verify the access token. In most cases it is preferable to use Introspection Authentication as that is meant for retrieving information from the token itself, whereas the user info endpoint is meant for retrieving information about the user for whom the token was given. The sequence diagram below looks almost identical to introspection authentication:
Patch the Plugin
Let’s patch the plugin that we created in the Kong configuration step:
- We want to only use the user info authentication, but we also enable the password grant for demoing purposes
- We want to search the bearer token for the user info from the headers only.
Reset the plugin configuration before patching.
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.bearer_token_param_type=header \
config.auth_methods=userinfo \
config.auth_methods=password # only enabled for demoing purposes
HTTP/1.1 200 OK
{
"id": "5f35b796-ced6-4c00-9b2a-90eef745f4f9",
"name": "openid-connect",
"service": {
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd"
},
"config": {
"auth_methods": [
"userinfo",
"password"
],
"bearer_token_param_type": [ "header" ]
}
}
Test the User Info Authentication
- Request the service with a bearer token:
http -v :8000 Authorization:"$(http -a john:doe :8000 | \ jq -r .headers.Authorization)"
or
http -v :8000 Authorization:"Bearer <access-token>"
GET / HTTP/1.1 Authorization: Bearer <access-token>
HTTP/1.1 200 OK
{ "headers": { "Authorization": "Bearer <access-token>" }, "method": "GET" }
- Done.
Kong OAuth Token Authentication
The OpenID Connect plugin can also verify the tokens issued by Kong OAuth 2.0 Plugin. This is very similar to third party identity provider issued JWT access token authentication or introspection authentication:
Prepare Kong OAuth Application
- Create a Consumer:
http -f put :8001/consumers/jane
- Create Kong OAuth Application for the consumer:
http -f put :8001/consumers/jane/oauth2/client \ name=demo \ client_secret=secret \ hash_secret=true
- Create a Route:
http -f put :8001/routes/auth paths=/auth
- Apply OAuth plugin to the Route:
http -f put :8001/plugins/7cdeaa2d-5faf-416d-8df5-533d1e4cd2c4 \ name=oauth2 \ route.name=auth \ config.global_credentials=true \ config.enable_client_credentials=true
- Test the token endpoint:
https -f --verify no post :8443/auth/oauth2/token \ client_id=client \ client_secret=secret \ grant_type=client_credentials
HTTP/1.1 200 OK
{ "access_token": "<access-token>", "expires_in": 7200, "token_type": "bearer" }
At this point we should be able to retrieve a new access token with:
https -f --verify no post :8443/auth/oauth2/token \
client_id=client \
client_secret=secret \
grant_type=client_credentials | \
jq -r .access_token
Output:
<access-token>
Patch the Plugin
Let’s patch the plugin that we created in the Kong configuration step:
- We want to only use the Kong OAuth authentication
- We want to search the bearer token for the Kong OAuth authentication from the headers only.
Reset the plugin configuration before patching.
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.auth_methods=kong_oauth2 \
config.bearer_token_param_type=header
HTTP/1.1 200 OK
{
"id": "5f35b796-ced6-4c00-9b2a-90eef745f4f9",
"name": "openid-connect",
"service": {
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd"
},
"config": {
"auth_methods": [ "kong_oauth2" ],
"bearer_token_param_type": [ "header" ]
}
}
Test the Kong OAuth Token Authentication
- Request the service with Kong OAuth token:
http -v :8000 Authorization:"Bearer $(https -f --verify no \ post :8443/auth/oauth2/token \ client_id=client \ client_secret=secret \ grant_type=client_credentials | \ jq -r .access_token)"
or
http -v :8000 Authorization:"Bearer <access-token>"
GET / HTTP/1.1 Authorization: Bearer <access-token>
HTTP/1.1 200 OK
{ "headers": { "Authorization": "Bearer <access-token>", "X-Consumer-Id": "<consumer-id>", "X-Consumer-Username": "jane" }, "method": "GET" }
- Done.
Session Authentication
Kong OpenID Connect plugin can issue a session cookie that can be used for further session authentication. To make OpenID Connect issue a session cookie, you need to first authenticate with one of the other grant / flows described above. In authorization code flow we already demonstrated session authentication when we used the redirect login action. The session authentication is described below:
Patch the Plugin
Let’s patch the plugin that we created in the Kong configuration step:
- We want to only use the session authentication, but we also enable the password grant for demoing purposes
Reset the plugin configuration before patching.
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.auth_methods=session \
config.auth_methods=password # only enabled for demoing purposes
HTTP/1.1 200 OK
{
"id": "5f35b796-ced6-4c00-9b2a-90eef745f4f9",
"name": "openid-connect",
"service": {
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd"
},
"config": {
"auth_methods": [
"session",
"password"
]
}
}
Test the Session Authentication
- Request the service with basic authentication credentials (created in the Keycloak configuration step),
and store the session:
http -v -a john:doe --session=john :8000
GET / HTTP/1.1 Authorization: Basic BEkg3bHT0ERXFmKr1qelBQYrLBeHb5Hr
HTTP/1.1 200 OK Set-Cookie: session=<session-cookie>; Path=/; SameSite=Lax; HttpOnly
{ "headers": { "Authorization": "Bearer <access-token>" }, "method": "GET" }
- Make request with a session cookie (stored above):
http -v --session=john :8000
GET / HTTP/1.1 Cookie: session=<session-cookie>
HTTP/1.1 200 OK
{ "headers": { "Authorization": "Bearer <access-token>" }, "method": "GET" }
- Done.
If you want to disable session creation with some grants, you can use the
config.disable_session
.
Authorization
Before you proceed, check that you have completed the preparations.
The OpenID Connect plugin has several features to do coarse grained authorization:
Claims Based Authorization
The following options can be configured to manage claims verification during authorization:
-
config.scopes_claim
andconfig.scopes_required
-
config.audience_claim
andconfig.audience_required
-
config.groups_claim
andconfig.groups_required
-
config.roles_claim
andconfig.roles_required
For example, the first configuration option, config.scopes_claim
, points to a source, from which the value is
retrieved and checked against the value of the second configuration option: config.scopes_required
.
Let’s take a look at a JWT access token:
Reset the plugin configuration before patching.
- Patch the plugin to enable the password grant:
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \ config.auth_methods=password
- Retrieve the content of an access token:
http -a john:doe :8000 | jq -r .headers.Authorization
HTTP/1.1 200 OK
Bearer <access-token>
- The signed JWT
<access-token>
(JWS) comes with three parts separated with a dot.
:<header>.<payload>.<signature>
(a JWS compact serialization format) - We are interested with the
<payload>
, and you should have something similar to:eyJleHAiOjE2MjI1NTY3MTMsImF1ZCI6ImFjY291bnQiLCJ0eXAiOiJCZWFyZXIiLC JzY29wZSI6Im9wZW5pZCBlbWFpbCBwcm9maWxlIiwicHJlZmVycmVkX3VzZXJuYW1l Ijoiam9obiIsImdpdmVuX25hbWUiOiJKb2huIiwiZmFtaWx5X25hbWUiOiJEb2UifQ
That can be base64 url decoded to the following
JSON
:{ "exp": 1622556713, "aud": "account", "typ": "Bearer", "scope": "openid email profile", "preferred_username": "john", "given_name": "John", "family_name": "Doe" }
This payload may contain arbitrary claims, such as user roles and groups, but as we didn’t configure them in Keycloak, let’s just use the claims that we have. In this case we want to authorize against the values in
scope
claim.
Let’s patch the plugin that we created in the Kong configuration step:
- We want to only use the password grant for demonstration purposes.
- We require the value
openid
andemail
to be present inscope
claim of the access token.
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.auth_methods=password \
config.scopes_claim=scope \
config.scopes_required="openid email"
HTTP/1.1 200 OK
{
"id": "5f35b796-ced6-4c00-9b2a-90eef745f4f9",
"name": "openid-connect",
"service": {
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd"
},
"config": {
"auth_methods": [ "password" ],
"scopes_claim": [ "scope" ],
"scopes_required": [ "openid email" ]
}
}
Now let’s see if we can still access the service:
http -v -a john:doe :8000
GET / HTTP/1.1
Authorization: Basic BEkg3bHT0ERXFmKr1qelBQYrLBeHb5Hr
HTTP/1.1 200 OK
{
"headers": {
"Authorization": "Bearer <access-token>"
},
"method": "GET"
}
Works as expected, but let’s try to add another authorization:
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.audience_claim=aud \
config.audience_required=httpbin
HTTP/1.1 200 OK
{
"id": "5f35b796-ced6-4c00-9b2a-90eef745f4f9",
"name": "openid-connect",
"service": {
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd"
},
"config": {
"auth_methods": [ "password" ],
"audience_claim": [ "scope" ],
"audience_required": [ "httpbin" ]
}
}
As we know, the access token has "aud": "account"
, and that does not match with "httpbin"
, so
the request should now be forbidden:
http -v -a john:doe :8000
HTTP/1.1 403 Forbidden
{
"message": "Forbidden"
}
A few words about config.scopes_claim
and config.scopes_required
(and the similar configuration options).
You may have noticed that config.scopes_claim
is an array of string elements. Why? It is used to traverse
the JSON when looking up a claim, take for example this imaginary payload:
{
"user": {
"name": "john",
"groups": [
"employee",
"marketing"
]
}
}
In this case you would probably want to use config.groups_claim
to point to groups
claim, but that claim
is not a top-level claim, so you need to traverse there:
- Find the
user
claim. - Inside the
user
claim, find thegroups
claim, and read its value:
{
"config": {
"groups_claim": [
"user",
"groups"
]
}
}
or
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.groups_claim=user \
config.groups_claim=groups
The value of a claim can be the following:
- a space separated string (such as
scope
claim usually is) - an JSON array of strings (such as the imaginary
groups
claim above) - a simple value, such as a
string
What about the config.groups_required
then? That is also an array?
That is correct, the required checks are arrays to allow logical and
/or
type of checks:
-
and
: use a space separated values -
or
: specify value(s) in separate array indices
For example:
{
"config": {
"groups_required": [
"employee marketing",
"super-admins"
]
}
}
or
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.groups_required="employee marketing" \
config.groups_required="super-admins"
The above means that a claim has to have:
-
employee
andmarketing
values in it, OR -
super-admins
value in it
ACL Plugin Authorization
The plugin can also be integrated with Kong ACL Plugin that provides access control functionality in form of allow and deny lists.
Please read the claims verification section for a basic information, as a lot of that applies here too.
Let’s first configure the OpenID Connect plugin for integration with the ACL plugin (please remove other authorization if enabled):
Reset the plugin configuration before patching.
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.auth_methods=password \
config.authenticated_groups_claim=scope
HTTP/1.1 200 OK
{
"id": "5f35b796-ced6-4c00-9b2a-90eef745f4f9",
"name": "openid-connect",
"service": {
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd"
},
"config": {
"auth_methods": [ "password" ],
"authorized_groups_claim": [ "scope" ]
}
}
Before we apply the ACL plugin, let’s try it once:
http -v -a john:doe :8000
HTTP/1.1 200 OK
{
"headers": {
"X-Authenticated-Groups": "openid, email, profile",
}
}
Interesting, the X-Authenticated-Groups
header was injected in a request.
This means that we are all good to add the ACL plugin:
http -f put :8001/plugins/b238b64a-8520-4bbb-b5ff-2972165cf3a2 \
name=acl \
service.name=openid-connect \
config.allow=openid
Let’s test it again:
http -v -a john:doe :8000
HTTP/1.1 200 OK
Let’s make it forbidden by changing it to a deny-list:
http -f patch :8001/plugins/b238b64a-8520-4bbb-b5ff-2972165cf3a2 \
config.allow= \
config.deny=profile
And try again:
http -v -a john:doe :8000
HTTP/1.1 403 Forbidden
{
"message": "You cannot consume this service"
}
Consumer Authorization
The third option for authorization is to use Kong consumers and dynamically map from a claim value to a Kong consumer. This means that we restrict the access to only those that do have a matching Kong consumer. Kong consumers can have ACL groups attached to them and be further authorized with the Kong ACL Plugin.
As a remainder our token payload looks like this:
{
"exp": 1622556713,
"aud": "account",
"typ": "Bearer",
"scope": "openid email profile",
"preferred_username": "john",
"given_name": "John",
"family_name": "Doe"
}
Out of these the preferred_username
claim looks promising for consumer mapping.
Let’s patch the plugin:
Reset the plugin configuration before patching.
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.auth_methods=password \
config.consumer_claim=preferred_username \
config.consumer_by=username
HTTP/1.1 200 OK
{
"id": "5f35b796-ced6-4c00-9b2a-90eef745f4f9",
"name": "openid-connect",
"service": {
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd"
},
"config": {
"auth_methods": [ "password" ],
"consumer_claim": [ "preferred_username" ],
"consumer_by": [ "username" ]
}
}
Before we proceed, let’s make sure we don’t have consumer john
:
http delete :8001/consumers/john
HTTP/1.1 204 No Content
Let’s try to access the service without a matching consumer:
http -a john:doe :8000
HTTP/1.1 403 Forbidden
{
"message": "Forbidden"
}
Now, let’s add the consumer:
http -f put :8001/consumers/john
HTTP/1.1 200 OK
{
"id": "<consumer-id>",
"username": "john"
}
Let’s try to access the service again:
http -a john:doe :8000
HTTP/1.1 200 OK
{
"headers": {
"Authorization": "Bearer <access-token>",
"X-Consumer-Id": "<consumer-id>",
"X-Consumer-Username": "john"
},
"method": "GET"
}
Nice, as you can see the plugin even added the X-Consumer-Id
and X-Consumer-Username
as request headers.
It is possible to make consumer mapping optional and non-authorizing by setting the
config.consumer_optional=true
.
Headers
Before you proceed, check that you have completed the preparations.
The OpenID Connect plugin can pass claim values, tokens, JWKs, and the session identifier to the upstream service
in request headers, and to the downstream client in response headers. By default, the plugin passes an access token
in Authorization: Bearer <access-token>
header to the upstream service (this can be controlled with
config.upstream_access_token_header
). The claim values can be taken from:
- an access token,
- an id token,
- an introspection response, or
- a user info response
Let’s take a look for an access token payload:
{
"exp": 1622556713,
"aud": "account",
"typ": "Bearer",
"scope": "openid email profile",
"preferred_username": "john",
"given_name": "John",
"family_name": "Doe"
}
To pass the preferred_username
claim’s value john
to the upstream with an Authenticated-User
header,
we need to patch our plugin:
Reset the plugin configuration before patching.
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.auth_methods=password \
config.upstream_headers_claims=preferred_username \
config.upstream_headers_names=authenticated_user
HTTP/1.1 200 OK
{
"id": "5f35b796-ced6-4c00-9b2a-90eef745f4f9",
"name": "openid-connect",
"service": {
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd"
},
"config": {
"auth_methods": [ "password" ],
"upstream_headers_claims": [ "preferred_username" ],
"upstream_headers_names": [ "authenticated_user" ]
}
}
Let’s see if it had any effect:
http -a john:doe :8000
HTTP/1.1 200 OK
{
"headers": {
"Authorization": "Bearer <access-token>",
"Authenticated-User": "john"
},
"method": "GET"
}
See the configuration parameters for other options.
Logout
The logout functionality is mostly useful together with session authentication that is mostly useful with authorization code flow.
As part of the logout, the OpenID Connect plugin implements several features:
- session invalidation
- token revocation
- relying party (RP) initiated logout
Let’s patch the OpenID Connect plugin to provide the logout functionality:
Reset the plugin configuration before patching.
http -f patch :8001/plugins/5f35b796-ced6-4c00-9b2a-90eef745f4f9 \
config.auth_methods=session \
config.auth_methods=password \
config.logout_uri_suffix=/logout \
config.logout_methods=POST \
config.logout_revoke=true
HTTP/1.1 200 OK
{
"id": "5f35b796-ced6-4c00-9b2a-90eef745f4f9",
"name": "openid-connect",
"service": {
"id": "5fa9e468-0007-4d7e-9aeb-49ca9edd6ccd"
},
"config": {
"auth_methods": [ "password", "session" ],
"logout_uri_suffix": "/logout",
"logout_methods": [ "POST" ],
"logout_revoke": true
}
}
Login and establish a session:
http -a john:doe --session=john :8000
HTTP/1.1 200 OK
Test that session authentication works:
http --session=john :8000
HTTP/1.1 200 OK
Logout, and follow the redirect:
http --session=john --follow -a john: post :8000/logout
HTTP/1.1 200 OK
We needed to pass
-a john:
as there seems to be a feature withHTTPie
that makes it to store the original basic authentication credentials in a session - not just the session cookies.
At this point the client has logged out from both Kong and the identity provider (Keycloak).
Check that the session is really gone:
http --session=john :8000
HTTP/1.1 401 Unauthorized
Debugging
The OpenID Connect plugin is complex, integrating with third-party identity providers can present challenges. If you have issues with the plugin or integration, try the following:
- Set Kong log level to
debug
, and check the Kongerror.log
(you can filter it withopenid-connect
)KONG_LOG_LEVEL=debug kong restart
- Set the Kong OpenID Connect plugin to display errors:
{ "config": { "display_errors": true } }
- Disable the Kong OpenID Connect plugin verifications and see if you get further, just for debugging purposes:
{ "config": { "verify_nonce": false, "verify_claims": false, "verify_signature": false } }
- See what kind of tokens the Kong OpenID Connect plugin gets:
{ "config": { "login_action": "response", "login_tokens": [ "tokens" ], "login_methods": [ "password", "client_credentials", "authorization_code", "bearer", "introspection", "userinfo", "kong_oauth2", "refresh_token", "session" ] } }
With session related issues, you can try to store the session data in Redis
or memcache
as that will make the session
cookie much smaller. It is rather common that big cookies do cause issues. You can also enable session
compression.
Also try to eliminate indirection as that makes it easier to find out where the problem is. By indirection, we mean other gateways, load balancers, NATs, and such in front of Kong. If there is such, you may look at using:
- port maps
-
X-Forwarded-*
headers