Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor finding rpId for login #2751

Merged
merged 3 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 126 additions & 6 deletions src/frontend/src/utils/iiConnection.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import { MetadataMapV2, _SERVICE } from "$generated/internet_identity_types";
import {
DeviceData,
MetadataMapV2,
_SERVICE,
} from "$generated/internet_identity_types";
import { DOMAIN_COMPATIBILITY } from "$src/featureFlags";
import {
IdentityMetadata,
RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS,
} from "$src/repositories/identityMetadata";
import { ActorSubclass } from "@dfinity/agent";
import { DelegationIdentity } from "@dfinity/identity";
import { AuthenticatedConnection } from "./iiConnection";
import { ActorSubclass, DerEncodedPublicKey, Signature } from "@dfinity/agent";
import { DelegationIdentity, WebAuthnIdentity } from "@dfinity/identity";
import { CredentialData, convertToCredentialData } from "./credential-devices";
import { AuthenticatedConnection, Connection } from "./iiConnection";
import { MultiWebAuthnIdentity } from "./multiWebAuthnIdentity";

const mockDevice: DeviceData = {
alias: "mockDevice",
metadata: [],
origin: [],
protection: { protected: null },
pubkey: new Uint8Array(),
key_type: { platform: null },
purpose: { authentication: null },
credential_id: [],
};

const mockDelegationIdentity = {
getDelegation() {
return {
Expand Down Expand Up @@ -37,17 +54,22 @@ const mockActor = {
return { Ok: { metadata: mockRawMetadata } };
}),
identity_metadata_replace: vi.fn().mockResolvedValue({ Ok: null }),
lookup: vi.fn().mockResolvedValue([mockDevice]),
} as unknown as ActorSubclass<_SERVICE>;

beforeEach(() => {
infoResponse = undefined;
vi.clearAllMocks();
vi.stubGlobal("location", {
origin: "https://identity.internetcomputer.org",
});
DOMAIN_COMPATIBILITY.reset();
});

test("initializes identity metadata repository", async () => {
const connection = new AuthenticatedConnection(
"12345",
MultiWebAuthnIdentity.fromCredentials([]),
MultiWebAuthnIdentity.fromCredentials([], undefined),
mockDelegationIdentity,
BigInt(1234),
mockActor
Expand All @@ -62,7 +84,7 @@ test("commits changes on identity metadata", async () => {
const userNumber = BigInt(1234);
const connection = new AuthenticatedConnection(
"12345",
MultiWebAuthnIdentity.fromCredentials([]),
MultiWebAuthnIdentity.fromCredentials([], undefined),
mockDelegationIdentity,
userNumber,
mockActor
Expand All @@ -88,3 +110,101 @@ test("commits changes on identity metadata", async () => {
],
]);
});

describe("Connection.login", () => {
beforeEach(() => {
vi.spyOn(MultiWebAuthnIdentity, "fromCredentials").mockImplementation(
() => {
const mockIdentity = {
getPublicKey: () => {
return {
toDer: () => new ArrayBuffer(0) as DerEncodedPublicKey,
toRaw: () => new ArrayBuffer(0),
rawKey: () => new ArrayBuffer(0),
derKey: () => new ArrayBuffer(0) as DerEncodedPublicKey,
};
},
} as unknown as WebAuthnIdentity;
class MockMultiWebAuthnIdentity extends MultiWebAuthnIdentity {
static fromCredentials(
credentials: CredentialData[],
rpId: string | undefined
) {
return new MockMultiWebAuthnIdentity(credentials, rpId);
}
override sign() {
this._actualIdentity = mockIdentity;
return Promise.resolve(new ArrayBuffer(0) as Signature);
}
}
return MockMultiWebAuthnIdentity.fromCredentials([], undefined);
}
);
});

it("login returns authenticated connection with expected rpID", async () => {
DOMAIN_COMPATIBILITY.set(true);
vi.stubGlobal("navigator", {
// Supports RoR
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
});
const connection = new Connection("aaaaa-aa", mockActor);

const loginResult = await connection.login(BigInt(12345));

expect(loginResult.kind).toBe("loginSuccess");
if (loginResult.kind === "loginSuccess") {
expect(loginResult.connection).toBeInstanceOf(AuthenticatedConnection);
expect(MultiWebAuthnIdentity.fromCredentials).toHaveBeenCalledTimes(1);
expect(MultiWebAuthnIdentity.fromCredentials).toHaveBeenCalledWith(
[convertToCredentialData(mockDevice)],
"identity.ic0.app"
);
}
});

it("login returns authenticated connection without rpID if flag is not enabled", async () => {
DOMAIN_COMPATIBILITY.set(false);
vi.stubGlobal("navigator", {
// Supports RoR
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
});
const connection = new Connection("aaaaa-aa", mockActor);

const loginResult = await connection.login(BigInt(12345));

expect(loginResult.kind).toBe("loginSuccess");
if (loginResult.kind === "loginSuccess") {
expect(loginResult.connection).toBeInstanceOf(AuthenticatedConnection);
expect(MultiWebAuthnIdentity.fromCredentials).toHaveBeenCalledTimes(1);
expect(MultiWebAuthnIdentity.fromCredentials).toHaveBeenCalledWith(
[convertToCredentialData(mockDevice)],
undefined
);
}
});

it("login returns authenticated connection without rpID if browser doesn't support it", async () => {
DOMAIN_COMPATIBILITY.set(true);
vi.stubGlobal("navigator", {
// Supports RoR
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0",
});
const connection = new Connection("aaaaa-aa", mockActor);

const loginResult = await connection.login(BigInt(12345));

expect(loginResult.kind).toBe("loginSuccess");
if (loginResult.kind === "loginSuccess") {
expect(loginResult.connection).toBeInstanceOf(AuthenticatedConnection);
expect(MultiWebAuthnIdentity.fromCredentials).toHaveBeenCalledTimes(1);
expect(MultiWebAuthnIdentity.fromCredentials).toHaveBeenCalledWith(
[convertToCredentialData(mockDevice)],
undefined
);
}
});
});
25 changes: 23 additions & 2 deletions src/frontend/src/utils/iiConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
VerifyTentativeDeviceResponse,
} from "$generated/internet_identity_types";
import { fromMnemonicWithoutValidation } from "$src/crypto/ed25519";
import { DOMAIN_COMPATIBILITY } from "$src/featureFlags";
import { features } from "$src/features";
import {
IdentityMetadata,
Expand All @@ -50,8 +51,10 @@ import {
import { Principal } from "@dfinity/principal";
import { isNullish, nonNullish } from "@dfinity/utils";
import { convertToCredentialData, CredentialData } from "./credential-devices";
import { findWebAuthnRpId, relatedDomains } from "./findWebAuthnRpId";
import { MultiWebAuthnIdentity } from "./multiWebAuthnIdentity";
import { isRecoveryDevice, RecoveryDevice } from "./recoveryDevice";
import { supportsWebauthRoR } from "./userAgent";
import { isWebAuthnCancel } from "./webAuthnErrorUtils";

/*
Expand Down Expand Up @@ -134,7 +137,11 @@ export interface IIWebAuthnIdentity extends SignIdentity {
}

export class Connection {
public constructor(readonly canisterId: string) {}
public constructor(
readonly canisterId: string,
// Used for testing purposes
readonly overrideActor?: ActorSubclass<_SERVICE>
) {}

identity_registration_start = async ({
tempIdentity,
Expand Down Expand Up @@ -369,13 +376,24 @@ export class Connection {
userNumber: bigint,
credentials: CredentialData[]
): Promise<LoginSuccess | WebAuthnFailed | AuthFail> => {
// TODO: Filter out the credentials from the used rpIDs.
const rpId =
DOMAIN_COMPATIBILITY.isEnabled() &&
supportsWebauthRoR(window.navigator.userAgent)
? findWebAuthnRpId(
window.location.origin,
credentials,
relatedDomains()
)
: undefined;

/* Recover the Identity (i.e. key pair) used when creating the anchor.
* If the "DUMMY_AUTH" feature is set, we use a dummy identity, the same identity
* that is used in the register flow.
*/
const identity = features.DUMMY_AUTH
? new DummyIdentity()
: MultiWebAuthnIdentity.fromCredentials(credentials);
: MultiWebAuthnIdentity.fromCredentials(credentials, rpId);
let delegationIdentity: DelegationIdentity;

// Here we expect a webauth exception if the user canceled the webauthn prompt (triggered by
Expand Down Expand Up @@ -520,6 +538,9 @@ export class Connection {
createActor = async (
identity?: SignIdentity
): Promise<ActorSubclass<_SERVICE>> => {
if (this.overrideActor !== undefined) {
return this.overrideActor;
}
const agent = await HttpAgent.create({
identity,
host: inferHost(),
Expand Down
25 changes: 8 additions & 17 deletions src/frontend/src/utils/multiWebAuthnIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,12 @@
* then we know which one the user is actually using
* - It doesn't support creating credentials; use `WebAuthnIdentity` for that
*/
import { DOMAIN_COMPATIBILITY } from "$src/featureFlags";
import { PublicKey, Signature, SignIdentity } from "@dfinity/agent";
import { DER_COSE_OID, unwrapDER, WebAuthnIdentity } from "@dfinity/identity";
import { isNullish } from "@dfinity/utils";
import borc from "borc";
import { CredentialData } from "./credential-devices";
import { findWebAuthnRpId, relatedDomains } from "./findWebAuthnRpId";
import { bufferEqual } from "./iiConnection";
import { supportsWebauthRoR } from "./userAgent";

/**
* A SignIdentity that uses `navigator.credentials`. See https://webauthn.guide/ for
Expand All @@ -27,15 +24,19 @@ export class MultiWebAuthnIdentity extends SignIdentity {
* @param json - json to parse
*/
public static fromCredentials(
credentialData: CredentialData[]
credentialData: CredentialData[],
rpId: string | undefined
): MultiWebAuthnIdentity {
return new this(credentialData);
return new this(credentialData, rpId);
}

/* Set after the first `sign`, see `sign()` for more info. */
protected _actualIdentity?: WebAuthnIdentity;

protected constructor(readonly credentialData: CredentialData[]) {
protected constructor(
readonly credentialData: CredentialData[],
readonly rpId: string | undefined
) {
super();
this._actualIdentity = undefined;
}
Expand Down Expand Up @@ -67,16 +68,6 @@ export class MultiWebAuthnIdentity extends SignIdentity {
return this._actualIdentity.sign(blob);
}

const rpId =
DOMAIN_COMPATIBILITY.isEnabled() &&
supportsWebauthRoR(window.navigator.userAgent)
? findWebAuthnRpId(
window.location.origin,
this.credentialData,
relatedDomains()
)
: undefined;

const result = (await navigator.credentials.get({
publicKey: {
allowCredentials: this.credentialData.map((cd) => ({
Expand All @@ -85,7 +76,7 @@ export class MultiWebAuthnIdentity extends SignIdentity {
})),
challenge: blob,
userVerification: "discouraged",
rpId,
rpId: this.rpId,
},
})) as PublicKeyCredential;

Expand Down
2 changes: 1 addition & 1 deletion src/showcase/src/flows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class MockAuthenticatedConnection extends AuthenticatedConnection {
constructor() {
super(
"12345",
MultiWebAuthnIdentity.fromCredentials([]),
MultiWebAuthnIdentity.fromCredentials([], undefined),
mockDelegationIdentity,
BigInt(12345),
mockActor
Expand Down
Loading