IAM gRPC Service Reference
Package platform.iam.v1 contains three gRPC services that together
implement the Authentication & Identity Management primitive. All services are
implemented by the IAM Service (services/iam/) and consumed primarily
by the API Gateway.
See the gRPC Overview for buf configuration, Go package conventions, deadlines, mTLS, and error mapping.
Proto files:
proto/platform/iam/v1/iam_service.proto— service definitionsproto/platform/iam/v1/iam_messages.proto— request/response messages
Go package: kernel.internal/platform-kernel/gen/go/platform/iam/v1;iamv1
IamService
User lifecycle management. Consumed by Gateway (/api/v1/users/* endpoints)
and by the Data Layer (tenant-scoped permission checks).
service IamService {
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc RetrieveUser(RetrieveUserRequest) returns (RetrieveUserResponse);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
}
CreateUser
Creates a new user within a tenant. The IAM service hashes the password with Argon2id and sends a verification email.
Request — CreateUserRequest
message CreateUserRequest {
string email = 1; // Required. Must be unique within the tenant.
string name = 2; // Required. Display name.
string tenant_id = 3; // Required. UUID of the owning tenant.
}
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | ✅ | Unique per tenant. Lowercased before storage. |
name | string | ✅ | Display name. Max 255 characters. |
tenant_id | string | ✅ | UUID v7 of the tenant. |
Response — CreateUserResponse
message CreateUserResponse {
string id = 1; // UUID v7 of the created user
string email = 2;
string name = 3;
string created_at = 4; // ISO 8601 UTC
}
gRPC errors:
| gRPC status | Condition |
|---|---|
ALREADY_EXISTS | Email already registered in this tenant |
INVALID_ARGUMENT | Missing required field or invalid email format |
NOT_FOUND | tenant_id does not exist |
RetrieveUser
Fetches a single user by ID.
Request — RetrieveUserRequest
message RetrieveUserRequest {
string id = 1; // Required. User UUID v7.
}
Response — RetrieveUserResponse
message RetrieveUserResponse {
string id = 1;
string email = 2;
string name = 3;
string created_at = 4; // ISO 8601 UTC
}
gRPC errors:
| gRPC status | Condition |
|---|---|
NOT_FOUND | User ID does not exist |
ListUsers
Cursor-paginated list of users. Defaults to all users; call GET /api/v1/users
for REST access with richer filtering by status.
Request — ListUsersRequest
message ListUsersRequest {
platform.common.v1.PaginationRequest pagination = 1;
}
| Field | Type | Notes |
|---|---|---|
pagination.limit | int32 | Max items per page. Default 20, max 100. |
pagination.cursor | string | Opaque cursor from previous ListUsersResponse.meta.cursor. |
Response — ListUsersResponse
message ListUsersResponse {
repeated RetrieveUserResponse data = 1;
platform.common.v1.PaginationMeta meta = 2;
}
UpdateUser
Partial update — only fields set in the request are changed (optional
fields: omitting a field leaves it unchanged).
Request — UpdateUserRequest
message UpdateUserRequest {
string id = 1; // Required. User UUID v7.
optional string name = 2; // Omit to keep current value.
optional string email = 3; // Omit to keep current value.
}
Response — UpdateUserResponse
message UpdateUserResponse {
string id = 1;
string email = 2;
string name = 3;
string updated_at = 4; // ISO 8601 UTC
}
gRPC errors:
| gRPC status | Condition |
|---|---|
NOT_FOUND | User ID does not exist |
ALREADY_EXISTS | New email already registered in this tenant |
INVALID_ARGUMENT | Invalid email format |
DeleteUser
Soft-delete: marks the user as deleted, blocks login, preserves all data.
This matches the REST DELETE /api/v1/users/:id semantics.
Request — DeleteUserRequest
message DeleteUserRequest {
string id = 1; // Required. User UUID v7.
}
Response — DeleteUserResponse
message DeleteUserResponse {} // Empty — success signals deletion
gRPC errors:
| gRPC status | Condition |
|---|---|
NOT_FOUND | User ID does not exist |
AuthService
Token issuance and validation. Consumed exclusively by the API Gateway for all authentication flows.
service AuthService {
rpc Login(LoginRequest) returns (LoginResponse);
rpc Refresh(RefreshRequest) returns (RefreshResponse);
rpc ValidateToken(ValidateTokenRequest) returns (ValidateTokenResponse);
}
JWT Configuration:
| Parameter | Value |
|---|---|
| Algorithm | ES256 (ECDSA P-256) |
| Access token TTL | 15 minutes |
| Refresh token TTL | 7 days with rotation |
| Signing key storage | HashiCorp Vault, rotated every 90 days |
Login
Validates credentials and returns a JWT access + refresh token pair.
Request — LoginRequest
message LoginRequest {
string email = 1; // Required.
string password = 2; // Required. Plaintext — verified against Argon2id hash.
string tenant_id = 3; // Required. Scope the login to a specific tenant.
}
Response — LoginResponse
message LoginResponse {
string access_token = 1; // JWT ES256, 15-min TTL
string refresh_token = 2; // Opaque rotation token, 7-day TTL
int64 expires_in = 3; // Seconds until access_token expires (900)
string token_type = 4; // Always "Bearer"
}
gRPC errors:
| gRPC status | Condition |
|---|---|
UNAUTHENTICATED | Invalid email or password (constant-time comparison, anti-enumeration) |
NOT_FOUND | Tenant does not exist |
PERMISSION_DENIED | User account is suspended or deleted |
Refresh
Exchanges a valid refresh token for a new access + refresh token pair. Implements rotation — each refresh token is single-use (with 10-second grace window for concurrent tab protection).
Request — RefreshRequest
message RefreshRequest {
string refresh_token = 1; // Required. Opaque refresh token from Login or previous Refresh.
}
Response — RefreshResponse
message RefreshResponse {
string access_token = 1;
string refresh_token = 2; // NEW token — invalidates the previous one after grace window
int64 expires_in = 3;
string token_type = 4;
}
gRPC errors:
| gRPC status | Condition |
|---|---|
UNAUTHENTICATED | Refresh token is invalid, expired, or already consumed (outside grace window) |
ValidateToken
Lightweight token validation and claims extraction. Called by Gateway on every authenticated request — must be sub-millisecond. Result is cached in Valkey with a TTL aligned to the token's remaining lifetime.
Request — ValidateTokenRequest
message ValidateTokenRequest {
string token = 1; // Required. JWT access token (without "Bearer " prefix).
}
Response — ValidateTokenResponse
message ValidateTokenResponse {
bool valid = 1; // true if the token is valid and not expired
string user_id = 2; // UUID v7 of the authenticated user (empty if invalid)
string tenant_id = 3; // UUID v7 of the tenant (empty if invalid)
}
gRPC errors:
| gRPC status | Condition |
|---|---|
UNAUTHENTICATED | Token is malformed, expired, or signature verification failed |
ValidateTokennever returnsUNAUTHENTICATEDfor an expired or invalid token — it returnsvalid: falsein the response body.UNAUTHENTICATEDis reserved for internal errors (e.g., inability to fetch the public key from Vault). This design allows the Gateway to distinguish corrupt tokens from infrastructure failures.
TenantHierarchyService
Provides B2B2B ancestor/descendant queries used by the Gateway delegation
middleware. This service is separated from IamService (Single Responsibility
Principle) — hierarchy queries are a distinct bounded context from user CRUD.
Consumers: API Gateway delegation middleware only. Implementation: IAM Service — closure table in PostgreSQL (
tenant_ancestors), enabling O(1) ancestry checks regardless of hierarchy depth.
service TenantHierarchyService {
rpc IsDescendant(IsDescendantRequest) returns (IsDescendantResponse);
rpc GetTenantStatus(GetTenantStatusRequest) returns (GetTenantStatusResponse);
}
IsDescendant
Checks whether child_id is a direct or transitive descendant of
ancestor_id in the B2B2B hierarchy. Used to authorize cross-tenant
delegation (operator acting on behalf of a child tenant).
Request — IsDescendantRequest
message IsDescendantRequest {
string ancestor_id = 1; // UUID of the operator (parent) tenant.
string child_id = 2; // UUID of the target (child) tenant.
}
| Field | Description |
|---|---|
ancestor_id | The operator tenant asserting delegation authority. |
child_id | The target tenant the operator wants to act on behalf of. |
Response — IsDescendantResponse
message IsDescendantResponse {
bool is_descendant = 1; // true when child_id is a direct or transitive descendant
// of ancestor_id (depth > 0 in tenant_ancestors closure table).
}
gRPC errors:
| gRPC status | Condition |
|---|---|
NOT_FOUND | ancestor_id or child_id does not exist |
Delegation logic (Gateway middleware):
1. Extract operator tenantId + target tenantId from delegation header
2. IsDescendant(ancestor=operator, child=target) → must be true
3. GetTenantStatus(target) → status must be "active"
4. If both pass → allow; otherwise → 403 Forbidden
GetTenantStatus
Returns the current lifecycle status of a tenant. Called by the Gateway
to verify the delegate target is active before proceeding.
Request — GetTenantStatusRequest
message GetTenantStatusRequest {
string tenant_id = 1; // UUID of the tenant to query.
}
Response — GetTenantStatusResponse
message GetTenantStatusResponse {
string status = 1; // "active" | "suspended" | "deleted"
// Gateway rejects delegation if status != "active".
}
| Status value | Meaning |
|---|---|
active | Tenant is operational — delegation allowed |
suspended | Subscription overdue (8–37 days) — delegation blocked |
deleted | Tenant soft-deleted — delegation blocked |
gRPC errors:
| gRPC status | Condition |
|---|---|
NOT_FOUND | tenant_id does not exist |
Security Notes
| Concern | Implementation |
|---|---|
| Password storage | Argon2id (NIST 800-63B), min 12 characters, unique salt per user |
| Token algorithm | ES256 (ECDSA P-256) — asymmetric; public key distributed to all verifiers |
| Anti-enumeration | Login always takes constant time regardless of whether email exists |
| MFA | TOTP (RFC 6238); if enabled, Login returns PERMISSION_DENIED with code MFA_REQUIRED — MFA verification happens at REST layer via POST /auth/mfa/verify |
| Refresh race | 10-second grace window (AUTH_REFRESH_GRACE_WINDOW_SEC=10) — concurrent tab refresh returns same token pair idempotently within window |
| Key rotation | JWT signing key rotated every 90 days via HashiCorp Vault; old keys remain valid until all issued tokens expire |