-
Notifications
You must be signed in to change notification settings - Fork 142
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Helper to find webauthn steps (#2833)
* Helper to find webauthn steps * Add comment not used * CR changes
- Loading branch information
Showing
2 changed files
with
158 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import { LEGACY_II_URL } from "$src/config"; | ||
import { CredentialData } from "./credential-devices"; | ||
import { PROD_DOMAINS } from "./findWebAuthnRpId"; | ||
import { findWebAuthnSteps } from "./findWebAuthnSteps"; | ||
|
||
describe("findWebAuthnSteps", () => { | ||
const currentOrigin = "https://identity.internetcomputer.org"; | ||
const nonCurrentOrigin1 = "https://identity.ic0.app"; | ||
const nonCurrentOrigin1RpId = new URL(nonCurrentOrigin1).hostname; | ||
const nonCurrentOrigin2 = "https://identity.icp0.io"; | ||
const nonCurrentOrigin2RpId = new URL(nonCurrentOrigin2).hostname; | ||
const relatedOrigins = PROD_DOMAINS; | ||
|
||
const createMockCredential = ( | ||
origin: string | undefined | ||
): CredentialData => ({ | ||
pubkey: new ArrayBuffer(32), | ||
credentialId: new ArrayBuffer(16), | ||
origin, | ||
}); | ||
|
||
it("should return an empty array if no devices are provided", () => { | ||
const result = findWebAuthnSteps({ | ||
supportsRor: true, | ||
devices: [], | ||
currentOrigin: currentOrigin, | ||
relatedOrigins, | ||
}); | ||
expect(result).toEqual([]); | ||
}); | ||
|
||
it("should use iframe if the RP ID does not match the current origin", () => { | ||
const result = findWebAuthnSteps({ | ||
supportsRor: true, | ||
devices: [createMockCredential(nonCurrentOrigin1)], | ||
currentOrigin: currentOrigin, | ||
relatedOrigins, | ||
}); | ||
|
||
expect(result).toEqual([{ useIframe: true, rpId: nonCurrentOrigin1RpId }]); | ||
}); | ||
|
||
it("should not use iframe if the RP ID matches the current origin", () => { | ||
const result = findWebAuthnSteps({ | ||
supportsRor: true, | ||
devices: [ | ||
createMockCredential(currentOrigin), | ||
createMockCredential(currentOrigin), | ||
], | ||
currentOrigin: currentOrigin, | ||
relatedOrigins, | ||
}); | ||
|
||
expect(result).toEqual([{ useIframe: false, rpId: undefined }]); | ||
}); | ||
|
||
it("should use ic0.app when origin is undefined", () => { | ||
const result = findWebAuthnSteps({ | ||
supportsRor: true, | ||
devices: [ | ||
createMockCredential(undefined), | ||
createMockCredential(LEGACY_II_URL), | ||
], | ||
currentOrigin: LEGACY_II_URL, | ||
relatedOrigins, | ||
}); | ||
|
||
expect(result).toEqual([{ useIframe: false, rpId: undefined }]); | ||
}); | ||
|
||
it("should handle multiple RP IDs and filter credentials accordingly", () => { | ||
const result = findWebAuthnSteps({ | ||
supportsRor: true, | ||
devices: [ | ||
createMockCredential(currentOrigin), | ||
createMockCredential(currentOrigin), | ||
createMockCredential(nonCurrentOrigin1), | ||
createMockCredential(nonCurrentOrigin2), | ||
createMockCredential(nonCurrentOrigin2), | ||
], | ||
currentOrigin: currentOrigin, | ||
relatedOrigins, | ||
}); | ||
|
||
expect(result).toEqual([ | ||
{ useIframe: false, rpId: undefined }, | ||
{ useIframe: true, rpId: nonCurrentOrigin1RpId }, | ||
{ useIframe: true, rpId: nonCurrentOrigin2RpId }, | ||
]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { CredentialData } from "./credential-devices"; | ||
import { | ||
excludeCredentialsFromOrigins, | ||
findWebAuthnRpId, | ||
} from "./findWebAuthnRpId"; | ||
|
||
export type WebAuthnStep = { | ||
useIframe: boolean; | ||
rpId: string | undefined; | ||
}; | ||
type Parameters = { | ||
// Does the user support Related Origin Requests? | ||
// Two sources are checked: the user agent and whether the user uses a third party provider for passkweys. | ||
// Not used at the moment. | ||
supportsRor: boolean; | ||
devices: CredentialData[]; | ||
currentOrigin: string; | ||
relatedOrigins: string[]; | ||
}; | ||
|
||
/** | ||
* Function that returns the ordered steps to try to perform the webauthn authentication. | ||
* | ||
* There are two dimensions in the steps: | ||
* - Use iframe for the webauthn authentication or not. | ||
* - Which RP ID to use. This is used for the iframe or for Related Origin Requests. | ||
* | ||
* Logic: | ||
* - To calculate the RP ID, we use the `findWebAuthnRpId` function. | ||
* - Calculate the RP ID first with all the credentials. | ||
* - For the subsequent RP IDs, the credentials' origin that matches the previous RP ID will be excluded. | ||
* - At the moment, we only use non-iframe if the RP ID matches the current origin. to avoid bad UX, if the RP ID doesn't match the current origin, the iframe will be used. | ||
* | ||
* @param {Parameters} params - The parameters to find the webauthn steps. | ||
* @returns {WebAuthnStep[]} The ordered steps to try to perform the webauthn authentication. | ||
*/ | ||
export const findWebAuthnSteps = ({ | ||
devices, | ||
currentOrigin, | ||
relatedOrigins, | ||
}: Parameters): WebAuthnStep[] => { | ||
const steps: WebAuthnStep[] = []; | ||
let filteredCredentials = devices; | ||
const rpIds = new Set<string | undefined>(); | ||
|
||
while (filteredCredentials.length > 0) { | ||
const rpId = findWebAuthnRpId( | ||
currentOrigin, | ||
filteredCredentials, | ||
relatedOrigins | ||
); | ||
// EXCEPTION: At the moment, to avoid bad UX, if the RP ID doesn't match the current origin, the iframe will be used. | ||
// This is because it's hard to find out whether a user's credentials come from a third party password manager or not. | ||
// The iframe workaround works for all users. | ||
const useIframe = | ||
rpId !== undefined && rpId !== new URL(currentOrigin).hostname; | ||
steps.push({ useIframe, rpId }); | ||
rpIds.add(rpId); | ||
filteredCredentials = excludeCredentialsFromOrigins( | ||
filteredCredentials, | ||
rpIds, | ||
currentOrigin | ||
); | ||
} | ||
|
||
return steps; | ||
}; |