|
|
@@ -1,11 +1,18 @@
|
|
|
-import { Body, Controller, Logger, Post } from '@nestjs/common';
|
|
|
+import { BadRequestException, Body, ConflictException, Controller, Get, Logger, NotFoundException, Post, Session, UseGuards } from '@nestjs/common';
|
|
|
+import { JwtService } from '@nestjs/jwt';
|
|
|
+import { AuthenticatorTransportFuture } from '@simplewebauthn/server';
|
|
|
+import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
|
|
+import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard';
|
|
|
+import { serverConfig } from 'src/config';
|
|
|
import { LoginPayload, User } from 'src/interface/interface';
|
|
|
import { AuthService } from 'src/services/auth.service';
|
|
|
+import { UsersService } from 'src/services/user.service';
|
|
|
+import { WebauthnService } from 'src/services/webauthn.service';
|
|
|
|
|
|
@Controller('auth')
|
|
|
export class AuthController {
|
|
|
private logger: Logger = new Logger(`Auth Controller`)
|
|
|
- constructor(private authService: AuthService) { }
|
|
|
+ constructor(private authService: AuthService, private webauthnService: WebauthnService, private usersService: UsersService, private jwtService: JwtService) { }
|
|
|
|
|
|
@Post('register')
|
|
|
async register(@Body() body: { name: string, email: string; password: string }) {
|
|
|
@@ -13,7 +20,7 @@ export class AuthController {
|
|
|
let user: User = body
|
|
|
return this.authService.register(user);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
@Post('login')
|
|
|
login(@Body() body: { email?: string; password?: string }) {
|
|
|
this.logger.log(`logging in ${body.email}`)
|
|
|
@@ -23,4 +30,221 @@ export class AuthController {
|
|
|
return this.authService.login(body as LoginPayload);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ @Post('webauthn-register-options')
|
|
|
+ async getRegistrationOptions(
|
|
|
+ @Body() body: { username: string },
|
|
|
+ @Session() session: Record<string, any>,
|
|
|
+ ) {
|
|
|
+ this.logger.log(`Registring options...`)
|
|
|
+ if (!body.username?.trim()) {
|
|
|
+ throw new BadRequestException('Missing or empty username');
|
|
|
+ }
|
|
|
+
|
|
|
+ const user = await this.getUserFromDb(body.username);
|
|
|
+ if (!user) {
|
|
|
+ throw new NotFoundException('User not found');
|
|
|
+ }
|
|
|
+
|
|
|
+ const options = await this.webauthnService.generateRegistrationOptions(user);
|
|
|
+ session.challenge = options.challenge;
|
|
|
+ return options;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ @Post('webauthn-register')
|
|
|
+ async registerCredential(
|
|
|
+ @Body() body: any,
|
|
|
+ @Session() session: Record<string, any>,
|
|
|
+ ) {
|
|
|
+ this.logger.log(`Registring ${body.name}`)
|
|
|
+ const expectedChallenge = session.challenge;
|
|
|
+ if (!expectedChallenge) {
|
|
|
+ throw new BadRequestException('Missing challenge in session');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. Verify the registration response
|
|
|
+ const verification = await this.webauthnService.verifyRegistrationResponse(body, expectedChallenge);
|
|
|
+ const { verified, registrationInfo } = verification;
|
|
|
+
|
|
|
+ if (!verified || !registrationInfo) {
|
|
|
+ throw new BadRequestException('Registration verification failed');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. Check if user exists
|
|
|
+ let user = await this.usersService.findByEmail(body.email);
|
|
|
+
|
|
|
+ if (!user) {
|
|
|
+ // 👇 Create new user if not found
|
|
|
+ user = await this.usersService.createUser({
|
|
|
+ name: body.name,
|
|
|
+ email: body.email,
|
|
|
+ password: '', // or null/undefined if you're not using passwords for passkey
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 👇 Optional: prevent duplicate registration
|
|
|
+ if (user.devices?.length) {
|
|
|
+ throw new ConflictException('User already registered with WebAuthn');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. Sanitize transports
|
|
|
+ const rawTransports = body.response?.transports ?? [];
|
|
|
+ const allowedTransports: AuthenticatorTransport[] = ['usb', 'ble', 'nfc', 'internal'];
|
|
|
+
|
|
|
+ const transports = (rawTransports as AuthenticatorTransportFuture[]).filter(
|
|
|
+ (t): t is AuthenticatorTransport => allowedTransports.includes(t as AuthenticatorTransport),
|
|
|
+ );
|
|
|
+
|
|
|
+ // Optional: Log dropped transports
|
|
|
+ const dropped = rawTransports.filter(
|
|
|
+ t => !allowedTransports.includes(t as AuthenticatorTransport),
|
|
|
+ );
|
|
|
+ if (dropped.length) {
|
|
|
+ console.warn('Dropped unsupported transports:', dropped);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. Store device
|
|
|
+ user.devices = user.devices || [];
|
|
|
+ user.devices.push({
|
|
|
+ credentialID: isoBase64URL.toBuffer(registrationInfo.credential.id),
|
|
|
+ credentialPublicKey: registrationInfo.credential.publicKey,
|
|
|
+ counter: registrationInfo.credential.counter,
|
|
|
+ transports, // ✅ now correct
|
|
|
+ });
|
|
|
+
|
|
|
+ // 5. Create JWT and return
|
|
|
+ const payload = { sub: user.id, name: user.name, email: user.email };
|
|
|
+ const token = this.jwtService.sign(payload);
|
|
|
+ let response = {
|
|
|
+ verified: true,
|
|
|
+ access_token: token,
|
|
|
+ name: body.name,
|
|
|
+ };
|
|
|
+
|
|
|
+ return response
|
|
|
+ }
|
|
|
+
|
|
|
+ @Post('webauthn-login-options')
|
|
|
+ async getLoginOptions(@Body() body: { email: string }, @Session() session: Record<string, any>) {
|
|
|
+ if (!body.email) throw new BadRequestException('Missing email');
|
|
|
+
|
|
|
+ const user = await this.usersService.findByEmail(body.email);
|
|
|
+ if (!user || !user.devices?.length) {
|
|
|
+ throw new NotFoundException('User or devices not found');
|
|
|
+ }
|
|
|
+
|
|
|
+ const options = await this.webauthnService.generateAuthenticationOptions(user.devices);
|
|
|
+ session.challenge = options.challenge;
|
|
|
+ session.email = user.email;
|
|
|
+ return options;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ @Post('webauthn-login')
|
|
|
+ async verifyLogin(@Body() body: any, @Session() session: Record<string, any>) {
|
|
|
+ const expectedChallenge = session.challenge;
|
|
|
+ const email = session.email;
|
|
|
+
|
|
|
+ if (!expectedChallenge || !email) {
|
|
|
+ throw new BadRequestException('Missing challenge or email in session');
|
|
|
+ }
|
|
|
+
|
|
|
+ const user = await this.usersService.findByEmail(email);
|
|
|
+ if (!user || !user.devices?.length) {
|
|
|
+ throw new NotFoundException('User or devices not found');
|
|
|
+ }
|
|
|
+
|
|
|
+ const device = user.devices.find(
|
|
|
+ d => isoBase64URL.fromBuffer(d.credentialID) === body.id || body.rawId === isoBase64URL.fromBuffer(d.credentialID),
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!device) {
|
|
|
+ throw new NotFoundException('Device not registered');
|
|
|
+ }
|
|
|
+
|
|
|
+ const verification = await this.webauthnService.verifyAuthenticationResponse(
|
|
|
+ body,
|
|
|
+ expectedChallenge,
|
|
|
+ device.credentialPublicKey,
|
|
|
+ isoBase64URL.fromBuffer(device.credentialID),
|
|
|
+ device.counter,
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!verification.verified) {
|
|
|
+ throw new BadRequestException('Authentication failed');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Optional: update device counter
|
|
|
+ device.counter = verification.authenticationInfo.newCounter;
|
|
|
+
|
|
|
+ await this.usersService.updateUser(user);
|
|
|
+
|
|
|
+ const payload = { sub: user.id, name: user.name, email: user.email };
|
|
|
+ const token = this.jwtService.sign(payload);
|
|
|
+
|
|
|
+ return {
|
|
|
+ verified: true,
|
|
|
+ access_token: token,
|
|
|
+ name: user.name,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ @Post('auth-options')
|
|
|
+ async getAuthenticationOptions(@Body() body: any, @Session() session: Record<string, any>) {
|
|
|
+ const devices = await this.getDevicesFromDb(body.userID);
|
|
|
+ const options = await this.webauthnService.generateAuthenticationOptions(devices);
|
|
|
+ session.challenge = options.challenge;
|
|
|
+ return options;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Post('webauthn-authenticate')
|
|
|
+ async verifyAuthentication(@Body() body: any, @Session() session: Record<string, any>) {
|
|
|
+ const user = await this.getUserFromDb(body.userID);
|
|
|
+ const device = await this.getDeviceByCredentialID(body.rawId);
|
|
|
+
|
|
|
+ const verification = await this.webauthnService.verifyAuthenticationResponse(
|
|
|
+ body,
|
|
|
+ session.challenge,
|
|
|
+ device.credentialPublicKey,
|
|
|
+ device.credentialID,
|
|
|
+ device.counter,
|
|
|
+ );
|
|
|
+
|
|
|
+ if (verification.verified) {
|
|
|
+ // Update counter in DB
|
|
|
+ }
|
|
|
+
|
|
|
+ return verification;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Get(`server`)
|
|
|
+ @UseGuards(JwtAuthGuard)
|
|
|
+ getServerAPIurl(): string {
|
|
|
+ this.logger.log(`attempted acquisition`)
|
|
|
+ return serverConfig.exposedUrl
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // -- Mock DB lookups (replace with your DB calls) --
|
|
|
+ async getUserFromDb(userID: string) {
|
|
|
+ return {
|
|
|
+ id: userID,
|
|
|
+ username: `${userID}@user.com`,
|
|
|
+ devices: [], // Array of previously registered devices
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ async getDevicesFromDb(userID: string) {
|
|
|
+ return []; // Retrieve from DB
|
|
|
+ }
|
|
|
+
|
|
|
+ async getDeviceByCredentialID(id: string) {
|
|
|
+ return {
|
|
|
+ credentialID: id,
|
|
|
+ credentialPublicKey: new Uint8Array(), // from DB
|
|
|
+ counter: 0,
|
|
|
+ };
|
|
|
+ }
|
|
|
}
|