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

Replace the following:

  • JWT: The token you received from Cloudflare Access.

  • ARGS: a URL-encoded parameters:

    • tenant=TENANT: your tenant identifier.
    • app=APP: the Groove.id app id for the Cloudflare Access integration.
    • date=DATE: the current date, formatted according to RFC3339.
    • jwt=JWT: the SHA256 hash of the JWT, base64 encoded.
  • SIG: the base64-encoded HMAC-SHA256 signature of ARGS using the Cloudflare Access integration’s signing key.

Generating a signed header

Here is an example of 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 as follows (with linebreaks added for readability):

Authentication: CFJWT eyJhbGciOiJSUzI1NiIsImtpZCI6IjU1MDRjNjQ4MGZkNjFkYTliNzg3YzI0ODVmMz
  g5NDVjZDlmZjAxODI1MzkxYzdiNWY1NmVmZDkxNjliYjZjNDQiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiODVjY
  2QyNjljMmMwZjBmZGQwYzlmYjA0MTNiMjkwOTg5NGRmNDk1NTEwMTY1ODY5OWNkM2MxMTU3YzQ5MWQ2NCJdLCJ
  lbWFpbCI6InJvc3NAZ3Jvb3ZlaWQuY29tIiwiZXhwIjoxNTQ0MTE3NzAzLCJpYXQiOjE1NDQwMzEzMDMsImlzc
  yI6Imh0dHBzOi8vb3NjYXJzaWVycmEuY2xvdWRmbGFyZWFjY2Vzcy5jb20iLCJub25jZSI6ImU5YzI0YzkzYzk
  0YjgzOGMwMTdhYTE5NmUwZDY4MjhjN2QzODM1NGYyMWQ3MzkzYTcxYjNhMTQ4NDY3M2MxMTEiLCJzdWIiOiJlN
  jE3MjEzOS0yNmJjLTQ1ZWUtOWMyNC0yN2I1ZjEwMzIwOTEifQ.JMtHH7xIhkhjzEPsAg7mbJsomQc3a5MQmnqS
  m73MnUiXf_LNPsSH704Qpvbhmsmb9FAhvFPUQSJ7te0gsFRZWUfWxfzAsW_4VLzRLN3H2ES0XOhZispRwQd9O1
  vxxwwxs5O7RGklQSNntqCkqFvfZfRWBM15CPB0UzQfY-i3cidSCPDFe_tpwrcHAf4ifd-eZj8IVE1Rhen0WbBn
  WG-ciNLU3KhMXnefykQVuFF7o8r2qc-LQQYTzX7KGxTqAGDdmdGQqrC4feF8YdCP-WVGcgPRQVbyjOS__2E7Vw
  oJojHd_77M08RFTZL9x86q24dxwqBxlmMFRJwV6FXKO62Chg 
  date=2018-12-05T17%3A40%3A08Z&app=rg1cKOzzzaB0wP&jwt=8FVVPYF9aKig4SLhpjVRQS6jRJt184ucj
  VnDC4GeuCA%3D&tenant=rg1cKOzzzaB0wP 2baRz/AZR8ahwQRQDHHSKd1fZX/6eXYTfkyRvYKHC28=
Last modified May 12, 2020: refactor docs (d7a7a5c1d)