Auth Service — Getting Started
This guide gets you connected to the Auth service and making your first calls in 10 minutes.
What is Auth?
The Auth service handles authentication — proving that a user is who they claim to be. It manages email/password credentials, OAuth provider connections (Google, GitHub, Discord), JWT issuance, and refresh token lifecycle.
Auth delegates authorization (users, tenants, roles, permissions) to the IAM service. Auth proves identity. IAM decides what that identity can do.
Connection Setup
Import the generated stubs
Add the Auth module as a dependency:
go get github.com/TopengDev/aenoxa_auth@latest
Create a gRPC connection
package main
import (
"log"
authv1 "github.com/TopengDev/aenoxa_auth/gen/auth/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.NewClient("localhost:50052",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
defer conn.Close()
authClient := authv1.NewAuthServiceClient(conn)
healthClient := authv1.NewHealthServiceClient(conn)
}
Quick Walkthrough
1. Register a new user
resp, err := authClient.Register(ctx, &authv1.RegisterRequest{
Email: "user@example.com",
Password: "SecurePass123!",
IdempotencyKey: "reg-001",
})
// resp.Tokens.AccessToken — user-scoped JWT (15 min)
// resp.Tokens.RefreshToken — opaque refresh token (30 days)
// resp.UserId — IAM user ID
2. Log in
resp, err := authClient.Login(ctx, &authv1.LoginRequest{
Email: "user@example.com",
Password: "SecurePass123!",
})
3. Select a tenant
The access token from login/register is user-scoped — it contains sub (user ID) and email but no tenant context. To get a tenant-scoped token, call SelectTenant:
import "google.golang.org/grpc/metadata"
// Attach the user-scoped JWT
ctx := metadata.AppendToOutgoingContext(ctx, "authorization", "bearer "+resp.Tokens.AccessToken)
tenantResp, err := authClient.SelectTenant(ctx, &authv1.SelectTenantRequest{
TenantId: "your-tenant-uuid",
})
// tenantResp.AccessToken — tenant-scoped JWT with permissions
4. Refresh tokens
When the access token expires, use the refresh token to get a new pair:
refreshResp, err := authClient.RefreshToken(ctx, &authv1.RefreshTokenRequest{
RefreshToken: resp.Tokens.RefreshToken,
})
// refreshResp.Tokens.AccessToken — new access token
// refreshResp.Tokens.RefreshToken — new refresh token (old one is now invalid)
5. Log out
_, err := authClient.Logout(ctx, &authv1.LogoutRequest{
RefreshToken: refreshToken,
})
This revokes the refresh token family server-side. The client app should also discard its stored tokens locally.
Authentication
Most Auth endpoints are public (Register, Login, OAuth, RefreshToken, Logout). Four endpoints require a user-scoped JWT in the authorization header:
| Endpoint | Auth Required |
|---|---|
SelectTenant | Yes |
ListUserTenants | Yes |
LinkOAuth | Yes |
UnlinkOAuth | Yes |
Pass the JWT via gRPC metadata:
ctx := metadata.AppendToOutgoingContext(ctx, "authorization", "bearer "+accessToken)
resp, err := authClient.ListUserTenants(ctx, &authv1.ListUserTenantsRequest{})
Testing with grpcurl
# Health check
grpcurl -plaintext localhost:50052 auth.v1.HealthService/Check
# Register
grpcurl -plaintext -d '{
"email": "user@example.com",
"password": "SecurePass123!",
"idempotency_key": "reg-001"
}' localhost:50052 auth.v1.AuthService/Register
# Login
grpcurl -plaintext -d '{
"email": "user@example.com",
"password": "SecurePass123!"
}' localhost:50052 auth.v1.AuthService/Login
# Refresh token
grpcurl -plaintext -d '{
"refresh_token": "<raw_refresh_token>"
}' localhost:50052 auth.v1.AuthService/RefreshToken
# List tenants (requires JWT)
grpcurl -plaintext \
-H "authorization: bearer <access_token>" \
localhost:50052 auth.v1.AuthService/ListUserTenants
# Select tenant (requires JWT)
grpcurl -plaintext \
-H "authorization: bearer <access_token>" \
-d '{"tenant_id": "<uuid>"}' \
localhost:50052 auth.v1.AuthService/SelectTenant
# Logout
grpcurl -plaintext -d '{
"refresh_token": "<raw_refresh_token>"
}' localhost:50052 auth.v1.AuthService/Logout
Client Setup Pattern
Create the Auth connection once at startup and share it:
type AuthClient struct {
conn *grpc.ClientConn
Auth authv1.AuthServiceClient
Health authv1.HealthServiceClient
}
func NewAuthClient(addr string) (*AuthClient, error) {
conn, err := grpc.NewClient(addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
return nil, err
}
return &AuthClient{
conn: conn,
Auth: authv1.NewAuthServiceClient(conn),
Health: authv1.NewHealthServiceClient(conn),
}, nil
}
func (c *AuthClient) Close() error {
return c.conn.Close()
}
Error Handling
Auth returns standard gRPC status codes:
| Code | Meaning | Action |
|---|---|---|
OK | Success | Proceed normally |
INVALID_ARGUMENT | Bad input (empty email, weak password) | Don't retry — fix input |
ALREADY_EXISTS | Email already registered | Show user-facing message |
UNAUTHENTICATED | Bad credentials, expired/revoked token | Redirect to login |
NOT_FOUND | Resource not found (tenant, membership) | Check the ID |
FAILED_PRECONDITION | Invalid state (e.g., can't unlink last auth method) | Show user-facing message |
INTERNAL | Server error | Log and alert |
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
resp, err := authClient.Login(ctx, req)
if err != nil {
st, _ := status.FromError(err)
switch st.Code() {
case codes.Unauthenticated:
// Invalid email or password
case codes.InvalidArgument:
// Bad input
default:
log.Printf("auth error: %s: %s", st.Code(), st.Message())
}
}
Next Steps
- API Reference — Complete endpoint documentation
- Token Flow — Two-phase JWT model, refresh rotation, claims structure
- OAuth Guide — Setting up and using OAuth providers
- Event Catalog — Consuming auth domain events via RabbitMQ