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
|
## Versioning
|
||||||
|
|
||||||
Every release is tagged at the **same version as the st-peter server** it
|
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
|
targets (see `VERSION`). A client tagged `v0.2.x` speaks the wire contract of
|
||||||
st-peter `v0.2.1`. The gRPC wire format is backward-compatible across patch
|
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
|
releases (field numbers and enum integer values are stable), so a client one
|
||||||
patch behind a server generally interoperates — but match versions for new
|
patch behind a server generally interoperates — but match versions for new
|
||||||
surface.
|
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
|
- `verify_two_factor(two_factor_id, code)` — complete an OTP challenge
|
||||||
- `lookup_user(actor_id, actor_token, identifier)` — resolve a user by
|
- `lookup_user(actor_id, actor_token, identifier)` — resolve a user by
|
||||||
email/phone/handle
|
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
|
- `bearer(...)` helper + the shared `aura_session` cookie name
|
||||||
|
|
||||||
The **admin surface** (`AdminClient` — Rust first; Go/TS expose the generated
|
The **admin surface** (`AdminClient` — Rust first; Go/TS expose the generated
|
||||||
|
|
@ -87,17 +90,17 @@ Then tag at `v$(cat VERSION)`.
|
||||||
Rust (`Cargo.toml`):
|
Rust (`Cargo.toml`):
|
||||||
|
|
||||||
```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:
|
Go:
|
||||||
|
|
||||||
```bash
|
```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`):
|
TypeScript (`package.json`):
|
||||||
|
|
||||||
```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
|
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
|
// LoginOutcome is the result of Login / VerifyTwoFactor. Exactly one of the
|
||||||
// three fields is set.
|
// three fields is set.
|
||||||
type LoginOutcome struct {
|
type LoginOutcome struct {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "st-peter-client"
|
name = "st-peter-client"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Official Rust client for st-peter (aura-users) — authentication over gRPC with a token-verify cache"
|
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"
|
repository = "https://git.awesomike.com/pub/st-peter-client"
|
||||||
|
|
|
||||||
|
|
@ -234,6 +234,99 @@ impl AuthClient {
|
||||||
Ok(interpret_auth_response(resp))
|
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).
|
/// Resolve a st-peter user by exact identifier (email / phone / handle).
|
||||||
///
|
///
|
||||||
/// Typical use: a service-admin role-grant page — resolve a colleague's
|
/// Typical use: a service-admin role-grant page — resolve a colleague's
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@st-peter/client",
|
"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",
|
"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",
|
"repository": "https://git.awesomike.com/pub/st-peter-client",
|
||||||
"license": "MIT OR Apache-2.0",
|
"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