Skip to content
Open
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
19 changes: 8 additions & 11 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { Body, Controller, Post } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { Controller, Post, UnauthorizedException } from '@nestjs/common';

@Controller('auth')
export class AuthController {
constructor(
private authService:AuthService
) {}
@Post('login')
async login(@Body() payload: any) {
const {expiry, ...authPayload}=payload
return this.authService.login(authPayload, expiry);
}
}
@Post('login')
async login() {
throw new UnauthorizedException(
'Direct login is disabled until wallet signature verification is implemented',
);
}
}
15 changes: 10 additions & 5 deletions backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import jwtConstants from './jwtConstants';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { getJwtSecret } from './jwt.config';

@Module({
imports: [
JwtModule.register({
JwtModule.registerAsync({
global: true,
secret:jwtConstants.secret
})
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: getJwtSecret(configService),
}),
}),
],
controllers: [AuthController],
providers: [AuthService, JwtService]
providers: [AuthService, JwtService],
})
export class AuthModule {}
73 changes: 73 additions & 0 deletions backend/src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { getJwtSecret } from './jwt.config';

const JWT_SECRET = 'test-secret-with-at-least-thirty-two-chars';

function createConfig(overrides: Record<string, string | undefined> = {}) {
const values = {
JWT_SECRET,
...overrides,
};

return {
get: jest.fn((key: string) => values[key]),
} as unknown as ConfigService;
}

describe('AuthService', () => {
it('requires JWT_SECRET to come from configuration', () => {
expect(() => getJwtSecret(createConfig({ JWT_SECRET: undefined }))).toThrow(
'JWT_SECRET must be set',
);
});

it('rejects unsupported caller-controlled JWT claims', async () => {
const service = new AuthService(new JwtService(), createConfig());

await expect(
service.login({ signature: 'sig', key: 'key', role: 'admin' }, '10y'),
).rejects.toThrow(BadRequestException);
});

it('signs only the DRep signature claims used by registration', async () => {
const service = new AuthService(new JwtService(), createConfig());

const { token } = await service.login(
{ signature: ' sig ', key: ' key ' },
'60s',
);
const decoded = await service.verifyJWT(token);

expect(decoded).toMatchObject({ signature: 'sig', key: 'key' });
expect(decoded.role).toBeUndefined();
expect(decoded.userId).toBeUndefined();
});

it('rejects tokens issued before JWT_NOT_BEFORE', async () => {
const issuedAt = Math.floor(Date.now() / 1000) - 60;
const token = await new JwtService().signAsync(
{ signature: 'sig', key: 'key', iat: issuedAt },
{ secret: JWT_SECRET, expiresIn: '1h' },
);
const service = new AuthService(
new JwtService(),
createConfig({ JWT_NOT_BEFORE: String(issuedAt + 1) }),
);

await expect(service.verifyJWT(token)).rejects.toThrow(
UnauthorizedException,
);
});
});

describe('AuthController', () => {
it('does not expose an unauthenticated token minting endpoint', async () => {
const controller = new AuthController();

await expect(controller.login()).rejects.toThrow(UnauthorizedException);
});
});
75 changes: 64 additions & 11 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,82 @@
import { Global, Injectable } from '@nestjs/common';
import {
BadRequestException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import jwtConstants from './jwtConstants';
import { getJwtNotBefore, getJwtSecret } from './jwt.config';

type DrepTokenPayload = {
signature: string;
key: string;
};

@Injectable()
export class AuthService {
constructor(private jwtService: JwtService) {}
async signJWT(payload: any, tte: number | string) {
const accessSecret = jwtConstants.secret;
constructor(
private jwtService: JwtService,
private configService: ConfigService,
) {}

async signJWT(payload: DrepTokenPayload, tte: number | string) {
const accessSecret = getJwtSecret(this.configService);
return this.jwtService.signAsync(payload, {
secret: accessSecret,
expiresIn: tte as string,
});
}

async verifyJWT(token: string) {
const accessSecret = jwtConstants.secret;
return this.jwtService.verifyAsync(token, {
const accessSecret = getJwtSecret(this.configService);
const decodedToken = await this.jwtService.verifyAsync(token, {
secret: accessSecret,
});

const tokenNotBefore = getJwtNotBefore(this.configService);
if (
tokenNotBefore !== null &&
typeof decodedToken.iat === 'number' &&
decodedToken.iat < tokenNotBefore
) {
throw new UnauthorizedException(
'JWT was issued before the configured rotation timestamp',
);
}

return decodedToken;
}
//the payload could consist of the
async login(payload: any, tte: number | string) {
//basically should check if the user signature is valid in the case of a drep or just provide a token for a normal user.
const token = await this.signJWT(payload, tte);

async login(payload: unknown, tte: number | string) {
const token = await this.signJWT(this.getDrepTokenPayload(payload), tte);
return { token };
}

async verifyLogin(token: string) {
// should check if there is an existing drep signature in the db. Well this is for dreps who have a profile.
}

private getDrepTokenPayload(payload: unknown): DrepTokenPayload {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new BadRequestException('Invalid login payload');
}

const { signature, key, ...extraClaims } = payload as Record<string, any>;
if (Object.keys(extraClaims).length > 0) {
throw new BadRequestException('Unsupported login claims');
}

if (typeof signature !== 'string' || signature.trim().length === 0) {
throw new BadRequestException('Missing DRep signature');
}

if (typeof key !== 'string' || key.trim().length === 0) {
throw new BadRequestException('Missing DRep signature key');
}

return {
signature: signature.trim(),
key: key.trim(),
};
}
}
30 changes: 30 additions & 0 deletions backend/src/auth/jwt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ConfigService } from '@nestjs/config';

const MIN_JWT_SECRET_LENGTH = 32;

export function getJwtSecret(configService: ConfigService): string {
const secret = configService.get<string>('JWT_SECRET');

if (!secret || secret.length < MIN_JWT_SECRET_LENGTH) {
throw new Error(
`JWT_SECRET must be set and at least ${MIN_JWT_SECRET_LENGTH} characters long`,
);
}

return secret;
}

export function getJwtNotBefore(configService: ConfigService): number | null {
const tokenNotBefore = configService.get<string>('JWT_NOT_BEFORE');
if (!tokenNotBefore) return null;

const numericTimestamp = Number(tokenNotBefore);
if (Number.isFinite(numericTimestamp)) return numericTimestamp;

const parsedTimestamp = Date.parse(tokenNotBefore);
if (!Number.isNaN(parsedTimestamp)) {
return Math.floor(parsedTimestamp / 1000);
}

throw new Error('JWT_NOT_BEFORE must be a Unix timestamp or ISO date');
}
5 changes: 0 additions & 5 deletions backend/src/auth/jwtConstants.ts

This file was deleted.