-
Notifications
You must be signed in to change notification settings - Fork 0
Support Oauth 2.0 #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mwfj
wants to merge
17
commits into
main
Choose a base branch
from
support-oauth-2.0
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
6422c92
Support Oauth 2.0
mwfj 915d33a
Apply jwt-cpp library
mwfj 8d8068f
Apply jwt-cpp lib
mwfj 90776b7
Apply jwt-cpp lib
mwfj fc52cc4
Apply jwt-cpp library
mwfj ef67dc2
Fix review comment
mwfj f6f08ad
Fix review comment
mwfj fbac7cf
Fix review comment
mwfj f9acaa4
Fix review comment
mwfj a6ea5cc
Fix review comment
mwfj 4750410
Fix review comment
mwfj 3c783ed
Fix review comment
mwfj cdeb91d
Fix review comment
mwfj 94fd47d
Fix review comment
mwfj d49eefc
Fix review comment
mwfj 308414b
Fix review comment
mwfj 9092e10
Fix review comment
mwfj File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| #pragma once | ||
|
|
||
| #include "common.h" | ||
| #include "auth/auth_context.h" | ||
| #include <nlohmann/json.hpp> | ||
| // <string>, <vector> via common.h | ||
|
|
||
| namespace auth { | ||
|
|
||
| // Helpers that translate a decoded-JWT payload OR an introspection-response | ||
| // JSON into the AuthContext we attach to HttpRequest. | ||
| // | ||
| // Kept separate from JwtVerifier / IntrospectionClient so the same | ||
| // claim-extraction logic is used by both paths and tested once. | ||
|
|
||
| // Build a scope list from the payload. OAuth 2.0 tokens conventionally use | ||
| // either `scope` (space-separated string) or `scp` (JSON array of strings). | ||
| // Some IdPs (e.g. Azure AD) also emit `scopes`. This helper accepts all three. | ||
| std::vector<std::string> ExtractScopes(const nlohmann::json& payload); | ||
|
|
||
| // Populate AuthContext from a decoded JWT payload plus operator-configured | ||
| // `claims_to_headers` keys (we only copy claims that the operator asks for | ||
| // into AuthContext::claims, to keep the context small). | ||
| // | ||
| // Also sets: | ||
| // ctx.issuer = payload["iss"] (if string) | ||
| // ctx.subject = payload["sub"] (if string) | ||
| // ctx.scopes = ExtractScopes(payload) | ||
| // | ||
| // Returns true when `sub` and `iss` are both present and string; false | ||
| // otherwise (caller returns 401 invalid_token). | ||
| bool PopulateFromPayload(const nlohmann::json& payload, | ||
| const std::vector<std::string>& claims_keys, | ||
| AuthContext& ctx); | ||
|
|
||
| // Check whether all required scopes are present in the token's scope list. | ||
| // Returns true iff every entry in `required` appears in `have`. Empty | ||
| // `required` is always accepted. | ||
| bool HasRequiredScopes(const std::vector<std::string>& have, | ||
| const std::vector<std::string>& required); | ||
|
|
||
| // Check whether the token's `aud` claim matches a required audience string. | ||
| // `aud` may be a string or array in JWT. Returns true iff `required` matches | ||
| // one of the audiences. Empty `required` is always accepted (no audience | ||
| // requirement). | ||
| bool MatchesAudience(const nlohmann::json& payload, | ||
| const std::string& required); | ||
|
|
||
| } // namespace auth |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| #pragma once | ||
|
|
||
| #include "common.h" | ||
| // <string>, <vector>, <map>, <unordered_map> via common.h | ||
|
|
||
| namespace auth { | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Introspection-mode config (RFC 7662 token introspection). | ||
| // --------------------------------------------------------------------------- | ||
| struct IntrospectionConfig { | ||
| std::string endpoint; // Full URL; required when mode=introspection | ||
| std::string client_id; // OAuth client id for the introspection request | ||
| std::string client_secret_env; // Env-var name holding the client secret (inline secret is rejected) | ||
| std::string auth_style = "basic"; // "basic" (Authorization header) or "body" (urlencoded body) | ||
| int timeout_sec = 3; // Per-request timeout for introspection POST | ||
| int cache_sec = 60; // Positive-result cache TTL (capped by token exp) | ||
| int negative_cache_sec = 10; // Negative-result cache TTL | ||
| int stale_grace_sec = 30; // Serve stale positive when IdP unreachable | ||
| int max_entries = 100000; // Per-issuer cache cap (LRU eviction on insert) | ||
| int shards = 16; // Sharded LRU shard count | ||
|
|
||
| bool operator==(const IntrospectionConfig& o) const { | ||
| return endpoint == o.endpoint && | ||
| client_id == o.client_id && | ||
| client_secret_env == o.client_secret_env && | ||
| auth_style == o.auth_style && | ||
| timeout_sec == o.timeout_sec && | ||
| cache_sec == o.cache_sec && | ||
| negative_cache_sec == o.negative_cache_sec && | ||
| stale_grace_sec == o.stale_grace_sec && | ||
| max_entries == o.max_entries && | ||
| shards == o.shards; | ||
| } | ||
| bool operator!=(const IntrospectionConfig& o) const { return !(*this == o); } | ||
| }; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Per-issuer config. One entry per trusted IdP. | ||
| // --------------------------------------------------------------------------- | ||
| struct IssuerConfig { | ||
| std::string name; // Config key (e.g. "google", "openai", "ours") | ||
| std::string issuer_url; // MUST be https:// | ||
| bool discovery = true; // Use OIDC .well-known/openid-configuration | ||
| std::string jwks_uri; // Optional static override; only used when discovery=false | ||
| std::string upstream; // Name of the UpstreamHostPool used for outbound IdP calls | ||
| std::string mode = "jwt"; // "jwt" or "introspection" | ||
| std::vector<std::string> audiences; // Accepted `aud` values | ||
| std::vector<std::string> algorithms = { // Per-issuer allowlist (asymmetric only in v1) | ||
| "RS256"}; | ||
| int leeway_sec = 30; // Clock-skew tolerance for exp/nbf/iat | ||
| int jwks_cache_sec = 300; // JWKS TTL | ||
| int jwks_refresh_timeout_sec = 5; // Per-refresh upstream timeout | ||
| int discovery_retry_sec = 30; // Retry interval if startup discovery fails | ||
| std::vector<std::string> required_claims; // Claims that MUST be present (beyond iss/exp/aud) | ||
| IntrospectionConfig introspection; // Only meaningful when mode=introspection | ||
|
|
||
| bool operator==(const IssuerConfig& o) const { | ||
| return name == o.name && issuer_url == o.issuer_url && | ||
| discovery == o.discovery && jwks_uri == o.jwks_uri && | ||
| upstream == o.upstream && mode == o.mode && | ||
| audiences == o.audiences && algorithms == o.algorithms && | ||
| leeway_sec == o.leeway_sec && | ||
| jwks_cache_sec == o.jwks_cache_sec && | ||
| jwks_refresh_timeout_sec == o.jwks_refresh_timeout_sec && | ||
| discovery_retry_sec == o.discovery_retry_sec && | ||
| required_claims == o.required_claims && | ||
| introspection == o.introspection; | ||
| } | ||
| bool operator!=(const IssuerConfig& o) const { return !(*this == o); } | ||
| }; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Per-policy config. Attached either: | ||
| // (a) inline on a proxy via ProxyConfig::auth (applies_to derived from route_prefix) | ||
| // (b) top-level via AuthConfig::policies (applies_to declared explicitly) | ||
| // --------------------------------------------------------------------------- | ||
| struct AuthPolicy { | ||
| std::string name; // Optional for inline policies; required for top-level | ||
| bool enabled = false; // Opt-in per policy; default off (prevents empty policy from gating) | ||
| std::vector<std::string> applies_to; // Path prefixes (used only for top-level policies) | ||
| std::vector<std::string> issuers; // Accepted issuer names (must match AuthConfig::issuers keys) | ||
| std::vector<std::string> required_scopes; // All must be present in token scope/scp | ||
| std::string required_audience; // Overrides issuer-level `audiences` when set | ||
| std::string on_undetermined = "deny"; // "deny" (default) or "allow" | ||
| std::string realm = "api"; // For WWW-Authenticate: Bearer realm="..." | ||
|
|
||
| bool operator==(const AuthPolicy& o) const { | ||
| return name == o.name && enabled == o.enabled && | ||
| applies_to == o.applies_to && issuers == o.issuers && | ||
| required_scopes == o.required_scopes && | ||
| required_audience == o.required_audience && | ||
| on_undetermined == o.on_undetermined && | ||
| realm == o.realm; | ||
| } | ||
| bool operator!=(const AuthPolicy& o) const { return !(*this == o); } | ||
| }; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Forward-overlay config. How validated identity is injected into the | ||
| // outbound (upstream) request header set by HeaderRewriter. Reload-mutable; | ||
| // held inside AuthManager as std::shared_ptr<const AuthForwardConfig> and | ||
| // snapshotted per-request at the start of the outbound hop. | ||
| // --------------------------------------------------------------------------- | ||
| struct AuthForwardConfig { | ||
| std::string subject_header = "X-Auth-Subject"; | ||
| std::string issuer_header = "X-Auth-Issuer"; | ||
| std::string scopes_header = "X-Auth-Scopes"; | ||
| std::string raw_jwt_header; // Empty (default) = disabled | ||
| std::map<std::string, std::string> claims_to_headers; // claim -> outbound header name | ||
| bool strip_inbound_identity_headers = true; // Drop inbound X-Auth-* to prevent spoofing | ||
| bool preserve_authorization = true; // Forward original Authorization header | ||
|
|
||
| bool operator==(const AuthForwardConfig& o) const { | ||
| return subject_header == o.subject_header && | ||
| issuer_header == o.issuer_header && | ||
| scopes_header == o.scopes_header && | ||
| raw_jwt_header == o.raw_jwt_header && | ||
| claims_to_headers == o.claims_to_headers && | ||
| strip_inbound_identity_headers == o.strip_inbound_identity_headers && | ||
| preserve_authorization == o.preserve_authorization; | ||
| } | ||
| bool operator!=(const AuthForwardConfig& o) const { return !(*this == o); } | ||
| }; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Top-level auth config block. | ||
| // --------------------------------------------------------------------------- | ||
| struct AuthConfig { | ||
| bool enabled = false; // Master switch | ||
| std::unordered_map<std::string, IssuerConfig> issuers; // Keyed by IssuerConfig::name (redundant but stable) | ||
| std::vector<AuthPolicy> policies; // Top-level policies with applies_to | ||
| AuthForwardConfig forward; // Outbound header overlay config | ||
| std::string hmac_cache_key_env; // Env-var name for process-local HMAC key; empty = generated | ||
|
|
||
| bool operator==(const AuthConfig& o) const { | ||
| return enabled == o.enabled && issuers == o.issuers && | ||
| policies == o.policies && forward == o.forward && | ||
| hmac_cache_key_env == o.hmac_cache_key_env; | ||
| } | ||
| bool operator!=(const AuthConfig& o) const { return !(*this == o); } | ||
| }; | ||
|
|
||
| } // namespace auth |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| #pragma once | ||
|
|
||
| #include "common.h" | ||
| #include <optional> | ||
| // <string>, <vector>, <map>, <optional> via common.h | ||
|
|
||
| namespace auth { | ||
|
|
||
| // AuthContext is the output of the auth middleware on successful validation. | ||
| // Attached to HttpRequest via a mutable field (see include/http/http_request.h). | ||
| // Read by downstream middleware, route handlers, and by HeaderRewriter when | ||
| // constructing the outbound (upstream) request header set. | ||
| struct AuthContext { | ||
| std::string issuer; // Validated `iss` claim | ||
| std::string subject; // Validated `sub` claim | ||
| std::vector<std::string> scopes; // From `scope` (space-sep) or `scp` (array) | ||
| std::map<std::string, std::string> claims; // Operator-selected claims (claims_to_headers source) | ||
| std::string policy_name; // Matched policy's name (observability) | ||
|
|
||
| // SENSITIVE — raw bearer token. NEVER log this field. Never include it | ||
| // in error messages, debug dumps, or diagnostic responses. Per | ||
| // LOGGING_STANDARDS.md, logs must reference `subject` (post-validation) | ||
| // only; the raw token must never appear. The middleware should populate | ||
| // this field ONLY when `AuthForwardConfig::raw_jwt_header` is non-empty | ||
| // (operator explicitly opted in to re-forwarding under a separate | ||
| // header name) — otherwise leave it empty to minimize the blast radius | ||
| // if a future request-dump helper accidentally serializes AuthContext. | ||
| std::string raw_token; | ||
| bool undetermined = false; // True when on_undetermined="allow" path forwarded | ||
|
|
||
| void Clear() { | ||
| issuer.clear(); | ||
| subject.clear(); | ||
| scopes.clear(); | ||
| claims.clear(); | ||
| policy_name.clear(); | ||
| raw_token.clear(); | ||
| undetermined = false; | ||
| } | ||
| }; | ||
|
|
||
| } // namespace auth |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.