JWT Token

This task shows you how to set up an Istio authorization policy to enforce access based on a JSON Web Token (JWT). An Istio authorization policy supports string typed, list-of-string typed, and space-delimited string typed JWT claims.

Before you begin

Before you begin this task, do the following:

  • Complete the Istio end user authentication task.

  • Read the Istio authorization concepts.

  • Install Istio using Istio installation guide.

  • Deploy two workloads: httpbin and curl. Deploy these in one namespace, for example foo. Both workloads run with an Envoy proxy in front of each. Deploy the example namespace and workloads using these commands:

    ZipZip
    $ kubectl create ns foo
    $ kubectl apply -f <(istioctl kube-inject -f @samples/httpbin/httpbin.yaml@) -n foo
    $ kubectl apply -f <(istioctl kube-inject -f @samples/curl/curl.yaml@) -n foo
  • Verify that curl successfully communicates with httpbin using this command:

    $ kubectl exec "$(kubectl get pod -l app=curl -n foo -o jsonpath={.items..metadata.name})" -c curl -n foo -- curl http://httpbin.foo:8000/ip -sS -o /dev/null -w "%{http_code}\n"
    200

Allow requests with valid JWT and list-typed claims

  1. The following command creates the jwt-example request authentication policy for the httpbin workload in the foo namespace. This policy for httpbin workload accepts a JWT issued by testing@secure.istio.io:

    $ kubectl apply -f - <<EOF
    apiVersion: security.istio.io/v1
    kind: RequestAuthentication
    metadata:
      name: "jwt-example"
      namespace: foo
    spec:
      selector:
        matchLabels:
          app: httpbin
      jwtRules:
      - issuer: "testing@secure.istio.io"
        jwksUri: "https://raw.githubusercontent.com/istio/istio/master/security/tools/jwt/samples/jwks.json"
    EOF
  2. Verify that a request with an invalid JWT is denied:

    $ kubectl exec "$(kubectl get pod -l app=curl -n foo -o jsonpath={.items..metadata.name})" -c curl -n foo -- curl "http://httpbin.foo:8000/headers" -sS -o /dev/null -H "Authorization: Bearer invalidToken" -w "%{http_code}\n"
    401
  3. Verify that a request without a JWT is allowed because there is no authorization policy:

    $ kubectl exec "$(kubectl get pod -l app=curl -n foo -o jsonpath={.items..metadata.name})" -c curl -n foo -- curl "http://httpbin.foo:8000/headers" -sS -o /dev/null -w "%{http_code}\n"
    200
  4. The following command creates the require-jwt authorization policy for the httpbin workload in the foo namespace. The policy requires all requests to the httpbin workload to have a valid JWT with requestPrincipal set to testing@secure.istio.io/testing@secure.istio.io. Istio constructs the requestPrincipal by combining the iss and sub of the JWT token with a / separator as shown:

    $ kubectl apply -f - <<EOF
    apiVersion: security.istio.io/v1
    kind: AuthorizationPolicy
    metadata:
      name: require-jwt
      namespace: foo
    spec:
      selector:
        matchLabels:
          app: httpbin
      action: ALLOW
      rules:
      - from:
        - source:
           requestPrincipals: ["testing@secure.istio.io/testing@secure.istio.io"]
    EOF
  5. Get the JWT that sets the iss and sub keys to the same value, testing@secure.istio.io. This causes Istio to generate the attribute requestPrincipal with the value testing@secure.istio.io/testing@secure.istio.io:

    $ TOKEN=$(curl https://raw.githubusercontent.com/istio/istio/master/security/tools/jwt/samples/demo.jwt -s) && echo "$TOKEN" | cut -d '.' -f2 - | base64 --decode
    {"exp":4685989700,"foo":"bar","iat":1532389700,"iss":"testing@secure.istio.io","sub":"testing@secure.istio.io"}
  6. Verify that a request with a valid JWT is allowed:

    $ kubectl exec "$(kubectl get pod -l app=curl -n foo -o jsonpath={.items..metadata.name})" -c curl -n foo -- curl "http://httpbin.foo:8000/headers" -sS -o /dev/null -H "Authorization: Bearer $TOKEN" -w "%{http_code}\n"
    200
  7. Verify that a request without a JWT is denied:

    $ kubectl exec "$(kubectl get pod -l app=curl -n foo -o jsonpath={.items..metadata.name})" -c curl -n foo -- curl "http://httpbin.foo:8000/headers" -sS -o /dev/null -w "%{http_code}\n"
    403
  8. The following command updates the require-jwt authorization policy to also require the JWT to have a claim named groups containing the value group1:

    $ kubectl apply -f - <<EOF
    apiVersion: security.istio.io/v1
    kind: AuthorizationPolicy
    metadata:
      name: require-jwt
      namespace: foo
    spec:
      selector:
        matchLabels:
          app: httpbin
      action: ALLOW
      rules:
      - from:
        - source:
           requestPrincipals: ["testing@secure.istio.io/testing@secure.istio.io"]
        when:
        - key: request.auth.claims[groups]
          values: ["group1"]
    EOF
  9. Get the JWT that sets the groups claim to a list of strings: group1 and group2:

    $ TOKEN_GROUP=$(curl https://raw.githubusercontent.com/istio/istio/master/security/tools/jwt/samples/groups-scope.jwt -s) && echo "$TOKEN_GROUP" | cut -d '.' -f2 - | base64 --decode
    {"exp":3537391104,"groups":["group1","group2"],"iat":1537391104,"iss":"testing@secure.istio.io","scope":["scope1","scope2"],"sub":"testing@secure.istio.io"}
  10. Verify that a request with the JWT that includes group1 in the groups claim is allowed:

    $ kubectl exec "$(kubectl get pod -l app=curl -n foo -o jsonpath={.items..metadata.name})" -c curl -n foo -- curl "http://httpbin.foo:8000/headers" -sS -o /dev/null -H "Authorization: Bearer $TOKEN_GROUP" -w "%{http_code}\n"
    200
  11. Verify that a request with a JWT, which doesn’t have the groups claim is rejected:

    $ kubectl exec "$(kubectl get pod -l app=curl -n foo -o jsonpath={.items..metadata.name})" -c curl -n foo -- curl "http://httpbin.foo:8000/headers" -sS -o /dev/null -H "Authorization: Bearer $TOKEN" -w "%{http_code}\n"
    403

Allow requests with valid JWT and space-delimited claims

Some JWT claims encode multiple values as a single space-delimited string rather than a JSON array. The OAuth2 scope claim is a well-known example of this pattern, defined in RFC 6749 as a space-separated list.

Istio always treats the scope and permission claims as space-delimited, splitting them into individual values that authorization policies can match against. For other custom claims that use the same encoding, use the spaceDelimitedClaims field in your JWTRule to opt them in explicitly. Without this field, a custom claim value like "admin editor" is matched as a single exact string.

{"iss": "testing@secure.istio.io", "roles": "admin editor"}
  1. Download the JWT generation script and signing key:

    $ wget --no-verbose https://raw.githubusercontent.com/istio/istio/master/security/tools/jwt/samples/gen-jwt.py
    $ wget --no-verbose https://raw.githubusercontent.com/istio/istio/master/security/tools/jwt/samples/key.pem
  2. The following command updates the jwt-example request authentication policy to declare that the roles claim is space-delimited:

    $ kubectl apply -f - <<EOF
    apiVersion: security.istio.io/v1
    kind: RequestAuthentication
    metadata:
      name: "jwt-example"
      namespace: foo
    spec:
      selector:
        matchLabels:
          app: httpbin
      jwtRules:
      - issuer: "testing@secure.istio.io"
        jwksUri: "https://raw.githubusercontent.com/istio/istio/master/security/tools/jwt/samples/jwks.json"
        spaceDelimitedClaims: ["roles"]
    EOF
  3. The following command updates the require-jwt authorization policy to require the roles claim to include the value admin:

    $ kubectl apply -f - <<EOF
    apiVersion: security.istio.io/v1
    kind: AuthorizationPolicy
    metadata:
      name: require-jwt
      namespace: foo
    spec:
      selector:
        matchLabels:
          app: httpbin
      action: ALLOW
      rules:
      - from:
        - source:
           requestPrincipals: ["testing@secure.istio.io/testing@secure.istio.io"]
        when:
        - key: request.auth.claims[roles]
          values: ["admin"]
    EOF
  4. Get a JWT whose roles claim is set to the space-delimited string "admin editor":

    $ TOKEN_ROLES=$(python3 ./gen-jwt.py ./key.pem --claims "roles:admin editor")
  5. Verify that a request with that JWT is allowed, because admin is present in the space-delimited roles claim:

    $ kubectl exec "$(kubectl get pod -l app=curl -n foo -o jsonpath={.items..metadata.name})" -c curl -n foo -- curl "http://httpbin.foo:8000/headers" -sS -o /dev/null -H "Authorization: Bearer $TOKEN_ROLES" -w "%{http_code}\n"
    200
  6. Get a JWT whose roles claim does not include admin:

    $ TOKEN_NO_ADMIN=$(python3 ./gen-jwt.py ./key.pem --claims "roles:editor")
  7. Verify that the request is denied:

    $ kubectl exec "$(kubectl get pod -l app=curl -n foo -o jsonpath={.items..metadata.name})" -c curl -n foo -- curl "http://httpbin.foo:8000/headers" -sS -o /dev/null -H "Authorization: Bearer $TOKEN_NO_ADMIN" -w "%{http_code}\n"
    403

Clean up

Remove the namespace foo:

$ kubectl delete namespace foo

If you downloaded gen-jwt.py and key.pem for the space-delimited claims section, remove them:

$ rm -f ./gen-jwt.py ./key.pem
Was this information useful?
Do you have any suggestions for improvement?

Thanks for your feedback!