Gloo supports authentication via OpenID Connect (OIDC). OIDC is an identity layer on top of the OAuth 2.0 protocol. In OAuth 2.0 flows, authentication is performed by an external Identity Provider (IdP) which, in case of success, returns an Access Token representing the user identity. The protocol does not define the contents and structure of the Access Token, which greatly reduces the portability of OAuth 2.0 implementations.
The goal of OIDC is to address this ambiguity by additionally requiring Identity Providers to return a well-defined ID Token. OIDC ID tokens follow the JSON Web Token standard and contain specific fields that your applications can expect and handle. This standardization allows you to switch between Identity Providers - or support multiple ones at the same time - with minimal, if any, changes to your downstream services; it also allows you to consistently apply additional security measures like Role-based Access Control (RBAC) based on the identity of your users, i.e. the contents of their ID token.
In this post, we're going to expose a Service running on Kubernetes using Gloo.
Then, we'll secure the access using Google OIDC.
As explained above, Google OIDC will return a JWT token, so we'll use Gloo to extract some claims from this token and to create new headers corresponding to these claims.
Finally, we'll see how Gloo RBAC rules can be created to leverage the claims contained in the JWT token.
Expose a Kubernetes Service
Let's start by deploying the
httpbin
service on our Kubernetes cluster:
apiVersion: v1
kind: ServiceAccount
metadata:
name: httpbin
---
apiVersion: v1
kind: Service
metadata:
name: httpbin
labels:
app: httpbin
spec:
ports:
- name: http
port: 8000
targetPort: 80
selector:
app: httpbin
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: 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
Gloo automatically discovers Kubernetes services, by default.
So, running the
glooctl get upstreams
command, you should be able to see a new Upstream created for our application:
+----------------------------------------------------+------------+----------+-------------------------------------+
| UPSTREAM | TYPE | STATUS | DETAILS |
+----------------------------------------------------+------------+----------+-------------------------------------+
| default-httpbin-8000 | Kubernetes | Accepted | svc name: httpbin |
| | | | svc namespace: default |
| | | | port: 8000 |
| | | | |
...
To explose the application you can run the following command:
glooctl add route \
--path-prefix / \
--dest-name default-httpbin-8000
It creates the following Virtual Service:
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
name: default
namespace: gloo-system
spec:
virtualHost:
domains:
- '*'
routes:
- matchers:
- prefix: /
routeAction:
single:
upstream:
name: default-httpbin-8000
namespace: gloo-system
status:
reportedBy: gateway
state: 1
subresourceStatuses:
'*v1.Proxy.gloo-system.gateway-proxy':
reportedBy: gloo
state: 1
The Virtual Service can be created using
kubectl
and this yaml instead of the
glooctl
command you have used.
And you can see how Gloo updates the
status
of the Virtual Service to show that it has been able to process it.
The
glooctl get virtualservices
command is using the
status
fields of the different objects to return the state of the different components. Here is the output you should get:
+-----------------+--------------+---------+------+----------+-----------------+----------------------------------+
| VIRTUAL SERVICE | DISPLAY NAME | DOMAINS | SSL | STATUS | LISTENERPLUGINS | ROUTES |
+-----------------+--------------+---------+------+----------+-----------------+----------------------------------+
| default | | * | none | Accepted | | / -> |
| | | | | | | gloo-system.default-httpbin-8000 |
| | | | | | | (upstream) |
+-----------------+--------------+---------+------+----------+-----------------+----------------------------------+
Update your
/etc/hosts
file to resolve
mydomain.com
by the IP address returned by the
glooctl proxy address
command (without the port number).
You can now access the application using the
mydomain.com
domain.
# curl http://mydomain.com/get
{
"args": {},
"headers": {
"Accept": "*/*",
"Content-Length": "0",
"Host": "mydomain.com",
"User-Agent": "curl/7.64.1",
"X-Envoy-Expected-Rq-Timeout-Ms": "15000"
},
"origin": "192.168.149.8",
"url": "http://mydomain.com/get"
}
As you can see, the
httpbin
application returns information about the request we sent.
Secure the application using HTTPS
Let's create an SSL certificate for the
mydomain.com
domain:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key -out tls.crt -subj "/CN=mydomain.com"
And create a Kubernetes secret with this certificate:
kubectl create secret tls upstream-tls --key tls.key \
--cert tls.crt --namespace gloo-system
Now, to enable HTTPS for our Virtual Service, you simply need to update it as follow:
kubectl apply -f - <<EOF
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
name: default
namespace: gloo-system
spec:
sslConfig:
secretRef:
name: upstream-tls
namespace: gloo-system
virtualHost:
domains:
- '*'
routes:
- matchers:
- prefix: /
routeAction:
single:
upstream:
name: default-httpbin-8000
namespace: gloo-system
You can now access the application using HTTPS:
# curl -k https://mydomain.com/get
{
"args": {},
"headers": {
"Accept": "*/*",
"Content-Length": "0",
"Host": "mydomain.com",
"User-Agent": "curl/7.64.1",
"X-Envoy-Expected-Rq-Timeout-Ms": "15000"
},
"origin": "192.168.149.8",
"url": "https://mydomain.com/get"
}
Authenticate the user with Google OIDC
Go to the
Google Developer Console to create credentials and to authorize the
mydomain.com
domain.
More details are available
here.
Create a Kubernetes
Secret
using the client secret you got when you created the credentials:
glooctl create secret oauth --namespace gloo-system --name google --client-secret $CLIENT_SECRET
Create an
AuthConfig
object using the client id and referencing the secret:
apiVersion: enterprise.gloo.solo.io/v1
kind: AuthConfig
metadata:
name: google-oidc
namespace: gloo-system
spec:
configs:
- oauth:
app_url: https://mydomain.com
callback_path: /callback
client_id: $CLIENT_ID
client_secret_ref:
name: google
namespace: gloo-system
issuer_url: https://accounts.google.com
scopes:
- email
Note that we have used the
scopes
parameter to indicate to the identity provider to include the email of the user in the claims of the JWT token it will return.
Now, let's update the Virtual Service to use Google OIDC Authentication:
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
name: default
namespace: gloo-system
spec:
sslConfig:
secretRef:
name: upstream-tls
namespace: gloo-system
virtualHost:
domains:
- '*'
routes:
- matchers:
- prefix: /
routeAction:
single:
upstream:
name: default-httpbin-8000
namespace: gloo-system
options:
extauth:
configRef:
name: google-oidc
namespace: gloo-system
Note that the
/callback
path is handled by the Virtual Service because we used the
/
prefix.
Go to
https://mydomain.com using your web browser.
You should be redirected to the Google Login page and then access the application.
Gloo has redirected you to the
/callback
page, with the information it got from Google OIDC added as a query string to create a Cookie.
Then, the normal flow has occurred to reach the application.
You should get the following output:
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "fr-fr",
"Content-Length": "0",
"Cookie": "access_token=ya29.a0AfH6SMBv8QNdWFn_sEI4iN_GrfCdG__m_8itq_t7mcYVskjeJrJRpuZQJf9AFKAzEbSABsAZ633hvpQzUzPL_bHJ37CmbcO3MnDlltlgRNVscY-7KmauWwpkpDvrdPfQ886pwsoUvZiLHhE--TnBhtezyLd8E1D-LdeZ3w; id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjE3OGFiMWRjNTkxM2Q5MjlkMzdjMjNkY2FhOTYxODcyZjhkNzBiNjgiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI2MzUzODgzNDA1MjEtMHNtNzFtZ29rZXFmbGs0ajlyZGJncHQyNnRkbGJybjMuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI2MzUzODgzNDA1MjEtMHNtNzFtZ29rZXFmbGs0ajlyZGJncHQyNnRkbGJybjMuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTc1NzcxOTAyMjM2ODAxODM2ODEiLCJlbWFpbCI6ImRqYW5ub3RAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiJEX1JJcWRHUl9xbWVzc29zdjUxRW5RIiwiaWF0IjoxNjAzMTAxMzMxLCJleHAiOjE2MDMxMDQ5MzF9.HHUO1JXRNA2348Erfla_PX7-s23us5Vr5WSpzvN7Ms0MmPXuZHyzxZSn4_ab-XhZPzqrKudYtbMpnVTlD-9fuPUC0Mm4ZIBh3cTxlH_4b512qzFU-gjStGdV0MGVBnVODDJFHOpOxywmd27aDnCPiEph-L4LyjCjPyEyqEA2bzQEaIE91pynWUgwj1BHajew_70zPVeKxwDplsJPyVM9LBNxr6LrfacaHUz_SvnkQimibEvdP9DotAHe5Msovdb44pdK8KhHawfLokCqc5Sb-baQ-ppWjfunRyMcaRCU1CI5ki7MTp4n95SlHC2Kr7XKg86oP4HTJWb5pStHgNA8qQ",
"Host": "mydomain.com",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15",
"X-Envoy-Expected-Rq-Timeout-Ms": "15000"
},
"origin": "192.168.149.8",
"url": "https://mydomain.com/get"
}
As you can see, the browser has sent the
cookie
as a header in the HTTP request.
Request transformation
Gloo is able to perform advanced transformations of the request and response.
Let's modify the Virtual Service using the yaml below:
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
name: default
namespace: gloo-system
spec:
sslConfig:
secretRef:
name: upstream-tls
namespace: gloo-system
virtualHost:
domains:
- '*'
routes:
- matchers:
- prefix: /
routeAction:
single:
upstream:
name: default-httpbin-8000
namespace: gloo-system
options:
extauth:
configRef:
name: google-oidc
namespace: gloo-system
stagedTransformations:
early:
requestTransforms:
- requestTransformation:
transformationTemplate:
extractors:
token:
header: 'cookie'
regex: '.*id_token=(.*)'
subgroup: 1
headers:
jwt:
text: '{{ token }}'
headerManipulation:
requestHeadersToRemove:
- "cookie"
This transformation is using a regular expression to extract the JWT token from the
cookie
header, creates a new
jwt
header that contains the token and removes the
cookie
header.
Here is the output you should get if you refresh the web page:
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "fr-fr",
"Content-Length": "0",
"Host": "mydomain.com",
"Jwt": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE3OGFiMWRjNTkxM2Q5MjlkMzdjMjNkY2FhOTYxODcyZjhkNzBiNjgiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI2MzUzODgzNDA1MjEtMHNtNzFtZ29rZXFmbGs0ajlyZGJncHQyNnRkbGJybjMuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI2MzUzODgzNDA1MjEtMHNtNzFtZ29rZXFmbGs0ajlyZGJncHQyNnRkbGJybjMuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTc1NzcxOTAyMjM2ODAxODM2ODEiLCJlbWFpbCI6ImRqYW5ub3RAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiJTX2VjWWJ6UURmWVBXeTI3Vm5YS093IiwiaWF0IjoxNjAzMTEwNDkzLCJleHAiOjE2MDMxMTQwOTN9.FMMxbHdbBjAjlqF7q5QWxPBIxJQR1hz-niO3VgOr7nh_mDpMx4FOhvor1YSukwt5HWPHvS79IyUw_nnCHNakFHh55PnXMPOh9yAmgAE_Vq0kZBe0Nhh94Av_Zl5MoUMcdoKElt4e98Rf13P87Sw6dwOVGLqi177nYkgIsSiOJHZ-Y2fNJxNoRbLYyKwgTWqrsaRRVf8gzVpex9g9u7s3FRwXfIMPImxCZWDWp1MEd0QLk9GDA7yzQy2PAQkygJnAppAPoyV9q5OSi9MjYiZ1turniuxn06VwIMYsHY-p_-dNg6W9emonN-7aGLUbrZ2gPcPrL2CDWeqHzcOIZlnh-A",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15",
"X-Envoy-Expected-Rq-Timeout-Ms": "15000",
"X-User-Id": "https://accounts.google.com;227577190223680133999"
},
"origin": "192.168.149.8",
"url": "https://mydomain.com/get"
}
You can see that the
jwt
header has been added to the request while the
cookie
header has been removed.
Extract information from the JWT token
First of all, we need to create a Gloo Upstream for the Google JWKS (JSON Web Key Sets):
apiVersion: gloo.solo.io/v1
kind: Upstream
metadata:
name: google-jkws
namespace: gloo-system
spec:
static:
hosts:
- addr: www.googleapis.com
port: 443
JWKS is a set of public keys that can be used to verify the JWT tokens.
Now, we can update the Virtual Service to validate the token, extract claims from the token and create new headers based on these claims.
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
name: vs1
namespace: gloo-system
spec:
sslConfig:
secretRef:
name: upstream-tls
namespace: gloo-system
virtualHost:
domains:
- '*'
routes:
- matchers:
- prefix: /
routeAction:
single:
upstream:
name: default-httpbin-8000
namespace: gloo-system
options:
extauth:
configRef:
name: google-oidc
namespace: gloo-system
stagedTransformations:
early:
requestTransforms:
- requestTransformation:
transformationTemplate:
extractors:
token:
header: 'cookie'
regex: '.*id_token=(.*)'
subgroup: 1
headers:
jwt:
text: '{{ token }}'
headerManipulation:
requestHeadersToRemove:
- "cookie"
jwt:
providers:
google:
issuer: https://accounts.google.com
tokenSource:
headers:
- header: Jwt
claimsToHeaders:
- claim: email
header: x-solo-claim-email
- claim: email_verified
header: x-solo-claim-email-verified
jwks:
remote:
url: https://www.googleapis.com/oauth2/v3/certs
upstreamRef:
name: google-jkws
namespace: gloo-system
Here is the output you should get if you refresh the web page:
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "fr-fr",
"Content-Length": "0",
"Host": "mydomain.com",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15",
"X-Envoy-Expected-Rq-Timeout-Ms": "15000",
"X-Solo-Claim-Email": "djannot@gmail.com",
"X-Solo-Claim-Email-Verified": "true",
"X-User-Id": "https://accounts.google.com;227577190223680133999"
},
"origin": "192.168.149.8",
"url": "https://mydomain.com/get"
}
As you can see, Gloo has added the
x-solo-claim-email
and
x-solo-claime-email-verified
headers using the information it has extracted from the JWT token.
It will allow the application to know who the user is and if his email has been verified.
RBAC using the claims of the JWT token
Gloo can also be used to set RBAC rules based on the claims of the JWT token returned by the identity provider.
Let's update the Virtual Service as follow:
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
name: default
namespace: gloo-system
spec:
sslConfig:
secretRef:
name: upstream-tls
namespace: gloo-system
virtualHost:
domains:
- '*'
routes:
- matchers:
- prefix: /
routeAction:
single:
upstream:
name: default-httpbin-8000
namespace: gloo-system
options:
extauth:
configRef:
name: google-oidc
namespace: gloo-system
stagedTransformations:
early:
requestTransforms:
- requestTransformation:
transformationTemplate:
extractors:
token:
header: 'cookie'
regex: '.*id_token=(.*)'
subgroup: 1
headers:
jwt:
text: '{{ token }}'
headerManipulation:
requestHeadersToRemove:
- "cookie"
jwt:
providers:
google:
issuer: https://accounts.google.com
tokenSource:
headers:
- header: Jwt
claimsToHeaders:
- claim: email
header: x-solo-claim-email
- claim: email_verified
header: x-solo-claim-email-verified
jwks:
remote:
url: https://www.googleapis.com/oauth2/v3/certs
upstreamRef:
name: google-jkws
namespace: gloo-system
rbac:
policies:
viewer:
permissions:
methods:
- GET
pathPrefix: /get
principals:
- jwtPrincipal:
claims:
email: djannot@gmail.com
If I refresh the web page, I can still access it as I have logged in using the Google account associated with the
djannot@gmail.com
email address.
But if I log in using another Google account or if I try to access another path than
/get
, I get the following response:
RBAC: access denied
Get Started with Gloo
Gloo is available in open source and enterprise editions addressing a wide range of edge and API gateway use cases. Learn more about Gloo by visiting the additional resources below.