| Server IP : 65.108.144.40 / Your IP : 216.73.217.165 Web Server : Apache/2.4.52 (Ubuntu) System : Linux ubuntu-8gb-hel1-1 5.15.0-173-generic #183-Ubuntu SMP Fri Mar 6 13:29:34 UTC 2026 x86_64 User : dev ( 1000) PHP Version : 8.2.30 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : ON Directory : /home/dev/webapps/fivewishes-bridge/src/auth/ |
Upload File : |
import crypto from 'crypto';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { base64UrlDecodeToBuffer, base64UrlEncode } from '../common/base64url';
import { AppError } from '../common/errors';
import type { AuthState } from './auth.types';
@Injectable()
export class StateService {
constructor(private readonly config: ConfigService) {}
createState(input: Omit<AuthState, 'nonce' | 'created_at'> & Partial<Pick<AuthState, 'nonce'>>): {
state: string;
nonce: string;
} {
const nonce = input.nonce ?? base64UrlEncode(crypto.randomBytes(16));
const state: AuthState = {
nonce,
return_to: input.return_to,
created_at: Math.floor(Date.now() / 1000),
};
const payload = Buffer.from(JSON.stringify(state), 'utf8');
const sig = this.sign(payload);
return { state: `${base64UrlEncode(payload)}.${base64UrlEncode(sig)}`, nonce };
}
verifyState(stateParam: string): AuthState {
const [payloadB64, sigB64] = stateParam.split('.', 2);
if (!payloadB64 || !sigB64) {
throw new AppError('Invalid state', { statusCode: 400, expose: true });
}
const payload = base64UrlDecodeToBuffer(payloadB64);
const sig = base64UrlDecodeToBuffer(sigB64);
const expected = this.sign(payload);
if (!crypto.timingSafeEqual(sig, expected)) {
throw new AppError('Invalid state signature', { statusCode: 400, expose: true });
}
const parsed = JSON.parse(payload.toString('utf8')) as AuthState;
if (!parsed?.nonce || typeof parsed.created_at !== 'number') {
throw new AppError('Invalid state payload', { statusCode: 400, expose: true });
}
// 10 minute max age
const ageSec = Math.floor(Date.now() / 1000) - parsed.created_at;
if (ageSec < 0 || ageSec > 10 * 60) {
throw new AppError('State expired', { statusCode: 400, expose: true });
}
return parsed;
}
private sign(payload: Buffer): Buffer {
const secret = this.config.get<string>('STATE_SIGNING_SECRET');
if (!secret) throw new AppError('Missing STATE_SIGNING_SECRET');
return crypto.createHmac('sha256', secret).update(payload).digest();
}
}