Appearance
Extensible authentication providers
Extensible authentication providers keep onboarding and login stable while provider options change by market, platform, and time.
Tags: NestJS Provider Port, Keycloak Federation, Provider Config, Web + Mobile PKCE
Area: Authentication / IdentityAccess
Keycloak remains the credential and federation boundary. CoachMe owns provider availability by product surface, app-facing provider metadata, verified identity normalization, audit, and the adapter contract that lets Google, Apple, Telegram, and future federated providers plug into the same coach and client flows.
Providers here are login identity, not messaging
This page covers federated login providers. WhatsApp and Telegram messaging are delivery channels for invitation links and one-time codes, not login providers — see the Integration Register. Telegram appears below only as a conditional OIDC login provider pending the sign-in spike; WhatsApp is not a login provider.
Provider Management And Runtime Flows
Provider logic is configured once and consumed by coach registration, client onboarding, and app session resolution without flow-specific provider branching.
Provider Discovery
- Web, coach mobile, or client mobile calls
GET /api/v1/auth/providerswith productsurface,platform,locale, optionalregion, and optional invitation context. - API reads enabled provider configuration loaded from deployment config files, filters providers by product surface, market, platform, and invitation policy, and hides disabled or unhealthy providers.
- API returns app-safe labels, display order, icon key, auth URL or Keycloak provider alias, required claims, and whether email or phone verification is expected.
- Client renders the list without hard-coding provider-specific registration or onboarding behavior.
Add Or Change Provider
- Deployment config defines a provider key, Keycloak alias, supported product surfaces, markets, verification requirements, and display metadata. Product admin UI for provider mutation is later.
- System validates that the provider adapter exists, Keycloak alias is reachable, redirect URLs are registered, and required claims can be normalized.
- Provider is enabled in
DRY_RUNorENABLEDmode during deployment and emits a configuration audit event. - Existing coach and client flows automatically pick up the provider through discovery, with no change to registration or onboarding flow code.
Verified Identity Normalization
- Keycloak completes OAuth/OIDC/password or chat-provider one-time-code handshake and issues a token to the client through PKCE.
- API bearer guard verifies issuer, audience, signature, expiry, and Keycloak client constraints.
- Provider adapter maps claims into
VerifiedProviderIdentity: provider ref, stable subject, email, phone, verification flags, display name, tenant, and raw claim summary. - Application services consume the normalized identity and never read provider-specific token fields directly.
Provider Outage Or Disable
- Health checks or admin action marks a provider
DISABLED,MAINTENANCE, orDEGRADED. - Provider discovery hides disabled providers or shows a temporary unavailable state, depending on product copy.
- Existing app sessions remain valid until normal token/session expiry unless the provider is disabled for a security incident.
- Security disables can trigger session revocation, audit review, and user-facing re-auth prompts with an alternate provider.
Implementation note
Provider extension means adding one adapter and one deployment configuration record. It must not require editing coach registration, invitation validation, client onboarding, or app session flow code.
Domain Model
IdentityAccess owns provider catalog, normalization policy, and provider health. Keycloak owns external federation secrets and protocol mechanics.
Aggregates & Entities
AuthProviderConfig: aggregate root for app-facing provider availability, display metadata, supported product surfaces, verification requirements, and lifecycle status.ProviderAdapter: application port implementation that normalizes Keycloak claims for one provider family.AuthIdentity: existing user-linked provider identity, unique by provider, issuer, tenant, and provider subject.ProviderHealthCheck: operational snapshot for discovery filtering, outage banners, and support visibility.ProviderConfigAuditEvent: append-only security/admin history for provider creation, disablement, secret rotation, and policy changes.
Value Objects & Rules
ProviderKey: stable internal key such asemail_password,google,apple, ortelegram; never use display names as identifiers.KeycloakProviderAlias: configured identity-provider alias used to build authorization URLs.ProductSurface: app surface where a provider can be shown, currentlyWEB,COACH_MOBILE, orCLIENT_MOBILE. Surface is not an authentication flow intent.VerificationPolicy: whether verified email, verified phone, or one of either is required before a CoachMe account can be created from that provider.ProviderStatus:DRAFT,DRY_RUN,ENABLED,MAINTENANCE,DISABLED,DEPRECATED.- Telegram satisfies phone verification when its consented
phoneclaim proves control of the account phone number (conditional, pending the OIDC login spike). WhatsApp is not a login provider; phone verification over WhatsApp is a delivery-channel code flow owned by onboarding, not a federated identity. - Client invitation onboarding requires either verified email or verified phone, then the invitation flow applies strict target binding.
- Provider subject uniqueness is scoped by provider, issuer, and tenant because enterprise/social providers can reuse subject values across issuers or tenants.
| Model | Owned By | Important Methods | Emits |
|---|---|---|---|
AuthProviderConfig | IdentityAccess | enable(), disable(), deprecate(), updateDisplay(), changePolicy() | AuthProviderEnabled, AuthProviderDisabled, AuthProviderPolicyChanged |
ProviderAdapterRegistry | IdentityAccess | getAdapter(), normalizeClaims(), assertRequiredClaims() | ProviderClaimsNormalized only for audit-safe telemetry |
ProviderHealthCheck | IdentityAccess / Operations | recordHealthy(), recordDegraded(), recordUnavailable() | AuthProviderHealthChanged |
Table Structure
PostgreSQL stores app-facing provider configuration and audit. Keycloak stores provider secrets, protocol setup, and external federation settings.
| Table | Purpose | Important Columns | Constraints & Indexes |
|---|---|---|---|
auth_provider_configs | App-facing provider catalog, policy, display metadata, and lifecycle state. | id, provider_key, keycloak_alias, status, display_name, icon_key, sort_order, supported_surfaces text[], supported_platforms text[], enabled_regions text[], verification_policy, metadata jsonb, created_at, updated_at, deprecated_at | Unique provider_key; unique active keycloak_alias; supported_surfaces contains product surface values only; indexes on status, supported_surfaces, sort_order; metadata cannot contain secrets. |
auth_provider_claim_mappings | Optional adapter mapping overrides for providers with non-standard claim names or market-specific payloads. | id, provider_config_id, claim_name, target_field, required, transform, created_at, updated_at | Unique (provider_config_id, target_field); allowed transforms are enum values, not executable code. |
auth_provider_health_checks | Latest and historical provider health snapshots for discovery, support, and operations. | id, provider_config_id, status, checked_at, latency_ms, error_code, details jsonb | Index (provider_config_id, checked_at desc); index on status; detailed rows retained for 30 days, then summarized or purged. |
auth_provider_audit_events | Append-only admin/security history for provider configuration changes. | id, provider_config_id, event_type, actor_user_id, old_values jsonb, new_values jsonb, reason, created_at | Index (provider_config_id, created_at desc); index (event_type, created_at); never application-soft-deleted; detailed metadata retained for 30 days before summarization or archival. |
auth_identities | Existing user-to-provider identity mapping used by registration and login. | provider, issuer, tenant, provider_subject, email_verified, phone_verified, raw_claims jsonb, unlinked_at | Unique active (provider, issuer, tenant, provider_subject); raw claims are minimized and retained for support/debug only. |
Lifecycle Policy
Provider records are configuration and audit assets. They should be disabled or deprecated, not casually deleted.
Provider Status Transitions
DRAFT: internal setup exists but clients never see it.DRY_RUN: visible to internal/admin testing only; health and claim normalization can be verified without public access.ENABLED: discoverable for configured product surfaces, platforms, and regions.MAINTENANCE: hidden or shown as temporarily unavailable; existing sessions continue unless security override is active.DISABLED: not discoverable and cannot start new login attempts; existing identity mappings remain for audit and recovery.DEPRECATED: still resolves existing sessions during a migration window, but cannot be chosen for new authentication starts.
Deletion And Retention
auth_provider_configs: no app soft delete. Use status plusdeprecated_at; hard deletion only in migrations before production use.auth_provider_claim_mappings: version through audit events and update in place while preserving old values in audit.auth_provider_health_checks: retain detailed rows for 30 days, then summarize or purge by scheduled retention job.auth_provider_audit_events: append-only with 30-day detailed metadata retention; redact secrets and PII before writing, then summarize or archive older rows.auth_identities: unlink withunlinked_at; do not delete identities while audit, fraud review, or account recovery can need them.
Guardrail
Secrets never live in CoachMe provider tables. OAuth client secrets, chat-provider credentials, and signing material remain in Keycloak or the deployment secret manager.
API Contracts
Public discovery is safe before login. V1 baseline provider configuration is read from deployment config files; admin mutation APIs are later operational tooling. Token normalization is internal to guards and application services.
| Endpoint | Purpose | Request | Response |
|---|---|---|---|
GET /api/v1/auth/providers | Return enabled providers for a product surface, platform, market, and optional invitation context. | surface, platform, optional region, locale, inviteToken | Ordered provider cards: key, label, icon, availability, Keycloak auth URL/alias, required verification, support copy. |
GET /api/v1/admin/auth/providers | List provider configuration and health for admin/support views. | Admin bearer token; optional status filter. | Provider configs, latest health, supported product surfaces, audit summary. |
POST /api/v1/admin/auth/providers | Post-V1: create a provider config after Keycloak/deployment setup exists. Not required for V1 baseline because config files are authoritative. | providerKey, keycloakAlias, display, supportedSurfaces, verificationPolicy, status | Created config and validation warnings. |
PATCH /api/v1/admin/auth/providers/{providerKey} | Post-V1: change provider status, display order, regions, product surfaces, or verification policy. V1 baseline changes ship through config-file deployment. | Partial config update plus required reason for status or policy changes. | Updated config and audit event id. |
POST /api/v1/admin/auth/providers/{providerKey}/health-check | Force a provider health and claim-normalization check. | Admin bearer token; optional dryRunClaims fixture key. | Health status, latency, missing claims, and setup warnings. |
Key Code Snippets
These snippets are implementation sketches for the planned TypeScript monorepo. They define the extension seam, not provider-specific business flows.
Provider adapter port
apps/api/src/identity-access/auth-providers/provider-adapter.ts
ts
export interface ProviderAdapter {
readonly providerKey: ProviderKey;
normalizeClaims(input: NormalizeProviderClaimsInput): VerifiedProviderIdentity;
buildAuthorizationContext(input: BuildProviderAuthContextInput): ProviderAuthContext;
assertConfiguration(config: AuthProviderConfig): ProviderConfigurationWarning[];
}
export type VerifiedProviderIdentity = {
ref: {
provider: ProviderKey;
issuer: string;
tenant?: string;
subject: string;
};
email?: string;
phone?: string;
emailVerified: boolean;
phoneVerified: boolean;
displayName?: string;
rawClaimSummary: Record<string, unknown>;
};Provider registry
apps/api/src/identity-access/auth-providers/provider-registry.ts
ts
@Injectable()
export class ProviderAdapterRegistry {
private readonly adapters = new Map<ProviderKey, ProviderAdapter>();
constructor(adapters: ProviderAdapter[]) {
for (const adapter of adapters) {
this.adapters.set(adapter.providerKey, adapter);
}
}
normalize(config: AuthProviderConfig, token: VerifiedKeycloakToken): VerifiedProviderIdentity {
const adapter = this.adapters.get(config.providerKey);
if (!adapter) throw new ApplicationError("AUTH_PROVIDER_ADAPTER_MISSING");
const identity = adapter.normalizeClaims({ config, claims: token.claims });
config.assertVerificationPolicy(identity);
return identity;
}
}Discovery query
apps/api/src/identity-access/use-cases/list-auth-providers.ts
ts
@Injectable()
export class ListAuthProvidersUseCase {
constructor(
private readonly providers: AuthProviderConfigRepository,
private readonly keycloakUrls: KeycloakAuthorizationUrlFactory,
) {}
async execute(input: ListAuthProvidersInput): Promise<AuthProviderCardDto[]> {
const configs = await this.providers.findDiscoverable({
surface: input.surface,
platform: input.platform,
region: input.region,
invitationContext: input.invitationContext,
});
return configs.map((config) => ({
key: config.providerKey,
label: config.displayNameFor(input.locale),
iconKey: config.iconKey,
status: config.publicAvailability(),
authUrl: this.keycloakUrls.forProvider(config.keycloakAlias, input.redirectUri),
requiredVerification: config.verificationPolicy,
}));
}
}Drizzle schema shape
apps/api/src/identity-access/db/auth-provider-schema.ts
ts
export const authProviderConfigs = pgTable("auth_provider_configs", {
id: uuid("id").primaryKey().defaultRandom(),
providerKey: text("provider_key").notNull(),
keycloakAlias: text("keycloak_alias").notNull(),
status: providerStatus("status").notNull().default("DRAFT"),
displayName: jsonb("display_name").$type<Record<LocaleCode, string>>().notNull(),
iconKey: text("icon_key").notNull(),
sortOrder: integer("sort_order").notNull().default(100),
supportedSurfaces: text("supported_surfaces").array().notNull(),
supportedPlatforms: text("supported_platforms").array().notNull(),
enabledRegions: text("enabled_regions").array().notNull().default(sql`ARRAY[]::text[]`),
verificationPolicy: verificationPolicy("verification_policy").notNull(),
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
deprecatedAt: timestamp("deprecated_at", { withTimezone: true }),
}, (table) => ({
providerKeyUnique: uniqueIndex("auth_provider_configs_provider_key_uq").on(table.providerKey),
aliasUnique: uniqueIndex("auth_provider_configs_keycloak_alias_uq").on(table.keycloakAlias),
statusIdx: index("auth_provider_configs_status_idx").on(table.status, table.sortOrder),
}));Events & Background Jobs
Provider changes are security-relevant and should be auditable. Operational checks should run outside request/response paths.
Domain And Integration Events
AuthProviderCreated: provider config created by migration or deployment config sync.AuthProviderEnabled/AuthProviderDisabled: status changes that affect discovery and login starts.AuthProviderPolicyChanged: verification, product surface, or region policy changed.AuthProviderHealthChanged: provider moved between healthy, degraded, unavailable, or maintenance states.ProviderClaimsNormalizationFailed: token claims could not satisfy adapter or verification policy.
Background Jobs
auth-provider-health-check: periodically validates Keycloak alias availability and optional provider metadata endpoints.auth-provider-audit-retention: archives or redacts detailed audit metadata older than 30 days.auth-provider-health-retention: keeps latest health records and summarizes or purges detailed operational rows older than 30 days.provider-disable-session-review: for security disables, finds active sessions and queues revocation or re-auth notices.
Permissions & Access Rules
Provider discovery is low-risk. Provider configuration and policy mutation are high-risk deployment operations for V1 baseline.
Public And Authenticated Access
- Anonymous users can call provider discovery only with safe filters; response must not expose secrets, internal IDs, raw Keycloak config, or unsupported provider metadata.
- Authenticated coaches and clients can see providers for their active app context only if their user status is active and the provider supports the requested product surface.
- Registration and onboarding flows must re-check provider config and verification policy after token verification; they cannot trust a provider key submitted by the client.
- Disabled, maintenance, and deprecated providers cannot start new authentication attempts unless an explicit internal/admin test flag is present.
Operations Access
- V1 baseline provider config is changed through reviewed deployment config files, not product admin UI.
- Deployment status and policy changes require a change reason and emit audit with actor or deployment identity, before/after values, and timestamp.
- Secret rotation is performed in Keycloak or secret manager, then referenced by provider config audit; secrets never appear in API payloads.
- Security disable can optionally revoke active sessions and block new authentication starts for the provider immediately.
Web, Coach Mobile, Client Mobile
All product surfaces consume the same provider discovery contract while keeping platform-specific redirect handling.
Coach Web
- Fetch provider cards for
WEBand render in configured display order. - Use Keycloak authorization URLs with web redirect URI and PKCE state.
- Show provider maintenance/unavailable copy without removing the whole auth screen.
- Settings can show the current provider identity for support and recovery context where policy allows.
Coach Mobile
- Fetch provider cards for
COACH_MOBILEand use Expo auth session/deep-link redirect handling with the same provider keys and labels returned by the API. - Cache provider list briefly for screen rendering, but refetch before starting auth to avoid launching a disabled provider.
- Support alternate provider prompts if one provider is disabled or fails during login.
Client Mobile
- During onboarding, call provider discovery for
CLIENT_MOBILEwith invitation validation context so only providers allowed for that invite/market are shown. - Never let provider login bypass invite validation or relationship creation rules.
- Preserve invite token and onboarding context through PKCE redirect using secure storage and server-side revalidation.
- Offline mode does not apply to starting authentication; show clear online-required states.
Test Scenarios & Resolved Decisions
Tests should prove V1 baseline providers are configured consistently and future providers can be added without editing existing registration and onboarding flows.
Tests
- Domain unit: provider config status transitions, verification policy enforcement, display filtering, and disabled/deprecated behavior.
- Adapter unit: email/password, Google, Apple, and Telegram claim fixtures normalize into the same
VerifiedProviderIdentityshape. - Repository integration: provider discovery filters by product surface, platform, region, status, and sort order.
- Repository integration: V1 baseline seed/config sync exposes email/password, Google, and Apple for configured markets, plus Telegram where the OIDC spike is proven.
- API integration:
GET /api/v1/auth/providershides disabled providers and never exposes secrets or internal config. - Config sync: create/update from deployment config requires a change reason for policy/status changes and writes audit rows.
- Verification policy: client onboarding accepts either verified email or verified phone, while invite consumption still enforces strict target binding.
- Provider behavior: Telegram federated login produces verified phone identity from a consented phone claim and can create/login users without email.
- Registration regression: coach registration and client onboarding flows use normalized identity only and reject tokens whose provider is disabled or fails verification policy.
- Retention: provider health checks and detailed provider audit metadata are summarized, archived, purged, or redacted after 30 days.
- Mobile/web E2E: provider list renders from API, starts PKCE flow, handles unavailable provider copy, and returns to the correct registration/onboarding step.
Resolved V1 Baseline Decisions
- All V1 baseline markets ship email/password, Google, and Apple login. Telegram is a conditional federated login provider pending the OIDC sign-in spike.
- WhatsApp is not a login provider. WhatsApp and Telegram messaging are delivery channels for invitation links and one-time codes, owned by the invitation and phone-verification flows, and never authenticate a user on their own.
- Phone number as a login credential (one-time code over SMS, WhatsApp, or Telegram) is deferred; it needs a custom Keycloak authenticator, not just a delivery channel.
- Provider config is managed through deployment config files for V1 baseline. Product admin UI can be added later.
- Minimum verified claim for client onboarding is either verified email or verified phone, with invitation target binding enforced by the onboarding flow.
- Provider health rows and detailed provider audit metadata are retained for 30 days before summarization, archival, purge, or redaction.