Skip to content

Commit da887f9

Browse files
authored
feat(auth-server): add prividium mode (#158)
1 parent f33af0e commit da887f9

File tree

23 files changed

+1491
-634
lines changed

23 files changed

+1491
-634
lines changed

cspell-config/cspell-zksync.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ sepolia
66
Zeek
77
zksync
88
ZKsync
9+
Prividium
10+
prividium

cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"**/*.ptau",
3333
"**/*.zkey",
3434
"packages/circuits/**",
35+
"packages/examples/demo-app/cache/**",
3536
"packages/auth-server/public/snarkjs.min.js"
3637
],
3738
"caseSensitive": true,

packages/auth-server/.env.example

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Default chain ID (optional)
2+
NUXT_PUBLIC_DEFAULT_CHAIN_ID=300
3+
4+
# AppKit Project ID
5+
NUXT_PUBLIC_APPKIT_PROJECT_ID=your-appkit-project-id
6+
7+
# Prividium Mode Configuration
8+
PRIVIDIUM_MODE=false
9+
10+
# Prividium SDK Configuration (required when PRIVIDIUM_MODE=true)
11+
PRIVIDIUM_CLIENT_ID=your-prividium-client-id
12+
PRIVIDIUM_RPC_PROXY_BASE_URL=https://rpc.prividium.io
13+
PRIVIDIUM_AUTH_BASE_URL=https://auth.prividium.io
14+
PRIVIDIUM_PERMISSIONS_BASE_URL=https://permissions.prividium.io
15+
16+
# OIDC Configuration
17+
NUXT_PUBLIC_SALT_SERVICE_URL=https://sso-oidc.zksync.dev/salt
18+
NUXT_PUBLIC_ZKEY_URL=https://sso-oidc.zksync.dev/zkey
19+
NUXT_PUBLIC_WITNESS_WASM_URL=https://sso-oidc.zksync.dev/witness
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
<template>
2+
<div class="h-full flex flex-col justify-center px-4">
3+
<div class="max-w-md mx-auto w-full">
4+
<AppAccountLogo class="dark:text-neutral-100 h-16 md:h-20 mb-14" />
5+
<!-- Error display -->
6+
<CommonHeightTransition :opened="!!error">
7+
<p class="pb-3 text-sm text-error-300 text-center">
8+
{{ error }}
9+
</p>
10+
</CommonHeightTransition>
11+
12+
<!-- Not authenticated: Show Prividium auth button -->
13+
<div
14+
v-if="!isAuthenticated"
15+
class="flex flex-col gap-5"
16+
>
17+
<ZkHighlightWrapper>
18+
<ZkButton
19+
class="w-full"
20+
:loading="loading"
21+
data-testid="prividium-login"
22+
@click="handlePrividiumLogin"
23+
>
24+
<template #icon>
25+
<svg
26+
class="w-5 h-5"
27+
viewBox="0 0 24 24"
28+
fill="currentColor"
29+
>
30+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z" />
31+
</svg>
32+
</template>
33+
Authorize with Prividium
34+
</ZkButton>
35+
</ZkHighlightWrapper>
36+
</div>
37+
38+
<!-- Authenticated: Show account creation flow or login options -->
39+
<div
40+
v-else
41+
class="flex flex-col"
42+
>
43+
<!-- Profile display -->
44+
<div class="bg-slate-50 dark:bg-slate-800 rounded-lg p-3 border border-slate-200 dark:border-slate-700 mb-6">
45+
<div class="flex items-center justify-between">
46+
<div class="flex items-center space-x-2">
47+
<div class="w-2 h-2 bg-green-500 rounded-full" />
48+
<span class="text-sm text-slate-600 dark:text-slate-400">
49+
{{ profile?.displayName || profile?.userId || "Authenticated User" }}
50+
</span>
51+
</div>
52+
<button
53+
class="text-xs text-slate-500 dark:text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 underline"
54+
@click="logout"
55+
>
56+
Sign Out
57+
</button>
58+
</div>
59+
</div>
60+
61+
<!-- Account creation or login flow -->
62+
<div
63+
v-if="!accountDeployed"
64+
class="flex flex-col gap-5"
65+
>
66+
<ZkHighlightWrapper>
67+
<ZkButton
68+
class="w-full"
69+
:loading="deployInProgress"
70+
data-testid="prividium-create-account"
71+
@click="deployAccount"
72+
>
73+
Create New Account
74+
</ZkButton>
75+
</ZkHighlightWrapper>
76+
77+
<ZkButton
78+
type="secondary"
79+
class="!text-slate-400"
80+
:loading="loginInProgress"
81+
data-testid="prividium-login-existing"
82+
@click="logIn"
83+
>
84+
Log In to Existing Account
85+
</ZkButton>
86+
</div>
87+
88+
<!-- Address association step (after account deployment) -->
89+
<div
90+
v-else
91+
class="space-y-6"
92+
>
93+
<!-- Progress indicator -->
94+
<div class="mb-6 px-8">
95+
<div class="flex items-center justify-between">
96+
<div class="flex flex-col items-center">
97+
<div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium bg-primary-300 text-white">
98+
99+
</div>
100+
<span class="text-xs text-slate-600 dark:text-slate-400 mt-2 text-center">Account Created</span>
101+
</div>
102+
<div
103+
:class="[
104+
'flex-1 h-1 mx-2 self-start mt-3.5',
105+
addressAssociated ? 'bg-primary-300' : 'bg-slate-200 dark:bg-slate-700',
106+
]"
107+
/>
108+
<div class="flex flex-col items-center">
109+
<div
110+
:class="[
111+
'w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ring-2 ring-offset-1',
112+
addressAssociated
113+
? 'bg-primary-300 text-white ring-primary-300 ring-offset-white dark:ring-offset-slate-900'
114+
: 'bg-white dark:bg-slate-900 text-primary-300 ring-primary-300 ring-offset-white dark:ring-offset-slate-900',
115+
]"
116+
>
117+
{{ addressAssociated ? '✓' : '2' }}
118+
</div>
119+
<span class="text-xs text-slate-600 dark:text-slate-400 mt-2 text-center">Complete Setup</span>
120+
</div>
121+
</div>
122+
</div>
123+
124+
<!-- Complete Setup -->
125+
<div class="bg-slate-50 dark:bg-slate-800 rounded-lg p-4">
126+
<p class="text-center text-sm text-slate-600 dark:text-slate-400 mb-4">
127+
Confirm your passkey to complete the setup.
128+
</p>
129+
<ZkButton
130+
class="w-full"
131+
:loading="associateInProgress"
132+
:disabled="addressAssociated"
133+
@click="executeAssociation"
134+
>
135+
{{ addressAssociated ? 'Setup Complete ✓' : 'Confirm Passkey' }}
136+
</ZkButton>
137+
</div>
138+
139+
<!-- Go Back Button -->
140+
<div class="text-center">
141+
<button
142+
class="text-sm text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 underline"
143+
@click="resetToInitialState"
144+
>
145+
Go Back
146+
</button>
147+
</div>
148+
</div>
149+
</div>
150+
</div>
151+
</div>
152+
</template>
153+
154+
<script lang="ts" setup>
155+
import { toHex, zeroAddress } from "viem";
156+
import { createZksyncPasskeyClient } from "zksync-sso/client/passkey";
157+
158+
const runtimeConfig = useRuntimeConfig();
159+
const chainId = runtimeConfig.public.chainId as SupportedChainId;
160+
161+
const prividiumAuthStore = usePrividiumAuthStore();
162+
const { loading, isAuthenticated, profile } = storeToRefs(prividiumAuthStore);
163+
const { createTransport } = useClientStore();
164+
const { login } = useAccountStore();
165+
const { fetchAddressAssociationMessage, associateAddress } = usePrividiumAddressAssociation();
166+
const { loginInProgress, loginToAccount } = useAccountLogin(chainId);
167+
const { registerInProgress: deployInProgress, createAccount, createAccountError } = useAccountCreate(chainId, true);
168+
169+
const accountDeploymentResult = ref<Awaited<ReturnType<typeof createAccount>> | null>(null);
170+
171+
// Use separate getClient since the one from client store requires login data to already be present
172+
// but we need it before logging user in
173+
const getClient = () => {
174+
if (!accountDeploymentResult.value) throw new Error("No deployed account available");
175+
const chain = supportedChains.find((chain) => chain.id === chainId);
176+
if (!chain) throw new Error(`Chain with id ${chainId} is not supported`);
177+
const contracts = contractsByChain[chainId];
178+
179+
const client = createZksyncPasskeyClient({
180+
address: accountDeploymentResult.value.address,
181+
credentialPublicKey: accountDeploymentResult.value.credentialPublicKey,
182+
userName: accountDeploymentResult.value.credentialId,
183+
userDisplayName: accountDeploymentResult.value.credentialId,
184+
contracts,
185+
chain,
186+
transport: createTransport(),
187+
});
188+
189+
return client;
190+
};
191+
192+
const { inProgress: associateInProgress, execute: executeAssociation, error: associationError } = useAsync(async () => {
193+
if (!accountDeploymentResult.value) {
194+
throw new Error("No deployed account to associate address with.");
195+
}
196+
197+
// Get passkey client with the deployed account
198+
const passkeyClient = getClient();
199+
200+
// Fetch association message
201+
const { message } = await fetchAddressAssociationMessage(passkeyClient.account.address);
202+
203+
// Sign with passkey
204+
const signature = await passkeyClient.signTypedData({
205+
domain: {
206+
name: "AddressAssociationVerifier",
207+
version: "1.0.0",
208+
chainId: chainId,
209+
verifyingContract: zeroAddress,
210+
},
211+
types: {
212+
AddressAssociation: [
213+
{ name: "message", type: "string" },
214+
],
215+
},
216+
primaryType: "AddressAssociation",
217+
message: {
218+
message,
219+
},
220+
});
221+
222+
// Associate the address
223+
await associateAddress(passkeyClient.account.address, message, signature);
224+
225+
login({
226+
username: accountDeploymentResult.value.credentialId,
227+
address: accountDeploymentResult.value.address,
228+
passkey: toHex(accountDeploymentResult.value.credentialPublicKey),
229+
});
230+
addressAssociated.value = true;
231+
232+
// Navigate to dashboard after successful association
233+
setTimeout(() => {
234+
navigateTo("/dashboard");
235+
}, 1000);
236+
});
237+
238+
const addressAssociated = ref(false);
239+
const accountDeployed = computed(() => !!accountDeploymentResult.value);
240+
const error = computed(() => createAccountError.value?.message || associationError.value?.message || "");
241+
242+
const resetToInitialState = () => {
243+
accountDeploymentResult.value = null;
244+
addressAssociated.value = false;
245+
};
246+
247+
const handlePrividiumLogin = async () => {
248+
await prividiumAuthStore.signInWithPopup();
249+
};
250+
251+
const logout = () => {
252+
prividiumAuthStore.signOut();
253+
resetToInitialState();
254+
};
255+
256+
const deployAccount = async () => {
257+
accountDeploymentResult.value = await createAccount();
258+
};
259+
260+
const logIn = async () => {
261+
const result = await loginToAccount();
262+
if (result?.success) {
263+
navigateTo("/dashboard");
264+
return;
265+
}
266+
if (result?.recoveryRequest?.isReady === false) {
267+
navigateTo(`/recovery/account-not-ready?address=${result!.recoveryRequest.accountAddress}`);
268+
return;
269+
}
270+
};
271+
</script>

packages/auth-server/components/app/NavMobileMenu.vue

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,10 @@
4040
/> {{ link.name }}
4141
</ZkLink>
4242
<ZkLink
43-
as="button"
43+
href="/logout"
4444
class="w-full"
4545
type="secondary"
4646
ui="justify-start dark:border-neutral-800 border-neutral-300 mt-4"
47-
@click="logout()"
4847
>
4948
<zk-icon
5049
icon="Logout"
@@ -61,13 +60,7 @@
6160
import { Dialog } from "radix-vue/namespaced";
6261
6362
const { mainNav } = useNav();
64-
const { logout: _logout } = useAccountStore();
6563
const open = ref(false);
66-
67-
const logout = () => {
68-
_logout();
69-
navigateTo("/");
70-
};
7164
</script>
7265

7366
<style lang="scss" scoped>

packages/auth-server/components/app/nav.vue

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
<ZkButtonIcon
4242
icon="Logout"
4343
class="mr-1 -ml-1 scale-[-1]"
44-
@click="logoutAndRedirect"
44+
@click="navigateTo('/logout')"
4545
/>
4646
</div>
4747
<div
@@ -85,13 +85,6 @@ onBeforeUnmount(() => {
8585
});
8686
8787
watch(windowWidth, checkWidths);
88-
89-
const { logout } = useAccountStore();
90-
91-
const logoutAndRedirect = () => {
92-
logout();
93-
navigateTo("/");
94-
};
9588
</script>
9689

9790
<style lang="scss" scoped>

0 commit comments

Comments
 (0)