JWT with external auth and transformation policies

Use JWT along with external auth and transformation policies together to secure access to your routes. The following example walks you through setting up the following resources:

Before you begin

This guide assumes that you use the same names for components like clusters, workspaces, and namespaces as in the getting started, and that your Kubernetes context is set to the cluster you store your Gloo config in (typically the management cluster). If you have different names, make sure to update the sample configuration files in this guide.

Follow the getting started instructions to:

  1. Set up Gloo Gateway in a single cluster.

  2. Deploy sample apps.

  3. Configure an HTTP listener on your gateway and set up basic routing for the sample apps.

  4. Make sure that you have the following CLI tools, or something comparable:

    • htpasswd to generate hashed, salted passwords.
    • base64 to encode strings.
  5. Make sure that the external auth service is installed and running. If not, install the external auth service in your cluster environment.

    kubectl get pods -A -l app=ext-auth-service
    
  6. Save the external address of the ingress gateway.

    export INGRESS_GW_IP=$(kubectl get svc -n gloo-mesh-gateways istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
    echo $INGRESS_GW_IP
    
    export INGRESS_GW_IP=$(kubectl get svc -n gloo-mesh-gateways istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
    echo $INGRESS_GW_IP
    

Step 1: Create an external auth policy

Create a basic external auth policy to secure access to the httpbin route. For more information about other types of external auth, see External authentication and authorization.

You can do the following steps in a different order, depending on when you want the policy to take effect. For example, you might want the policy to always take effect as soon as the route is created. To do so, you can create the policy before you add the route to the route table.
  1. Generate a salt and hashed password for your user credentials. The following example uses the htpasswd tool for a user named user.

    htpasswd -nbm user password
    

    Example output:

    user:$apr1$TYiryv0/$8BvzLUO9IfGPGGsPnAgSu1
    
  2. Retrieve the salt and hashed password from the output of the previous step.

    • Salt: TYiryv0/
    • Hashed password: 8BvzLUO9IfGPGGsPnAgSu1
  3. Create a basic external auth policy that uses the salt and hashed password from the previous step.

    kubectl apply -f - <<EOF
    apiVersion: security.policy.gloo.solo.io/v2
    kind: ExtAuthPolicy
    metadata:
      name: httpbin-ext-auth
      namespace: httpbin
    spec:
      applyToRoutes:
      - route:
          labels:
            route: httpbin
      config:
        glooAuth:
          booleanExpr: jwt || basic
          configs:
          - name: basic
            basicAuth:
              apr:
                users:
                  user:
                    hashedPassword: 8BvzLUO9IfGPGGsPnAgSu1
                    salt: TYiryv0/
          - jwt: {}
            name: jwt
        server:
          name: httpbin-ext-auth-server
          namespace: gloo-mesh-addons
    EOF
    

    Review the following table to understand this configuration. For more information, see the API docs.

    Setting Description
    applyToRoutes Use labels to configure which routes to apply the policy to. This example label matches the app and route from the example route table that you apply separately. If omitted and you do not have another selector such as applyToDestinations, the policy applies to all routes in the workspace.
    booleanExpr Set the order for processing auth configs when multiple authentication methods are used. In this example, requests that have a JWT are processed first. The basic auth that this external auth policy configures is only processed if a JWT is not included in the request. This setting saves extra processing time so that clients do not need to include both auth methods in their requests.
    name: basic Set the name for the basic auth configuration, such as basic. This name must match the name that you set in the booleanExpr.
    basicAuth Configure the basic auth credentials to use to authenticate requests. The example sets up user credentials for a user named user in the required APR1 format. For more information, see the API reference.
    hashedPassword The hashed password that you generated in the previous step. The example sets 8BvzLUO9IfGPGGsPnAgSu1.
    salt The salt, or random data that hashes the password, that you generated in the previous step. The example sets TYiryv0/.
    jwt Add a dummy external auth service to support multiple authentication methods with JWT.
    server The ExtAuthServer resource that represents the server for the policy to use, which you create in the next step.
  4. Create an external auth server to use for your policy.

    kubectl apply -f - <<EOF
    apiVersion: admin.gloo.solo.io/v2
    kind: ExtAuthServer
    metadata:
      name: httpbin-ext-auth-server
      namespace: gloo-mesh-addons
    spec:
      destinationServer:
        port:
          number: 8083
        ref:
          cluster: $CLUSTER_NAME
          name: ext-auth-service
          namespace: gloo-mesh-addons
    EOF
    
  5. Send an unauthenticated request to the httpbin app.

    curl -vik --resolve www.example.com:80:${INGRESS_GW_IP} -H "X-httpbin: true" http://www.example.com:80/get
    
    curl -vik --resolve www.example.com:443:${INGRESS_GW_IP} -H "X-httpbin: true" https://www.example.com:443/get
    

    Example output: Notice that the request is denied with a 401 Unauthorized response.

    HTTP/2 401
    
  6. Encode the expected user credentials from the external auth policy in base64 format.

    echo -n "user:password" | base64
    

    Example output:

    dXNlcjpwYXNzd29yZA==
    
  7. Repeat the request to the httpbin app, including the authorization header with the user credentials. This time, the request succeeds.

    curl -vik --resolve www.example.com:80:${INGRESS_GW_IP} -H "X-httpbin: true" -H "Authorization: basic dXNlcjpwYXNzd29yZA==" http://www.example.com:80/get
    
    curl -vik --resolve www.example.com:443:${INGRESS_GW_IP} -H "X-httpbin: true" -H "Authorization: basic dXNlcjpwYXNzd29yZA==" https://www.example.com:443/get
    

    Example output:

    HTTP/2 200
    

Good job! You just secured basic access to the httpbin app. Now, you can add JWT verification with a JWT policy.

Step 2: Create a JWT policy

Create a basic JWT policy with a remote JWKS server to add claims from the JWT as headers to requests to the httpbin route. For more information about JSON web tokens, see JWT.

  1. Get the following details for your remote JWKS server from your OIDC provider, such as Keycloak or Okta. Keep in mind that this server must be accessible from your cluster.

    • Verify that the claims you want to pass along in the request header are included in the JWT that the OIDC provider generates, such as org and email claims.
    • Get the Issuer URL for your OAuth2 server instance. The following examples show what the URLs might look like from common providers.
      • Okta default server: https://dev-1234567.okta.com/oauth2/default
      • Deployed Keycloak instance in AWS with a solo realm: http://a1aa111aaaa1a-123456789.us-west-1.elb.amazonaws.com:8080/auth/realms/solo
    • Get the remote JWKS URL to fetch the public keys. The following examples are from common providers.
      • Okta default server: https://dev-1234567.okta.com/oauth2/default/v1/keys
      • Deployed Keycloak instance in AWS with a solo realm: http://a1aa111aaaa1a-123456789.us-west-1.elb.amazonaws.com:8080/auth/realms/solo/protocol/openid-connect/certs
  2. Create an external service to represent the remote JWKS server. The following example uses an okta host and assumes that you set up Okta and your Gloo virtual gateway for the httpbin app to listen for HTTPS traffic.

    kubectl apply -f - <<EOF
    apiVersion: networking.gloo.solo.io/v2
    kind: ExternalService
    metadata:
      name: httpbin-jwks
      namespace: httpbin
      labels:
        expose: "true"
        host: okta
    spec:
      hosts:
      - dev-1234567.okta.com
      ports:
      - name: https
        number: 443
        protocol: HTTPS
        clientsideTls: {}
    EOF
    
  3. Create a basic JWT policy with the remote JWKS issuer details that you previously retrieved.

    kubectl apply -f - <<EOF
    apiVersion: security.policy.gloo.solo.io/v2
    kind: JWTPolicy
    metadata:
      annotations:
        cluster.solo.io/cluster: ""
      name: httpbin-jwt
      namespace: httpbin
    spec:
      applyToRoutes:
      - route:
          labels:
            route: httpbin
      config:
        phase:
          preAuthz: {}
        validationPolicy: ALLOW_MISSING
        providers:
          okta:
            keepToken: true
            claimsToHeaders:
            - append: true
              claim: org
              header: x-org
            - append: true
              claim: email
              header: x-email
            issuer: https://dev-1234567.okta.com/oauth2/default
            remote:
              url: https://dev-1234567.okta.com/oauth2/default/v1/keys
              destinationRef:
                kind: EXTERNAL_SERVICE
                port:
                  number: 443
                ref:
                  name: httpbin-jwks
            tokenSource:                    
              headers:  
              - name: X-Auth
                prefix: 'Bearer '
              queryParams:       
              - auth_token
    EOF
    

    Review the following table to understand this configuration. For more information, see the API docs.

    Setting Description
    applyToRoutes Use labels to configure which routes to apply the policy to. This example label matches the app and route from the example route table that you apply separately. If omitted and you do not have another selector such as applyToDestinations, the policy applies to all routes in the workspace. Note that this policy applies to the same httpbin route that the external auth policy applies to.
    validationPolicy Set to ALLOW_MISSING to allow requests to succeed even if JWT authentication is missing, but fail when an invalid JWT token is presented. This way, clients do not have to pass a JWT in order to access the httpbin app. However, when clients do pass a JWT, the JWT is validated so that subsequent steps, like a transformation policy, use validated information.
    providers Configure the JWT providers for the policy with the information that you previously retrieved. This example sets up an okta provider.
    claimsToHeaders Set the claims from the JWT payload that you want to extract and add as headers to the request before the request is forwarded to the upstream destination. This example extracts the org and email claims.
    issuer Set the JWT issuer that you previously retrieved.
    remote.url Configure the reference to the remote JWKS server with the details that you previously retrieved.
    remote.destinationRef Add the details of the external service that you created to refer to the remote JWKS server.
    tokenSource Specify where Gloo Gateway looks for the token to use for JWT authentication. If unset, the default locations are tried in the following order:
    1. The authorization header X-Auth with the value Bearer <token>.
    2. The query parameter for access_token, such as https://<path>?access_token=<token>.
    You can set up multiple JWT tokens to verify per request. If a request has multiple header and query parameters, all tokens must be valid for Gloo Gateway to accept the request.
  4. Create a transformation policy that uses the information in the JWT email claim to create a new header called user.

    kubectl apply -f - <<EOF
    apiVersion: trafficcontrol.policy.gloo.solo.io/v2
    kind: TransformationPolicy
    metadata:
      name: httpbin-transform-user
      namespace: httpbin
    spec:
      applyToRoutes:
      - route:
          labels:
            route: httpbin
      config:
        phase:
          postAuthz: {}
        request:
          injaTemplate:
            extractors:
              user:
                header: 'X-Email'
                regex: '(.*)@.*$'
                subgroup: 1
            headers:
              x-user:
                text: "{{ user }}"
    EOF
    

    Review the following table to understand this configuration. For more information, see the API docs.

    Setting Description
    applyToRoutes Use labels to configure which routes to apply the policy to. This example label matches the app and route from the example route table that you apply separately. If omitted and you do not have another selector such as applyToDestinations, the policy applies to all routes in the workspace. Note that this policy applies to the same httpbin route that the JWT policy applies to.
    phase Set the phase to postAuthz so that the transformation policy can use the information previously extracted from the JWT claim during the auth phase.
    request Configure an inja template to extract information from the existing headers, transform the information with a regular expression, and add a new request header. In the example, the policy extracts the X-Email header, grabs the information before the @ sign in the email address, and adds this information to a new X-User request header.

Step 3: Verify secured access to httpbin

To verify access with a JWT, you need an access token from your OIDC provider to include in the request.

  1. Get an access token from your OIDC provider. The steps vary by provider, and you might need several details from your OIDC provider, such as the client ID, client secret, and token endpoint. For more information, consult your OIDC provider, such as the following guides that use the Postman and OIDC Debugger tools to get an access token.

  2. Send an authenticated request to the httpbin app without the JWT. Notice that the request succeeds because you set the JWT validation to be optional (validationPolicy: ALLOW_MISSING).

    curl -vik --resolve www.example.com:80:${INGRESS_GW_IP} -H "X-httpbin: true" -H "Authorization: basic dXNlcjpwYXNzd29yZA==" http://www.example.com:80/get
    
    curl -vik --resolve www.example.com:443:${INGRESS_GW_IP} -H "X-httpbin: true" -H "Authorization: basic dXNlcjpwYXNzd29yZA==" https://www.example.com:443/get
    

  3. Send a request to the httpbin app again, this time with a fake JWT. The request fails because the JWT is not valid.

    curl -vik --resolve www.example.com:80:${INGRESS_GW_IP} -H "X-httpbin: true" -H "X-Auth: Bearer fake-jwt" http://www.example.com:80/get
    
    curl -vik --resolve www.example.com:443:${INGRESS_GW_IP} -H "X-httpbin: true" -H "X-Auth: Bearer fake-jwt" https://www.example.com:443/get
    

    Example output: Notice that the error message describes how the JWT is not formatted correctly.

    HTTP/2 401
    www-authenticate: Bearer realm="https://example.com:443/get", error="invalid_token"
    ...
    Jwt is not in the form of Header.Payload.Signature with two dots and 3 sections
    
  4. Repeat the request to the httpbin app, including the access token that you previously retrieved in the "X-Auth: Bearer <access-token>" header. Don't include the X-Authorization header with basic auth, to avoid an unnecessary hop to the external auth service. This time, the JWT is validated and the request succeeds.

    curl -vik --resolve www.example.com:80:${INGRESS_GW_IP} -H "X-httpbin: true" -H "X-Auth: Bearer <access-token>" http://www.example.com:80/get
    
    curl -vik --resolve www.example.com:443:${INGRESS_GW_IP} -H "X-httpbin: true" -H "X-Auth: Bearer <access-token>" https://www.example.com:443/get
    

    Example output: Notice that the request includes the org and email claims as headers to the httpbin app. Also, the transformation policy extracted the email from the JWT claim and added an X-User header.

    HTTP/2 200
    ...
    {
    "args": {}, 
    "headers": {
      "Accept": "*/*", 
      "Authorization": "basic dXNlcjpwYXNzd29yZA==", 
      "Host": "example.com", 
      "User-Agent": "curl/7.87.0", 
      "X-Auth": "Bearer eyJra...", 
      "X-B3-Sampled": "0", 
      "X-B3-Spanid": "c8547b27e22e0890", 
      "X-B3-Traceid": "eb4be0e080550506c8547b27e22e0890", 
      "X-Email": "user1@solo.io", 
      "X-Envoy-Attempt-Count": "1", 
      "X-Envoy-Decorator-Operation": "httpbin.httpbin.svc.cluster.local:8000/*", 
      "X-Envoy-Internal": "true", 
      "X-Envoy-Peer-Metadata": "ChQ...", 
      "X-Envoy-Peer-Metadata-Id": "router~10.16.2.29~istio-ingressgateway-1-18-1-78b97d9d8d-vgmmn.gloo-mesh-gateways~gloo-mesh-gateways.svc.cluster.local", 
      "X-Httpbin": "true", 
      "X-Org": "solo.io",
      "X-User": "user1"
    }
       

Cleanup

You can optionally remove the resources that you set up as part of this guide.
kubectl -n httpbin delete ExtAuthPolicy httpbin-ext-auth
kubectl -n gloo-mesh-addons delete ExtAuthServer httpbin-ext-auth-server
kubectl -n httpbin delete ExternalService httpbin-jwks
kubectl -n httpbin delete JWTPolicy httpbin-jwt
kubectl -n httpbin delete TransformationPolicy httpbin-transform-user