IAM Integration Quick Start
This guide gets you connected to IAM and making your first calls in 15 minutes.
What is IAM?
The IAM service is the source of truth for identity and access control. It manages realms, tenants, users, memberships, roles, permissions, and invitations. It does not handle authentication (login, passwords, OAuth) or issue tokens — that belongs in a separate auth service.
Connection Setup
Import the generated stubs
Add the IAM module as a dependency:
go get github.com/TopengDev/aenoxa_iam@latest
Create a gRPC connection
package main
import (
"log"
iamv1 "github.com/TopengDev/aenoxa_iam/gen/iam/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.NewClient("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
defer conn.Close()
// Create service clients
realmClient := iamv1.NewRealmServiceClient(conn)
tenantClient := iamv1.NewTenantServiceClient(conn)
userClient := iamv1.NewUserServiceClient(conn)
membershipClient := iamv1.NewMembershipServiceClient(conn)
roleClient := iamv1.NewRoleServiceClient(conn)
invitationClient := iamv1.NewInvitationServiceClient(conn)
apiKeyClient := iamv1.NewAPIKeyServiceClient(conn) // requires bootstrap key
}
Authentication
IAM supports two authentication modes (disabled by default). When enabled, every request must include credentials.
API Key (service-to-service)
IAM supports two types of API keys:
- Bootstrap keys — configured via the
IAM_AUTH_APIKEYSenvironment variable. These are root-of-trust credentials with full access to all endpoints, including API key management. - Database-backed keys — created at runtime via the
APIKeyService. These can access all endpoints except the APIKeyService itself (preventing privilege escalation).
Use API keys when your service calls IAM directly. Set the key via gRPC metadata:
import "google.golang.org/grpc/metadata"
ctx := metadata.AppendToOutgoingContext(ctx, "x-api-key", "your-api-key")
resp, err := tenantClient.CreateTenant(ctx, req)
JWT (user-context forwarding)
Use this when forwarding an end-user's identity. Pass the JWT via the authorization header:
ctx := metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+jwtToken)
resp, err := tenantClient.CreateTenant(ctx, req)
JWT claims structure:
| Claim | Type | Description |
|---|---|---|
sub | string (UUID) | User ID |
tenant_id | string (UUID) | Tenant context |
membership_id | string (UUID) | Membership ID (for permission checks via CheckPermission) |
email | string | User email |
scope | string | Space-separated permissions |
iss | string | Must match configured issuer |
aud | string[] | Must include configured audience |
Recommended pattern
Create a helper that attaches auth to every outgoing call:
func withAPIKey(ctx context.Context, key string) context.Context {
return metadata.AppendToOutgoingContext(ctx, "x-api-key", key)
}
Or use a gRPC unary interceptor for automatic injection:
func apiKeyInterceptor(key string) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply any,
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
ctx = metadata.AppendToOutgoingContext(ctx, "x-api-key", key)
return invoker(ctx, method, req, reply, cc, opts...)
}
}
conn, _ := grpc.NewClient("iam:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(apiKeyInterceptor("your-api-key")),
)
Walkthrough: Complete RBAC Setup
This example creates a full identity hierarchy and checks a permission.
package main
import (
"context"
"fmt"
"log"
iamv1 "github.com/TopengDev/aenoxa_iam/gen/iam/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.NewClient("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
ctx := context.Background()
realmClient := iamv1.NewRealmServiceClient(conn)
tenantClient := iamv1.NewTenantServiceClient(conn)
userClient := iamv1.NewUserServiceClient(conn)
membershipClient := iamv1.NewMembershipServiceClient(conn)
roleClient := iamv1.NewRoleServiceClient(conn)
// 1. Create a realm
realm, err := realmClient.CreateRealm(ctx, &iamv1.CreateRealmRequest{
Key: "saas",
Name: "SaaS Platform",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Realm: %s\n", realm.Realm.Id)
// 2. Create a tenant
tenant, err := tenantClient.CreateTenant(ctx, &iamv1.CreateTenantRequest{
RealmId: realm.Realm.Id,
Slug: "acme",
DisplayName: "Acme Corp",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Tenant: %s\n", tenant.Tenant.Id)
// 3. Create a user
email := "admin@acme.com"
user, err := userClient.CreateUser(ctx, &iamv1.CreateUserRequest{
Email: &email,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("User: %s\n", user.User.Id)
// 4. Create a membership (user joins tenant)
membership, err := membershipClient.CreateMembership(ctx, &iamv1.CreateMembershipRequest{
TenantId: tenant.Tenant.Id,
UserId: user.User.Id,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Membership: %s\n", membership.Membership.Id)
// 5. Create a role
role, err := roleClient.CreateRole(ctx, &iamv1.CreateRoleRequest{
TenantId: tenant.Tenant.Id,
Key: "admin",
Name: "Administrator",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Role: %s\n", role.Role.Id)
// 6. List permissions (seeded at DB init)
roles, err := roleClient.ListRoles(ctx, &iamv1.ListRolesRequest{
TenantId: tenant.Tenant.Id,
})
if err != nil {
log.Fatal(err)
}
// 7. Add a permission to the role
// Use a known permission key - get its ID first
// Permissions are global (not tenant-scoped) and seeded at startup:
// tenants.read, tenants.write, tenants.suspend,
// users.read, users.write, users.suspend,
// memberships.read, memberships.write, memberships.suspend,
// roles.read, roles.write, roles.assign,
// invitations.read, invitations.write, invitations.manage
_ = roles // use ListRoles or GetRole to find permission IDs
// 8. Assign role to membership
_, err = roleClient.AssignRole(ctx, &iamv1.AssignRoleRequest{
MembershipId: membership.Membership.Id,
RoleId: role.Role.Id,
})
if err != nil {
log.Fatal(err)
}
fmt.Println("Role assigned")
// 9. Check permission
check, err := roleClient.CheckPermission(ctx, &iamv1.CheckPermissionRequest{
MembershipId: membership.Membership.Id,
PermissionKey: "tenants.read",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("tenants.read allowed: %v\n", check.Allowed)
}
Client Setup Patterns
Singleton client
Create the IAM connection once at startup and share it across your application:
type IAMClient struct {
conn *grpc.ClientConn
Realms iamv1.RealmServiceClient
Tenants iamv1.TenantServiceClient
Users iamv1.UserServiceClient
Memberships iamv1.MembershipServiceClient
Roles iamv1.RoleServiceClient
Invitations iamv1.InvitationServiceClient
APIKeys iamv1.APIKeyServiceClient // requires bootstrap key
}
func NewIAMClient(addr, apiKey string) (*IAMClient, error) {
conn, err := grpc.NewClient(addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(apiKeyInterceptor(apiKey)),
)
if err != nil {
return nil, err
}
return &IAMClient{
conn: conn,
Realms: iamv1.NewRealmServiceClient(conn),
Tenants: iamv1.NewTenantServiceClient(conn),
Users: iamv1.NewUserServiceClient(conn),
Memberships: iamv1.NewMembershipServiceClient(conn),
Roles: iamv1.NewRoleServiceClient(conn),
Invitations: iamv1.NewInvitationServiceClient(conn),
APIKeys: iamv1.NewAPIKeyServiceClient(conn),
}, nil
}
func (c *IAMClient) Close() error {
return c.conn.Close()
}
Set deadlines
Always set a deadline on your context. IAM applies a default 10s timeout if none is set, but your service should set its own:
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
resp, err := iam.Tenants.GetTenant(ctx, &iamv1.GetTenantRequest{Id: tenantID})
Handle errors
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
resp, err := iam.Users.CreateUser(ctx, req)
if err != nil {
st, ok := status.FromError(err)
if !ok {
return fmt.Errorf("unexpected error: %w", err)
}
switch st.Code() {
case codes.AlreadyExists:
// Safe to treat as success — resource already created
case codes.InvalidArgument:
// Bad input — fix and don't retry
case codes.NotFound:
// Referenced resource doesn't exist
case codes.ResourceExhausted:
// Rate limited — backoff and retry
case codes.Unavailable:
// Transient — retry with backoff
default:
return fmt.Errorf("IAM error: %s: %s", st.Code(), st.Message())
}
}
Next Steps
- API Reference — Complete endpoint documentation
- Event Catalog — Consuming IAM domain events via RabbitMQ
- Integration Patterns — RBAC enforcement, multi-tenancy, retry strategies