verify_api_key in all three languages + Rust key-management wrappers — v0.2.2
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 <noreply@anthropic.com>
This commit is contained in:
parent
78c6e1a0d9
commit
30876f17f2
13
README.md
13
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"
|
||||
```
|
||||
|
|
|
|||
28
go/auth.go
28
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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<AuthenticatedUser> {
|
||||
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<String>,
|
||||
expires_at: i64, // 0 = never
|
||||
) -> Result<authpb::CreateApiKeyResponse> {
|
||||
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<Vec<authpb::ApiKeyInfo>> {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
BIN
ts/src/auth.ts
BIN
ts/src/auth.ts
Binary file not shown.
Loading…
Reference in New Issue