Cloudflare がオリジンへリクエストを送るとき、リクエストには アプリケーショントークン が Cf-Access-Jwt-Assertion リクエストヘッダーとして含まれます。ブラウザー経由のリクエストでは、トークンは CF_Authorization Cookie としても渡されます。
Cloudflare は、アカウント固有の鍵ペアでトークンに署名します。リクエストが Access からのものであり、悪意のある第三者からではないことを確認するため、公開鍵でトークンを検証します。Cookie は必ず渡されるとは限らないため、CF_Authorization Cookie ではなく Cf-Access-Jwt-Assertion ヘッダーを検証することを推奨します。
署名鍵ペアの公開鍵は https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/certs にあります。<your-team-name> は Cloudflare One の チーム名 です。
デフォルトでは、Access は 6 週間ごとに署名鍵をローテーションします。そのため、ローテーションに合わせて鍵をプログラムで、または手動で更新する必要があります。ローテーション後も、更新のための猶予として、以前の鍵は 7 日間有効です。
API で鍵を手動ローテーションすることもできます。テストやセキュリティ目的で使えます。
次の例のとおり、https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/certs には公開鍵が 2 つ含まれます。新しいトークンすべての署名に使う現在の鍵と、ローテーションで外れた以前の鍵です。
keys: JWK 形式の両方の鍵public_cert: PEM 形式の現在の鍵public_certs: PEM 形式の両方の鍵
{
"keys": [
{
"kid": "1a1c3986a44ce6390be42ec772b031df8f433fdc71716db821dc0c39af3bce49",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"e": "AQAB",
"n": "5PKw-...-AG7MyQ"
},
{
"kid": "6c3bffef71bb0a90c9cbef3b7c0d4a1c7b4b8b76b80292a623afd9dac45d1c65",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"e": "AQAB",
"n": "pwVn...AA6Hw"
}
],
"public_cert": {
"kid": "6c3bffef71bb0a90c9cbef3b7c0d4a1c7b4b8b76b80292a623afd9dac45d1c65",
"cert": "-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE----- "
},
"public_certs": [
{
"kid": "1a1c3986a44ce6390be42ec772b031df8f433fdc71716db821dc0c39af3bce49",
"cert": "-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE----- "
},
{
"kid": "6c3bffef71bb0a90c9cbef3b7c0d4a1c7b4b8b76b80292a623afd9dac45d1c65",
"cert": "-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE----- "
}
]
}トークンを手動で検証する手順は次のとおりです。
-
Cf-Access-Jwt-Assertionリクエストヘッダーから JWT をコピーします。 -
jwt.io ↗ を開きます。
-
RS256 アルゴリズムを選択します。
-
JWT を Encoded ボックスに貼り付けます。
-
Payload ボックスで、
issフィールドがチームドメイン(https://<your-team-name>.cloudflareaccess.com)を指していることを確認します。jwt.ioはトークン検証用の公開鍵を取得するためにissの値を使います。 -
ページに Signature Verified と表示されることを確認します。
これで、このリクエストが Access から送られたものとして扱えます。
オリジンサーバーで自動スクリプトを実行し、受信リクエストを検証できます。掲載しているサンプルコードは、リクエストからアプリケーショントークンを取得し、公開鍵と照合して署名を確認します。サンプルコードには、自分のチームドメインと Application Audience(AUD)タグを入れます。
Cloudflare Access は、アプリケーションごとに一意の AUD タグを割り当てます。トークンペイロードの aud クレームは、その JWT がどのアプリケーション向けに有効かを示します。
AUD タグを取得する手順は次のとおりです。
- Cloudflare ダッシュボード ↗ で Zero Trust > Access controls > Applications を開きます。
- 対象アプリケーションの Configure を選択します。
- Additional settings から Application Audience (AUD) Tag をコピーします。
AUD タグをトークン検証スクリプトに貼り付けます。Access アプリケーションを削除または再作成しない限り、AUD タグは変わりません。
Cloudflare Access を Worker の前に置いている場合でも、Worker は受信リクエストの Cf-Access-Jwt-Assertion ヘッダーに Cloudflare Access が追加する JWT を検証する必要があります。
次のコードは、jose NPM パッケージ ↗ を使って JWT を検証します。
import { jwtVerify, createRemoteJWKSet } from "jose";
export default {
async fetch(request, env, ctx) {
// Verify the POLICY_AUD environment variable is set
if (!env.POLICY_AUD) {
return new Response("Missing required audience", {
status: 403,
headers: { "Content-Type": "text/plain" },
});
}
// Get the JWT from the request headers
const token = request.headers.get("cf-access-jwt-assertion");
// Check if token exists
if (!token) {
return new Response("Missing required CF Access JWT", {
status: 403,
headers: { "Content-Type": "text/plain" },
});
}
try {
// Create JWKS from your team domain
const JWKS = createRemoteJWKSet(
new URL(`${env.TEAM_DOMAIN}/cdn-cgi/access/certs`),
);
// Verify the JWT
const { payload } = await jwtVerify(token, JWKS, {
issuer: env.TEAM_DOMAIN,
audience: env.POLICY_AUD,
});
// Token is valid, proceed with your application logic
return new Response(`Hello ${payload.email || "authenticated user"}!`, {
headers: { "Content-Type": "text/plain" },
});
} catch (error) {
// Token verification failed
const message = error instanceof Error ? error.message : "Unknown error";
return new Response(`Invalid token: ${message}`, {
status: 403,
headers: { "Content-Type": "text/plain" },
});
}
},
};import { jwtVerify, createRemoteJWKSet } from "jose";
interface Env {
POLICY_AUD: string;
TEAM_DOMAIN: string;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// Verify the POLICY_AUD environment variable is set
if (!env.POLICY_AUD) {
return new Response("Missing required audience", {
status: 403,
headers: { "Content-Type": "text/plain" },
});
}
// Get the JWT from the request headers
const token = request.headers.get("cf-access-jwt-assertion");
// Check if token exists
if (!token) {
return new Response("Missing required CF Access JWT", {
status: 403,
headers: { "Content-Type": "text/plain" },
});
}
try {
// Create JWKS from your team domain
const JWKS = createRemoteJWKSet(
new URL(`${env.TEAM_DOMAIN}/cdn-cgi/access/certs`)
);
// Verify the JWT
const { payload } = await jwtVerify(token, JWKS, {
issuer: env.TEAM_DOMAIN,
audience: env.POLICY_AUD,
});
// Token is valid, proceed with your application logic
return new Response(
`Hello ${payload.email || "authenticated user"}!`,
{
headers: { "Content-Type": "text/plain" },
}
);
} catch (error) {
// Token verification failed
const message = error instanceof Error ? error.message : "Unknown error";
return new Response(`Invalid token: ${message}`, {
status: 403,
headers: { "Content-Type": "text/plain" },
});
}
},
};Worker に次の 環境変数 を追加します。
POLICY_AUD: アプリケーションの AUD タグTEAM_DOMAIN:https://<your-team-name>.cloudflareaccess.com。<your-team-name>は実際の チーム名 に置き換えます。
これらの変数は、Worker の Wrangler 設定ファイル に追加するか、Cloudflare ダッシュボードの Workers & Pages > your-worker > Settings > Environment Variables で設定できます。
package main
import (
"context"
"fmt"
"net/http"
"github.com/coreos/go-oidc/v3/oidc"
)
var (
ctx = context.TODO()
teamDomain = "https://test.cloudflareaccess.com"
certsURL = fmt.Sprintf("%s/cdn-cgi/access/certs", teamDomain)
// The Application Audience (AUD) tag for your application
policyAUD = "4714c1358e65fe4b408ad6d432a5f878f08194bdb4752441fd56faefa9b2b6f2"
config = &oidc.Config{
ClientID: policyAUD,
}
keySet = oidc.NewRemoteKeySet(ctx, certsURL)
verifier = oidc.NewVerifier(teamDomain, 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)
}pip で次をインストールします。
- flask
- requests
- PyJWT
- cryptography
from flask import Flask, request
import requests
import jwt
import json
import os
app = Flask(__name__)
# The Application Audience (AUD) tag for your application
POLICY_AUD = os.getenv("POLICY_AUD")
# Your CF Access team domain
TEAM_DOMAIN = os.getenv("TEAM_DOMAIN")
CERTS_URL = "{}/cdn-cgi/access/certs".format(TEAM_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():
# Check for the POLICY_AUD environment variable
if not POLICY_AUD:
return "missing required audience", 403
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 that has the email when needed
jwt.decode(token, key=key, audience=POLICY_AUD, algorithms=['RS256'])
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()const express = require("express");
const jose = require("jose");
// The Application Audience (AUD) tag for your application
const AUD = process.env.POLICY_AUD;
// Your CF Access team domain
const TEAM_DOMAIN = process.env.TEAM_DOMAIN;
const CERTS_URL = `${TEAM_DOMAIN}/cdn-cgi/access/certs`;
const JWKS = jose.createRemoteJWKSet(new URL(CERTS_URL));
// verifyToken is a middleware to verify a CF authorization token
const verifyToken = async (req, res, next) => {
// Check for the AUD environment variable
if (!AUD) {
return res.status(403).send({
status: false,
message: "missing required audience",
});
}
const token = req.headers["cf-access-jwt-assertion"];
// Make sure that the incoming request has our token header
if (!token) {
return res.status(403).send({
status: false,
message: "missing required cf authorization token",
});
}
try {
const result = await jose.jwtVerify(token, JWKS, {
issuer: TEAM_DOMAIN,
audience: AUD,
});
req.user = result.payload;
next();
} catch (err) {
return res.status(403).send({
status: false,
message: "invalid token",
});
}
};
const app = express();
app.use(verifyToken);
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(3333);