Skip to content

Commit

Permalink
Authenticate via the Identity canister (#5471)
Browse files Browse the repository at this point in the history
  • Loading branch information
hpeebles authored Apr 24, 2024
1 parent a4599cf commit 83e1a0f
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 115 deletions.
37 changes: 14 additions & 23 deletions frontend/openchat-agent/src/services/identityAgent.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { IdentityClient } from "./identity/identity.client";
import type { DerEncodedPublicKey, Identity, SignIdentity } from "@dfinity/agent";
import { DelegationChain, DelegationIdentity } from "@dfinity/identity";
import type {
ChallengeAttempt,
CheckAuthPrincipalResponse,
CreateOpenChatIdentityError,
MigrateLegacyPrincipalResponse,
import type { Identity, SignIdentity } from "@dfinity/agent";
import { DelegationIdentity } from "@dfinity/identity";
import {
buildDelegationIdentity,
type ChallengeAttempt,
type CheckAuthPrincipalResponse,
type CreateOpenChatIdentityError,
type MigrateLegacyPrincipalResponse,
toDer,
} from "openchat-shared";

export class IdentityAgent {
Expand Down Expand Up @@ -78,22 +80,11 @@ export class IdentityAgent {
return undefined;
}

const delegations = [
{
delegation: getDelegationResponse.delegation,
signature: getDelegationResponse.signature,
},
];

const delegationChain = DelegationChain.fromDelegations(
delegations,
userKey.buffer as DerEncodedPublicKey,
return buildDelegationIdentity(
userKey,
sessionKey,
getDelegationResponse.delegation,
getDelegationResponse.signature,
);

return DelegationIdentity.fromDelegation(sessionKey, delegationChain);
}
}

function toDer(key: SignIdentity): Uint8Array {
return new Uint8Array(key.getPublicKey().toDer() as ArrayBuffer);
}
2 changes: 1 addition & 1 deletion frontend/openchat-agent/src/services/openchatAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3271,7 +3271,7 @@ export class OpenChatAgent extends EventTarget {
return this._signInWithEmailClient.submitVerificationCode(email, code, sessionKey);
}

getSignInByEmailDelegation(
getSignInWithEmailDelegation(
email: string,
sessionKey: Uint8Array,
expiration: bigint,
Expand Down
115 changes: 74 additions & 41 deletions frontend/openchat-client/src/openchat.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-case-declarations */
import type { Identity } from "@dfinity/agent";
import { AuthClient } from "@dfinity/auth-client";
import { type Identity } from "@dfinity/agent";
import { AuthClient, type AuthClientStorage, IdbStorage } from "@dfinity/auth-client";
import { get, writable } from "svelte/store";
import DRange from "drange";
import {
Expand Down Expand Up @@ -81,11 +81,7 @@ import { showTrace } from "./utils/profiling";
import { CachePrimer } from "./utils/cachePrimer";
import { Poller } from "./utils/poller";
import { RecentlyActiveUsersTracker } from "./utils/recentlyActiveUsersTracker";
import {
idbAuthClientStore,
lsAuthClientStore,
selectedAuthProviderStore,
} from "./stores/authProviders";
import { selectedAuthProviderStore } from "./stores/authProviders";
import { blockedUsers } from "./stores/blockedUsers";
import { undeletingMessagesStore } from "./stores/undeletingMessages";
import {
Expand Down Expand Up @@ -430,8 +426,12 @@ import {
ONE_HOUR,
LEDGER_CANISTER_CHAT,
OPENCHAT_VIDEO_CALL_USER_ID,
IdentityStorage,
NoMeetingToJoin,
featureRestricted,
buildDelegationIdentity,
toDer,
storeIdentity,
} from "openchat-shared";
import { failedMessagesStore } from "./stores/failedMessages";
import {
Expand Down Expand Up @@ -492,6 +492,7 @@ import type { SendMessageResponse } from "openchat-shared";
import { applyTranslationCorrection } from "./stores/i18n";
import { getUserCountryCode } from "./utils/location";
import { isBalanceGate } from "openchat-shared";
import { ECDSAKeyIdentity } from "@dfinity/identity";

const MARK_ONLINE_INTERVAL = 61 * 1000;
const SESSION_TIMEOUT_NANOS = BigInt(30 * 24 * 60 * 60 * 1000 * 1000 * 1000); // 30 days
Expand All @@ -505,9 +506,12 @@ const MAX_USERS_TO_UPDATE_PER_BATCH = 500;
const MAX_INT32 = Math.pow(2, 31) - 1;

export class OpenChat extends OpenChatAgentWorker {
private _ocIdentityStorage: IdentityStorage;
private _userLocation: string | undefined;
private _authClientStorage: AuthClientStorage = new IdbStorage();
private _authClient: Promise<AuthClient>;
private _identity: Identity | undefined;
private _authPrincipal: string | undefined;
private _ocIdentity: Identity | undefined;
private _liveState: LiveState;
identityState = writable<IdentityState>({ kind: "loading_user" });
private _logger: Logger;
Expand Down Expand Up @@ -561,15 +565,18 @@ export class OpenChat extends OpenChatAgentWorker {
console.warn("GEO: Unable to determine user's country location", err);
});

this._ocIdentityStorage = new IdentityStorage();
this._authClient = AuthClient.create({
idleOptions: {
disableIdle: true,
disableDefaultIdleCallback: true,
},
storage: idbAuthClientStore,
storage: this._authClientStorage,
});

this._authClient.then((c) => c.getIdentity()).then((id) => this.loadedIdentity(id));
this._authClient
.then((c) => c.getIdentity())
.then((authIdentity) => this.loadedAuthenticationIdentity(authIdentity));
}

private chatUpdated(chatId: ChatIdentifier, updatedEvents: UpdatedEvent[]): void {
Expand All @@ -596,9 +603,11 @@ export class OpenChat extends OpenChatAgentWorker {
this.dispatchEvent(new ChatUpdated({ chatId, threadRootMessageIndex: undefined }));
}

private loadedIdentity(id: Identity) {
this._identity = id;
private loadedAuthenticationIdentity(id: Identity) {
currentUser.set(anonymousUser());
chatsInitialised.set(false);
const anon = id.getPrincipal().isAnonymous();
this._authPrincipal = anon ? undefined : id.getPrincipal().toString();
this.identityState.set(anon ? { kind: "anon" } : { kind: "loading_user" });
this.loadUser(anon);
}
Expand All @@ -623,11 +632,7 @@ export class OpenChat extends OpenChatAgentWorker {
identityProvider: this.buildAuthProviderUrl(authProvider),
maxTimeToLive: SESSION_TIMEOUT_NANOS,
derivationOrigin: this.config.iiDerivationOrigin,
onSuccess: () => {
currentUser.set(anonymousUser());
chatsInitialised.set(false);
this.loadedIdentity(c.getIdentity());
},
onSuccess: () => this.loadedAuthenticationIdentity(c.getIdentity()),
onError: (err) => {
this.identityState.set({ kind: "anon" });
console.warn("Login error from auth client: ", err);
Expand Down Expand Up @@ -718,14 +723,18 @@ export class OpenChat extends OpenChatAgentWorker {
private async loadUser(anon: boolean = false) {
await this.connectToWorker();

if (!anon) {
this._ocIdentity = await this._ocIdentityStorage.get();
}

this.startRegistryPoller();
this.startExchangeRatePoller();

this.sendRequest({ kind: "loadFailedMessages" }).then((res) =>
failedMessagesStore.initialise(MessageContextMap.fromMap(res)),
);

if (anon) {
if (this._ocIdentity === undefined) {
// short-circuit if we *know* that the user is anonymous
this.onCreatedUser(anonymousUser());
return;
Expand Down Expand Up @@ -782,19 +791,18 @@ export class OpenChat extends OpenChatAgentWorker {
}

onCreatedUser(user: CreatedUser): void {
if (this._identity === undefined) {
throw new Error("onCreatedUser called before the user's identity has been established");
}
this.user.set(user);
this._cachePrimer = new CachePrimer(this, user, (ev) => this.dispatchEvent(ev));
this.setDiamondStatus(user.diamondStatus);
const id = this._identity;
const id = this._ocIdentity;

this.sendRequest({ kind: "createUserClient", userId: user.userId });
startMessagesReadTracker(this);
this.startOnlinePoller();
startSwCheckPoller();
this.startSession(id).then(() => this.logout());
if (id !== undefined) {
this.startSession(id).then(() => this.logout());
}
this.startChatsPoller();
this.startUserUpdatePoller();
initNotificationStores();
Expand Down Expand Up @@ -873,18 +881,17 @@ export class OpenChat extends OpenChatAgentWorker {
);
}

logout(): Promise<void> {
return this._authClient.then((c) => {
return c.logout().then(() => window.location.replace("/"));
});
async logout(): Promise<void> {
await Promise.all([
this._ocIdentityStorage.remove(),
this._authClient.then((c) => c.logout()),
]).then(() => window.location.replace("/"));
}

async previouslySignedIn(): Promise<boolean> {
const KEY_STORAGE_IDENTITY = "identity";
const ls = await lsAuthClientStore.get(KEY_STORAGE_IDENTITY);
const idb = await idbAuthClientStore.get(KEY_STORAGE_IDENTITY);
const identity = ls != null || idb != null;
return this._liveState.userCreated && identity;
const identity = await this._authClientStorage.get(KEY_STORAGE_IDENTITY);
return this._liveState.userCreated && identity !== null;
}

unreadThreadMessageCount(
Expand Down Expand Up @@ -1209,11 +1216,13 @@ export class OpenChat extends OpenChatAgentWorker {
}

verifyAccessGate(gate: AccessGate): Promise<string | undefined> {
if (gate.kind !== "credential_gate") return Promise.resolve(undefined);
if (gate.kind !== "credential_gate" || this._authPrincipal === undefined) {
return Promise.resolve(undefined);
}

return verifyCredential(
this.config.internetIdentityUrl,
this._identity!.getPrincipal().toString(),
this._authPrincipal,
gate.credential.issuerOrigin,
gate.credential.credentialType,
gate.credential.credentialArguments,
Expand Down Expand Up @@ -5997,22 +6006,34 @@ export class OpenChat extends OpenChatAgentWorker {
async signInWithEmailVerificationCode(
email: string,
code: string,
sessionKey: Uint8Array,
sessionKey: ECDSAKeyIdentity,
): Promise<SignInWithEmailVerificationCodeResponse> {
const sessionKeyDer = toDer(sessionKey);
const submitCodeResponse = await this.sendRequest({
kind: "submitEmailVerificationCode",
email,
code,
sessionKey,
sessionKey: sessionKeyDer,
});

if (submitCodeResponse.kind === "success") {
return await this.sendRequest({
const getDelegationResponse = await this.sendRequest({
kind: "getSignInWithEmailDelegation",
email,
sessionKey,
sessionKey: sessionKeyDer,
expiration: submitCodeResponse.expiration,
});
if (getDelegationResponse.kind === "success") {
const identity = buildDelegationIdentity(
submitCodeResponse.userKey,
sessionKey,
getDelegationResponse.delegation,
getDelegationResponse.signature,
);
await storeIdentity(this._authClientStorage, sessionKey, identity.getDelegation());
this.loadedAuthenticationIdentity(identity);
}
return getDelegationResponse;
} else {
return submitCodeResponse;
}
Expand All @@ -6036,24 +6057,36 @@ export class OpenChat extends OpenChatAgentWorker {
token: "eth" | "sol",
address: string,
signature: string,
sessionKey: Uint8Array,
sessionKey: ECDSAKeyIdentity,
): Promise<GetDelegationResponse> {
const sessionKeyDer = toDer(sessionKey);
const loginResponse = await this.sendRequest({
kind: "loginWithWallet",
token,
address,
signature,
sessionKey,
sessionKey: sessionKeyDer,
});

if (loginResponse.kind === "success") {
return await this.sendRequest({
const getDelegationResponse = await this.sendRequest({
kind: "getDelegationWithWallet",
token,
address,
sessionKey,
sessionKey: sessionKeyDer,
expiration: loginResponse.expiration,
});
if (getDelegationResponse.kind === "success") {
const identity = buildDelegationIdentity(
loginResponse.userKey,
sessionKey,
getDelegationResponse.delegation,
getDelegationResponse.signature,
);
await storeIdentity(this._authClientStorage, sessionKey, identity.getDelegation());
this.loadedAuthenticationIdentity(identity);
}
return getDelegationResponse;
} else {
return loginResponse;
}
Expand Down
4 changes: 0 additions & 4 deletions frontend/openchat-client/src/stores/authProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { AuthProvider } from "openchat-shared";
import { writable } from "svelte/store";
import { configKeys } from "../utils/config";
import { enumFromStringValue } from "../utils/enums";
import { IdbStorage, LocalStorage } from "@dfinity/auth-client";

export const selectedAuthProviderStore = createStore();

Expand Down Expand Up @@ -33,6 +32,3 @@ function createStore() {
set: (authProvider: AuthProvider): void => _set(authProvider),
};
}

export const idbAuthClientStore = new IdbStorage();
export const lsAuthClientStore = new LocalStorage();
27 changes: 27 additions & 0 deletions frontend/openchat-shared/src/utils/identity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { type Delegation, DelegationChain, DelegationIdentity } from "@dfinity/identity";
import { type DerEncodedPublicKey, type Signature, SignIdentity } from "@dfinity/agent";

export function buildDelegationIdentity(
userKey: Uint8Array,
sessionKey: SignIdentity,
delegation: Delegation,
signature: Signature,
): DelegationIdentity {
const delegations = [
{
delegation,
signature,
},
];

const delegationChain = DelegationChain.fromDelegations(
delegations,
userKey.buffer as DerEncodedPublicKey,
);

return DelegationIdentity.fromDelegation(sessionKey, delegationChain);
}

export function toDer(key: SignIdentity): Uint8Array {
return new Uint8Array(key.getPublicKey().toDer() as ArrayBuffer);
}
Loading

0 comments on commit 83e1a0f

Please sign in to comment.