Skip to main content

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 definitions
  • proto/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.
}
FieldTypeRequiredNotes
emailstringUnique per tenant. Lowercased before storage.
namestringDisplay name. Max 255 characters.
tenant_idstringUUID 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 statusCondition
ALREADY_EXISTSEmail already registered in this tenant
INVALID_ARGUMENTMissing required field or invalid email format
NOT_FOUNDtenant_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 statusCondition
NOT_FOUNDUser 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;
}
FieldTypeNotes
pagination.limitint32Max items per page. Default 20, max 100.
pagination.cursorstringOpaque 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 statusCondition
NOT_FOUNDUser ID does not exist
ALREADY_EXISTSNew email already registered in this tenant
INVALID_ARGUMENTInvalid 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 statusCondition
NOT_FOUNDUser 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:

ParameterValue
AlgorithmES256 (ECDSA P-256)
Access token TTL15 minutes
Refresh token TTL7 days with rotation
Signing key storageHashiCorp 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 statusCondition
UNAUTHENTICATEDInvalid email or password (constant-time comparison, anti-enumeration)
NOT_FOUNDTenant does not exist
PERMISSION_DENIEDUser 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 statusCondition
UNAUTHENTICATEDRefresh 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 statusCondition
UNAUTHENTICATEDToken is malformed, expired, or signature verification failed

ValidateToken never returns UNAUTHENTICATED for an expired or invalid token — it returns valid: false in the response body. UNAUTHENTICATED is 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.
}
FieldDescription
ancestor_idThe operator tenant asserting delegation authority.
child_idThe 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 statusCondition
NOT_FOUNDancestor_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 valueMeaning
activeTenant is operational — delegation allowed
suspendedSubscription overdue (8–37 days) — delegation blocked
deletedTenant soft-deleted — delegation blocked

gRPC errors:

gRPC statusCondition
NOT_FOUNDtenant_id does not exist

Security Notes

ConcernImplementation
Password storageArgon2id (NIST 800-63B), min 12 characters, unique salt per user
Token algorithmES256 (ECDSA P-256) — asymmetric; public key distributed to all verifiers
Anti-enumerationLogin always takes constant time regardless of whether email exists
MFATOTP (RFC 6238); if enabled, Login returns PERMISSION_DENIED with code MFA_REQUIRED — MFA verification happens at REST layer via POST /auth/mfa/verify
Refresh race10-second grace window (AUTH_REFRESH_GRACE_WINDOW_SEC=10) — concurrent tab refresh returns same token pair idempotently within window
Key rotationJWT signing key rotated every 90 days via HashiCorp Vault; old keys remain valid until all issued tokens expire