Agach Identity System (internal/identity/)
Overview
Authentication, authorization, SSO, team management, and daemon node onboarding.
Domain Types
Core (domain/types.go)
UserID,TeamID— UUID-based identifiersMemberRole— "admin" or "member"Team— ID, Name, Slug, Description, timestampsUser— ID, Email, DisplayName, PasswordHash, SSOProvider, SSOSubject, Role, TeamID, timestampsActor— UserID, Email, Role +IsAdmin()method (request-scoped auth context)DaemonActor— NodeID, OwnerUserID, Mode +IsZero()method
Nodes (domain/node.go)
NodeID,OnboardingCodeID— UUID-based identifiersNodeMode— "default" or "shared"NodeStatus— "active" or "revoked"Node— ID, OwnerUserID, Name, Mode, Status, RefreshTokenHash, LastSeenAt, RevokedAt, timestampsOnboardingCode— ID, Code (6-digit), CreatedByUserID, NodeMode, NodeName, ExpiresAt, UsedAt, UsedByNodeIDNodeAccess— ID, NodeID, UserID (nullable), TeamID (nullable)
SSO Config (domain/ssoconfig.go)
SsoProvider— Name, Icon, SAML (nullable), OIDC (nullable)OIDCConfig— IssuerURL, ClientID, ClientSecret, RedirectURL, ScopesSAMLConfig— MetadataURL, EntityID, ACSURL, Certificate (not yet supported)
Token TTLs (domain/ttl.go)
- DefaultRefreshTokenTTL = 7 days
- DefaultRememberMeTokenTTL = 30 days
- DefaultDaemonJWTTTL = 30 days
Errors (domain/errors.go)
ErrUnauthorized, ErrForbidden, ErrUserNotFound, ErrInvalidCredentials, ErrEmailAlreadyExists, ErrSSOUserNoPassword, ErrTeamNotFound, ErrTeamSlugConflict, ErrSSOProviderNotFound, ErrSSONotSupported, ErrNodeNotFound, ErrNodeRevoked, ErrOnboardingCodeNotFound/Expired/Used
Repository Interfaces
UserRepository— Create, FindByID, FindByEmail, FindBySSO, Update, ListAll, ListByTeamTeamRepository— Create, FindByID, FindBySlug, List, Update, DeleteNodeRepository— Create, FindByID, ListByOwner, ListActiveByOwner, Update, UpdateLastSeenOnboardingCodeRepository— Create, FindByCode, MarkUsed (FOR UPDATE lock), DeleteExpiredNodeAccessRepository— GrantUser/Team, RevokeUser/Team, ListByNode, HasAccess (ON CONFLICT DO NOTHING)
Service Interfaces
AuthCommands— Register, Login, LoginSSO, RefreshToken, Logout, UpdateProfile, ChangePassword, RefreshDaemonTokenAuthQueries— ValidateJWT, ValidateDaemonJWT, GetCurrentUserTeamCommands— CreateTeam, UpdateTeam, DeleteTeam, AddUserToTeam, RemoveUserFromTeam, SetUserRoleTeamQueries— ListTeams, GetTeam, ListUsers, ListTeamMembersOnboardingCommands— GenerateCode, CompleteOnboardingNodeCommands— RevokeNode, UpdateNodeAccess, RenameNodeNodeQueries— ListNodes, GetNode
App Layer
Auth (app/auth.go)
- bcrypt cost 12, min password 8 chars, min secret 32 bytes
- Access token TTL: 15 minutes
- Refresh token: 7 days (30 days with remember_me)
- JWT claims: sub, email, role, token_type, iat, exp
- Daemon JWT claims: sub (nodeID), owner_id, mode, token_type
SSO (app/sso.go)
- OIDC only (SAML not yet supported)
- Full flow: discovery → code exchange → JWK fetch → ID token validation
- RSA + ECDSA (P-256/P-384/P-521) key support
- Auto-creates user on first SSO login
Teams (app/teams.go)
- All mutations require
actor.IsAdmin()
Onboarding (app/onboarding.go)
- 6-digit numeric codes with uniqueness retries (3 attempts)
- 15-minute code expiry
- Refresh token: 32 random bytes, bcrypt hashed (cost 12)
Nodes (app/nodes.go)
- All mutations require node ownership (node.OwnerUserID == actor.UserID)
- UpdateNodeAccess requires NodeModeShared
HTTP Routes
Auth (rate limited: 5 requests / 15 minutes per IP)
POST /api/auth/register — Register new user
POST /api/auth/login — Login (email/password, remember_me)
POST /api/auth/refresh — Refresh access token (refresh_token cookie)
POST /api/auth/logout — Clear refresh_token cookie
GET /api/auth/me — Get current user
PATCH /api/auth/me — Update display name
POST /api/auth/me/password — Change password
SSO
GET /api/auth/sso/providers — List configured providers
GET /api/auth/sso/{provider}/authorize — OIDC authorization initiation
GET /api/auth/sso/{provider}/callback — OIDC callback (returns #sso_token=...)
Teams
GET /api/identity/teams — List teams
POST /api/identity/teams — Create team (admin)
DELETE /api/identity/teams/{id} — Delete team (admin)
GET /api/identity/users — List users (admins see emails)
PUT /api/identity/users/{id}/team — Add user to team (admin)
DELETE /api/identity/users/{id}/team — Remove from team (admin)
PUT /api/identity/users/{id}/role — Set user role (admin)
Onboarding & Nodes
POST /api/onboarding/codes — Generate onboarding code (auth required)
POST /api/onboarding/complete — Complete onboarding (unauthenticated)
POST /api/daemon/refresh — Refresh daemon token (unauthenticated)
GET /api/nodes — List nodes (auth required)
GET /api/nodes/{id} — Get node (ownership required)
DELETE /api/nodes/{id} — Revoke node (ownership required)
PATCH /api/nodes/{id}/name — Rename node
PUT /api/nodes/{id}/access — Update access grants (shared mode only)
Security
- Bcrypt password hashing (cost 12)
- HMAC-SHA256 JWT signing (≥ 32 byte secret)
- pgp_sym_encrypt for sensitive DB columns (password_hash, sso_subject)
- HttpOnly, Secure, SameSite=Strict cookies
- OIDC state verification with HMAC signatures
- Default admin seeded on first run: admin@agach.local / admin (env overridable)
Init Wiring (init.go)
- Create repositories (runs migrations)
- Create SSOService if providers configured
- Wire auth service (with/without nodes)
- Wire team, onboarding, node services
- Seed default admin if no users exist
- Return System struct with all services