Skip to content
Closed
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
40 changes: 19 additions & 21 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ path = "tests/object_path.rs"
name = "fixtures"
path = "tests/fixtures.rs"

[[test]]
name = "keys"
path = "tests/keys.rs"

[dependencies]
# Multistore
multistore = { version = "0.7.2", features = ["azure", "gcp"] }
Expand All @@ -44,6 +48,7 @@ multistore-sts = "0.7.2"
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = "0.4"

# HTTP
http = "1"
Expand All @@ -56,6 +61,11 @@ sha2 = "0.10"
# Tracing
tracing = "0.1"

# Native only: `tests/keys.rs` generates a throwaway signing key.
[dev-dependencies]
rand = "0.8"
rsa = "0.9"

# Wasm-only dependencies (Cloudflare Workers runtime)
[target.'cfg(target_arch = "wasm32")'.dependencies]
# Pulled in transitively via object_store -> rand. getrandom doesn't support
Expand Down Expand Up @@ -86,3 +96,13 @@ web-sys = { version = "0.3", features = [
] }
worker = { version = "=0.7.5", features = ["http"] }
worker-macros = { version = "=0.7.5", features = ["http"] }

# Until the multistore release that carries developmentseed/multistore#146
# (fail-closed trust fields, `allow_missing_exp_from`) and #147
# (`JwtSigner::sign_claims`). Drop this and bump the versions above on release.
[patch.crates-io]
multistore = { git = "https://github.com/developmentseed/multistore", branch = "oidc/sign-claims" }
multistore-cf-workers = { git = "https://github.com/developmentseed/multistore", branch = "oidc/sign-claims" }
multistore-oidc-provider = { git = "https://github.com/developmentseed/multistore", branch = "oidc/sign-claims" }
multistore-path-mapping = { git = "https://github.com/developmentseed/multistore", branch = "oidc/sign-claims" }
multistore-sts = { git = "https://github.com/developmentseed/multistore", branch = "oidc/sign-claims" }
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ Client Request: GET /{account}/{product}/{key}
| `OPTIONS *` | CORS preflight |
| `GET /.well-known/openid-configuration` | OIDC discovery document |
| `GET /.well-known/jwks.json` | JSON Web Key Set for JWT verification |
| `POST /.sts` | `AssumeRoleWithWebIdentity`: an ID token or API key for credentials |
| `POST /.keys` | Sign an API key for a service account (called by source.coop) |

Write operations (`PUT`, `POST`, `DELETE`, `PATCH`) return `405 Method Not Allowed`.

Expand Down Expand Up @@ -147,6 +149,24 @@ When configured with an RSA key, the proxy acts as its own OpenID Connect identi

These endpoints are only active when `OIDC_PROVIDER_KEY` is configured.

### API Keys

An API key ([ADR-013](adrs/013-api-keys.md)) is a JWT signed with the same key,
prefixed `sck_`, that a service account exchanges at `/.sts` exactly as a user
exchanges an ID token: `AssumeRoleWithWebIdentity` with the key as
`WebIdentityToken` and `RoleArn=_default`. The proxy verifies it against its
own signing key in process (a Worker cannot fetch its own JWKS), then asks the
Source API whether the key is still active before minting — cached for 60s, so
revoking a key stops new exchanges within a minute. Expired, revoked and
unknown keys all get the same `InvalidIdentityToken` answer; the reason is
logged under the response's `x-request-id`.

`POST /.keys` signs a key. source.coop calls it with the manager's ID token
(`Authorization: Bearer`) and `{"account_id", "jti", "expires_at"}` once it
has written the key's record; the proxy checks with the Source API that the
caller manages the account before signing. Like `/.sts`, it answers 501 until
`AUTH_AUDIENCE` is set.

### Setup

Generate an RSA key pair, store it as a GitHub environment secret, and deploy:
Expand Down
2 changes: 2 additions & 0 deletions adrs/013-api-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ An API key JWT contains:
{
"iss": "https://data.source.coop",
"sub": "<account_id>",
"aud": "https://data.source.coop",
"jti": "<unique_key_id>",
"iat": 1711929600,
"exp": 1743465600,
Expand All @@ -45,6 +46,7 @@ An API key JWT contains:

- `iss` is the proxy's own issuer URL, not `auth.source.coop` (which is Ory Network and outside Source Cooperative's control for token minting)
- `sub` identifies the Source Cooperative account that owns the key
- `aud` is the proxy's own issuer too: the proxy is the only party meant to accept a key, and an exchange requires every token to name its audience
- `jti` is a unique key identifier used for revocation checks
- `exp` is optional — keys without an expiry are valid until explicitly revoked
- `type` distinguishes API key JWTs from other tokens the proxy may issue (e.g. outbound federation tokens)
Expand Down
Loading
Loading