Implement per-request FIDO2 token binding across all components

Each request to a gated endpoint now triggers a fresh FIDO2 assertion.
Challenge = SHA256(url|method|nonce) — bound to the specific resource.
The self-contained assertion bundle lets the server verify independently
without calling back to the phone.

- fido2_ops.dart: GetAssertionResult gains clientDataJson; getAssertion
  accepts optional challenge override
- proxy_service.dart: _handleAuthGetToken accepts {url,method,nonce},
  derives challenge, runs card assertion, returns b64url bundle
- filter_proxy.dart: _getAuthToken(uri, method) generates nonce and
  passes binding fields to Component 2
- component3/phone.go: stateless GetTokenForRequest(url, method) —
  no session caching, no expiry, one card touch per request
- component3/proxy.go: use GetTokenForRequest
- component3/main.go: remove --user flag (Component 2 picks enrolled user)
- k_server_app.py: _verify_assertion_token() — verifies path+method
  match, challenge claim, and ECDSA-P256 signature; accepts both
  legacy X-Proxy-Token and new Bearer assertion tokens

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Morten V. Christiansen 2026-05-08 12:01:23 +02:00
parent ffa5bea1c7
commit 3fc40fc395
8 changed files with 235 additions and 137 deletions

View File

@ -652,6 +652,21 @@ CTAP2 cmd=0x02 body=113 bytes → getAssertion OK auth_data=37 bytes sig=71 byte
- `proxy.go`: `handleHTTP` extracts `port` from URL (defaults `"80"`), passes to `IsGated`; `handleConnect` passes `portStr` to `IsGated`. - `proxy.go`: `handleHTTP` extracts `port` from URL (defaults `"80"`), passes to `IsGated`; `handleConnect` passes `portStr` to `IsGated`.
- `phone.go`: added `getToken()` calling `/auth/get-token` — avoids FIDO2 card interaction if the phone already has an active session. `EnsureSession()` tries `getToken()` first, falls back to `login()`. Fixed `login()` JSON field: `expires_in``ttl_seconds` (actual server field name). `go build ./...` passes. - `phone.go`: added `getToken()` calling `/auth/get-token` — avoids FIDO2 card interaction if the phone already has an active session. `EnsureSession()` tries `getToken()` first, falls back to `login()`. Fixed `login()` JSON field: `expires_in``ttl_seconds` (actual server field name). `go build ./...` passes.
### Parallel-change note: Component 1 and Component 3 share the same proxy logic
Component 3 (`component3/`) and Component 1 (`k_phone/lib/filter_proxy.dart`) implement the same core behaviour: intercept HTTP/HTTPS traffic, decide per-request whether the target is gated, fetch a WebAuthn token if so, and call the endpoint directly with the token. Any structural change to one (new gating logic, token-binding changes, CONNECT handling, error semantics) will almost certainly need a corresponding change in the other. Treat them as a pair: when modifying Component 3, check Component 1 for the same fix, and vice versa.
### Work completed (2026-05-08, per-request token binding)
- `fido2_ops.dart`: `GetAssertionResult` now includes `clientDataJson`; `getAssertion()` accepts optional `challenge` param for binding.
- `proxy_service.dart`: `_handleAuthGetToken` rewritten — accepts `{url, method, nonce}`, derives `challenge = SHA256(url|method|nonce)`, calls card (getAssertion), returns self-contained assertion bundle as base64url Bearer token. No session involved.
- `filter_proxy.dart`: `_getAuthToken(uri, method)` generates a secure 16-byte nonce, posts `{url, method, nonce}` to Component 2, uses returned assertion token directly.
- `component3/phone.go`: rewritten as stateless `GetTokenForRequest(url, method)` — no session caching, no mutex, no expiry tracking.
- `component3/proxy.go`: `handleHTTP` uses `GetTokenForRequest(r.URL.String(), r.Method)`.
- `component3/main.go`: `--user` flag removed (Component 2 picks the enrolled user).
- `k_server_app.py`: `_verify_assertion_token()` added — decodes bundle, verifies path+method match, verifies challenge claim, verifies ECDSA-P256 signature over authData||clientDataHash using public key extracted from bundle's credentialData. `_is_proxy_authorized()` accepts either X-Proxy-Token (legacy k_proxy path) or Bearer assertion token.
- 46/46 Flutter tests pass; `go build ./...` clean; `flutter analyze` no issues.
### Next action ### Next action
1. Deploy to a real Android phone with physical ChromeCard via USB 1. Deploy to a real Android phone with physical ChromeCard via USB

View File

@ -2,7 +2,6 @@ package main
import ( import (
"flag" "flag"
"fmt"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -12,17 +11,10 @@ import (
func main() { func main() {
listen := flag.String("listen", "127.0.0.1:9090", "local proxy address (configure browser to use this)") listen := flag.String("listen", "127.0.0.1:9090", "local proxy address (configure browser to use this)")
phoneURL := flag.String("phone", "http://192.168.1.10:8771", "phone base URL (Component 1/2)") phoneURL := flag.String("phone", "http://192.168.1.10:8771", "phone base URL (Component 1/2)")
username := flag.String("user", "", "FIDO2 username (required)")
gatedFile := flag.String("gated", "", "gated hosts file (default: ~/.config/component3/gated_hosts.txt)") gatedFile := flag.String("gated", "", "gated hosts file (default: ~/.config/component3/gated_hosts.txt)")
verbose := flag.Bool("v", false, "verbose logging") verbose := flag.Bool("v", false, "verbose logging")
flag.Parse() flag.Parse()
if *username == "" {
fmt.Fprintln(os.Stderr, "error: -user is required")
flag.Usage()
os.Exit(1)
}
cfgDir := defaultConfigDir() cfgDir := defaultConfigDir()
if err := os.MkdirAll(cfgDir, 0700); err != nil { if err := os.MkdirAll(cfgDir, 0700); err != nil {
log.Fatalf("cannot create config dir: %v", err) log.Fatalf("cannot create config dir: %v", err)
@ -38,7 +30,7 @@ func main() {
log.Printf("loaded %d gated entries from %s", gated.Len(), *gatedFile) log.Printf("loaded %d gated entries from %s", gated.Len(), *gatedFile)
} }
phone := NewPhoneClient(*phoneURL, *username) phone := NewPhoneClient(*phoneURL)
proxy := &Proxy{ proxy := &Proxy{
phone: phone, phone: phone,

View File

@ -2,126 +2,67 @@ package main
import ( import (
"bytes" "bytes"
"crypto/rand"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"sync"
"time"
) )
// PhoneClient acquires and caches a session token from Component 2 on the phone. // PhoneClient fetches a per-request FIDO2 assertion token from Component 2.
// The token represents a completed FIDO2 assertion (user fingerprint on card). // There is no session caching — each call triggers a card interaction.
// Component 3 includes this token in Authorization headers when calling endpoints
// directly, so the server can verify that a valid card session was established.
type PhoneClient struct { type PhoneClient struct {
baseURL string baseURL string
username string
mu sync.Mutex
token string
expiresAt time.Time
} }
func NewPhoneClient(baseURL, username string) *PhoneClient { func NewPhoneClient(baseURL string) *PhoneClient {
return &PhoneClient{baseURL: baseURL, username: username} return &PhoneClient{baseURL: baseURL}
} }
// EnsureSession returns a valid session token. It first tries /auth/get-token // GetTokenForRequest calls /auth/get-token with url+method+nonce, triggering
// (no card interaction if the phone already has an active session), then falls // a fresh FIDO2 assertion on the card. Returns the self-contained assertion
// back to /session/login (triggers FIDO2 assertion on the card). // bundle that the endpoint can verify independently.
func (c *PhoneClient) EnsureSession() (string, error) { func (c *PhoneClient) GetTokenForRequest(rawURL, method string) (string, error) {
c.mu.Lock() nonce, err := randomHex(16)
defer c.mu.Unlock()
if c.token != "" && time.Now().Before(c.expiresAt) {
return c.token, nil
}
if tok, expiresIn, err := c.getToken(); err == nil {
ttl := time.Duration(expiresIn) * time.Second
if ttl <= 0 {
ttl = 5 * time.Minute
}
c.token = tok
c.expiresAt = time.Now().Add(ttl - 30*time.Second)
return tok, nil
}
log.Printf("phone: logging in as %q (FIDO2 card interaction required)", c.username)
return c.login()
}
// Invalidate clears the cached token, forcing a fresh login on the next call.
func (c *PhoneClient) Invalidate() {
c.mu.Lock()
c.token = ""
c.mu.Unlock()
}
// getToken calls /auth/get-token on the phone and returns the token if the phone
// already has an active session. Avoids card interaction. Caller must hold c.mu.
func (c *PhoneClient) getToken() (string, int, error) {
resp, err := http.Post(c.baseURL+"/auth/get-token", "application/json", bytes.NewReader([]byte("{}")))
if err != nil { if err != nil {
return "", 0, err return "", fmt.Errorf("nonce: %w", err)
} }
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body) body, _ := json.Marshal(map[string]string{
var result struct { "url": rawURL,
Ok bool `json:"ok"` "method": method,
Token string `json:"token"` "nonce": nonce,
ExpiresIn int `json:"expires_in"` })
Error string `json:"error"`
}
if err := json.Unmarshal(raw, &result); err != nil {
return "", 0, fmt.Errorf("parse token response: %w", err)
}
if !result.Ok {
return "", 0, fmt.Errorf("no active session: %s", result.Error)
}
return result.Token, result.ExpiresIn, nil
}
// login posts to /session/login on the phone and stores the returned token. resp, err := http.Post(c.baseURL+"/auth/get-token", "application/json", bytes.NewReader(body))
// Caller must hold c.mu.
func (c *PhoneClient) login() (string, error) {
body, _ := json.Marshal(map[string]string{"username": c.username})
resp, err := http.Post(c.baseURL+"/session/login", "application/json", bytes.NewReader(body))
if err != nil { if err != nil {
return "", fmt.Errorf("phone unreachable (%s): %w", c.baseURL, err) return "", fmt.Errorf("phone unreachable (%s): %w", c.baseURL, err)
} }
defer resp.Body.Close() defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body) raw, _ := io.ReadAll(resp.Body)
var result struct { var result struct {
Ok bool `json:"ok"` Ok bool `json:"ok"`
SessionToken string `json:"session_token"` Token string `json:"token"`
TtlSeconds int `json:"ttl_seconds"` Error string `json:"error"`
Error string `json:"error"`
} }
if err := json.Unmarshal(raw, &result); err != nil { if err := json.Unmarshal(raw, &result); err != nil {
return "", fmt.Errorf("parse phone response: %w (body: %s)", err, raw) return "", fmt.Errorf("parse token response: %w (body: %s)", err, raw)
} }
if !result.Ok { if !result.Ok {
return "", fmt.Errorf("login rejected: %s", result.Error) return "", fmt.Errorf("auth failed: %s", result.Error)
} }
if result.SessionToken == "" { if result.Token == "" {
return "", fmt.Errorf("phone returned empty session token") return "", fmt.Errorf("phone returned empty token")
} }
return result.Token, nil
ttl := time.Duration(result.TtlSeconds) * time.Second }
if ttl <= 0 {
ttl = 5 * time.Minute func randomHex(n int) (string, error) {
} b := make([]byte, n)
// Expire the cached token 30 s early to avoid racing the server-side expiry. if _, err := rand.Read(b); err != nil {
c.token = result.SessionToken return "", err
c.expiresAt = time.Now().Add(ttl - 30*time.Second) }
return hex.EncodeToString(b), nil
log.Printf("phone: session acquired (expires in %v)", ttl)
return c.token, nil
} }

View File

@ -70,7 +70,7 @@ func (p *Proxy) handleHTTP(w http.ResponseWriter, r *http.Request) {
stripHopByHop(out.Header) stripHopByHop(out.Header)
if isGated { if isGated {
token, err := p.phone.EnsureSession() token, err := p.phone.GetTokenForRequest(r.URL.String(), r.Method)
if err != nil { if err != nil {
http.Error(w, "auth: "+err.Error(), http.StatusUnauthorized) http.Error(w, "auth: "+err.Error(), http.StatusUnauthorized)
return return

View File

@ -46,11 +46,13 @@ class GetAssertionResult {
final Uint8List authData; final Uint8List authData;
final Uint8List signature; final Uint8List signature;
final Uint8List clientDataHash; final Uint8List clientDataHash;
final String clientDataJson;
GetAssertionResult({ GetAssertionResult({
required this.authData, required this.authData,
required this.signature, required this.signature,
required this.clientDataHash, required this.clientDataHash,
required this.clientDataJson,
}); });
} }
@ -116,15 +118,17 @@ Future<MakeCredentialResult> makeCredential(
/// Runs CTAP2 authenticatorGetAssertion against the card on [cid]. /// Runs CTAP2 authenticatorGetAssertion against the card on [cid].
/// [credentialDataB64] is the base64url of the stored AttestedCredentialData. /// [credentialDataB64] is the base64url of the stored AttestedCredentialData.
/// [challenge] overrides the random challenge use for per-request token binding.
Future<GetAssertionResult> getAssertion( Future<GetAssertionResult> getAssertion(
int cid, int cid,
String credentialDataB64, String credentialDataB64, {
) async { Uint8List? challenge,
}) async {
final credData = _b64uDecode(credentialDataB64); final credData = _b64uDecode(credentialDataB64);
final credId = _extractCredentialId(credData); final credId = _extractCredentialId(credData);
final challenge = _randomBytes(32); final actualChallenge = challenge ?? _randomBytes(32);
final clientDataJson = _buildClientDataJson('webauthn.get', challenge); final clientDataJson = _buildClientDataJson('webauthn.get', actualChallenge);
final clientDataHash = _sha256(utf8.encode(clientDataJson)); final clientDataHash = _sha256(utf8.encode(clientDataJson));
final requestMap = CborMap({ final requestMap = CborMap({
@ -154,6 +158,7 @@ Future<GetAssertionResult> getAssertion(
authData: authData, authData: authData,
signature: signature, signature: signature,
clientDataHash: clientDataHash, clientDataHash: clientDataHash,
clientDataJson: clientDataJson,
); );
} }

View File

@ -17,6 +17,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@ -405,7 +406,7 @@ class FilterProxy {
) async { ) async {
String token; String token;
try { try {
token = await _getAuthToken(); token = await _getAuthToken(uri, method);
} catch (_) { } catch (_) {
_deny(client, sub, 407, 'Proxy Authentication Required'); _deny(client, sub, 407, 'Proxy Authentication Required');
return; return;
@ -449,9 +450,16 @@ class FilterProxy {
await _forwardHttpRequest(client, sub, upstream, out.toString(), remainder, contentLength); await _forwardHttpRequest(client, sub, upstream, out.toString(), remainder, contentLength);
} }
// Calls POST /auth/get-token on Component 2 and returns the bearer token. // Calls POST /auth/get-token on Component 2 with per-request binding and
// Throws if no active session or Component 2 is unreachable. // returns the bearer token (a self-contained FIDO2 assertion bundle).
Future<String> _getAuthToken() async { // Throws if card is unavailable or Component 2 is unreachable.
Future<String> _getAuthToken(Uri uri, String method) async {
final nonce = _secureHex(16);
final payload = utf8.encode(jsonEncode({
'url': uri.toString(),
'method': method,
'nonce': nonce,
}));
final httpClient = HttpClient() final httpClient = HttpClient()
..connectionTimeout = const Duration(seconds: 5); ..connectionTimeout = const Duration(seconds: 5);
try { try {
@ -459,8 +467,8 @@ class FilterProxy {
Uri(scheme: 'http', host: '127.0.0.1', port: _component2Port, path: '/auth/get-token'), Uri(scheme: 'http', host: '127.0.0.1', port: _component2Port, path: '/auth/get-token'),
); );
req.headers.contentType = ContentType.json; req.headers.contentType = ContentType.json;
req.contentLength = 2; req.contentLength = payload.length;
req.write('{}'); req.add(payload);
final resp = await req.close(); final resp = await req.close();
final body = await resp.transform(utf8.decoder).join(); final body = await resp.transform(utf8.decoder).join();
final json = jsonDecode(body) as Map<String, dynamic>; final json = jsonDecode(body) as Map<String, dynamic>;
@ -473,6 +481,13 @@ class FilterProxy {
} }
} }
String _secureHex(int bytes) {
final rng = Random.secure();
return List.generate(bytes, (_) => rng.nextInt(256))
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join();
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers // Helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -1,6 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:flutter_background_service/flutter_background_service.dart'; import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
@ -439,37 +442,72 @@ class _ProxyServer {
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Auth token endpoint (v2 architecture) // Auth token endpoint (v2 architecture per-request token binding)
// //
// Component 1 (filter_proxy) and Component 3 (Go binary) call this to get // Component 1 (filter_proxy) and Component 3 (Go binary) call this with
// a bearer token they can attach to requests when calling endpoints directly. // {url, method, nonce} for each gated request. A fresh FIDO2 assertion is
// Component 2 never calls endpoints itself it only issues tokens. // produced with challenge = SHA256(url|method|nonce). The self-contained
// assertion bundle is returned as a base64url Bearer token the server can
// verify without calling back to this service.
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
Future<void> _handleAuthGetToken(HttpRequest req) async { Future<void> _handleAuthGetToken(HttpRequest req) async {
await _drainBody(req); final body = await _readJson(req);
if (body == null) return;
// If there is already an active session return its token no card needed. final url = body['url'] as String? ?? '';
final active = _sessions.anyActive(); final method = body['method'] as String? ?? '';
if (active != null) { final nonce = body['nonce'] as String? ?? '';
final (token, session) = active;
final secondsRemaining = if (url.isEmpty || method.isEmpty || nonce.isEmpty) {
session.expires.difference(DateTime.now()).inSeconds.clamp(0, 99999); await _send(req.response, 400, {'ok': false, 'error': 'url, method, nonce required'});
await _send(req.response, 200, {
'ok': true,
'token': token,
'username': session.username,
'expires_in': secondsRemaining,
});
return; return;
} }
// No active session caller must trigger /session/login first. if (!_cardAttached || _cardCid == null) {
await _send(req.response, 401, { await _send(req.response, 503, {'ok': false, 'error': 'card not available'});
'ok': false, return;
'error': 'no active session', }
'login_required': true,
// Find first enrolled user with a FIDO2 credential.
final users = await _db.list();
Enrollment? enrolled;
for (final u in users) {
if (u.hasCredential) { enrolled = u; break; }
}
if (enrolled == null) {
await _send(req.response, 401, {'ok': false, 'error': 'no enrolled credential'});
return;
}
// Challenge = SHA256(url | "|" | method | "|" | nonce)
final challenge = Uint8List.fromList(
sha256.convert(utf8.encode('$url|$method|$nonce')).bytes,
);
GetAssertionResult assertionResult;
try {
assertionResult = await getAssertion(_cardCid!, enrolled.credentialDataB64!, challenge: challenge);
} catch (e) {
await _send(req.response, 401, {'ok': false, 'error': 'card assertion failed: $e'});
return;
}
// Self-contained bundle the server can verify without calling back to the phone.
final bundleJson = jsonEncode({
'v': 1,
'url': url,
'method': method,
'nonce': nonce,
'authData': base64Url.encode(assertionResult.authData).replaceAll('=', ''),
'sig': base64Url.encode(assertionResult.signature).replaceAll('=', ''),
'cdj': base64Url.encode(utf8.encode(assertionResult.clientDataJson)).replaceAll('=', ''),
'cred': enrolled.credentialDataB64,
'user': enrolled.username,
}); });
final token = base64Url.encode(utf8.encode(bundleJson)).replaceAll('=', '');
await _send(req.response, 200, {'ok': true, 'token': token, 'username': enrolled.username});
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------

View File

@ -10,6 +10,8 @@ All state is process-local and resets on restart.
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import base64
import hashlib
import json import json
import ssl import ssl
import threading import threading
@ -19,6 +21,86 @@ from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
def _b64u_decode(s: str) -> bytes:
padded = s + "=" * ((4 - len(s) % 4) % 4)
return base64.urlsafe_b64decode(padded)
def _verify_assertion_token(token: str, request_path: str, request_method: str) -> bool:
"""Verify a base64url-encoded FIDO2 per-request assertion bundle.
Bundle fields (JSON, then base64url-encoded):
v version (1)
url full URL used to derive the challenge
method HTTP method used to derive the challenge
nonce random hex nonce used to derive the challenge
authData base64url authenticator data
sig base64url ECDSA signature
cdj base64url clientDataJson bytes
cred base64url AttestedCredentialData (aaguid+credIdLen+credId+coseKey)
user enrolled username (informational)
"""
try:
import cbor2
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric.ec import (
ECDSA,
EllipticCurvePublicNumbers,
SECP256R1,
)
from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.exceptions import InvalidSignature
bundle = json.loads(_b64u_decode(token).decode("utf-8"))
# Path and method must match the actual request.
bundle_path = urlparse(bundle["url"]).path
if bundle_path != request_path:
return False
if bundle["method"].upper() != request_method.upper():
return False
url = bundle["url"]
method = bundle["method"]
nonce = bundle["nonce"]
# Verify challenge claim: challenge == b64u(SHA256(url|method|nonce))
binding = f"{url}|{method}|{nonce}".encode()
expected_challenge = base64.urlsafe_b64encode(hashlib.sha256(binding).digest()).rstrip(b"=").decode()
cdj_bytes = _b64u_decode(bundle["cdj"])
cdj = json.loads(cdj_bytes)
if cdj.get("type") != "webauthn.get":
return False
if cdj.get("challenge") != expected_challenge:
return False
# Verify ECDSA-P256 signature over authData || SHA256(clientDataJson).
auth_data = _b64u_decode(bundle["authData"])
signature = _b64u_decode(bundle["sig"])
client_data_hash = hashlib.sha256(cdj_bytes).digest()
message = auth_data + client_data_hash
# Extract P-256 public key from AttestedCredentialData.
cred_data = _b64u_decode(bundle["cred"])
cred_id_len = (cred_data[16] << 8) | cred_data[17]
cose_bytes = cred_data[18 + cred_id_len:]
cose_key = cbor2.loads(cose_bytes)
x = cose_key[-2]
y = cose_key[-3]
pub_key = EllipticCurvePublicNumbers(
x=int.from_bytes(x, "big"),
y=int.from_bytes(y, "big"),
curve=SECP256R1(),
).public_key(default_backend())
pub_key.verify(signature, message, ECDSA(SHA256()))
return True
except (InvalidSignature, Exception):
return False
class ServerState: class ServerState:
# All state is process-local; a restart resets the counter to zero. # All state is process-local; a restart resets the counter to zero.
def __init__(self, proxy_token: str): def __init__(self, proxy_token: str):
@ -52,7 +134,17 @@ class Handler(BaseHTTPRequestHandler):
self.rfile.read(length) self.rfile.read(length)
def _is_proxy_authorized(self) -> bool: def _is_proxy_authorized(self) -> bool:
return self.headers.get("X-Proxy-Token") == self.state.proxy_token # Accept legacy X-Proxy-Token (k_proxy_app.py) or FIDO2 assertion Bearer.
if self.headers.get("X-Proxy-Token") == self.state.proxy_token:
return True
auth = self.headers.get("Authorization", "")
if auth.startswith("Bearer "):
return _verify_assertion_token(
auth[7:].strip(),
request_path=urlparse(self.path).path,
request_method=self.command,
)
return False
def do_GET(self) -> None: # noqa: N802 def do_GET(self) -> None: # noqa: N802
path = urlparse(self.path).path path = urlparse(self.path).path