Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,26 @@ Coverage includes actual PostgreSQL writes, all field types, SQL views, CSV outp
Forms portal endpoints, inclusive UTC date filtering, and full CSV exports are
specified in [docs/api.md](docs/api.md). The dev and production `/lets-talk` migration,
Kafka contract, seed command, and rollout order are in [docs/lets-talk.md](docs/lets-talk.md).

## Shared authentication and bus client (v6 refresh)

This branch uses `tc-core-library-js` **4.0.0-rc.1**. Use its named
`createJwtVerifier` / `createJwtAuthenticator`, `createM2MClient`, and
`createBusApiClient` exports for shared authentication and bus API publishing.
Service-specific roles, scopes, sessions and ownership checks remain in this service.
The former bus wrapper dependency and local JWT cryptography must not be reintroduced.

RSA verification requires an exact configured issuer and audience (`JWT_AUDIENCE`
for client tokens, `AUTH0_AUDIENCE` for machine/API tokens, or this service's explicit
issuer-policy settings). HMAC secrets must be at least 32 bytes; no default secret
is supplied. Expiry is enforced. Machine identity comes from the shared verifier,
not the presence of scopes. Supply real development credentials through the service's
existing environment configuration before deployment.

Bus publishers use `createBusApiClient(config).postEvent(event)` with the existing
event envelope and optional key. Configure `BUSAPI_URL` for v6 and the existing
OAuth credentials/token provider. The client uses bounded HTTPS requests and does
not retry POSTs automatically. Application retry/outbox behavior remains local.

See the [core migration guide](https://github.com/topcoder-platform/tc-core-library-js/blob/v4.0.0-rc.1/MIGRATION.md)
for the API mapping, compatibility requirements, and rollout scope.
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@
"pg": "8.23.0",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.2",
"tc-bus-api-wrapper": "github:topcoder-platform/tc-bus-api-wrapper#297a9c0adcdb97661257e7825bee9c3f5578b833",
"tc-core-library-js": "github:topcoder-platform/tc-core-library-js#323567bc50e433ae488b656f9f94e821ebaf3062"
"tc-core-library-js": "github:topcoder-platform/tc-core-library-js#v4.0.0-rc.1"
},
"devDependencies": {
"@eslint/js": "9.39.2",
Expand Down
276 changes: 8 additions & 268 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,3 @@ allowBuilds:
dtrace-provider: false
# The shared Bus API wrapper depends on Topcoder's Git-hosted core library.
blockExoticSubdeps: false
overrides:
tc-core-library-js: github:topcoder-platform/tc-core-library-js#323567bc50e433ae488b656f9f94e821ebaf3062
16 changes: 7 additions & 9 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { Reflector } from '@nestjs/core';
import type { Request } from 'express';
import jwt from 'jsonwebtoken';
import { middleware, type AuthenticatorRequest } from 'tc-core-library-js';
import { createJwtAuthenticator, type AuthenticatorRequest } from 'tc-core-library-js';
import { CONFIG, type AppConfig } from './config';

export type Permission = 'public' | 'manage' | 'report';
Expand All @@ -35,7 +35,7 @@ export interface ActorRequest extends Request {
@Injectable()
export class AuthGuard implements CanActivate {
private readonly authenticator: ReturnType<
typeof middleware.jwtAuthenticator
typeof createJwtAuthenticator
>;

/**
Expand All @@ -47,7 +47,9 @@ export class AuthGuard implements CanActivate {
@Inject(CONFIG) private readonly config: AppConfig,
private readonly reflector: Reflector,
) {
this.authenticator = middleware.jwtAuthenticator({
this.authenticator = createJwtAuthenticator({
AUTH0_AUDIENCE: config.machineAudience,
JWT_AUDIENCE: config.jwtAudience,
AUTH_SECRET: config.authSecret,
VALID_ISSUERS: JSON.stringify(config.issuers),
});
Expand Down Expand Up @@ -101,7 +103,7 @@ export class AuthGuard implements CanActivate {
* @throws Error for unsupported algorithms, invalid tokens or missing time claims.
*/
private async authenticate(token: string): Promise<Record<string, unknown>> {
// The shared verifier does not call back for unsupported algorithms.
// Reject unsupported algorithms before invoking the shared verifier.
const decoded = jwt.decode(token, { complete: true });
if (!decoded || !['HS256', 'RS256'].includes(decoded.header.alg))
throw new Error('Unsupported token algorithm.');
Expand Down Expand Up @@ -148,11 +150,7 @@ export function normalizeActor(
',',
).map((r) => r.toLowerCase());
const scopes = stringList(claims.scope ?? claims.scopes, /\s+/);
const machine =
claims.gty === 'client-credentials' ||
claims.isMachine === true ||
(typeof claims.sub === 'string' && claims.sub.endsWith('@clients')) ||
(scopes.length > 0 && roles.length === 0);
const machine = claims.isMachine === true;
const userId = claims.userId ?? claims[`${namespace}userId`];
const memberId =
!machine &&
Expand Down
6 changes: 5 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'dotenv/config';
import type { BusApiConfiguration } from 'tc-bus-api-wrapper';
import type { BusApiConfiguration } from 'tc-core-library-js';

/** Runtime configuration validated once before Nest starts serving requests. */
export interface AppConfig {
Expand All @@ -8,6 +8,8 @@ export interface AppConfig {
port: number;
origins: string[];
authSecret: string;
jwtAudience?: string;
machineAudience?: string;
issuers: string[];
claimNamespace: string;
throttleLimit: number;
Expand Down Expand Up @@ -49,6 +51,8 @@ export function readConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
port: integerSetting(env.PORT ?? '3000', 1, 65535),
origins,
authSecret,
jwtAudience: env.JWT_AUDIENCE,
machineAudience: env.AUTH0_AUDIENCE,
issuers,
claimNamespace: env.AUTH_CLAIM_NAMESPACE ?? 'https://topcoder.com/',
throttleLimit: integerSetting(env.THROTTLE_LIMIT ?? '30', 1, 10000),
Expand Down
2 changes: 1 addition & 1 deletion src/integrations/event-bus.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
Injectable,
ServiceUnavailableException,
} from '@nestjs/common';
import createBusApiClient, { type BusApiClient } from 'tc-bus-api-wrapper';
import { createBusApiClient as createBusApiClient, type BusApiClient } from 'tc-core-library-js';
import { CONFIG, type AppConfig } from '../config';
import type { AnswerValue } from '../forms/validation';

Expand Down
33 changes: 0 additions & 33 deletions src/types/tc-bus-api-wrapper.d.ts

This file was deleted.

30 changes: 0 additions & 30 deletions src/types/tc-core-library-js.d.ts

This file was deleted.

1 change: 1 addition & 0 deletions test/auth-rs256.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ beforeAll(async () => {
'https://api.topcoder-dev.com',
]),
// Obsolete deployment settings must not keep rejecting Auth0 browser tokens.
JWT_AUDIENCE: 'BXWXUWnilVUPdN01t2Se29Tw2ZYNGZvH',
AUTH_MODE: 'hs256',
AUTH_AUDIENCE: 'forms-api',
}),
Expand Down
13 changes: 4 additions & 9 deletions test/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ describe('Topcoder bearer authentication', () => {
{ sub: 42, aud: 'forms-api' },
{ gty: 'client-credentials' },
{ isMachine: true },
{ scope: 'read:forms-submissions', roles: [] },
])('rejects invalid or nonlegacy claims %#', async (claims) => {
await expect(authenticate(claims)).rejects.toMatchObject({ status: 401 });
});
Expand All @@ -127,18 +126,14 @@ describe('Topcoder bearer authentication', () => {
},
);

it('keeps machine scope authorization separate from administrator roles', async () => {
it('rejects machine claims signed under the legacy audience-free human policy', async () => {
const machine = {
sub: 'client@clients',
aud: 'forms-api',
gty: 'client-credentials',
userId: undefined, roles: undefined,
};
await expect(authenticate(machine)).rejects.toMatchObject({ status: 403 });
expect(
await authenticate({ ...machine, scope: 'read:forms-submissions' }),
).toMatchObject({
machine: true,
memberId: undefined,
});
await expect(authenticate(machine)).rejects.toMatchObject({ status: 401 });
await expect(authenticate({ ...machine, scope: 'read:forms-submissions' })).rejects.toMatchObject({ status: 401 });
});
});
2 changes: 1 addition & 1 deletion test/event-bus.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { postEvent, createClient } = vi.hoisted(() => {
const postEvent = vi.fn();
return { postEvent, createClient: vi.fn(() => ({ postEvent })) };
});
vi.mock('tc-bus-api-wrapper', () => ({ default: createClient }));
vi.mock('tc-core-library-js', async (importOriginal) => ({ ...await importOriginal<typeof import('tc-core-library-js')>(), createBusApiClient: createClient }));

const baseEnv = {
DATABASE_URL: 'postgresql://localhost/forms',
Expand Down
2 changes: 1 addition & 1 deletion test/forms.integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
import { syncForm, type ManagedForm } from '../integrations/payload/forms';

const { postEvent } = vi.hoisted(() => ({ postEvent: vi.fn() }));
vi.mock('tc-bus-api-wrapper', () => ({ default: () => ({ postEvent }) }));
vi.mock('tc-core-library-js', async (importOriginal) => ({ ...await importOriginal<typeof import('tc-core-library-js')>(), createBusApiClient: () => ({ postEvent }) }));

describe('forms API with real PostgreSQL', () => {
let app: Awaited<ReturnType<typeof createApp>>;
Expand Down
Loading