auth.controller.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. import { BadRequestException, Body, ConflictException, Controller, Get, Logger, NotFoundException, Post, Session, UseGuards } from '@nestjs/common';
  2. import { JwtService } from '@nestjs/jwt';
  3. import { AuthenticatorTransportFuture } from '@simplewebauthn/server';
  4. import { isoBase64URL } from '@simplewebauthn/server/helpers';
  5. import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard';
  6. import { serverConfig } from 'src/config';
  7. import { UsersService } from 'src/users/user.service';
  8. import { WebauthnService } from 'src/auth/webauthn.service';
  9. import { LoginPayload, User } from 'src/interface/interface';
  10. @Controller('auth')
  11. export class AuthController {
  12. private logger: Logger = new Logger(`Auth Controller`)
  13. constructor(private webauthnService: WebauthnService, private usersService: UsersService, private jwtService: JwtService) { }
  14. @Post('webauthn-register-options')
  15. async getRegistrationOptions(
  16. @Body() body: { username: string },
  17. @Session() session: Record<string, any>,
  18. ) {
  19. this.logger.log(`Registering options...`)
  20. if (!body.username?.trim()) {
  21. throw new BadRequestException('Missing or empty username');
  22. }
  23. const user = await this.getUserFromDb(body.username);
  24. if (!user) {
  25. throw new NotFoundException('User not found');
  26. }
  27. const options = await this.webauthnService.generateRegistrationOptions(user);
  28. session.challenge = options.challenge;
  29. return options;
  30. }
  31. @Post('webauthn-register')
  32. async registerCredential(
  33. @Body() body: any,
  34. @Session() session: Record<string, any>,
  35. ) {
  36. this.logger.log(`Registering ${body.name}`)
  37. const expectedChallenge = session.challenge;
  38. if (!expectedChallenge) {
  39. throw new BadRequestException('Missing challenge in session');
  40. }
  41. // 1. Verify the registration response
  42. const verification = await this.webauthnService.verifyRegistrationResponse(body, expectedChallenge);
  43. const { verified, registrationInfo } = verification;
  44. if (!verified || !registrationInfo) {
  45. throw new BadRequestException('Registration verification failed');
  46. }
  47. // 2. Check if user exists
  48. let user = await this.usersService.findByEmail(body.email);
  49. if (!user) {
  50. // 👇 Create new user if not found
  51. user = await this.usersService.createUser({
  52. name: body.name,
  53. email: body.email,
  54. password: '', // or null/undefined if you're not using passwords for passkey
  55. });
  56. } else {
  57. // 👇 Optional: prevent duplicate registration
  58. if (user.devices?.length) {
  59. throw new ConflictException('User already registered with WebAuthn');
  60. }
  61. }
  62. // 3. Sanitize transports
  63. const rawTransports = body.response?.transports ?? [];
  64. const allowedTransports: AuthenticatorTransport[] = ['usb', 'ble', 'nfc', 'internal'];
  65. const transports = (rawTransports as AuthenticatorTransportFuture[]).filter(
  66. (t): t is AuthenticatorTransport => allowedTransports.includes(t as AuthenticatorTransport),
  67. );
  68. // Optional: Log dropped transports
  69. const dropped = rawTransports.filter(
  70. t => !allowedTransports.includes(t as AuthenticatorTransport),
  71. );
  72. if (dropped.length) {
  73. console.warn('Dropped unsupported transports:', dropped);
  74. }
  75. // 4. Store device
  76. user.devices = user.devices || [];
  77. user.devices.push({
  78. credentialID: isoBase64URL.toBuffer(registrationInfo.credential.id),
  79. credentialPublicKey: registrationInfo.credential.publicKey,
  80. counter: registrationInfo.credential.counter,
  81. transports, // ✅ now correct
  82. });
  83. // 5. Create JWT and return
  84. const payload = { sub: user.id, name: user.name, email: user.email };
  85. const token = this.jwtService.sign(payload);
  86. let response = {
  87. verified: true,
  88. access_token: token,
  89. name: body.name,
  90. };
  91. return response
  92. }
  93. @Post('webauthn-login-options')
  94. async getLoginOptions(
  95. @Body() body: { email?: string; passkey?: boolean },
  96. @Session() session: Record<string, any>,
  97. ) {
  98. if (body.passkey) {
  99. this.logger.log(`Generating passkey login options...`);
  100. const options = await this.webauthnService.generateAuthenticationOptions(); // No allowCredentials
  101. session.challenge = options.challenge;
  102. return options;
  103. }
  104. if (!body.email) {
  105. throw new BadRequestException('Missing email');
  106. }
  107. this.logger.log(`Generating login options for ${body.email}...`);
  108. const user = await this.usersService.findByEmail(body.email);
  109. if (!user || !user.devices?.length) {
  110. throw new NotFoundException('User or devices not found');
  111. }
  112. const options = await this.webauthnService.generateAuthenticationOptions(user.devices);
  113. session.challenge = options.challenge;
  114. session.email = user.email;
  115. return options;
  116. }
  117. @Post('webauthn-login')
  118. async verifyLogin(@Body() body: any, @Session() session: Record<string, any>) {
  119. const expectedChallenge = session.challenge;
  120. if (!expectedChallenge) {
  121. throw new BadRequestException('Missing challenge in session');
  122. }
  123. let user;
  124. // Case 1: email-based login
  125. if (session.email) {
  126. user = await this.usersService.findByEmail(session.email);
  127. } else {
  128. // Case 2: passkey login (discoverable credential)
  129. const credentialId = body.id || body.rawId;
  130. user = await this.usersService.findByCredentialId(credentialId);
  131. }
  132. if (!user || !user.devices?.length) {
  133. throw new NotFoundException('User or devices not found');
  134. }
  135. const credentialId = body.id || body.rawId;
  136. const device = user.devices.find(
  137. d => isoBase64URL.fromBuffer(d.credentialID) === credentialId,
  138. );
  139. if (!device) {
  140. throw new NotFoundException('Device not registered');
  141. }
  142. const verification = await this.webauthnService.verifyAuthenticationResponse(
  143. body,
  144. expectedChallenge,
  145. device.credentialPublicKey,
  146. isoBase64URL.fromBuffer(device.credentialID),
  147. device.counter,
  148. );
  149. if (!verification.verified) {
  150. throw new BadRequestException('Authentication failed');
  151. }
  152. device.counter = verification.authenticationInfo.newCounter;
  153. await this.usersService.updateUser(user);
  154. const payload = { sub: user.id, name: user.name, email: user.email };
  155. const token = this.jwtService.sign(payload);
  156. return {
  157. verified: true,
  158. access_token: token,
  159. name: user.name,
  160. };
  161. }
  162. @Post('auth-options')
  163. async getAuthenticationOptions(@Body() body: any, @Session() session: Record<string, any>) {
  164. const devices = await this.getDevicesFromDb(body.userID);
  165. const options = await this.webauthnService.generateAuthenticationOptions(devices);
  166. session.challenge = options.challenge;
  167. return options;
  168. }
  169. @Post('webauthn-authenticate')
  170. async verifyAuthentication(@Body() body: any, @Session() session: Record<string, any>) {
  171. const user = await this.getUserFromDb(body.userID);
  172. const device = await this.getDeviceByCredentialID(body.rawId);
  173. const verification = await this.webauthnService.verifyAuthenticationResponse(
  174. body,
  175. session.challenge,
  176. device.credentialPublicKey,
  177. device.credentialID,
  178. device.counter,
  179. );
  180. if (verification.verified) {
  181. // Update counter in DB
  182. }
  183. return verification;
  184. }
  185. @Post('register')
  186. async register(@Body() body: User) {
  187. this.logger.log(`[REGISTER] Incoming registration request for: ${body.email}`);
  188. if (!body.email || !body.password || !body.name) {
  189. this.logger.warn(`[REGISTER] Missing required fields`);
  190. throw new BadRequestException('Missing required fields');
  191. }
  192. let user;
  193. try {
  194. user = await this.usersService.createUser(body);
  195. this.logger.log(`[REGISTER] ✅ User created successfully: ${user.email}`);
  196. } catch (err) {
  197. this.logger.error(`[REGISTER] ❌ Failed to create user: ${err.message}`);
  198. throw new ConflictException(err);
  199. }
  200. // Generate JWT after successful registration
  201. const payload = { sub: user.id, name: user.name, email: user.email };
  202. const token = this.jwtService.sign(payload);
  203. this.logger.debug(`[REGISTER] Returning success response for: ${body.email}`);
  204. return {
  205. verified: true,
  206. access_token: token,
  207. name: user.name,
  208. };
  209. }
  210. @Post('login')
  211. async login(@Body() body: LoginPayload) {
  212. this.logger.log(`[LOGIN] Incoming login request for: ${body.email}`);
  213. if (!body.email || !body.password) {
  214. this.logger.warn(`[LOGIN] Missing email or password`);
  215. throw new BadRequestException('Missing email or password');
  216. }
  217. let user;
  218. try {
  219. user = await this.usersService.validateUser(body);
  220. this.logger.log(`[LOGIN] ✅ User validated: ${user.email}`);
  221. } catch (err) {
  222. this.logger.error(`[LOGIN] ❌ Invalid credentials or error: ${err.message}`);
  223. throw new BadRequestException(err);
  224. }
  225. // Generate JWT for login as well
  226. const payload = { sub: user.id, name: user.name, email: user.email };
  227. const token = this.jwtService.sign(payload);
  228. this.logger.debug(`[LOGIN] Returning success response for: ${body.email}`);
  229. return {
  230. verified: true,
  231. access_token: token,
  232. name: user.name,
  233. };
  234. }
  235. @Get(`server`)
  236. @UseGuards(JwtAuthGuard)
  237. getServerAPIurl(): string {
  238. this.logger.log(`attempted acquisition`)
  239. return serverConfig.exposedUrl
  240. }
  241. // -- Mock DB lookups (replace with your DB calls) --
  242. async getUserFromDb(userID: string) {
  243. return {
  244. id: userID,
  245. username: `${userID}@user.com`,
  246. devices: [], // Array of previously registered devices
  247. };
  248. }
  249. async getDevicesFromDb(userID: string) {
  250. return []; // Retrieve from DB
  251. }
  252. async getDeviceByCredentialID(id: string) {
  253. return {
  254. credentialID: id,
  255. credentialPublicKey: new Uint8Array(), // from DB
  256. counter: 0,
  257. };
  258. }
  259. }