Skip to content

Commit 374c88f

Browse files
authored
feat(auth-server): session security hardening (#148)
* fix: remove redundant NUXT_PUBLIC_APP_URL usage * fix: auth server session security hardening
1 parent bb3eebd commit 374c88f

File tree

30 files changed

+578
-580
lines changed

30 files changed

+578
-580
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ simplifying user authentication, session management, and transaction processing.
1717
- 🧩 Modular smart accounts based on
1818
[ERC-7579](https://eips.ethereum.org/EIPS/eip-7579#modules)
1919
- 🔑 Passkey authentication (no seed phrases)
20-
- ⏰ Sessions w/ easy configuration and management
20+
- ⏰ Sessions with easy configuration and management
2121
- 💰 Integrated paymaster support
2222
- ❤️‍🩹 Account recovery
2323
- 💻 Simple SDKs : JavaScript, iOS/Android _(Coming Soon)_
@@ -117,7 +117,8 @@ This monorepo is comprised of the following packages, products, and examples:
117117
session key management
118118
- `packages/contracts` are the on-chain smart contracts behind ZKsync SSO
119119
accounts
120-
- `examples/nft-quest` is an app demonstrating the use of ZKsync SSO w/ sessions
120+
- `examples/nft-quest` is an app demonstrating the use of ZKsync SSO with
121+
sessions
121122
- `examples/nft-quest-contracts` are the smart contracts for `nft-quest`
122123
- `examples/demo-app` is a test app mostly used for CI testing
123124
- `examples/bank-demo` is an app demonstrating the fully embedded experience

examples/demo-app/pages/index.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
1515
@click="address ? disconnectWallet() : connectWallet(true)"
1616
>
17-
Connect w/ Session
17+
Connect with Session
1818
</button>
1919
<div
2020
v-if="address"
@@ -42,7 +42,7 @@
4242
:disabled="isSendingEth"
4343
@click="sendTokens(true)"
4444
>
45-
Send 0.1 ETH w/ Paymaster
45+
Send 0.1 ETH with Paymaster
4646
</button>
4747

4848
<div

examples/demo-app/tests/create-account.spec.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ test.beforeEach(async ({ page }) => {
4444
await expect(page.getByText("ZKsync SSO Demo")).toBeVisible();
4545
});
4646

47-
test("Create account w/ session and send ETH", async ({ page }) => {
47+
test("Create account with session and send ETH", async ({ page }) => {
4848
// Click the Connect button
49-
await page.getByRole("button", { name: "Connect w/ Session", exact: true }).click();
49+
await page.getByRole("button", { name: "Connect with Session", exact: true }).click();
5050

5151
// Ensure popup is displayed
5252
await page.waitForTimeout(2000);
@@ -105,9 +105,9 @@ test("Create account w/ session and send ETH", async ({ page }) => {
105105
.toBeGreaterThan(endBalance + 0.1);
106106
});
107107

108-
test("Create account w/ session and send ETH w/ paymaster", async ({ page }) => {
108+
test("Create account with session and send ETH with paymaster", async ({ page }) => {
109109
// Click the Connect button
110-
await page.getByRole("button", { name: "Connect w/ Session", exact: true }).click();
110+
await page.getByRole("button", { name: "Connect with Session", exact: true }).click();
111111

112112
// Ensure popup is displayed
113113
await page.waitForTimeout(2000);
@@ -156,9 +156,9 @@ test("Create account w/ session and send ETH w/ paymaster", async ({ page }) =>
156156
.replace("Balance: ", "")
157157
.replace(" ETH", "");
158158

159-
// Send some eth w/ paymaster
160-
await page.getByRole("button", { name: "Send 0.1 ETH w/ Paymaster", exact: true }).click();
161-
await expect(page.getByRole("button", { name: "Send 0.1 ETH w/ Paymaster", exact: true })).toBeEnabled();
159+
// Send some eth with paymaster
160+
await page.getByRole("button", { name: "Send 0.1 ETH with Paymaster", exact: true }).click();
161+
await expect(page.getByRole("button", { name: "Send 0.1 ETH with Paymaster", exact: true })).toBeEnabled();
162162
const endBalance = +(await page.getByText("Balance:").innerText())
163163
.replace("Balance: ", "")
164164
.replace(" ETH", "");
@@ -268,7 +268,7 @@ test("Create passkey account and send ETH", async ({ page }) => {
268268
.toBeGreaterThan(endBalance + 0.1);
269269
});
270270

271-
test("Create passkey account and send ETH w/ paymaster", async ({ page }) => {
271+
test("Create passkey account and send ETH with paymaster", async ({ page }) => {
272272
// Click the Connect button
273273
await page.getByRole("button", { name: "Connect", exact: true }).click();
274274

@@ -325,8 +325,8 @@ test("Create passkey account and send ETH w/ paymaster", async ({ page }) => {
325325
.replace("Balance: ", "")
326326
.replace(" ETH", "");
327327

328-
// Send some eth w/ paymaster
329-
await page.getByRole("button", { name: "Send 0.1 ETH w/ Paymaster", exact: true }).click();
328+
// Send some eth with paymaster
329+
await page.getByRole("button", { name: "Send 0.1 ETH with Paymaster", exact: true }).click();
330330

331331
// Wait for Auth Server to pop back up
332332
await page.waitForTimeout(2000);
@@ -362,7 +362,7 @@ test("Create passkey account and send ETH w/ paymaster", async ({ page }) => {
362362
await page.waitForTimeout(2000);
363363

364364
// Confirm transfer completed and balance updated
365-
await expect(page.getByRole("button", { name: "Send 0.1 ETH w/ Paymaster", exact: true })).toBeEnabled();
365+
await expect(page.getByRole("button", { name: "Send 0.1 ETH with Paymaster", exact: true })).toBeEnabled();
366366
const endBalance = +(await page.getByText("Balance:").innerText())
367367
.replace("Balance: ", "")
368368
.replace(" ETH", "");

packages/auth-server/components/account-recovery/guardian-flow/Step4ConfirmLater.vue

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,10 @@
2929
</template>
3030

3131
<script setup lang="ts">
32-
import { useRuntimeConfig } from "nuxt/app";
3332
import { computed } from "vue";
3433
3534
import { useAccountStore } from "~/stores/account";
3635
37-
const config = useRuntimeConfig();
3836
const { address } = useAccountStore();
3937
4038
const props = defineProps<{
@@ -46,6 +44,6 @@ const emit = defineEmits<{
4644
}>();
4745
4846
const recoveryUrl = computed(() => {
49-
return `${config.public.appUrl}/recovery/guardian/confirm-guardian?accountAddress=${address}&guardianAddress=${props.guardianAddress}`;
47+
return new URL(`/recovery/guardian/confirm-guardian?accountAddress=${address}&guardianAddress=${props.guardianAddress}`, window.location.origin).toString();
5048
});
5149
</script>

packages/auth-server/components/account-recovery/passkey-generation-flow/Step3ConfirmLater.vue

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@
3636
import type { Address } from "viem";
3737
import type { RegisterNewPasskeyReturnType } from "zksync-sso/client/passkey";
3838
39-
const runtimeConfig = useRuntimeConfig();
40-
4139
const props = defineProps<{
4240
accountAddress: Address;
4341
newPasskey: RegisterNewPasskeyReturnType;
@@ -61,6 +59,6 @@ const recoveryUrl = computedAsync(async () => {
6159
6260
queryParams.set("checksum", checksum);
6361
64-
return `${runtimeConfig.public.appUrl}/recovery/guardian/confirm-recovery?${queryParams.toString()}`;
62+
return new URL(`/recovery/guardian/confirm-recovery?${queryParams.toString()}`, window.location.origin).toString();
6563
});
6664
</script>
Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,60 @@
11
<template>
2-
<div class="rounded-xl bg-primary-50 dark:bg-primary-500/20 p-2">
2+
<div
3+
class="alert-container"
4+
:class="`variant-${variant}`"
5+
>
36
<div class="flex">
47
<div
58
v-if="$slots.icon"
6-
class="shrink-0 text-blue-400 size-5"
9+
class="alert-icon-container"
710
>
811
<slot name="icon" />
912
</div>
10-
<div class="ml-3 text-primary-300 dark:text-primary-100">
13+
<div class="alert-content-container">
1114
<slot />
1215
</div>
1316
</div>
1417
</div>
1518
</template>
1619

17-
<script setup lang="ts">
18-
20+
<script lang="ts" setup>
21+
defineProps({
22+
variant: {
23+
type: String as PropType<"primary" | "error">,
24+
default: "primary",
25+
},
26+
});
1927
</script>
28+
29+
<style lang="scss" scoped>
30+
.alert-container {
31+
@apply rounded-xl p-2;
32+
&.variant-primary {
33+
@apply bg-primary-50 dark:bg-primary-500/20;
34+
35+
.alert-icon-container {
36+
@apply text-blue-400;
37+
}
38+
.alert-content-container {
39+
@apply text-primary-300 dark:text-primary-100;
40+
}
41+
}
42+
&.variant-error {
43+
@apply bg-error-50 dark:bg-error-500/20;
44+
45+
.alert-icon-container {
46+
@apply text-red-400;
47+
}
48+
.alert-content-container {
49+
@apply text-error-300 dark:text-error-100;
50+
}
51+
}
52+
53+
.alert-icon-container {
54+
@apply shrink-0 size-5;
55+
}
56+
.alert-content-container {
57+
@apply ml-3;
58+
}
59+
}
60+
</style>

packages/auth-server/components/session/Metadata.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
</h1>
2424
<p
2525
v-if="domain"
26-
class="text-sm w-fit text-center border border-neutral-800 bg-neutral-800/50 mt-3 mx-auto px-4 py-1 rounded-3xl"
26+
class="text-sm w-fit text-center text-neutral-300 border border-neutral-800 bg-neutral-800/50 mt-3 mx-auto px-4 py-1 rounded-3xl"
2727
>
2828
{{ domain }}
2929
</p>

packages/auth-server/components/session/template.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
>
77
<slot name="header" />
88
</div>
9-
<div class="flex-grow p-4 overflow-y-auto">
9+
<div
10+
id="sessionScrollableArea"
11+
class="flex-grow p-4 overflow-y-auto"
12+
>
1013
<slot />
1114
</div>
1215
<div class=" border-t border-neutral-800 p-4">

packages/auth-server/components/session/tokens.vue

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
{{ formatPricePretty(totalUsd) }}
1616
</span>
1717
</div>
18-
<div v-else>
18+
<div
19+
v-else
20+
class="text-error-500"
21+
>
1922
Unlimited
2023
</div>
2124
</div>
@@ -49,7 +52,30 @@
4952
:price="item.token.price"
5053
:icon-url="item.token.iconUrl"
5154
:amount="item.amount.toString()"
52-
/>
55+
>
56+
<template
57+
v-if="item.amount === 'unlimited'"
58+
#right
59+
>
60+
<ZkTooltip :label="`Unlimited ${item.token.symbol} spend limit requested`">
61+
<div class="flex items-center gap-2 text-error-500">
62+
<span class="text-sm underline underline-offset-2 decoration-dotted">Attention</span>
63+
<ExclamationCircleIcon class="w-6 h-6 inline-block flex-shrink-0" />
64+
</div>
65+
</ZkTooltip>
66+
</template>
67+
<template
68+
v-else-if="formatTokenPriceToNumber(item.amount, item.token.decimals, item.token.price || 0) > 1000"
69+
#right
70+
>
71+
<ZkTooltip :label="`High ${item.token.symbol} spend limit requested`">
72+
<div class="flex items-center gap-2 text-error-500">
73+
<span class="text-sm underline underline-offset-2 decoration-dotted">Attention</span>
74+
<ExclamationCircleIcon class="w-6 h-6 inline-block flex-shrink-0" />
75+
</div>
76+
</ZkTooltip>
77+
</template>
78+
</TokenAmount>
5379
</template>
5480
<div
5581
v-if="onchainActionsCount"
@@ -64,26 +90,14 @@
6490
</template>
6591

6692
<script setup lang="ts">
67-
import { useNow } from "@vueuse/core";
68-
import type { SessionConfig } from "zksync-sso/utils";
93+
import { ExclamationCircleIcon } from "@heroicons/vue/24/outline";
6994
70-
const props = defineProps<{
71-
session: Omit<SessionConfig, "signer">;
95+
defineProps<{
96+
onchainActionsCount: UseSessionConfigInfoReturn["onchainActionsCount"];
97+
fetchTokensError: UseSessionConfigInfoReturn["fetchTokensError"];
98+
tokensLoading: UseSessionConfigInfoReturn["tokensLoading"];
99+
spendLimitTokens: UseSessionConfigInfoReturn["spendLimitTokens"];
100+
hasUnlimitedSpend: UseSessionConfigInfoReturn["hasUnlimitedSpend"];
101+
totalUsd: UseSessionConfigInfoReturn["totalUsd"];
72102
}>();
73-
74-
const { requestChain } = storeToRefs(useRequestsStore());
75-
76-
const now = useNow({ interval: 1000 });
77-
const {
78-
onchainActionsCount,
79-
fetchTokensError,
80-
tokensLoading,
81-
spendLimitTokens,
82-
hasUnlimitedSpend,
83-
totalUsd,
84-
} = useSessionConfigInfo(
85-
computed(() => requestChain.value!.id),
86-
props.session,
87-
now,
88-
);
89103
</script>

packages/auth-server/components/token/TokenAmount.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<span
1919
v-if="tokenPrice"
2020
class="text-neutral-500 text-sm"
21-
>&nbsp;(~{{ tokenPrice }})</span>
21+
>&nbsp;({{ tokenPrice }})</span>
2222
</div>
2323
</template>
2424
<template
@@ -81,8 +81,9 @@ const formattedAmount = computed(() => {
8181
return formatAmount(BigInt(props.amount), props.decimals);
8282
});
8383
const tokenPrice = computed(() => {
84-
if (!props.price || isUnlimitedAmount.value) return;
85-
return formatTokenPrice(BigInt(props.amount), props.decimals, props.price);
84+
if (!props.price) return;
85+
if (isUnlimitedAmount.value) return "";
86+
return `~${formatTokenPrice(BigInt(props.amount), props.decimals, props.price)}`;
8687
});
8788
</script>
8889

0 commit comments

Comments
 (0)