End User Authentication with Keycloak
In this how-to guide, you'll add user authentication and authorization at an Ingress Gateway using Keycloak as the Identity Provider.
Before you get started, make sure you've
✓ Installed the TSB management plane
✓ Onboarded a cluster
✓ Installed Keycloak with HTTPS enabled
This example will use a demo of the httpbin application that's been tested on GKE. If you intend to follow these steps for production use, make sure you update the application information in the relevant fields with your information.
In this guide, you'll:
✓ Add authentication and authorization to an Ingress Gateway for a demo httpbin
application.
✓ Define two roles and two users: an admin user (called Jack) that can do
everything, and a normal user (Sally) that can only do GET /status
.
✓ Configure your Ingress Gateway to allow all access to the admin role and
only GET /status
to a normal role.
✓ Log in with each user and validate whether the admin user Jack
, can access
everything and user Sally
who has the normal role is only able to
GET /status
What is an OpenID provider?
An OpenID provider is an OAuth 2.0 authorization server which offers authentication as a service. It ensures the end-user is authenticated and provides claims about the end-user, and the authentication event to the client application. In this example, you'll use Keycloak as the OpenID Provider. You can replicate similar steps with other OpenID providers such as Auth0 or Okta.
In this how-to, we will use https://keycloak.example.com
as our Keycloak URL.
You should change this to your own Keycloak URL.
Configuring Keycloak as an OpenID provider
Login to the Keycloak admin interface.
If you already have the Realm, Roles and Users created, go straight to the Client section.
Realm
Start by creating Realm. If this is your first time logging in to Keycloak, you'll have a default master Realm. This is used to manage access to the Keycloak interface and should not be used to configure your open ID provider. So you'll need to create a new realm.
- Click the Add Realm button
- Set the Realm name -- in this example it's
tetrate
. - Click Create
Role
In the created Realm, add two new Roles: admin and normal.
- Click Roles in the left side menu
- Select the Add Role button
- Set the name as admin
- Click Save
- Add another Role with name normal following the same steps as above
Users
Add two users -- Jack and Sally -- and map them to their new roles:
- Click Users in the left side menu
- Select the Add user button
- Fill the details for
Jack
- Click Save
- Select the Credentials tab
- Set a password for
Jack
- Click Role Mappings tab
- Add the admin role
- Add another user with the name
Sally
and follow the steps above, adding anormal
role in the Role Mappings tab
Client
Clients are entities that can request Keycloak to authenticate a user. In this case, Keycloak will provide a Single Sign-On that a user will log in into, retrieve a JWT token, and use that token to authenticate to your Ingress Gateway managed by TSB.
Adding a new Client.
- Click Clients in the left side menu
- Select the Client Create button
- Client ID:
tetrateapp
- Client Protocol: openid-connect
- Root URL: https://www.keycloak.org/app/, (https://www.keycloak.org/app/ is an SPA testing application available on the Keycloak website).
- Click Save
Then make some updates in the Client.
First, increase the token lifespan to ensure that it doesn't expire too quickly, or during testing.
- In the Settings tab, scroll down, select Advanced Settings
- Set the Access Token Lifespan to 2 hours
- Click Save
Then, you need to add two mappers so that Keycloak can generate a JWT with data that you use in the TSB Ingress Gateway.
You'll need to add two types of mappers - an Audience and a Role mapper:
Mappers | Purpose |
---|---|
Audience mapper | Adds a client id in the audience field in JWT token. This ensures that you can limit JWT to specific clients. |
Role mappers | Changes the role from nested struct to array in the JWT token. Currently, TSB cannot handle nested fields in JWT claims. This has been fixed in Istio 1.8 and will be added to TSB in future releases. |
-
Select the Mappers tab
-
Click the Create button and enter the following information:
- Name: Audience mapper
- Mapper Type: Audience
- Included Client Audience:
tetrateapp
-
Click Save
-
Return to the Mappers tab
-
Click on the Create button and enter the following information:
- Name: Role mapper
- Mapper Type: User Realm Role
- Token Claim Name: roles
- Claim JSON Type: String Leave multi-valued, add to ID token, Add to access token, and Add user info to ‘on'
-
Click Save
Test User Sign In
Now you have your client configured, sign in and inspect your JWT token
- Go to https://www.keycloak.org/app/ and enter the following information:
- Keycloak URL:
https://keycloak.example.com/auth
- Realm:
tetrate
- Client:
tetrateapp
- Keycloak URL:
- Click Save
To inspect the JWT token.
- Open the browser console
- Click on the Network tab
- Sign in with user Jack's credentials.
- Look up a request to the
token
. In the response, get theaccess_token
. - Paste your token into https://jwt.io/
You'll see the following information from your JWT token. You only need to note
three fields that you'll use in your Ingress Gateway configuration: iss
,
aud
, and roles
.
{
"exp": 1606908135,
"iat": 1606900935,
"auth_time": 1606900917,
"jti": "c1e45982-38c6-4d0d-b201-9d823eed4c0a",
"iss": "https://keycloak.example.com/auth/realms/tetrate",
"aud": [
"tetrateapp",
"account"
],
"sub": "06765a3f-b09f-4c46-a0f9-0285c3924409",
"typ": "Bearer",
"azp": "tetrateapp",
"nonce": "f96cd9eb-af9e-4e41-8591-ffc01fd94dd0",
...
"scope": "openid email profile",
"email_verified": true,
"roles": [
"offline_access",
"admin",
"uma_authorization"
],
"name": "Jack White",
"preferred_username": "jack",
"given_name": "Jack",
"family_name": "White",
"email": "jack@tetrate.com"
}
You can also get a user JWT token using OAuth's Resource Owner Password Flow. This flow is enabled by default when you create a Keycloak Client.
curl --request POST \
--url https://keycloak.example.com/auth/realms/tetrate/protocol/openid-connect/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'X-B3-Sampled: 1' \
--data client_id=tetrateapp \
--data password=<user_password> \
--data username=jack \
--data grant_type=password \
--data 'scope=openid email profile'
Deploying the Httpbin application with Ingress Gateway
Deploy the httpbin
application along with the Ingress Gateway.
Create the following httpbin.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: httpbin
namespace: httpbin
---
apiVersion: v1
kind: Service
metadata:
name: httpbin
namespace: httpbin
labels:
app: httpbin
spec:
ports:
- name: http
port: 8000
targetPort: 80
selector:
app: httpbin
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: httpbin
namespace: httpbin
spec:
replicas: 1
selector:
matchLabels:
app: httpbin
version: v1
template:
metadata:
labels:
app: httpbin
version: v1
spec:
serviceAccountName: httpbin
containers:
- image: docker.io/kennethreitz/httpbin
imagePullPolicy: IfNotPresent
name: httpbin
ports:
- containerPort: 80
Deploy httpbin
using the kubectl commands to your onboarded clusters
kubectl create namespace httpbin
kubectl label namespace httpbin istio-injection=enabled --overwrite=true
kubectl apply -n httpbin -f httpbin.yaml
Confirm all services and pods are running
kubectl get pods -n httpbin
Create an Ingress Gateway ingress.yaml
apiVersion: install.tetrate.io/v1alpha1
kind: IngressGateway
metadata:
name: tsb-gateway-httpbin
namespace: httpbin
spec:
kubeSpec:
service:
type: LoadBalancer
Apply your changes
kubectl apply -n httpbin -f ingress.yaml
Confirm that all services and pods are running. Make sure that you wait until the Ingress Gateway has its external IP assigned.
kubectl get pods -n httpbin
kubectl get svc -n httpbin
Get the Ingress Gateway IP
export GATEWAY_HTTPBIN_IP=$(kubectl -n httpbin get service tsb-gateway-httpbin -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
Configuring Workspaces and Ingress Gateway
Now that you have your application running, you need to create workspaces and configure your Ingress Gateway. You will need TSB running and tctl for this.
If you run TSB demo install, you will have a default tenant named tetrate
and
a default cluster named demo
which we use in the following configuration
yamls. If you are using this in production, please change it to your own tenant
and cluster.
Workspace
Create a workspace.yaml
apiversion: api.tsb.tetrate.io/v2
kind: Workspace
metadata:
tenant: tetrate
name: httpbin-ws
spec:
namespaceSelector:
names:
- 'demo/httpbin'
---
apiVersion: gateway.tsb.tetrate.io/v2
kind: Group
metadata:
tenant: tetrate
workspace: httpbin-ws
name: httpbin-gw
spec:
namespaceSelector:
names:
- 'demo/httpbin'
configMode: BRIDGED
Apply your changes
tctl apply -f workspace.yaml
Make sure that the workspace is created
tctl get workspaces httpbin-ws
Expected output:
NAME
httpbin-ws
Next, create an Ingress Gateway to allow access to httpbin from outside the mesh. You'll start with an insecure Gateway that has no authentication.
IngressGateway
Create the following gateway-no-auth.yaml
. In this example, httpbin-certs
is already set for HTTPS connections.
apiVersion: gateway.tsb.tetrate.io/v2
kind: IngressGateway
metadata:
name: httpbin-gw-ingress
group: httpbin-gw
workspace: httpbin-ws
tenant: tetrate
spec:
workloadSelector:
namespace: httpbin
labels:
app: tsb-gateway-httpbin
http:
- name: httpbin
port: 8443
hostname: 'httpbin.tetrate.com'
tls:
mode: SIMPLE
secretName: httpbin-certs
routing:
rules:
- route:
host: 'httpbin/httpbin.httpbin.svc.cluster.local'
port: 8000
Apply with tctl
tctl apply -f gateway-no-auth.yaml
Verify that you have a gateway created in the httpbin namespace
kubectl get gateway -n httpbin httpbin-gw-ingress -o yaml
Example output
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
annotations:
tsb.tetrate.io/fqn: tenants/tetrate/workspaces/httpbin-ws/gatewaygroups/httpbin-gw/ingressgateways/httpbin-gw-ingress
xcp.tetrate.io/contentHash: ea6e317d90873ee3
creationTimestamp: "2020-12-03T00:52:32Z"
generation: 2
labels:
xcp.tetrate.io/gatewayGroup: httpbin-gw
xcp.tetrate.io/workspace: httpbin-ws
name: httpbin-gw-ingress
namespace: httpbin
resourceVersion: "6006430"
selfLink: /apis/networking.istio.io/v1beta1/namespaces/httpbin/gateways/httpbin-gw-ingress
uid: ab0ad2d9-b3db-40ac-9926-0e440d7d8c85
spec:
selector:
app: tsb-gateway-httpbin
servers:
- hosts:
- httpbin/httpbin.tetrate.com
port:
name: http-httpbin
number: 8443
protocol: HTTP
- hosts:
- httpbin/httpbin.tetrate.com
port:
name: mtls-httpbin
number: 15443
protocol: HTTPS
tls:
mode: ISTIO_MUTUAL
Try to access the httpbin by sending it a request
export GATEWAY_HTTPBIN_IP=$(kubectl -n httpbin get service tsb-gateway-httpbin -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -k -v "https://httpbin.tetrate.com/get" \
--resolve "httpbin.tetrate.com:443:${GATEWAY_HTTPBIN_IP}" \
-H "X-B3-Sampled: 1"
Enabling Authentication and Authorization at Ingress
Now, add the authentication and authorization to your Ingress Gateway by
creating the following gateway-with-auth.yaml
apiVersion: gateway.tsb.tetrate.io/v2
kind: IngressGateway
metadata:
name: httpbin-gw-ingress
group: httpbin-gw
workspace: httpbin-ws
tenant: tetrate
spec:
workloadSelector:
namespace: httpbin
labels:
app: tsb-gateway-httpbin
http:
- name: httpbin
port: 8443
hostname: 'httpbin.tetrate.com'
tls:
mode: SIMPLE
secretName: httpbin-certs
authentication:
jwt:
issuer: https://keycloak.example.com/auth/realms/tetrate
audiences:
- tetrateapp
jwks_uri: https://keycloak.example.com/auth/realms/tetrate/protocol/openid-connect/certs
authorization:
local:
rules:
- name: admin
from:
- jwt:
iss: 'https://keycloak.example.com/auth/realms/tetrate'
sub: '*'
other:
roles: admin
to:
- paths: ['*']
- name: normal
from:
- jwt:
iss: 'https://keycloak.example.com/auth/realms/tetrate'
sub: '*'
other:
roles: normal
to:
- paths: ['/status/*']
methods: ['GET']
routing:
rules:
- route:
host: 'httpbin/httpbin.httpbin.svc.cluster.local'
port: 8000
Notice that in the authentication block -- the audiences are set to
tetrateapp
, which was set previously in the JWT token.
The authorization block sets two rules: one for the admin role which can access
everything and another for the normal role which only can access GET /status
.
Now, apply the changes. Since you have the same name as the previous
gateway-no-auth.yaml
, it will update your previous gateway.
tctl apply -f gateway-with-auth.yaml
If you try to access httpbin
without a JWT token you will get a 403
error
curl -k -o /dev/null -s \
-w "%{http_code}\n" "https://httpbin.tetrate.com/get" \
--resolve "httpbin.tetrate.com:443:${GATEWAY_HTTPBIN_IP}" \
-H "X-B3-Sampled: 1"
403
Access httpbin with JWT Token
Try to access the Gateway with a JWT token. Get the token using Keycloak sample
app or curl
as explained before, and use the token to make HTTP requests with
curl
for both users Jack and Sally. Replace <jack_access_token>
and
<sally_access_token>
in the curl
command below to get the user's JWT token.
Try to access GET /get
with Jack's token (our admin user)
curl -k -o /dev/null -s \
-w "%{http_code}\n" "https://httpbin.tetrate.com/get" \
--resolve "httpbin.tetrate.com:443:${GATEWAY_HTTPBIN_IP}" \
--header "Authorization: Bearer <jack_access_token>" \
--header "X-B3-Sampled: 1"
200
Try to access GET /get
with Sally's token (our normal user). The request
should fail because any user with a normal role only is allowed to access
GET /status/*
curl -k -o /dev/null -s \
-w "%{http_code}\n" "https://httpbin.tetrate.com/get" \
--resolve "httpbin.tetrate.com:443:${GATEWAY_HTTPBIN_IP}" \
--header "Authorization: Bearer <sally_access_token>" \
--header "X-B3-Sampled: 1"
403
Try to access GET /status/200
with Sally's token. The request should succeed
because any user with a normal role is allowed to access GET /status/*
curl -k -o /dev/null -s \
-w "%{http_code}\n" "https://httpbin.tetrate.com/status/200" \
--resolve "httpbin.tetrate.com:443:${GATEWAY_HTTPBIN_IP}" \
--header "Authorization: Bearer <sally_access_token>" \
--header "X-B3-Sampled: 1"
200