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.