Skip to Content
LGC — LoginCrew

LGC — LoginCrew

LGC is the centralized OAuth 2.0 / OIDC authentication service for the JinjiCrew ecosystem. It manages person identity and provides single sign-on (SSO) across all connected product apps.

Key principle: LGC stays pure identity. It knows nothing about companies, products, or subscriptions — that’s ADC’s job.

Services

ServicePortTechPurpose
lgc_api3100Go + Echo + MySQLOAuth backend, identity management
lgc_web3101Next.js + React 19 (npm)Authentication frontend (login flow)
lgc_cp3103Next.js + React 19 (Bun)Operator control panel

Infrastructure: MySQL 8 (port 13306), Valkey (Redis fork, for future caching).

Architecture

LGC is built with domain-driven design. Four bounded contexts, each with domain/app/infra layers:

Dependency flow: infra → app → domain (never the reverse). Cross-domain calls happen at the app layer (e.g., authentication app calls person app). Audit is the sink — everything writes into it, nothing reads from it.

See Architecture  for cross-system design patterns.

What LGC Does

  • Authentication — multi-step login flow: identify → verify → authorize → consent → token exchange
  • SSO — shared lgc_session cookie enables seamless cross-app authentication
  • Session management — two-layer session model, validation, userinfo, logout
  • Registration and provisioning — self-registration and bulk user provisioning with pre-created consents
  • Access controlrequires_grant gate and person app grants for restricted apps
  • Person management — polymorphic identifiers (email, phone, company code, Google)
  • OAuth client registry — each product app registers as an OAuth client
  • Audit logging — append-only audit, request, and security logs

Data Model

TableDomainKey ColumnsPurpose
personspersonxid, name_display, name_legalUser identity records
identitiespersonxid, person_id, type, identifier, challengeLogin methods (email, SMS, Google, company code) — polymorphic
clientsclientxid, client_id, allowed_scopes, requires_grantRegistered OAuth clients (product apps)
api_keysclientxid, client_id, key_hash, roleService-to-service authentication
person_app_grantsclientxid, person_id, client_id, access_levelPer-person access grants for restricted apps
sessionsauthenticationxid, person_id, client_id, token_hashActive auth sessions (hashed tokens)
authorization_codesauthenticationxid, code_hash, person_id, client_id, scopesOAuth authorization codes (hashed, single-use, 10-min TTL)
consent_recordsauthenticationperson_id, client_id, scopesUser consent to OAuth scopes
audit_logaudittarget_type, target_xid, action, agent_xidAppend-only event trail
request_logauditrequest_id, method, path, status_codeHTTP request log (query strings stripped)
security_logauditevent_type, identifier, ip_addressFailed/suspicious auth attempts

[!NOTE] XID pattern — every entity table has both id (internal auto-increment PK) and xid (char(20) external ID via rs/xid ). APIs, JWTs, and logs use xid — internal IDs never cross layer boundaries. FK resolution uses SQL subqueries: (SELECT id FROM persons WHERE xid = ?).

[!NOTE] Hard deletes — LGC uses hard deletes over soft deletes (privacy-first). The audit log preserves the event history. Sessions, authorization codes, and persons are deleted from their tables — no deleted_at columns. Grants are the exception: they use soft-delete via a revoked_at column so revocation history is visible in the control panel (see Access Control).

Scope Model

Scopes flow through three trust boundaries before reaching the consuming app:

  1. Allowed scopes — the ceiling set on the client. Requested scopes must be a subset.
  2. Consent — what the person approves on the consent screen. Stored for future logins.
  3. Auth code — the approved scopes embedded in the single-use authorization code, carried to token exchange.

Security Summary

ConcernMechanismDetail
Passwordsargon2idSalted, non-deterministic hash. Raw value never stored.
Lookup tokens (sessions, auth codes)SHA-256Deterministic hash for DB lookup. Raw value lives in cookie or shown once.
Verification tokensHMACSigned by server, 5-minute TTL. Bridges verify → authorize.
Client secretsargon2idHashed at registration, raw shown once.
Sessions24h absolute + 2h idleHard-deleted on logout or expiry.
Authorization codes10-min TTL, single-useused_at set on exchange; replay returns error.
AuditAppend-onlyThree tables, never updated or deleted.
DeletesHard delete (grants use soft-delete)Privacy-first. Audit log preserves event history. Grants soft-deleted via revoked_at.

[!TIP] API reference — full request/response schemas, examples, and error codes are in the rendered API docs .

Client Authentication

LGC has two distinct mechanisms for authenticating callers. They solve different problems and use different hashing strategies.

Client secrets

Standard OAuth 2.0 client authentication. A client secret proves “I am this OAuth client” during token exchange. The client backend sends client_id + client_secret in the POST /auth/token request body.

  • Stored on the clients table (client_secret column)
  • Hashed with argon2id (salted, non-deterministic) — same approach as passwords
  • Raw value shown once at client registration, never retrievable again
  • Confidential clients (server-side apps) have a secret; public clients (mobile) have NULL
  • Only used for one endpoint: POST /auth/token

API keys

Machine-to-machine authentication for role-gated endpoints. An API key proves “I have permission to call this category of endpoints.” Sent via the X-API-Key header, validated by middleware before the request reaches the handler.

  • Stored on the api_keys table (key_hash column)
  • Hashed with SHA-256 (deterministic) — looked up by hash, not compared like passwords
  • Each key belongs to exactly one OAuth client (client_id FK)
  • Each key has a role that controls which endpoints it can access
  • last_used_at bumped on each use

A single client can have one secret and multiple API keys with different roles. For example, jinjicrew might have a client_secret for token exchange, a service key for session validation, and a provisioner key for bulk user creation.

Why two hash strategies

MechanismHashReason
Client secretsargon2idCompared against a known hash (like passwords). Salted to resist rainbow tables.
API keysSHA-256Looked up by hash value in the DB. Must be deterministic so the same raw key always produces the same hash for WHERE key_hash = ?.

See API Key Roles for the full role breakdown.

API Reference

Login Flow

EndpointMethodPurpose
/auth/identifyPOSTParse identifier (email/phone/code), return identity type + challenge method
/auth/verifyPOSTValidate credentials, return verification token (5-min TTL)
/auth/authorizePOSTCheck client + scopes, return authorization code or consent_required
/auth/consentPOSTRecord scope approval, return authorization code

SSO

EndpointMethodPurpose
/auth/login-sessionPOSTCreate a client-unbound login session (lgc_session cookie)

Token Exchange

EndpointMethodPurpose
/auth/tokenPOSTExchange authorization code for session token (backend-to-backend)

Session Management

EndpointMethodPurpose
/auth/sessionGETValidate session, bump last_active_at
/auth/sessionDELETERevoke session (hard delete)
/auth/userinfoGETFetch person identity filtered by consented scopes

Person Management

EndpointMethodPurpose
/personsPOSTSelf-register a new person with primary identity

Provisioning

EndpointMethodAuthPurpose
/provisioning/personsPOSTAPI key (provisioner)Provision user with optional pre-created consent
/provisioning/persons/{xid}/challengePATCHAPI key (provisioner)Change a provisioned user’s password
/provisioning/persons/{xid}/identifierPATCHAPI key (provisioner)Change a provisioned user’s identifier

Admin

EndpointMethodAuthPurpose
/admin/personsGETAPI key (admin)List persons (paginated, searchable)
/admin/persons/{xid}GETAPI key (admin)Get person with identities and grants
/admin/persons/{xid}PATCHAPI key (admin)Update person name fields
/admin/persons/{xid}DELETEAPI key (admin)Delete person (hard delete)
/admin/clientsGETAPI key (admin)List all active clients
/admin/identitiesGETAPI key (admin)List identities for a person
/admin/identities/{xid}PATCHAPI key (admin)Update identity identifier
/admin/identities/{xid}/challengePATCHAPI key (admin)Set identity password
/admin/grantsPOSTAPI key (admin)Create a person app grant
/admin/grantsGETAPI key (admin)List grants by person or client
/admin/grants/{xid}PATCHAPI key (admin)Update grant access level
/admin/grants/{xid}DELETEAPI key (admin)Revoke a grant (soft delete via revoked_at)

[!TIP] Full API docs — see lgc-api.pages.dev  for complete OpenAPI documentation with request/response schemas and examples.

Domain Architecture

internal/domains/ person/ — persons + identities domain/ — entities, identifier parser, errors app/ — IdentifyPerson, VerifyChallenge, CreatePerson infra/ — SQLRepository authentication/ — sessions, authorization_codes, consent_records domain/ — entities with IsExpired/IsIdle/IsUsed/CoversScopes app/ — CreateSession, ValidateSession, RevokeSession, IssueAuthorizationCode, ExchangeAuthorizationCode, CheckConsent, RecordConsent infra/ — SQLRepository client/ — clients, api_keys, person_app_grants domain/ — Client (HasRedirectURI, AllowsScopes), APIKey, Grant app/ — FindClient, AuthenticateClient, grant CRUD infra/ — SQLRepository audit/ — audit_log, request_log, security_log (all append-only) domain/ — AuditLogEntry, RequestLogEntry, SecurityEvent app/ — RecordAuditLogEntry, RecordRequestLogEntry, RecordSecurityEvent infra/ — SQLRepository

Domain dependencies:

DomainDepends on
authenticationperson, client, audit
personaudit
clientaudit
auditnothing (the sink)

Local Development

cd lgc_main pairin # Starts db, api, web, cp in split-pane TUI

Or manually:

docker compose up # MySQL on port 13306 cd services/api && joka migrate up --auto && go run ./cmd/api # API on 3100 cd services/web && npm install && npm run dev # Web on 3101 cd services/cp && bun install && bun dev # CP on 3103
Last updated on