webauthn.service.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. // src/webauthn/webauthn.service.ts
  2. import {
  3. generateRegistrationOptions,
  4. verifyRegistrationResponse,
  5. generateAuthenticationOptions,
  6. verifyAuthenticationResponse,
  7. VerifiedRegistrationResponse,
  8. VerifiedAuthenticationResponse,
  9. RegistrationResponseJSON,
  10. } from '@simplewebauthn/server';
  11. import { Injectable, Logger } from '@nestjs/common';
  12. import { isoUint8Array } from '@simplewebauthn/server/helpers';
  13. import { isoBase64URL } from '@simplewebauthn/server/helpers';
  14. import { serverConfig } from 'src/config/config';
  15. @Injectable()
  16. export class WebauthnService {
  17. private logger: Logger = new Logger(`WebAuthn`)
  18. private rpName = serverConfig.rpName
  19. private rpID = serverConfig.rpId
  20. private origin = serverConfig.origin
  21. public devices: any[] = []; // In-memory device store
  22. constructor() {
  23. this.logger.log(`Web Authn service instantiated...`)
  24. }
  25. async generateRegistrationOptions(user: {
  26. id: string;
  27. username: string;
  28. devices: {
  29. credentialID: Uint8Array;
  30. transports?: AuthenticatorTransport[];
  31. }[];
  32. }) {
  33. return generateRegistrationOptions({
  34. rpName: this.rpName,
  35. rpID: this.rpID,
  36. userID: isoUint8Array.fromUTF8String(user.id),
  37. userName: user.username,
  38. timeout: 60000,
  39. attestationType: 'none',
  40. excludeCredentials: user.devices.map(dev => ({
  41. id: isoBase64URL.fromBuffer(dev.credentialID),
  42. type: 'public-key',
  43. transports: dev.transports,
  44. })),
  45. authenticatorSelection: {
  46. userVerification: 'preferred',
  47. residentKey: 'preferred',
  48. },
  49. });
  50. }
  51. async verifyRegistrationResponse(
  52. responseBody: RegistrationResponseJSON,
  53. expectedChallenge: string,
  54. ): Promise<VerifiedRegistrationResponse> {
  55. this.logger.log('Verifying registration response...');
  56. this.logger.debug(JSON.stringify(responseBody, null, 2));
  57. return verifyRegistrationResponse({
  58. response: responseBody,
  59. expectedChallenge,
  60. expectedOrigin: this.origin,
  61. expectedRPID: this.rpID,
  62. });
  63. }
  64. async generateAuthenticationOptions(registeredDevices?: {
  65. credentialID: Uint8Array;
  66. transports?: AuthenticatorTransport[];
  67. }[]) {
  68. const allowCredentials = registeredDevices?.length
  69. ? registeredDevices.map(dev => ({
  70. id: isoBase64URL.fromBuffer(dev.credentialID),
  71. type: 'public-key',
  72. transports: dev.transports,
  73. }))
  74. : undefined; // 💡 omit if no devices or we want discoverable login
  75. return generateAuthenticationOptions({
  76. rpID: this.rpID,
  77. timeout: 60000,
  78. userVerification: 'preferred',
  79. allowCredentials, // ✅ undefined = allow browser to try passkeys
  80. });
  81. }
  82. async verifyAuthenticationResponse(
  83. responseBody: any,
  84. expectedChallenge: string,
  85. credentialPublicKey: Uint8Array,
  86. credentialID: string, // stored as base64url in DB
  87. counter: number,
  88. ): Promise<VerifiedAuthenticationResponse> {
  89. return verifyAuthenticationResponse({
  90. response: responseBody,
  91. expectedChallenge,
  92. expectedOrigin: this.origin,
  93. expectedRPID: this.rpID,
  94. credential: {
  95. id: credentialID, // must be base64url string
  96. publicKey: credentialPublicKey,
  97. counter,
  98. },
  99. });
  100. }
  101. storeDeviceInfo(deviceInfo: any): void {
  102. this.devices.push(deviceInfo)
  103. console.log(this.devices)
  104. }
  105. findDeviceByCredentialID(
  106. devices: {
  107. credentialID: Uint8Array;
  108. credentialPublicKey: Uint8Array;
  109. counter: number;
  110. transports?: AuthenticatorTransport[];
  111. }[],
  112. credentialID: string, // base64url
  113. ) {
  114. return devices.find(dev =>
  115. isoBase64URL.fromBuffer(dev.credentialID) === credentialID
  116. );
  117. }
  118. }