Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,14 @@ USAGE_SUBJECT_TYPE=api_key_user
# Generic OIDC (raw IdP access tokens with azp/sub):
# OIDC_ISSUER=https://your-tenant.auth0.com/
# OIDC_AUDIENCE=https://api.your-platform.com
# OIDC_JWKS_URI= # defaults to ${OIDC_ISSUER}/.well-known/jwks.json
# OIDC_JWKS_URI= # optional override; else OIDC Discovery → jwks_uri
# OIDC_CLIENT_CLAIM=azp
# OIDC_SUBJECT_CLAIM=sub
# OIDC_SUBJECT_TYPE=oidc_user
# OIDC_REQUIRED_SCOPES=
# OIDC_TOKEN_EXCHANGE_BASE_URL= # optional; enables app_<24hex>_<secret> composite key exchange
# OIDC_EXCHANGE_M2M_CLIENT_ID=
# OIDC_EXCHANGE_M2M_CLIENT_SECRET=

# Reference only — not read by Compose services; for minting test JWTs.
# Canonical copy: auth0-provisioner/provision/.env.livepeer
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,17 @@ Shared keys (`WEBHOOK_SECRET`, `KAFKA_BROKERS`, `KAFKA_GATEWAY_TOPIC`) are liste
| `DEMO_API_KEYS` | `api_key`, optional | JSON map of extra keys, e.g. `{"sk_other":{"clientId":"app-b","userId":"user-b"}}` |
| `USAGE_SUBJECT_TYPE` | `api_key`, optional | Default `api_key_user`; stamped on API-key identities |
| `API_KEY_PREFIX` | `api_key`, optional | Default `sk_` |
| `OIDC_ISSUER` | `oidc` | JWT issuer / JWKS host |
| `OIDC_ISSUER` | `oidc` | JWT issuer / JWKS host (trailing slash and surrounding whitespace are normalized) |
| `OIDC_AUDIENCE` | `oidc` | Expected JWT audience |
| `OIDC_JWKS_URI` | `oidc`, optional | Defaults to `${OIDC_ISSUER}/.well-known/jwks.json` |
| `OIDC_JWKS_URI` | `oidc`, optional | Explicit JWKS override; otherwise resolved via OIDC Discovery (`${OIDC_ISSUER}/.well-known/openid-configuration` → `jwks_uri`) |
| `OIDC_CLIENT_CLAIM` | `oidc`, optional | Tenant claim (default `azp`; Auth0 clearinghouse uses `app_client_id`) |
| `OIDC_SUBJECT_CLAIM` | `oidc`, optional | End-user claim (default `sub`; Auth0 clearinghouse uses `external_user_id`) |
| `OIDC_SUBJECT_TYPE` | `oidc`, optional | Default `oidc_user`; Auth0 clearinghouse uses `external_user_id` |
| `OIDC_REQUIRED_SCOPES` | `oidc`, optional | Space- or comma-separated required scopes (e.g. `sign:job`) |
| `OIDC_TOKEN_EXCHANGE_BASE_URL` | `oidc`, optional | Origin only (no path/query/hash). Enables composite `Bearer app_<24hex>_<secret>`: RFC 8693 exchange at `/api/v1/apps/{clientId}/oidc/token`, then JWT verify (`RS256`/`ES256`, required `exp`, 60s clock skew) |
| `OIDC_EXCHANGE_M2M_CLIENT_ID` / `OIDC_EXCHANGE_M2M_CLIENT_SECRET` | `oidc`, optional | Optional client credentials for the exchange request (both or neither) |

OIDC JWT verification accepts only `RS256` and `ES256` (asymmetric algorithms). Tokens must include `exp` and the configured subject claim.

`PORT` is set by Compose (`8090`), not `.env`. See the `identity-webhook` block in [`.env.example`](.env.example) for Auth0 vs generic OIDC examples.

Expand Down
29 changes: 24 additions & 5 deletions identity-webhook/legacy-env.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,27 @@ describe("pymthouse embedded flow (handleAuthorize + legacy config)", () => {
assert.equal(body.identity.usage_subject_type, "external_user_id");
});

it("authorizes a composite app_*.pmth_* via mocked exchange", async () => {
const { token, jwks } = await setupPymthouseJwt();
it("authorizes a composite app_*_* via mocked exchange", async () => {
const { publicKey, privateKey } = await generateKeyPair("RS256");
const jwk = await exportJWK(publicKey);
jwk.kid = "pymthouse-test";
jwk.alg = "RS256";
jwk.use = "sig";
const jwks = createLocalJWKSet({ keys: [jwk] });
const clientId = "app_abc123abc123abc123abcdef";
const token = await new SignJWT({
client_id: clientId,
external_user_id: "user-456",
scope: "sign:job",
})
.setProtectedHeader({ alg: "RS256", kid: "pymthouse-test" })
.setIssuer(ISSUER)
.setAudience(defaultSignerWebhookJwtAudience(ISSUER))
.setIssuedAt()
.setExpirationTime("5m")
.sign(privateKey);

const { createOidcVerifier } = await import("./verifiers.mjs");
const clientId = "app_abc123";
const fetchImpl = async (input) => {
const url = String(input);
assert.ok(url.includes(`/api/v1/apps/${clientId}/oidc/token`));
Expand Down Expand Up @@ -205,14 +222,16 @@ describe("pymthouse embedded flow (handleAuthorize + legacy config)", () => {
"content-type": "application/json",
},
body: JSON.stringify({
headers: { Authorization: [`Bearer ${clientId}.pmth_deadbeef`] },
headers: {
Authorization: [`Bearer ${clientId}_pmth_deadbeef`],
},
}),
});

const response = await handleAuthorize(request, config);
assert.equal(response.status, 200);
const body = await response.json();
assert.equal(body.status, 200);
assert.equal(body.auth_id, "app_abc123:user-456");
assert.equal(body.auth_id, `${clientId}:user-456`);
});
});
6 changes: 3 additions & 3 deletions identity-webhook/protocol.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@
}
}

/** Strip a `Bearer ` prefix (case-insensitive); returns the raw token otherwise. */
/** Extract the token from `Bearer <token>` (RFC 6750); empty if scheme is missing. */
export function bearerToken(authorization) {
const value = (authorization ?? "").trim();
return value.replace(/^Bearer\s+/i, "").trim();
const m = /^Bearer\s+(.+)$/i.exec((authorization ?? "").trim());

Check warning on line 47 in identity-webhook/protocol.mjs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Simplify this regular expression to reduce its runtime, as it has super-linear performance due to backtracking.

See more on https://sonarcloud.io/project/issues?id=pymthouse_clearinghouse-1&issues=AZ9sOtEVywlvVH9iphvE&open=AZ9sOtEVywlvVH9iphvE&pullRequest=6

Check failure

Code scanning / CodeQL

Polynomial regular expression used on uncontrolled data High

This
regular expression
that depends on
library input
may run slow on strings starting with 'bearer ' and with many repetitions of ' '.
This
regular expression
that depends on
library input
may run slow on strings starting with 'bearer ' and with many repetitions of ' '.
This
regular expression
that depends on
library input
may run slow on strings starting with 'bearer ' and with many repetitions of ' '.
return m ? m[1].trim() : "";
}

function timingSafeEqualStrings(a, b) {
Expand Down
4 changes: 2 additions & 2 deletions identity-webhook/protocol.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ const fakeVerifier = {
const config = { webhookSecret: SECRET, endUserAuth: fakeVerifier };

describe("bearerToken", () => {
it("strips a case-insensitive Bearer prefix", () => {
it("requires a case-insensitive Bearer scheme", () => {
assert.equal(bearerToken("Bearer sk_abc"), "sk_abc");
assert.equal(bearerToken("bearer sk_abc"), "sk_abc");
assert.equal(bearerToken("sk_abc"), "sk_abc");
assert.equal(bearerToken("sk_abc"), "");
assert.equal(bearerToken(undefined), "");
});
});
Expand Down
Loading