Advanced Authentication Workflows with OpenID Connect using Gloo API Gateway
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.
- About Gloo API Gateway
- Request an enterprise trial
- Register for an upcoming event
- Join the conversation in the community slack