From 30876f17f2f159ce20bf55ce8b9a4eca936cc785 Mon Sep 17 00:00:00 2001 From: Michael Netshipise Date: Wed, 10 Jun 2026 22:46:54 +0200 Subject: [PATCH] =?UTF-8?q?verify=5Fapi=5Fkey=20in=20all=20three=20languag?= =?UTF-8?q?es=20+=20Rust=20key-management=20wrappers=20=E2=80=94=20v0.2.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The service-credential seam: VerifyApiKey returns the key's owning service user + roles (same AuthenticationResponse shape), so consumers build their Ctx identically to a session. Cached ~60s per key, namespaced away from token cache entries. Rust additionally wraps CreateApiKey/ListApiKeys/RevokeApiKey. Versioning note: client patch releases may lead the server within a minor line when only exposing existing server surface. Co-Authored-By: Claude Fable 5 --- README.md | 13 ++++--- VERSION | 2 +- go/auth.go | 28 +++++++++++++++ rust/Cargo.toml | 2 +- rust/src/lib.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++++++ ts/package.json | 2 +- ts/src/auth.ts | Bin 7528 -> 8404 bytes 7 files changed, 132 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 49bd1ce..7a2cc44 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ aura-users authorizes the acting admin by its own assignability rules). ## Versioning Every release is tagged at the **same version as the st-peter server** it -targets (see `VERSION`). A client tagged `v0.2.1` speaks the wire contract of -st-peter `v0.2.1`. The gRPC wire format is backward-compatible across patch +targets (see `VERSION`). A client tagged `v0.2.x` speaks the wire contract of +st-peter `v0.2.1`+ (client patch releases may lead the server within a minor line when they only expose existing server surface — v0.2.2 adds API-key wrappers). The gRPC wire format is backward-compatible across patch releases (field numbers and enum integer values are stable), so a client one patch behind a server generally interoperates — but match versions for new surface. @@ -51,6 +51,9 @@ The wrapper surface is the same in all three languages: - `verify_two_factor(two_factor_id, code)` — complete an OTP challenge - `lookup_user(actor_id, actor_token, identifier)` — resolve a user by email/phone/handle +- `verify_api_key(key)` — service-credential path: the key's owning service + user + roles (cached); Rust adds `create_api_key` / `list_api_keys` / + `revoke_api_key` management wrappers - `bearer(...)` helper + the shared `aura_session` cookie name The **admin surface** (`AdminClient` — Rust first; Go/TS expose the generated @@ -87,17 +90,17 @@ Then tag at `v$(cat VERSION)`. Rust (`Cargo.toml`): ```toml -st-peter-client = { git = "https://git.awesomike.com/pub/st-peter-client.git", tag = "v0.2.1" } +st-peter-client = { git = "https://git.awesomike.com/pub/st-peter-client.git", tag = "v0.2.2" } ``` Go: ```bash -go get git.awesomike.com/pub/st-peter-client/go@v0.2.1 +go get git.awesomike.com/pub/st-peter-client/go@v0.2.2 ``` TypeScript (`package.json`): ```json -"@st-peter/client": "git+https://git.awesomike.com/pub/st-peter-client.git#v0.2.1" +"@st-peter/client": "git+https://git.awesomike.com/pub/st-peter-client.git#v0.2.2" ``` diff --git a/VERSION b/VERSION index 0c62199..ee1372d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.1 +0.2.2 diff --git a/go/auth.go b/go/auth.go index 018bf8d..0cb701f 100644 --- a/go/auth.go +++ b/go/auth.go @@ -116,6 +116,34 @@ func (c *AuthClient) VerifyTokenScoped(ctx context.Context, token string, roleSc return user, nil } +// VerifyApiKey verifies a service API key, returning the key's OWNING +// SERVICE USER plus that user's roles — consumers build their context +// identically to a session. Cached ~60s per key, like tokens. +func (c *AuthClient) VerifyApiKey(ctx context.Context, apiKey string) (*pb.AuthenticatedUser, error) { + key := cacheKey(apiKey, []string{"#api-key"}) // namespaced away from tokens + + c.mu.RLock() + if e, ok := c.cache[key]; ok && time.Since(e.at) < authCacheTTL { + c.mu.RUnlock() + return e.user, nil + } + c.mu.RUnlock() + + resp, err := c.svc.VerifyApiKey(ctx, &pb.VerifyApiKeyRequest{ApiKey: apiKey}) + if err != nil { + return nil, err + } + if !resp.GetSuccess() || resp.GetAuthenticatedUser() == nil { + return nil, ErrUnauthorized + } + user := resp.GetAuthenticatedUser() + + c.mu.Lock() + c.cache[key] = cacheEntry{user: user, at: time.Now()} + c.mu.Unlock() + return user, nil +} + // LoginOutcome is the result of Login / VerifyTwoFactor. Exactly one of the // three fields is set. type LoginOutcome struct { diff --git a/rust/Cargo.toml b/rust/Cargo.toml index aee2af8..b417ecc 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "st-peter-client" -version = "0.2.1" +version = "0.2.2" edition = "2021" description = "Official Rust client for st-peter (aura-users) — authentication over gRPC with a token-verify cache" repository = "https://git.awesomike.com/pub/st-peter-client" diff --git a/rust/src/lib.rs b/rust/src/lib.rs index c791472..789d02d 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -234,6 +234,99 @@ impl AuthClient { Ok(interpret_auth_response(resp)) } + /// Verify a service **API key** (`VerifyApiKey` — same response shape as + /// token verification): yields the key's **owning service user** plus that + /// user's roles, so consumers build their `Ctx` identically to a session + /// (the producer seam: a service user holds targeted roles like anyone). + /// Cached ~60s per key, like tokens. + pub async fn verify_api_key(&self, api_key: &str) -> Result { + let key = cache_key(api_key, &["#api-key"]); // namespaced away from tokens + let ttl = std::time::Duration::from_secs(AUTH_CACHE_TTL_SECS); + + if let Some(u) = { + let cache = self.cache.read().await; + cache + .get(&key) + .and_then(|(u, at)| (at.elapsed() < ttl).then(|| u.clone())) + } { + return Ok(u); + } + + let mut client = self.inner.clone(); + let resp = client + .verify_api_key(authpb::VerifyApiKeyRequest { + api_key: api_key.to_string(), + }) + .await? + .into_inner(); + if !resp.success { + return Err(Error::Unauthorized); + } + let user = resp.authenticated_user.ok_or(Error::Unauthorized)?; + + self.cache + .write() + .await + .insert(key, (user.clone(), std::time::Instant::now())); + Ok(user) + } + + /// Mint an API key for the session's user (`full key returned ONCE`). + pub async fn create_api_key( + &self, + token: &str, + name: &str, + scopes: Vec, + expires_at: i64, // 0 = never + ) -> Result { + let resp = self + .inner + .clone() + .create_api_key(authpb::CreateApiKeyRequest { + token: token.to_string(), + name: name.to_string(), + scopes, + expires_at, + }) + .await? + .into_inner(); + if !resp.success { + return Err(Error::Rejected { code: resp.result_code, message: resp.message }); + } + Ok(resp) + } + + /// List the session user's API keys. + pub async fn list_api_keys(&self, token: &str) -> Result> { + let resp = self + .inner + .clone() + .list_api_keys(authpb::ListApiKeysRequest { token: token.to_string() }) + .await? + .into_inner(); + if !resp.success { + return Err(Error::Rejected { code: resp.result_code, message: resp.message }); + } + Ok(resp.keys) + } + + /// Revoke an API key by id. + pub async fn revoke_api_key(&self, token: &str, key_id: &str) -> Result<()> { + let resp = self + .inner + .clone() + .revoke_api_key(authpb::RevokeApiKeyRequest { + token: token.to_string(), + key_id: key_id.to_string(), + }) + .await? + .into_inner(); + if !resp.success { + return Err(Error::Rejected { code: resp.result_code, message: resp.message }); + } + Ok(()) + } + /// Resolve a st-peter user by exact identifier (email / phone / handle). /// /// Typical use: a service-admin role-grant page — resolve a colleague's diff --git a/ts/package.json b/ts/package.json index 73e3517..e093243 100644 --- a/ts/package.json +++ b/ts/package.json @@ -1,6 +1,6 @@ { "name": "@st-peter/client", - "version": "0.2.1", + "version": "0.2.2", "description": "Official TypeScript client for st-peter (aura-users) — authentication over gRPC with a token-verify cache", "repository": "https://git.awesomike.com/pub/st-peter-client", "license": "MIT OR Apache-2.0", diff --git a/ts/src/auth.ts b/ts/src/auth.ts index 1e3873c887f604e013ff8944bbc2e1c285e38021..1caf30772b3d2fafe989e7ed4afd59669db0d206 100644 GIT binary patch delta 396 zcmXw#PfG$(6vY=pL=^o)aT6RWNW-)?Es`KHi6JmivaPR~D?a8qGrsr68pP^5Odllh zTeNQ3B3iZTGc?b!-Q~RVe)pd9bM{etb2DqTfL~M8rcp>r&sgSt>v;vhA-W`aKLUdy zIp#K@*0SJIgzuMEM4W`9pa*261BIKrrq#Scr_t_O^#*Q}0f7ir%F+X-5hkF-DSbf- z1>5&jI3%fXA9CRs6qiU;eMyfR+#yeMn+Xv?`(PIXW?qqd5FKQut=66czxTv&jez%y^oQc8&=^{*vCJ>6%Stpxa F{sZ3bhu{DJ delta 17 ZcmccO_`+&KHuL5t<^w{Tr%T$h0{}@02L}KE