Files
task-stream/libs/api/identity/infrastructure/src/lib/identity-infrastructure.module.ts
Josep Piguillem cbaad17fc7 fix(auth): separate Pireon clients for authorization code and password
Pireon OAuth clients allow a single grant type. With one PIREON_CLIENT_ID of
type password, email + password worked but "Sign in with Pireon" failed with
`unauthorized_client`.

- PIREON_CLIENT_ID/SECRET: authorization_code client (Sign in with Pireon,
  Swagger UI). New PIREON_PASSWORD_CLIENT_ID/SECRET: password client, falling
  back to the former so existing setups keep working.
- Refresh tries both clients: a refresh token only works with its issuer.
- PIREON_PASSWORD_GRANT=false now also rejects password grants in the API
  (before it only hid the form).
- /api/auth/config returns clientId: null when there is no authorization_code
  client, and the web hides "Sign in with Pireon".
- docker-compose passes the new variables; docs 05/07, .env.example,
  openapi.json; unit tests for the client selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:41:50 +02:00

52 lines
2.1 KiB
TypeScript

import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import {
IdentityProvider,
SessionCache,
UserRegistry,
} from '@task-stream/api/identity/application';
import { UserFactory } from '@task-stream/api/identity/domain';
import { MongoUserRegistry } from './mongo/mongo-user.registry';
import { UserDocument, UserSchema } from './mongo/user.schema';
import { PIREON_CONFIG, PireonConfig } from './pireon/pireon.config';
import { PireonIdentityProvider } from './pireon/pireon-identity.provider';
import { RedisSessionCache } from './redis/redis-session.cache';
/** Driven adapters of the identity context: Pireon (HTTP), Redis and MongoDB. */
@Module({
imports: [
MongooseModule.forFeature([
{ name: UserDocument.name, schema: UserSchema },
]),
],
providers: [
UserFactory,
{
provide: PIREON_CONFIG,
inject: [ConfigService],
useFactory: (config: ConfigService): PireonConfig => ({
baseUrl: config.getOrThrow<string>('PIREON_BASE_URL'),
sessionTtlSeconds: Number(config.get('AUTH_SESSION_TTL_SECONDS', 60)),
requestTimeoutMs: Number(config.get('PIREON_TIMEOUT_MS', 5000)),
clientId: config.get<string>('PIREON_CLIENT_ID') || undefined,
clientSecret: config.get<string>('PIREON_CLIENT_SECRET') || undefined,
passwordClientId:
config.get<string>('PIREON_PASSWORD_CLIENT_ID') || undefined,
passwordClientSecret:
config.get<string>('PIREON_PASSWORD_CLIENT_SECRET') || undefined,
scopes: String(config.get('PIREON_SCOPES', 'users:read'))
.split(/\s+/)
.filter(Boolean),
passwordGrant:
String(config.get('PIREON_PASSWORD_GRANT', 'true')) !== 'false',
}),
},
{ provide: IdentityProvider, useClass: PireonIdentityProvider },
{ provide: SessionCache, useClass: RedisSessionCache },
{ provide: UserRegistry, useClass: MongoUserRegistry },
],
exports: [IdentityProvider, SessionCache, UserRegistry],
})
export class IdentityInfrastructureModule {}