summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/server/src')
-rw-r--r--packages/server/src/authentication/verifyAuthenticationResponse.test.ts8
-rw-r--r--packages/server/src/helpers/decodeClientDataJSON.ts2
-rw-r--r--packages/server/src/helpers/generateUserID.test.ts16
-rw-r--r--packages/server/src/helpers/generateUserID.ts21
-rw-r--r--packages/server/src/helpers/index.ts9
-rw-r--r--packages/server/src/helpers/iso/isoBase64URL.ts10
-rw-r--r--packages/server/src/metadata/parseJWT.ts4
-rw-r--r--packages/server/src/registration/generateRegistrationOptions.test.ts40
-rw-r--r--packages/server/src/registration/generateRegistrationOptions.ts25
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts4
-rw-r--r--packages/server/src/registration/verifyRegistrationResponse.test.ts12
11 files changed, 106 insertions, 45 deletions
diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts
index 844726f..f4046a6 100644
--- a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts
+++ b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts
@@ -390,7 +390,7 @@ Deno.test('should pass verification if custom challenge verifier returns true',
actualChallenge: string;
arbitraryData: string;
} = JSON.parse(
- isoBase64URL.toString(challenge),
+ isoBase64URL.toUTF8String(challenge),
);
return parsedChallenge.actualChallenge ===
'K3QxOjnVJLiGlnVEp5va5QJeMVWNf_7PYgutgbAtAUA';
@@ -448,7 +448,7 @@ Deno.test('should pass verification if custom challenge verifier returns a Promi
actualChallenge: string;
arbitraryData: string;
} = JSON.parse(
- isoBase64URL.toString(challenge),
+ isoBase64URL.toUTF8String(challenge),
);
return Promise.resolve(
parsedChallenge.actualChallenge ===
@@ -597,7 +597,7 @@ const assertionResponse: AuthenticationResponseJSON = {
clientExtensionResults: {},
type: 'public-key',
};
-const assertionChallenge = isoBase64URL.fromString(
+const assertionChallenge = isoBase64URL.fromUTF8String(
'totallyUniqueValueEveryTime',
);
const assertionOrigin = 'https://dev.dontneeda.pw';
@@ -627,7 +627,7 @@ const assertionFirstTimeUsedResponse: AuthenticationResponseJSON = {
type: 'public-key',
clientExtensionResults: {},
};
-const assertionFirstTimeUsedChallenge = isoBase64URL.fromString(
+const assertionFirstTimeUsedChallenge = isoBase64URL.fromUTF8String(
'totallyUniqueValueEveryAssertion',
);
const assertionFirstTimeUsedOrigin = 'https://dev.dontneeda.pw';
diff --git a/packages/server/src/helpers/decodeClientDataJSON.ts b/packages/server/src/helpers/decodeClientDataJSON.ts
index aeb4251..8230f42 100644
--- a/packages/server/src/helpers/decodeClientDataJSON.ts
+++ b/packages/server/src/helpers/decodeClientDataJSON.ts
@@ -5,7 +5,7 @@ import type { Base64URLString } from '../deps.ts';
* Decode an authenticator's base64url-encoded clientDataJSON to JSON
*/
export function decodeClientDataJSON(data: Base64URLString): ClientDataJSON {
- const toString = isoBase64URL.toString(data);
+ const toString = isoBase64URL.toUTF8String(data);
const clientData: ClientDataJSON = JSON.parse(toString);
return _decodeClientDataJSONInternals.stubThis(clientData);
diff --git a/packages/server/src/helpers/generateUserID.test.ts b/packages/server/src/helpers/generateUserID.test.ts
new file mode 100644
index 0000000..b15cab8
--- /dev/null
+++ b/packages/server/src/helpers/generateUserID.test.ts
@@ -0,0 +1,16 @@
+import { assert, assertNotEquals } from 'https://deno.land/std@0.198.0/assert/mod.ts';
+
+import { generateUserID } from './generateUserID.ts';
+
+Deno.test('should return a buffer of 32 bytes', async () => {
+ const userID = await generateUserID();
+
+ assert(userID.byteLength === 32);
+});
+
+Deno.test('should return random bytes on each execution', async () => {
+ const userID1 = await generateUserID();
+ const userID2 = await generateUserID();
+
+ assertNotEquals(userID1, userID2);
+});
diff --git a/packages/server/src/helpers/generateUserID.ts b/packages/server/src/helpers/generateUserID.ts
new file mode 100644
index 0000000..eaf9bb0
--- /dev/null
+++ b/packages/server/src/helpers/generateUserID.ts
@@ -0,0 +1,21 @@
+import { isoCrypto } from './iso/index.ts';
+
+/**
+ * Generate a suitably random value to be used as user ID
+ */
+export async function generateUserID(): Promise<Uint8Array> {
+ /**
+ * WebAuthn spec says user.id has a max length of 64 bytes. I prefer how 32 random bytes look
+ * after they're base64url-encoded so I'm choosing to go with that here.
+ */
+ const newUserID = new Uint8Array(32);
+
+ await isoCrypto.getRandomValues(newUserID);
+
+ return _generateUserIDInternals.stubThis(newUserID);
+}
+
+// Make it possible to stub the return value during testing
+export const _generateUserIDInternals = {
+ stubThis: (value: Uint8Array) => value,
+};
diff --git a/packages/server/src/helpers/index.ts b/packages/server/src/helpers/index.ts
index 30cf867..09b2f33 100644
--- a/packages/server/src/helpers/index.ts
+++ b/packages/server/src/helpers/index.ts
@@ -5,6 +5,7 @@ import { decodeAttestationObject } from './decodeAttestationObject.ts';
import { decodeClientDataJSON } from './decodeClientDataJSON.ts';
import { decodeCredentialPublicKey } from './decodeCredentialPublicKey.ts';
import { generateChallenge } from './generateChallenge.ts';
+import { generateUserID } from './generateUserID.ts';
import { getCertificateInfo } from './getCertificateInfo.ts';
import { isCertRevoked } from './isCertRevoked.ts';
import { parseAuthenticatorData } from './parseAuthenticatorData.ts';
@@ -23,6 +24,7 @@ export {
decodeClientDataJSON,
decodeCredentialPublicKey,
generateChallenge,
+ generateUserID,
getCertificateInfo,
isCertRevoked,
isoBase64URL,
@@ -42,7 +44,12 @@ import type {
} from './decodeAttestationObject.ts';
import type { CertificateInfo } from './getCertificateInfo.ts';
import type { ClientDataJSON } from './decodeClientDataJSON.ts';
-import type { COSEPublicKey, COSEPublicKeyEC2, COSEPublicKeyOKP, COSEPublicKeyRSA } from './cose.ts';
+import type {
+ COSEPublicKey,
+ COSEPublicKeyEC2,
+ COSEPublicKeyOKP,
+ COSEPublicKeyRSA,
+} from './cose.ts';
import type { ParsedAuthenticatorData } from './parseAuthenticatorData.ts';
export type {
diff --git a/packages/server/src/helpers/iso/isoBase64URL.ts b/packages/server/src/helpers/iso/isoBase64URL.ts
index 99bf82a..5c2388d 100644
--- a/packages/server/src/helpers/iso/isoBase64URL.ts
+++ b/packages/server/src/helpers/iso/isoBase64URL.ts
@@ -41,16 +41,16 @@ export function toBase64(base64urlString: string): string {
}
/**
- * Encode a string to base64url
+ * Encode a UTF-8 string to base64url
*/
-export function fromString(ascii: string): string {
- return base64.fromString(ascii, true);
+export function fromUTF8String(utf8String: string): string {
+ return base64.fromString(utf8String, true);
}
/**
- * Decode a base64url string into its original string
+ * Decode a base64url string into its original UTF-8 string
*/
-export function toString(base64urlString: string): string {
+export function toUTF8String(base64urlString: string): string {
return base64.toString(base64urlString, true);
}
diff --git a/packages/server/src/metadata/parseJWT.ts b/packages/server/src/metadata/parseJWT.ts
index a86dacd..3b04aea 100644
--- a/packages/server/src/metadata/parseJWT.ts
+++ b/packages/server/src/metadata/parseJWT.ts
@@ -6,8 +6,8 @@ import { isoBase64URL } from '../helpers/iso/index.ts';
export function parseJWT<T1, T2>(jwt: string): [T1, T2, string] {
const parts = jwt.split('.');
return [
- JSON.parse(isoBase64URL.toString(parts[0])) as T1,
- JSON.parse(isoBase64URL.toString(parts[1])) as T2,
+ JSON.parse(isoBase64URL.toUTF8String(parts[0])) as T1,
+ JSON.parse(isoBase64URL.toUTF8String(parts[1])) as T2,
parts[2],
];
}
diff --git a/packages/server/src/registration/generateRegistrationOptions.test.ts b/packages/server/src/registration/generateRegistrationOptions.test.ts
index 4c5e0c1..6f8282e 100644
--- a/packages/server/src/registration/generateRegistrationOptions.test.ts
+++ b/packages/server/src/registration/generateRegistrationOptions.test.ts
@@ -1,14 +1,15 @@
-import { assertEquals } from 'https://deno.land/std@0.198.0/assert/mod.ts';
+import { assertEquals, assertRejects } from 'https://deno.land/std@0.198.0/assert/mod.ts';
import { returnsNext, stub } from 'https://deno.land/std@0.198.0/testing/mock.ts';
import { generateRegistrationOptions } from './generateRegistrationOptions.ts';
import { _generateChallengeInternals } from '../helpers/generateChallenge.ts';
+import { isoBase64URL, isoUint8Array } from '../helpers/index.ts';
Deno.test('should generate credential request options suitable for sending via JSON', async () => {
const rpName = 'SimpleWebAuthn';
const rpID = 'not.real';
const challenge = 'totallyrandomvalue';
- const userID = '1234';
+ const userID = isoUint8Array.fromUTF8String('1234');
const userName = 'usernameHere';
const timeout = 1;
const attestationType = 'indirect';
@@ -35,7 +36,7 @@ Deno.test('should generate credential request options suitable for sending via J
id: rpID,
},
user: {
- id: userID,
+ id: isoBase64URL.fromBuffer(userID),
name: userName,
displayName: userDisplayName,
},
@@ -64,7 +65,6 @@ Deno.test('should map excluded credential IDs if specified', async () => {
rpName: 'SimpleWebAuthn',
rpID: 'not.real',
challenge: 'totallyrandomvalue',
- userID: '1234',
userName: 'usernameHere',
excludeCredentials: [
{
@@ -91,7 +91,6 @@ Deno.test('defaults to 60 seconds if no timeout is specified', async () => {
rpName: 'SimpleWebAuthn',
rpID: 'not.real',
challenge: 'totallyrandomvalue',
- userID: '1234',
userName: 'usernameHere',
});
@@ -103,7 +102,6 @@ Deno.test('defaults to none attestation if no attestation type is specified', as
rpName: 'SimpleWebAuthn',
rpID: 'not.real',
challenge: 'totallyrandomvalue',
- userID: '1234',
userName: 'usernameHere',
});
@@ -115,7 +113,6 @@ Deno.test('defaults to empty string for displayName if no userDisplayName is spe
rpName: 'SimpleWebAuthn',
rpID: 'not.real',
challenge: 'totallyrandomvalue',
- userID: '1234',
userName: 'usernameHere',
});
@@ -127,7 +124,6 @@ Deno.test('should set authenticatorSelection if specified', async () => {
rpName: 'SimpleWebAuthn',
rpID: 'not.real',
challenge: 'totallyrandomvalue',
- userID: '1234',
userName: 'usernameHere',
authenticatorSelection: {
authenticatorAttachment: 'cross-platform',
@@ -151,7 +147,6 @@ Deno.test('should set extensions if specified', async () => {
rpName: 'SimpleWebAuthn',
rpID: 'not.real',
challenge: 'totallyrandomvalue',
- userID: '1234',
userName: 'usernameHere',
extensions: { appid: 'simplewebauthn' },
});
@@ -163,7 +158,6 @@ Deno.test('should include credProps if extensions are not provided', async () =>
const options = await generateRegistrationOptions({
rpName: 'SimpleWebAuthn',
rpID: 'not.real',
- userID: '1234',
userName: 'usernameHere',
});
@@ -174,7 +168,6 @@ Deno.test('should include credProps if extensions are provided', async () => {
const options = await generateRegistrationOptions({
rpName: 'SimpleWebAuthn',
rpID: 'not.real',
- userID: '1234',
userName: 'usernameHere',
extensions: { appid: 'simplewebauthn' },
});
@@ -194,7 +187,6 @@ Deno.test('should generate a challenge if one is not provided', async () => {
const options = await generateRegistrationOptions({
rpID: 'not.real',
rpName: 'SimpleWebAuthn',
- userID: '1234',
userName: 'usernameHere',
});
@@ -208,7 +200,6 @@ Deno.test('should treat string challenges as UTF-8 strings', async () => {
const options = await generateRegistrationOptions({
rpID: 'not.real',
rpName: 'SimpleWebAuthn',
- userID: '1234',
userName: 'usernameHere',
challenge: 'こんにちは',
});
@@ -223,7 +214,6 @@ Deno.test('should use custom supported algorithm IDs as-is when provided', async
const options = await generateRegistrationOptions({
rpID: 'not.real',
rpName: 'SimpleWebAuthn',
- userID: '1234',
userName: 'usernameHere',
supportedAlgorithmIDs: [-7, -8, -65535],
});
@@ -242,7 +232,6 @@ Deno.test('should require resident key if residentKey option is absent but requi
const options = await generateRegistrationOptions({
rpID: 'not.real',
rpName: 'SimpleWebAuthn',
- userID: '1234',
userName: 'usernameHere',
authenticatorSelection: {
requireResidentKey: true,
@@ -257,7 +246,6 @@ Deno.test('should discourage resident key if residentKey option is absent but re
const options = await generateRegistrationOptions({
rpID: 'not.real',
rpName: 'SimpleWebAuthn',
- userID: '1234',
userName: 'usernameHere',
authenticatorSelection: {
requireResidentKey: false,
@@ -272,7 +260,6 @@ Deno.test('should prefer resident key if both residentKey and requireResidentKey
const options = await generateRegistrationOptions({
rpID: 'not.real',
rpName: 'SimpleWebAuthn',
- userID: '1234',
userName: 'usernameHere',
});
@@ -284,7 +271,6 @@ Deno.test('should set requireResidentKey to true if residentKey if set to requir
const options = await generateRegistrationOptions({
rpID: 'not.real',
rpName: 'SimpleWebAuthn',
- userID: '1234',
userName: 'usernameHere',
authenticatorSelection: {
residentKey: 'required',
@@ -299,7 +285,6 @@ Deno.test('should set requireResidentKey to false if residentKey if set to prefe
const options = await generateRegistrationOptions({
rpID: 'not.real',
rpName: 'SimpleWebAuthn',
- userID: '1234',
userName: 'usernameHere',
authenticatorSelection: {
residentKey: 'preferred',
@@ -314,7 +299,6 @@ Deno.test('should set requireResidentKey to false if residentKey if set to disco
const options = await generateRegistrationOptions({
rpID: 'not.real',
rpName: 'SimpleWebAuthn',
- userID: '1234',
userName: 'usernameHere',
authenticatorSelection: {
residentKey: 'discouraged',
@@ -330,9 +314,23 @@ Deno.test('should prefer Ed25519 in pubKeyCredParams', async () => {
rpName: 'SimpleWebAuthn',
rpID: 'not.real',
challenge: 'totallyrandomvalue',
- userID: '1234',
userName: 'usernameHere',
});
assertEquals(options.pubKeyCredParams[0].alg, -8);
});
+
+Deno.test('should raise if string is specified for userID', async () => {
+ await assertRejects(
+ () =>
+ generateRegistrationOptions({
+ rpName: 'SimpleWebAuthn',
+ rpID: 'not.real',
+ userName: 'usernameHere',
+ // @ts-ignore: Pretending a dev missed a refactor between v9 and v10
+ userID: 'customUserID',
+ }),
+ Error,
+ 'String values for `userID` are no longer supported. See https://simplewebauthn.dev/docs/advanced/server/custom-user-ids',
+ );
+});
diff --git a/packages/server/src/registration/generateRegistrationOptions.ts b/packages/server/src/registration/generateRegistrationOptions.ts
index 887452b..1828350 100644
--- a/packages/server/src/registration/generateRegistrationOptions.ts
+++ b/packages/server/src/registration/generateRegistrationOptions.ts
@@ -9,13 +9,14 @@ import type {
PublicKeyCredentialParameters,
} from '../deps.ts';
import { generateChallenge } from '../helpers/generateChallenge.ts';
+import { generateUserID } from '../helpers/generateUserID.ts';
import { isoBase64URL, isoUint8Array } from '../helpers/iso/index.ts';
export type GenerateRegistrationOptionsOpts = {
rpName: string;
rpID: string;
- userID: string;
userName: string;
+ userID?: Uint8Array;
challenge?: string | Uint8Array;
userDisplayName?: string;
timeout?: number;
@@ -104,8 +105,8 @@ export async function generateRegistrationOptions(
const {
rpName,
rpID,
- userID,
userName,
+ userID,
challenge = await generateChallenge(),
userDisplayName = '',
timeout = 60000,
@@ -164,6 +165,24 @@ export async function generateRegistrationOptions(
_challenge = isoUint8Array.fromUTF8String(_challenge);
}
+ /**
+ * Explicitly disallow use of strings for userID anymore because `isoBase64URL.fromBuffer()` below
+ * will return an empty string if one gets through!
+ */
+ if (typeof userID === 'string') {
+ throw new Error(
+ `String values for \`userID\` are no longer supported. See https://simplewebauthn.dev/docs/advanced/server/custom-user-ids`,
+ );
+ }
+
+ /**
+ * Generate a user ID if one is not provided
+ */
+ let _userID = userID;
+ if (!_userID) {
+ _userID = await generateUserID();
+ }
+
return {
challenge: isoBase64URL.fromBuffer(_challenge),
rp: {
@@ -171,7 +190,7 @@ export async function generateRegistrationOptions(
id: rpID,
},
user: {
- id: userID,
+ id: isoBase64URL.fromBuffer(_userID),
name: userName,
displayName: userDisplayName,
},
diff --git a/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts b/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts
index 5862cc5..29a20f1 100644
--- a/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts
@@ -43,10 +43,10 @@ export async function verifyAttestationAndroidSafetyNet(
const jwtParts = jwt.split('.');
const HEADER: SafetyNetJWTHeader = JSON.parse(
- isoBase64URL.toString(jwtParts[0]),
+ isoBase64URL.toUTF8String(jwtParts[0]),
);
const PAYLOAD: SafetyNetJWTPayload = JSON.parse(
- isoBase64URL.toString(jwtParts[1]),
+ isoBase64URL.toUTF8String(jwtParts[1]),
);
const SIGNATURE: SafetyNetJWTSignature = jwtParts[2];
diff --git a/packages/server/src/registration/verifyRegistrationResponse.test.ts b/packages/server/src/registration/verifyRegistrationResponse.test.ts
index 89b4694..09f2123 100644
--- a/packages/server/src/registration/verifyRegistrationResponse.test.ts
+++ b/packages/server/src/registration/verifyRegistrationResponse.test.ts
@@ -775,7 +775,7 @@ Deno.test('should pass verification if custom challenge verifier returns true',
actualChallenge: string;
arbitraryData: string;
} = JSON.parse(
- isoBase64URL.toString(challenge),
+ isoBase64URL.toUTF8String(challenge),
);
return parsedChallenge.actualChallenge ===
'xRsYdCQv5WZOqmxReiZl6C9q5SfrZne4lNSr9QVtPig';
@@ -823,7 +823,7 @@ Deno.test('should pass verification if custom challenge verifier returns a Promi
actualChallenge: string;
arbitraryData: string;
} = JSON.parse(
- isoBase64URL.toString(challenge),
+ isoBase64URL.toUTF8String(challenge),
);
return Promise.resolve(
parsedChallenge.actualChallenge ===
@@ -1011,7 +1011,7 @@ const attestationFIDOU2F: RegistrationResponseJSON = {
type: 'public-key',
clientExtensionResults: {},
};
-const attestationFIDOU2FChallenge = isoBase64URL.fromString(
+const attestationFIDOU2FChallenge = isoBase64URL.fromUTF8String(
'totallyUniqueValueEveryAttestation',
);
@@ -1033,7 +1033,7 @@ const attestationPacked: RegistrationResponseJSON = {
clientExtensionResults: {},
type: 'public-key',
};
-const attestationPackedChallenge = isoBase64URL.fromString(
+const attestationPackedChallenge = isoBase64URL.fromUTF8String(
's6PIbBnPPnrGNSBxNdtDrT7UrVYJK9HM',
);
@@ -1065,7 +1065,7 @@ const attestationPackedX5C: RegistrationResponseJSON = {
type: 'public-key',
clientExtensionResults: {},
};
-const attestationPackedX5CChallenge = isoBase64URL.fromString(
+const attestationPackedX5CChallenge = isoBase64URL.fromUTF8String(
'totallyUniqueValueEveryTime',
);
@@ -1085,6 +1085,6 @@ const attestationNone: RegistrationResponseJSON = {
type: 'public-key',
clientExtensionResults: {},
};
-const attestationNoneChallenge = isoBase64URL.fromString(
+const attestationNoneChallenge = isoBase64URL.fromUTF8String(
'hEccPWuziP00H0p5gxh2_u5_PC4NeYgd',
);