Skip to content

Commit

Permalink
Implement canister methods and link to frontend to add and remove Ope…
Browse files Browse the repository at this point in the history
…nID credentials.
  • Loading branch information
sea-snake committed Jan 23, 2025
1 parent 5a7e8ef commit 1680e22
Show file tree
Hide file tree
Showing 14 changed files with 314 additions and 97 deletions.
2 changes: 1 addition & 1 deletion dfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"candid": "src/internet_identity/internet_identity.did",
"wasm": "internet_identity.wasm.gz",
"build": "bash -c 'II_DEV_CSP=1 II_FETCH_ROOT_KEY=1 II_DUMMY_CAPTCHA=${II_DUMMY_CAPTCHA:-1} scripts/build'",
"init_arg": "(opt record { captcha_config = opt record { max_unsolved_captchas= 50:nat64; captcha_trigger = variant {Static = variant {CaptchaDisabled}}}})",
"init_arg": "(opt record { captcha_config = opt record { max_unsolved_captchas= 50:nat64; captcha_trigger = variant {Static = variant {CaptchaDisabled}}}; openid_google = opt opt record { client_id = \"45431994619-cbbfgtn7o0pp0dpfcg2l66bc4rcg7qbu.apps.googleusercontent.com\" }})",
"shrink": false
},
"test_app": {
Expand Down
31 changes: 28 additions & 3 deletions src/frontend/generated/internet_identity_idl.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,13 @@ export const idlFactory = ({ IDL }) => {
'purpose' : Purpose,
'credential_id' : IDL.Opt(CredentialId),
});
const Aud = IDL.Text;
const Iss = IDL.Text;
const Sub = IDL.Text;
const OpenIdCredential = IDL.Record({
'aud' : IDL.Text,
'iss' : IDL.Text,
'sub' : IDL.Text,
'aud' : Aud,
'iss' : Iss,
'sub' : Sub,
'delegation_principal' : IDL.Principal,
'metadata' : MetadataMapV2,
'last_usage_timestamp' : Timestamp,
Expand Down Expand Up @@ -322,6 +325,18 @@ export const idlFactory = ({ IDL }) => {
'AlreadyInProgress' : IDL.Null,
'RateLimitExceeded' : IDL.Null,
});
const JWT = IDL.Text;
const Salt = IDL.Vec(IDL.Nat8);
const OpenIdCredentialAddError = IDL.Variant({
'DuplicateOpenIdCredential' : IDL.Null,
'Unauthorized' : IDL.Principal,
'JwtVerificationFailed' : IDL.Null,
});
const OpenIdCredentialKey = IDL.Tuple(Iss, Sub);
const OpenIdCredentialRemoveError = IDL.Variant({
'OpenIdCredentialNotFound' : IDL.Null,
'Unauthorized' : IDL.Principal,
});
const UserKey = PublicKey;
const PrepareIdAliasRequest = IDL.Record({
'issuer' : FrontendHostname,
Expand Down Expand Up @@ -515,6 +530,16 @@ export const idlFactory = ({ IDL }) => {
),
'init_salt' : IDL.Func([], [], []),
'lookup' : IDL.Func([UserNumber], [IDL.Vec(DeviceData)], ['query']),
'openid_credential_add' : IDL.Func(
[IdentityNumber, JWT, Salt],
[IDL.Variant({ 'Ok' : IDL.Null, 'Err' : OpenIdCredentialAddError })],
[],
),
'openid_credential_remove' : IDL.Func(
[IdentityNumber, OpenIdCredentialKey],
[IDL.Variant({ 'Ok' : IDL.Null, 'Err' : OpenIdCredentialRemoveError })],
[],
),
'prepare_delegation' : IDL.Func(
[UserNumber, FrontendHostname, SessionKey, IDL.Opt(IDL.Nat64)],
[UserKey, Timestamp],
Expand Down
29 changes: 26 additions & 3 deletions src/frontend/generated/internet_identity_types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface ArchiveInfo {
'archive_config' : [] | [ArchiveConfig],
'archive_canister' : [] | [Principal],
}
export type Aud = string;
export type AuthnMethod = { 'PubKey' : PublicKeyAuthn } |
{ 'WebAuthn' : WebAuthn };
export type AuthnMethodAddError = { 'InvalidMetadata' : string };
Expand Down Expand Up @@ -217,6 +218,8 @@ export interface InternetIdentityStats {
'canister_creation_cycles_cost' : bigint,
'event_aggregations' : Array<[string, Array<[string, bigint]>]>,
}
export type Iss = string;
export type JWT = string;
export type KeyType = { 'platform' : null } |
{ 'seed_phrase' : null } |
{ 'cross_platform' : null } |
Expand All @@ -240,13 +243,21 @@ export type MetadataMapV2 = Array<
>;
export interface OpenIdConfig { 'client_id' : string }
export interface OpenIdCredential {
'aud' : string,
'iss' : string,
'sub' : string,
'aud' : Aud,
'iss' : Iss,
'sub' : Sub,
'delegation_principal' : Principal,
'metadata' : MetadataMapV2,
'last_usage_timestamp' : Timestamp,
}
export type OpenIdCredentialAddError = { 'DuplicateOpenIdCredential' : null } |
{ 'Unauthorized' : Principal } |
{ 'JwtVerificationFailed' : null };
export type OpenIdCredentialKey = [Iss, Sub];
export type OpenIdCredentialRemoveError = {
'OpenIdCredentialNotFound' : null
} |
{ 'Unauthorized' : Principal };
export type PrepareIdAliasError = { 'InternalCanisterError' : string } |
{ 'Unauthorized' : Principal };
export interface PrepareIdAliasRequest {
Expand Down Expand Up @@ -274,6 +285,7 @@ export type RegistrationFlowNextStep = {
'CheckCaptcha' : { 'captcha_png_base64' : string }
} |
{ 'Finish' : null };
export type Salt = Uint8Array | number[];
export type SessionKey = PublicKey;
export interface SignedDelegation {
'signature' : Uint8Array | number[],
Expand All @@ -291,6 +303,7 @@ export interface StreamingCallbackHttpResponse {
export type StreamingStrategy = {
'Callback' : { 'token' : Token, 'callback' : [Principal, string] }
};
export type Sub = string;
export type Timestamp = bigint;
export type Token = {};
export type UserKey = PublicKey;
Expand Down Expand Up @@ -413,6 +426,16 @@ export interface _SERVICE {
>,
'init_salt' : ActorMethod<[], undefined>,
'lookup' : ActorMethod<[UserNumber], Array<DeviceData>>,
'openid_credential_add' : ActorMethod<
[IdentityNumber, JWT, Salt],
{ 'Ok' : null } |
{ 'Err' : OpenIdCredentialAddError }
>,
'openid_credential_remove' : ActorMethod<
[IdentityNumber, OpenIdCredentialKey],
{ 'Ok' : null } |
{ 'Err' : OpenIdCredentialRemoveError }
>,
'prepare_delegation' : ActorMethod<
[UserNumber, FrontendHostname, SessionKey, [] | [bigint]],
[UserKey, Timestamp]
Expand Down
52 changes: 42 additions & 10 deletions src/frontend/src/flows/manage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
DeviceData,
DeviceWithUsage,
IdentityAnchorInfo,
OpenIdCredential,
OpenIdCredentialAddError,
} from "$generated/internet_identity_types";
import identityCardBackground from "$src/assets/identityCardBackground.png";
import {
Expand Down Expand Up @@ -32,7 +34,6 @@ import { setupKey, setupPhrase } from "$src/flows/recovery/setupRecovery";
import { I18n } from "$src/i18n";
import { AuthenticatedConnection, Connection } from "$src/utils/iiConnection";
import { TemplateElement, renderPage } from "$src/utils/lit-html";
import { OpenIDCredential } from "$src/utils/mockOpenID";
import {
GOOGLE_REQUEST_CONFIG,
createAnonymousNonce,
Expand All @@ -46,7 +47,12 @@ import {
isRecoveryDevice,
isRecoveryPhrase,
} from "$src/utils/recoveryDevice";
import { OmitParams, shuffleArray, unreachable } from "$src/utils/utils";
import {
OmitParams,
isCanisterError,
shuffleArray,
unreachable,
} from "$src/utils/utils";
import { Principal } from "@dfinity/principal";
import { isNullish, nonNullish } from "@dfinity/utils";
import { TemplateResult, html } from "lit-html";
Expand Down Expand Up @@ -177,9 +183,9 @@ const displayManageTemplate = ({
onAddDevice: () => void;
addRecoveryPhrase: () => void;
addRecoveryKey: () => void;
credentials: OpenIDCredential[];
credentials: OpenIdCredential[];
onLinkAccount: () => void;
onUnlinkAccount: (credential: OpenIDCredential) => void;
onUnlinkAccount: (credential: OpenIdCredential) => void;
dapps: KnownDapp[];
exploreDapps: () => void;
identityBackground: PreLoadImage;
Expand Down Expand Up @@ -276,7 +282,7 @@ export const renderManage = async ({
// There's nowhere to go from here (i.e. all flows lead to/start from this page), so we
// loop forever
for (;;) {
let anchorInfo: IdentityAnchorInfo & { credentials: OpenIDCredential[] };
let anchorInfo: IdentityAnchorInfo;
try {
// Ignore the `commitMetadata` response, it's not critical for the application.
void connection.commitMetadata();
Expand All @@ -298,7 +304,7 @@ export const renderManage = async ({
userNumber,
connection,
anchorInfo.devices,
anchorInfo.credentials,
anchorInfo.openid_credentials[0] ?? [],
identityBackground
);
connection = newConnection ?? connection;
Expand Down Expand Up @@ -327,7 +333,7 @@ export const displayManage = async (
userNumber: bigint,
connection: AuthenticatedConnection,
devices_: DeviceWithUsage[],
credentials: OpenIDCredential[],
credentials: OpenIdCredential[],
identityBackground: PreLoadImage
): Promise<void | AuthenticatedConnection> => {
const i18n = new I18n();
Expand Down Expand Up @@ -390,19 +396,45 @@ export const displayManage = async (
toast.error(copy.account_already_linked);
return;
}
await connection.addJWT(jwt, salt);
await connection.addOpenIdCredential(jwt, salt);
resolve();
} catch (error) {
if (isPermissionError(error)) {
toast.error(copy.third_party_sign_in_permission_required);
return;
}
if (isCanisterError<OpenIdCredentialAddError>(error)) {
switch (error.type) {
case "Unauthorized":
toast.error(copy.authentication_failed);
console.error(
`Authentication unexpectedly failed: ${error
.value(error.type)
.toText()}`
);
break;
case "JwtVerificationFailed":
toast.error(copy.jwt_signature_invalid);
break;
case "DuplicateOpenIdCredential":
toast.error(copy.account_already_linked);
break;
default: {
// Make sure all error cases are covered,
// else this will throw a TS error here.
const _ = error.type satisfies never;
}
}
return;
}
throw error;
}
};
const onUnlinkAccount = async (credential: OpenIDCredential) => {
const onUnlinkAccount = async (credential: OpenIdCredential) => {
if (!confirm(copy.unlink_account_confirmation.toString())) {
return;
}
await connection.removeJWT(credential.iss, credential.sub);
await connection.removeOpenIdCredential(credential.iss, credential.sub);
resolve();
};

Expand Down
4 changes: 3 additions & 1 deletion src/frontend/src/flows/manage/linkedAccountsSection.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"link_additional_account": "Link additional account",
"max_linked_accounts_reached": "You can link up to 100 online accounts. You must unlink an account before you can link another.",
"unlink": "Unlink",
"last_used": "Last used"
"last_used": "Last used",
"authentication_failed": "Authentication failed",
"jwt_signature_invalid": "The account could not be linked due to an invalid signature"
}
}
10 changes: 5 additions & 5 deletions src/frontend/src/flows/manage/linkedAccountsSection.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { OpenIdCredential } from "$generated/internet_identity_types";
import { googleIcon } from "$src/components/icons";
import copyJson from "$src/flows/manage/linkedAccountsSection.json";
import { I18n } from "$src/i18n";
import { OpenIDCredential } from "$src/utils/mockOpenID";
import { getMetadataString } from "$src/utils/openID";
import { formatLastUsage } from "$src/utils/time";
import { nonNullish } from "@dfinity/utils";
Expand All @@ -20,9 +20,9 @@ export const linkedAccountsSection = ({
onUnlinkAccount,
hasOtherAuthMethods,
}: {
credentials: OpenIDCredential[];
credentials: OpenIdCredential[];
onLinkAccount: () => void;
onUnlinkAccount: (credential: OpenIDCredential) => void;
onUnlinkAccount: (credential: OpenIdCredential) => void;
hasOtherAuthMethods: boolean;
}): TemplateResult => {
const i18n = new I18n();
Expand Down Expand Up @@ -77,9 +77,9 @@ export const accountItem = ({
unlink,
unlinkAvailable,
}: {
credential: OpenIDCredential;
credential: OpenIdCredential;
index: number;
unlink: (credential: OpenIDCredential) => void;
unlink: (credential: OpenIdCredential) => void;
unlinkAvailable: boolean;
}) => {
const i18n = new I18n();
Expand Down
36 changes: 20 additions & 16 deletions src/frontend/src/utils/iiConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ import {
IdentityInfo,
IdentityInfoError,
IdentityMetadataReplaceError,
JWT,
KeyType,
MetadataMapV2,
PreparedIdAlias,
PublicKey,
Purpose,
RegistrationFlowNextStep,
Salt,
SessionKey,
Timestamp,
UserNumber,
Expand All @@ -35,8 +37,11 @@ import {
IdentityMetadataRepository,
} from "$src/repositories/identityMetadata";
import { addAnchorCancelledRpId, getCancelledRpIds } from "$src/storage";
import { JWT, MockOpenID, OpenIDCredential, Salt } from "$src/utils/mockOpenID";
import { diagnosticInfo, unknownToString } from "$src/utils/utils";
import {
CanisterError,
diagnosticInfo,
unknownToString,
} from "$src/utils/utils";
import {
Actor,
ActorSubclass,
Expand Down Expand Up @@ -149,8 +154,6 @@ export interface IIWebAuthnIdentity extends SignIdentity {
}

export class Connection {
protected _mockOpenID = new MockOpenID();

public constructor(
readonly canisterId: string,
// Used for testing purposes
Expand Down Expand Up @@ -679,15 +682,9 @@ export class AuthenticatedConnection extends Connection {
return this.actor;
}

getAnchorInfo = async (): Promise<
IdentityAnchorInfo & { credentials: OpenIDCredential[] }
> => {
getAnchorInfo = async (): Promise<IdentityAnchorInfo> => {
const actor = await this.getActor();
const anchorInfo = await actor.get_anchor_info(this.userNumber);
const mockedAnchorInfo = await this._mockOpenID.get_anchor_info(
this.userNumber
);
return { ...anchorInfo, ...mockedAnchorInfo };
return actor.get_anchor_info(this.userNumber);
};

getPrincipal = async ({
Expand Down Expand Up @@ -930,12 +927,19 @@ export class AuthenticatedConnection extends Connection {
return { error: "internal_error" };
};

addJWT = async (jwt: JWT, salt: Salt): Promise<void> => {
await this._mockOpenID.add_jwt(this.userNumber, jwt, salt);
addOpenIdCredential = async (jwt: JWT, salt: Salt): Promise<void> => {
const actor = await this.getActor();
const res = await actor.openid_credential_add(this.userNumber, jwt, salt);
if ("Err" in res) throw new CanisterError(res.Err);
};

removeJWT = async (iss: string, sub: string): Promise<void> => {
await this._mockOpenID.remove_jwt(this.userNumber, iss, sub);
removeOpenIdCredential = async (iss: string, sub: string): Promise<void> => {
const actor = await this.getActor();
const res = await actor.openid_credential_remove(this.userNumber, [
iss,
sub,
]);
if ("Err" in res) throw new CanisterError(res.Err);
};
}

Expand Down
Loading

0 comments on commit 1680e22

Please sign in to comment.