JWTの検証
Cloudflareがオリジンにリクエストを送信すると、そのリクエストにはアプリケーショントークンがCf-Access-Jwt-AssertionリクエストヘッダーおよびCF_Authorizationクッキーとして含まれます。
Cloudflareは、あなたのアカウントに固有のキー ペアでトークンに署名します。リクエストが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----- " } ]}トークンを手動で検証するには:
-
CF_AuthorizationクッキーまたはCf-Access-Jwt-AssertionリクエストヘッダーからJWTをコピーします。 -
jwt.io ↗に移動します。
-
RS256アルゴリズムを選択します。
-
JWTをEncodedボックスに貼り付けます。
-
Payloadボックスで、
issフィールドがあなたのチームドメイン(https://<your-team-name>.cloudflareaccess.com)を指していることを確認します。jwt.ioは、トークン検証のために公開鍵を取得するためにiss値を使用します。 -
ページにSignature Verifiedと表示されていることを確認します。
これで、このリクエストがAccessによって送信されたものであると信頼できます。
オリジンサーバーで自動化されたスクリプトを実行して、受信リクエストを検証できます。提供されたサンプルコードは、リクエストからアプリケーショントークンを取得し、その署名を公開鍵に対してチェックします。サンプルコードに自分のチームドメインとアプリケーションオーディエンスタグ(AUD)を挿入する必要があります。
Cloudflare Accessは、各アプリケーションに一意のAUDタグを割り当てます。トークンペイロード内のaudクレームは、JWTが有効なアプリケーションを指定します。
AUDタグを取得するには:
- Zero Trust ↗に移動し、Access > Applicationsに進みます。
- アプリケーションのConfigureを選択します。
- Overviewタブで、Application Audience (AUD) Tagをコピーします。
これで、AUDタグをトークン検証スクリプトに貼り付けることができます。AUDタグは、Accessアプリケーションを削除または再作成しない限り、変更されることはありません。
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)}pipで以下をインストールします:
- flask
- requests
- PyJWT
- cryptography
from flask import Flask, requestimport requestsimport jwtimport jsonimport osapp = 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_tokendef hello_world(): return 'こんにちは、世界!'
if __name__ == '__main__': app.run()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)