feat: switch oidc client to confidential (backend token exchange)
The provisioned IdP client (https://auth.forgecore.work) is confidential rather than public/PKCE-only, so a client secret must never reach the browser. The frontend now only performs the Authorization Code + PKCE redirect itself (hand-rolled PKCE, oidc-client-ts dependency removed) and hands the resulting code + verifier to a new, intentionally unauthenticated POST /api/v1/auth/session endpoint, which performs the code-for-tokens exchange server-side using OIDC_CLIENT_SECRET and returns only {accessToken, expiresIn} — refresh_token/id_token are never forwarded to the client. New required backend env vars: OIDC_CLIENT_ID, OIDC_CLIENT_SECRET. Added frontend/proxy.conf.json so the Angular dev server forwards /api and /health to the local API without needing CORS.
This commit is contained in:
@@ -6,12 +6,14 @@ import { HealthModule } from './health/health.module';
|
||||
import { VersionModule } from './version/version.module';
|
||||
import { UsersApiModule } from './users/users.module';
|
||||
import { TripsApiModule } from './trips/trips.module';
|
||||
import { AuthApiModule } from './auth/auth.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigurationModule,
|
||||
HealthModule,
|
||||
VersionModule,
|
||||
AuthApiModule,
|
||||
UsersApiModule,
|
||||
TripsApiModule,
|
||||
],
|
||||
|
||||
23
backend/apps/api/src/auth/auth-session.controller.spec.ts
Normal file
23
backend/apps/api/src/auth/auth-session.controller.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { AuthSessionController } from './auth-session.controller';
|
||||
|
||||
describe('AuthSessionController', () => {
|
||||
it('POST /auth/session exchanges the authorization code via the token exchange service', async () => {
|
||||
const tokenExchange = {
|
||||
exchangeAuthorizationCode: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ accessToken: 'at-1', expiresIn: 3600 }),
|
||||
};
|
||||
const controller = new AuthSessionController(tokenExchange as never);
|
||||
|
||||
const dto = {
|
||||
code: 'code-1',
|
||||
codeVerifier: 'verifier-1',
|
||||
redirectUri: 'http://localhost:4200/auth/callback',
|
||||
};
|
||||
await expect(controller.createSession(dto)).resolves.toEqual({
|
||||
accessToken: 'at-1',
|
||||
expiresIn: 3600,
|
||||
});
|
||||
expect(tokenExchange.exchangeAuthorizationCode).toHaveBeenCalledWith(dto);
|
||||
});
|
||||
});
|
||||
15
backend/apps/api/src/auth/auth-session.controller.ts
Normal file
15
backend/apps/api/src/auth/auth-session.controller.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { TokenExchangeService } from '../../../../libs/auth/src';
|
||||
import type { AuthorizationCodeExchangeRequest, AuthorizationCodeExchangeResult } from '../../../../libs/auth/src';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthSessionController {
|
||||
constructor(private readonly tokenExchange: TokenExchangeService) {}
|
||||
|
||||
@Post('session')
|
||||
createSession(
|
||||
@Body() dto: AuthorizationCodeExchangeRequest,
|
||||
): Promise<AuthorizationCodeExchangeResult> {
|
||||
return this.tokenExchange.exchangeAuthorizationCode(dto);
|
||||
}
|
||||
}
|
||||
7
backend/apps/api/src/auth/auth.module.ts
Normal file
7
backend/apps/api/src/auth/auth.module.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthSessionController } from './auth-session.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [AuthSessionController],
|
||||
})
|
||||
export class AuthApiModule {}
|
||||
Reference in New Issue
Block a user