Authenticating with CFJWT

Cloudflare JWT

When using the Cloudflare Access integration it may be handy to make requests to the Groove.id API in the context of the remote user. Cloudflare Access sets a header (Cf-Access-Jwt-Assertion) and a cookie (CF_Authorization) containing a JSON Web Token which proves the remote identity of the user. You can also use this JWT to authenticate to the Groove.id API.

To do this you will need a signing key from the Cloudflare access app. The signing key is available in the administrative interface.

To authenticate using a CFJWT, add the following header to your request:

Authorization: CFJWT $jwt $args $sig

where:

  • \$jwt is the token you received from Cloudflare Access.

  • \$args is the URL encoded set of parameters:

    • tenant - your tenant identifier.
    • app - the Groove.id app id for the Cloudflare Access integration.
    • date - the current date, formatted according to RFC3339.
    • jwt - the SHA256 hash of the JWT, base64 encoded.
  • \$sig is the base64-encoded HMAC-SHA256 signature of args using the Cloudflare Access integration’s signing key.

Generating a signed header

Here’s some example code for computing the authentication header in Python:

import urllib, time, hashlib, hmac

def auth_header(jwt, signing_key, app_id, tenant_id):
  args = urllib.urlencode({
    "tenant": tenant_id,
    "app": app_id,
    "date": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
    "jwt": hashlib.sha256(jwt).digest().encode("base64").strip(),
  })
  sig = hmac.new(
    signing_key, # key
    args, # msg
    hashlib.sha256 # digest
  ).digest().encode("base64").strip()
  return " ".join(["CFJWT", jwt, args, sig])

Here’s an implementation of that same function in Go:

func authHeader(jwt, signingKey, appID, tenantID string) string {
    jwtSum := sha256.Sum256([]byte(jwt))
    args := url.Values{
        "tenant": {tenantID},
        "app":    {appID},
        "date":   {time.Now().Format(time.RFC3339)},
        "jwt":    {base64.StdEncoding.EncodeToString(jwtSum[:])},
    }.Encode()

    h := hmac.New(sha256.New, []byte(signingKey))
    h.Write([]byte(args))
    sig := base64.StdEncoding.EncodeToString(h.Sum(nil))

    return "CFJWT " + jwt + " " + args + " " + sig
}

Example

The args parameter might look something like this, with line breaks added for readability.

date=2018-12-05T17%3A40%3A08Z
&app=rg1cKOzzzaB0wP
&jwt=8FVVPYF9aKig4SLhpjVRQS6jRJt184ucjVnDC4GeuCA%3D
&tenant=rg1cKOzzzaB0wP

If the signing key is hgc354HF1n1ZmjhWZ6Ter8LS6x7V then the HMAC-SHA256 of args is:

2baRz/AZR8ahwQRQDHHSKd1fZX/6eXYTfkyRvYKHC28=

And the final header is:

Authentication: CFJWT eyJhbGciOiJSUzI1NiIsImtpZCI6IjU1MDRjNjQ4MGZkNjFkYTliNzg3YzI0ODVmMzg5NDVjZDlmZjAxODI1MzkxYzdiNWY1NmVmZDkxNjliYjZjNDQiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiODVjY2QyNjljMmMwZjBmZGQwYzlmYjA0MTNiMjkwOTg5NGRmNDk1NTEwMTY1ODY5OWNkM2MxMTU3YzQ5MWQ2NCJdLCJlbWFpbCI6InJvc3NAZ3Jvb3ZlaWQuY29tIiwiZXhwIjoxNTQ0MTE3NzAzLCJpYXQiOjE1NDQwMzEzMDMsImlzcyI6Imh0dHBzOi8vb3NjYXJzaWVycmEuY2xvdWRmbGFyZWFjY2Vzcy5jb20iLCJub25jZSI6ImU5YzI0YzkzYzk0YjgzOGMwMTdhYTE5NmUwZDY4MjhjN2QzODM1NGYyMWQ3MzkzYTcxYjNhMTQ4NDY3M2MxMTEiLCJzdWIiOiJlNjE3MjEzOS0yNmJjLTQ1ZWUtOWMyNC0yN2I1ZjEwMzIwOTEifQ.JMtHH7xIhkhjzEPsAg7mbJsomQc3a5MQmnqSm73MnUiXf_LNPsSH704Qpvbhmsmb9FAhvFPUQSJ7te0gsFRZWUfWxfzAsW_4VLzRLN3H2ES0XOhZispRwQd9O1vxxwwxs5O7RGklQSNntqCkqFvfZfRWBM15CPB0UzQfY-i3cidSCPDFe_tpwrcHAf4ifd-eZj8IVE1Rhen0WbBnWG-ciNLU3KhMXnefykQVuFF7o8r2qc-LQQYTzX7KGxTqAGDdmdGQqrC4feF8YdCP-WVGcgPRQVbyjOS__2E7VwoJojHd_77M08RFTZL9x86q24dxwqBxlmMFRJwV6FXKO62Chg date=2018-12-05T17%3A40%3A08Z&app=rg1cKOzzzaB0wP&jwt=8FVVPYF9aKig4SLhpjVRQS6jRJt184ucjVnDC4GeuCA%3D&tenant=rg1cKOzzzaB0wP 2baRz/AZR8ahwQRQDHHSKd1fZX/6eXYTfkyRvYKHC28=