コンテンツにスキップ

JWTの検証

Cloudflareがオリジンにリクエストを送信すると、そのリクエストにはアプリケーショントークンCf-Access-Jwt-AssertionリクエストヘッダーおよびCF_Authorizationクッキーとして含まれます。

Cloudflareは、あなたのアカウントに固有のキー ペアでトークンに署名します。リクエストがAccessから送信されたものであり、悪意のある第三者からのものでないことを確認するために、公開鍵でトークンを検証する必要があります。

Access署名キー

署名キー ペアの公開鍵は、https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/certsにあります。ここで、<your-team-name>はあなたのZero Trust チーム名です。

デフォルトでは、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----- "
}
]
}

JWTを手動で検証する

トークンを手動で検証するには:

  1. CF_AuthorizationクッキーまたはCf-Access-Jwt-AssertionリクエストヘッダーからJWTをコピーします。

  2. jwt.ioに移動します。

  3. RS256アルゴリズムを選択します。

  4. JWTをEncodedボックスに貼り付けます。

  5. Payloadボックスで、issフィールドがあなたのチームドメイン(https://<your-team-name>.cloudflareaccess.com)を指していることを確認します。jwt.ioは、トークン検証のために公開鍵を取得するためにiss値を使用します。

  6. ページにSignature Verifiedと表示されていることを確認します。

これで、このリクエストがAccessによって送信されたものであると信頼できます。

プログラムによる検証

オリジンサーバーで自動化されたスクリプトを実行して、受信リクエストを検証できます。提供されたサンプルコードは、リクエストからアプリケーショントークンを取得し、その署名を公開鍵に対してチェックします。サンプルコードに自分のチームドメインとアプリケーションオーディエンスタグ(AUD)を挿入する必要があります。

AUDタグを取得する

Cloudflare Accessは、各アプリケーションに一意のAUDタグを割り当てます。トークンペイロード内のaudクレームは、JWTが有効なアプリケーションを指定します。

AUDタグを取得するには:

  1. Zero Trustに移動し、Access > Applicationsに進みます。
  2. アプリケーションのConfigureを選択します。
  3. Overviewタブで、Application Audience (AUD) Tagをコピーします。

これで、AUDタグをトークン検証スクリプトに貼り付けることができます。AUDタグは、Accessアプリケーションを削除または再作成しない限り、変更されることはありません。

Golangの例

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)
// アプリケーションのApplication Audience (AUD)タグ
policyAUD = "4714c1358e65fe4b408ad6d432a5f878f08194bdb4752441fd56faefa9b2b6f2"
config = &oidc.Config{
ClientID: policyAUD,
}
keySet = oidc.NewRemoteKeySet(ctx, certsURL)
verifier = oidc.NewVerifier(teamDomain, keySet, config)
)
// VerifyTokenはCF Accessトークンを検証するミドルウェアです
func VerifyToken(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
headers := r.Header
// 受信リクエストにトークンヘッダーがあることを確認します
// CF_AUTHORIZATIONのクッキーも確認できます
accessJWT := headers.Get("Cf-Access-Jwt-Assertion")
if accessJWT == "" {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("リクエストにトークンがありません"))
return
}
// アクセストークンを検証します
ctx := r.Context()
_, err := verifier.Verify(ctx, accessJWT)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(fmt.Sprintf("無効なトークン: %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("ようこそ"))
})
}
func main() {
http.Handle("/", VerifyToken(MainHandler()))
http.ListenAndServe(":3000", nil)
}

Pythonの例

pipで以下をインストールします:

  • flask
  • requests
  • PyJWT
  • cryptography
from flask import Flask, request
import requests
import jwt
import json
import os
app = Flask(__name__)
# アプリケーションのApplication Audience (AUD)タグ
POLICY_AUD = os.getenv("POLICY_AUD")
# あなたのCF Accessチームドメイン
TEAM_DOMAIN = os.getenv("TEAM_DOMAIN")
CERTS_URL = "{}/cdn-cgi/access/certs".format(TEAM_DOMAIN)
def _get_public_keys():
"""
戻り値:
PyJWTで使用可能なRSA公開鍵のリスト。
"""
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):
"""
CF Access JWTを検証するためのFlask API呼び出しをラップするデコレーター
"""
def wrapper():
token = ''
if 'CF_Authorization' in request.cookies:
token = request.cookies['CF_Authorization']
else:
return "必要なcf認証トークンがありません", 403
keys = _get_public_keys()
# デコーダーにキーセットを渡せないため、キーをループします
valid_token = False
for key in keys:
try:
# decodeは、必要に応じてメールアドレスを含むクレームを返します
jwt.decode(token, key=key, audience=POLICY_AUD, algorithms=['RS256'])
valid_token = True
break
except:
pass
if not valid_token:
return "無効なトークン", 403
return f()
return wrapper
@app.route('/')
@verify_token
def hello_world():
return 'こんにちは、世界!'
if __name__ == '__main__':
app.run()

JavaScriptの例

const express = require('express');
const cookieParser = require('cookie-parser');
const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');
// アプリケーションのApplication Audience (AUD)タグ
const AUD = process.env.POLICY_AUD;
// あなたのCF Accessチームドメイン
const TEAM_DOMAIN = process.env.TEAM_DOMAIN;
const CERTS_URL = `${TEAM_DOMAIN}/cdn-cgi/access/certs`;
const client = jwksClient({
jwksUri: CERTS_URL
});
const getKey = (header, callback) => {
client.getSigningKey(header.kid, function(err, key) {
callback(err, key?.getPublicKey());
});
}
// verifyTokenはCF認証トークンを検証するミドルウェアです
const verifyToken = (req, res, next) => {
const token = req.cookies['CF_Authorization'];
// 受信リクエストにトークンヘッダーがあることを確認します
if (!token) {
return res.status(403).send({ status: false, message: '必要なcf認証トークンがありません' });
}
jwt.verify(token, getKey, { audience: AUD }, (err, decoded) => {
if (err) {
return res.status(403).send({ status: false, message: '無効なトークン' });
}
req.user = decoded;
next();
});
}
const app = express();
app.use(cookieParser());
app.use(verifyToken);
app.get('/', (req, res) => {
res.send('こんにちは、世界!');
});
app.listen(3333)