Validating JWT Tokens

Cloudflare Access generated JWT tokens are available in response header as Cf-Access-Jwt-Assertion and cookie as CF_Authorization. If you are not using Argo Tunnel, the JWT token should be validated by your application to verify the authenticity of these tokens and secure your origin.

Cloudflare uses RS256 to sign the JWT token using public private key pair. RS256 follows an asymmetric algorithm which means a private key is used to sign the JWT tokens and a separate public key is used to verify the signature. Upon configuring Access, the public certificates should be available under https://<Your Authentication Domain>/cdn-cgi/access/certs. If your application url is example.com then your certificate URL would be https://example.cloudflareaccess.com/cdn-cgi/access/certs

Manual Verification

Prerequisite

Install lokey Install jq

  1. Run this command in your terminal after installing the prerequsites.

    $ curl -s https://<your auth domain>/cdn-cgi/access/certs | jq .keys[0] | lokey to pem
    
    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA01SvMv4TgFIECQgzHaRL
    DGVaKhRQHjgdiSOpbqhHQMdcNtBIM0HAQbrs7YS6sQCCdZC5wCvlq3xgqdU5J6k
    YI5OCSsIWXKkobAl6PbXHdN0bJximeiHGa3O0hMREP6RKBoI6ayNmZ3WlVGWY
    6ie47KGqN69l7fPKyZvszb4GdpxE0r8gllZZwIuPjzlghXRlrkaP48ucQwo+tq
    PSSdDdW57TCFmy+G547W5iWZWJIeNkfVu9t6FktvCwSZ1ekum3X7IQcd0O0DWSR
    Aj9tzNDPkzOeSFxmQkKpWs8Qw7ZBIfLOsO3DCH6VPNhS2cqhw1AAMunh8alDKQU
    aQIDAQAB
    -----END PUBLIC KEY-----
    If you get an error while running lokey, try again after installing python six library. pip install six==1.10.0

  2. Go to jwt.io

  3. Switch the algorithm to RS256

  4. Paste your JWT token into the block on the left

  5. Enter the public key above in the public key box on the right

  6. Make sure the signature says verified

Programmatic Verification

Prerequisite

Application AUD: This can be obtained from Cloudflare dashboard by navigating to the Access tab and click on the settings button on your application’s access policy as shown below. aud-tag Certificate URL: https://<Your Authentication Domain>/cdn-cgi/access/certs

JWT Issuer: https://<Your Authentication Domain>

Golang Example

package main
 
 
import (
    "context"
    "fmt"
    "net/http"
 
    "github.com/coreos/go-oidc"
)
 
var (
    ctx        = context.TODO()
    authDomain = "https://test.cloudflareaccess.com"
    certsURL   = fmt.Sprintf("%s/cdn-cgi/access/certs", authDomain)
 
    // policyAUD is your application AUD value
    policyAUD = "4714c1358e65fe4b408ad6d432a5f878f08194bdb4752441fd56faefa9b2b6f2"
 
    config = &oidc.Config{
        ClientID: policyAUD,
    }
    keySet   = oidc.NewRemoteKeySet(ctx, certsURL)
    verifier = oidc.NewVerifier(authDomain, keySet, config)
)
 
// VerifyToken is a middleware to verify a CF Access token
func VerifyToken(next http.Handler) http.Handler {
    fn := func(w http.ResponseWriter, r *http.Request) {
        headers := r.Header

        // Make sure that the incoming request has our token header
        //  Could also look in the cookies for CF_AUTHORIZATION
        accessJWT := headers.Get("Cf-Access-Jwt-Assertion")
        if accessJWT == "" {
            w.WriteHeader(http.StatusUnauthorized)
            w.Write([]byte("No token on the request"))
            return
        }
 
        // Verify the access token
        ctx := r.Context()
        _, err := verifier.Verify(ctx, accessJWT)
        if err != nil {
            w.WriteHeader(http.StatusUnauthorized)
            w.Write([]byte(fmt.Sprintf("Invalid token: %s", err.Error())))
            return
        }
        next.ServeHTTP(w, r)
    }
    return http.HandlerFunc(fn)
}
 
func MainHandler() http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("welcome"))
    })
}
 
func main() {
    http.Handle("/", VerifyToken(MainHandler()))
    http.ListenAndServe(":3000", nil)
}

Python Example

You need to pip install the following:

  • flask
  • requests
  • PyJWT

from flask import Flask, request
import requests
import jwt
import json
import os
app = Flask(__name__)
 
 
# Your policies audience tag
POLICY_AUD = os.getenv("POLICY_AUD")
 
# Your CF Access Authentication domain
AUTH_DOMAIN = os.getenv("AUTH_DOMAIN")
CERTS_URL = "{}/cdn-cgi/access/certs".format(AUTH_DOMAIN)
 
def _get_public_keys():
    """
    Returns:
        List of RSA public keys usable by PyJWT.
    """
    r = requests.get(CERTS_URL)
    public_keys = []
    jwk_set = r.json()
    for key_dict in jwk_set['keys']:
        public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(key_dict))
        public_keys.append(public_key)
    return public_keys
 
def verify_token(f):
    """
    Decorator that wraps a Flask API call to verify the CF Access JWT
    """
    def wrapper():
        token = ''
        if 'CF_Authorization' in request.cookies:
            token = request.cookies['CF_Authorization']
        else:
            return "missing required cf authorization token", 403
        keys = _get_public_keys()
 
        # Loop through the keys since we can't pass the key set to the decoder
        valid_token = False
        for key in keys:
            try:
                # decode returns the claims which has the email if you need it
                jwt.decode(token, key=key, audience=POLICY_AUD)
                valid_token = True
                break
            except:
                pass
        if not valid_token:
            return "invalid token", 403
 
        return f()
    return wrapper
 
 
@app.route('/')
@verify_token
def hello_world():
    return 'Hello, World!'
 
 
if __name__ == '__main__':
    app.run()