JWT ライブラリ関数の使用

Flex Gateway ポリシー開発キット (PDK) の JWT ライブラリを使用したポリシープロジェクトの例は、 「JWT Validation Policy Example (JWT 検証ポリシーの例)」​を参照してください。

JSON Web Token (JWT) は、2 者間で転送されるクレームを表す URL セキュアな方法です。JWT トークンには、JSON Web Signature (JWS) のペイロードとして、またはプレーンテキストの JSON Web Encryption (JWE) 構造体として JSON オブジェクトでエンコードされたクレームが含まれており、これによりクレームはデジタル署名され、メッセージ認証コード (MAC) で保護されます。トークンは署名されるので、その情報と情報源を信頼できます。

PDK の JWT ライブラリには、JWT シグネチャーを検証し、すべての受信要求のクレームを抽出して検証するためのツールセットが用意されています。

JWT トークンの抽出

PDK は、認証ヘッダーにある ​Bearer <token>​ という形式の JWT トークンを抽出するための ​bearer​ 関数を持つ ​TokenProvider​ 構造体を提供しています。次の例は、​bearer​ 関数の使い方を示しています。

// [...]
use pdk::jwt::*;
// [...]

async fn filter(
    state: RequestState,
) -> Flow<()> {
    let headers_state = state.into_headers_state().await;

    // Extract token
    let token = TokenProvider::bearer(headers_state.handler())?;
// [...]

異なるソースや異なる形式の JWT トークンを使用する場合は、要求の他の部分を取得する Rust ロジックを実装できます。DataWeave パラメーターを使用して抽出を構成し、JWT ソースを動的に変更できるポリシーを作成します。

JWT トークンの検証

JWT ライブラリは、受信要求の認証トークンの署名検証を実行するためのヘルパーを提供しています。ポリシーで使用されるアルゴリズムに応じて、対応する設定で ​SignatureValidator​ を初期化します。

pub fn new(algorithm: SigningAlgorithm, key_length: SigningKeyLength, key: String) -> Result<SignatureValidator, JWTError>

PDK は以下の署名アルゴリズムとキー長をサポートしています。

署名アルゴリズム

署名キーの長さ

RSA

256、384、512

HMAC

256、384、512

ES

256、384

SigningAlgorithm​ 列挙型は、利用可能な署名アルゴリズムを定義します。

pub enum SigningAlgorithm {
    Rsa,
    Hmac,
    Es,
}

SigningKeyLength​ 列挙型は、利用可能な署名キーの長さを定義します。

pub enum SigningKeyLength {
    Len256,
    Len384,
    Len512,
}

SignatureValidator​ インスタンスは ​SignatureValidation​ trait を実装します。この ​SignatureValidation​ trait は、トークンを​文字列型​のパラメーターとして受け取り、解析された ​JWTClaims​ を含む​結果​を返すか、エラーの場合は ​JWTError​ を返す ​validate​ 関数を提供します。

pub trait SignatureValidation {
    fn validate(&self, token: String) -> Result<JWTClaims, JWTError>;
}

次のコード例は、​SignatureValidator​ を初期化し、256 バイトの HMAC アルゴリズムで署名された受信トークンを検証する方法を示しています。

use anyhow::Result;
use pdk::hl::*;
use pdk::jwt::*;
// [...]

async fn filter(
    state: RequestState,
    signature_validator: &SignatureValidator,
) -> Flow<()> {
    let headers_state = state.into_headers_state().await;

    // Extract token
    let token = TokenProvider::bearer(headers_state.handler())?;

    if token.is_err() {
        return Flow::Break(Response::new(401).with_body("Bearer not found"));
    }

    // Validating signature
    let claims = signature_validator.validate(token.unwrap());

    if claims.is_err() {
        return Flow::Break(Response::new(401).with_body("Invalid token"));
    }
    // [...]
}

#[entrypoint]
async fn configure(launcher: Launcher, Configuration(configuration): Configuration) -> Result<()> {
    let config: Config = serde_json::from_slice(&configuration)?;

    let signature_validator = SignatureValidator::new(
        model::SigningAlgorithm::Hmac,
        model::SigningKeyLength::Len256,
        config.secret.clone(),
    )?;

    launcher
        .launch(on_request(|request| {
            filter(request, &signature_validator)
        }))
        .await?;
    Ok(())
}

SignatureValidator​ は初期化時にポリシーの設定が正しいことを確認するため、​#[entrypoint]​ 関数で ​SignatureValidator​ を初期化すると、API が要求を受信したときではなく、ポリシーが適用されたときに早期にエラーがスローされます。

初期化後、ポリシーはラップされた関数の ​on_request​ に ​SignatureValidator​ を渡し、受信要求に対して ​validate​ メソッドを実行します。

クレームへのアクセスとヘッダーへの伝搬

署名検証​の実行後、結果には JWT トークンから解析された JWT クレームが含まれます。

署名が無効な場合、または署名を検証しないことを選択した場合、PDK は ​JWTClaimsParser​ によってクレームを解析します。この構造体は、​Result<JWTClaims, JWTError>​ を返す ​parse​ メソッドを提供します。

// Being "token" a String that contains a JWT token
let parsed_claims = JWTClaimsParser::parse(token);

if claims.is_err() {
    return Flow::Break(Response::new(401).with_body("Invalid token"));
}

署名検証または解析メソッドを実行したら、​JWTClaims​ 構造体が提供する以下のメソッドを使用して JWT クレームにアクセスします。

pub fn audience(&self) -> Option<Result<Vec<String>, JWTError>>

pub fn not_before(&self) -> Option<DateTime<Utc>>

pub fn expiration(&self) -> Option<DateTime<Utc>>

pub fn issued_at(&self) -> Option<DateTime<Utc>>

pub fn issuer(&self) -> Option<String>

pub fn jti(&self) -> Option<String>

pub fn nonce(&self) -> Option<String>

pub fn subject(&self) -> Option<String>

pub fn has_claim(&self, name: &str) -> bool

pub fn get_claim<T>(&self, name: &str) -> Option<T> where T: ValueRetrieval,

pub fn has_header(&self, name: &str) -> bool

pub fn get_header(&self, name: &str) -> Option<String>

pub fn get_claims(&self) -> pdk_script::Value

pub fn get_headers(&self) -> pdk_script::Value

提供されているメソッドは、それぞれの標準 JWT クレームを返すように設計されています。

get_claim​ メソッドは、標準またはカスタムクレームを返します。​get_claim​ は異なる対象変数型をサポートしているため、ユーザーは出力種別を指定する必要があります。 get_claim​ は、出力種別として ​String​、​f64​、​Vec<String>​、​chrono::DateTime<chono::Utc>​、​serde_json::Value​ をサポートしています。クレームがトークン中に存在しない場合に備えて、​Option​ で種別をラップする必要があります。例:

let some_custom_claim: Option<String> = claims.get_claim("username");

次の例では、ラップされた関数から JWT トークンを解析し、カスタムクレームを取得して、リクエストヘッダーに伝播する方法を示しています。

async fn filter(
    state: RequestState,
) -> Flow<()> {

    let headers_state = state.into_headers_state().await;

    // Extract token
    let token = TokenProvider::bearer(headers_state.handler())?;

    if token.is_err() {
        return Flow::Break(Response::new(401).with_body("Bearer not found"));
    }

    // Being "token" a String that contains a JWT token
    let parsed_claims = JWTClaimsParser::parse(token.unwrap());

    if claims.is_err() {
        return Flow::Break(Response::new(401).with_body("Invalid token"));
    }

    let claims = claims.unwrap();

    let username: Option<String> = claims.get_claim("username");

    if let Some(custom_claim) = some_custom_claim {
        headers_state
            .handler()
            .set_header("username", custom_claim.as_str());
    }

    Flow::Continue(())
}

クレームの検証

通常、JWT トークンには、検証可能な標準および非標準クレームが含まれています。JWT トークンのヘッダーまたはペイロードに存在するクレームを取得するには、​クレームへのアクセスとヘッダーへの伝搬​ のメソッドを使用します。取得したら、クレームの値を検証します。

次のコード例では、JWT トークンについて以下を検証しています。

  • exp​ クレームによって期限切れではないことを検証。

  • aud​ クレームによって正しいオーディエンス値を持っていることを検証。

  • DataWeave 式ポリシーパラメーターとして設定されたカスタムバリデーターを使用して、正しいカスタム ​role​ クレームを持っていることを検証。

use chrono::Utc;
use pdk::hl::*;
use pdk::jwt::*;
use pdk::logger::info;
use pdk::script::Evaluator;

async fn filter(
    state: RequestState,
    mut custom_validator: Evaluator<'_>,
) -> Flow<()> {
    let headers_state = state.into_headers_state().await;
    let token = TokenProvider::bearer(headers_state.handler());

    if token.is_err() {
        return Flow::Break(Response::new(400).with_body("Bearer not found"));
    }

    // Being "token" a String that contains a JWT token
    let parsed_claims = JWTClaimsParser::parse(token.unwrap());

    if claims.is_err() {
        return Flow::Break(Response::new(401).with_body("Invalid token"));
    }

    let claims = claims.unwrap();

    // Validating token expiration
    if let Some(exp) = claims.expiration() {
        if exp < Utc::now() {
            return Flow::Break(Response::new(400).with_body("token expired"));
        }
    } else {
        return Flow::Break(Response::new(400).with_body("token missing exp claim"));
    }

    // Validating audience
    if let Some(aud) = claims.audience() {
         if !aud_value.iter_mut().any(|a| a == "myAudience") {
            return Flow::Break(Response::new(400).with_body("token does not have the expected audience"));
        }
    }

    // Custom claim validation
    custom_validator.bind_vars("claimSet", claims.get_claims());
    let result = custom_validator_role_claim.eval();
    if !result.ok().and_then(|value| value.as_bool()).unwrap_or_default() {
        return Flow::Break(Response::new(400).with_body("custom token validations failed."));
    }

    Flow::Continue(())
}

#[entrypoint]
async fn configure(launcher: Launcher, Configuration(configuration): Configuration) -> Result<()> {
    let config: Config = serde_json::from_slice(&configuration)?;

    launcher
        .launch(on_request(|request| {
            filter(request, config.custom_validator_role.evaluator())
        }))
        .await?;
    Ok(())
}

上の例では、​gcl.yaml​ スキーマ定義で次のように入力パラメーターとして設定されている DataWeave 式によってカスタム ​role​ クレームが定義されています。

# [...]
spec:
  extends:
    - name: extension-definition
      namespace: default
  properties:
    customValidatorRole:
      type: string
      format: dataweave
      default: "#[vars.claimSet.role == 'superRole']"
      # [...]
  required:
    - customValidatorRole