Skip to content

Commit 82e8224

Browse files
committed
add JWT signing and redirect for login flow
1 parent ca75681 commit 82e8224

File tree

10 files changed

+360
-323
lines changed

10 files changed

+360
-323
lines changed

apps/login/next.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const nextConfig: NextConfig = {
55
async headers() {
66
return [
77
{
8-
source: "/api/request",
8+
source: "/api/:path*",
99
headers: [
1010
{
1111
key: "Access-Control-Allow-Origin",

apps/login/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"next": "15.1.6",
1818
"react": "19.0.0",
1919
"react-dom": "19.0.0",
20+
"server-only": "^0.0.1",
2021
"thirdweb": "workspace:*"
2122
},
2223
"devDependencies": {

apps/login/public/twl.js

Lines changed: 40 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,32 @@
22
(function () {
33
const globalSetup = getSetup();
44

5-
const USER_ADDRESS_KEY = "tw.login:userAddress";
6-
const SESSION_KEY_ADDRESS_KEY = "tw.login:sessionKeyAddress";
5+
const JWT_KEY = "tw.login:jwt";
76
const CODE_KEY = "tw.login:code";
87

98
function main() {
109
// check if redirected first, this sets up the logged in state if it was from redirect
11-
const params = parseURLHash(new URL(window.location));
12-
if (params && params.code === localStorage.getItem(CODE_KEY)) {
13-
// reset the URL hash
10+
const result = parseURL(new URL(window.location));
11+
console.log(result);
12+
if (
13+
result &&
14+
result.length === 2 &&
15+
result[1] === localStorage.getItem(CODE_KEY)
16+
) {
17+
// reset the URL
1418
window.location.hash = "";
15-
// reset the code
16-
localStorage.setItem(CODE_KEY, params.code);
17-
// write the userAddress to local storage
18-
localStorage.setItem(USER_ADDRESS_KEY, params.userAddress);
19-
// write the sessionKeyAddress to local storage
20-
localStorage.setItem(SESSION_KEY_ADDRESS_KEY, params.sessionKeyAddress);
19+
window.location.search = "";
20+
21+
// write the jwt to local storage
22+
localStorage.setItem(JWT_KEY, result[0]);
2123
}
2224

23-
const userAddress = localStorage.getItem(USER_ADDRESS_KEY);
24-
const sessionKeyAddress = localStorage.getItem(SESSION_KEY_ADDRESS_KEY);
25+
// always reset the code
26+
localStorage.removeItem(CODE_KEY);
2527

26-
if (userAddress && sessionKeyAddress) {
28+
const jwt = localStorage.getItem(JWT_KEY);
29+
30+
if (jwt) {
2731
// handle logged in state
2832
handleIsLoggedIn();
2933
} else {
@@ -37,10 +41,16 @@
3741

3842
window.thirdweb = {
3943
isLoggedIn: true,
40-
getAddress: () => getAddress(),
44+
getUser: async () => {
45+
const res = await fetch(`${globalSetup.baseUrl}/api/user`, {
46+
headers: {
47+
Authorization: `Bearer ${localStorage.getItem(JWT_KEY)}`,
48+
},
49+
});
50+
return res.json();
51+
},
4152
logout: () => {
42-
window.localStorage.removeItem(USER_ADDRESS_KEY);
43-
window.localStorage.removeItem(SESSION_KEY_ADDRESS_KEY);
53+
window.localStorage.removeItem(JWT_KEY);
4454
window.location.reload();
4555
},
4656
};
@@ -51,20 +61,19 @@
5161
}
5262

5363
function onLogin() {
54-
const code = window.crypto.getRandomValues(new Uint8Array(4)).join("");
64+
const code = window.crypto.getRandomValues(new Uint8Array(16)).join("");
5565
localStorage.setItem(CODE_KEY, code);
5666
// redirect to the login page
5767
const redirect = new URL(globalSetup.baseUrl);
5868
redirect.searchParams.set("code", code);
5969
redirect.searchParams.set("clientId", globalSetup.clientId);
60-
redirect.searchParams.set("redirect", window.location.href);
70+
redirect.searchParams.set(
71+
"redirect",
72+
window.location.origin + window.location.pathname,
73+
);
6174
window.location.href = redirect.href;
6275
}
6376

64-
function getAddress() {
65-
return localStorage.getItem(USER_ADDRESS_KEY);
66-
}
67-
6877
// utils
6978
function getSetup() {
7079
const el = document.currentScript;
@@ -82,44 +91,21 @@
8291

8392
/**
8493
* @param {URL} url
85-
* @returns null | { [key: string]: string }
94+
* @returns null | [string, string]
8695
*/
87-
function parseURLHash(url) {
88-
if (!url.hash) {
89-
return null;
90-
}
96+
function parseURL(url) {
9197
try {
92-
return decodeHash(url.hash);
98+
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
99+
const code = url.searchParams.get("code");
100+
if (!hash || !code) {
101+
return null;
102+
}
103+
return [hash, code];
93104
} catch {
94105
// if this fails, invalid data -> return null
95106
return null;
96107
}
97108
}
98109

99-
/**
100-
* Decodes a URL hash string to extract the three keys.
101-
*
102-
* @param {string} hash - A string like "#eyJrZXkxIjoiVmFsdWU..."
103-
* @returns {{ userAddress: string, sessionKeyAddress: string, code: string }} An object with the three keys
104-
*/
105-
function decodeHash(hash) {
106-
// Remove the "#" prefix, if present.
107-
const base64Data = hash.startsWith("#") ? hash.slice(1) : hash;
108-
109-
// Decode the Base64 string, then parse the JSON.
110-
const jsonString = atob(base64Data);
111-
const data = JSON.parse(jsonString);
112-
113-
// data should have the shape { userAddress, sessionKeyAddress, code }.
114-
if (
115-
"userAddress" in data &&
116-
"sessionKeyAddress" in data &&
117-
"code" in data
118-
) {
119-
return data;
120-
}
121-
return null;
122-
}
123-
124110
main();
125111
})();

apps/login/src/app/api/request/route.salty

Lines changed: 0 additions & 36 deletions
This file was deleted.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { type NextRequest, NextResponse } from "next/server";
2+
import { verifyJWT } from "../../authorization/jwt";
3+
4+
export const GET = async (req: NextRequest) => {
5+
const jwt = req.headers.get("Authorization")?.split("Bearer ")[1];
6+
if (!jwt) {
7+
return NextResponse.json(
8+
{
9+
message: "No JWT provided",
10+
},
11+
{
12+
status: 401,
13+
},
14+
);
15+
}
16+
17+
try {
18+
const verifiedPayload = await verifyJWT(jwt);
19+
return NextResponse.json({
20+
address: verifiedPayload.sub,
21+
});
22+
} catch (e) {
23+
console.error("failed", e);
24+
return NextResponse.json(
25+
{
26+
message: "Invalid JWT",
27+
},
28+
{
29+
status: 401,
30+
},
31+
);
32+
}
33+
};
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"server only";
2+
3+
import "server-only";
4+
import { createThirdwebClient } from "thirdweb";
5+
import { verifyEOASignature } from "thirdweb/auth";
6+
import { decodeJWT, encodeJWT, stringify } from "thirdweb/utils";
7+
import { privateKeyToAccount, randomPrivateKey } from "thirdweb/wallets";
8+
9+
const client = createThirdwebClient({
10+
clientId: "e9ba48c289e0cc3d06a23bfd370cc111",
11+
});
12+
const privateKey = process.env.THIRDWEB_ADMIN_PRIVATE_KEY || randomPrivateKey();
13+
14+
if (!privateKey) {
15+
throw new Error("Missing THIRDWEB_ADMIN_PRIVATE_KEY");
16+
}
17+
18+
const serverAccount = privateKeyToAccount({
19+
client,
20+
privateKey: privateKey,
21+
});
22+
23+
export async function signJWT(data: {
24+
address: string;
25+
sesionKeySignerAddress: string;
26+
code: string;
27+
}) {
28+
"use server";
29+
return await encodeJWT({
30+
account: serverAccount,
31+
payload: {
32+
iss: serverAccount.address,
33+
sub: data.address,
34+
aud: data.sesionKeySignerAddress,
35+
exp: new Date(Date.now() + 1000 * 60 * 60),
36+
nbf: new Date(),
37+
iat: new Date(),
38+
jti: data.code,
39+
},
40+
});
41+
}
42+
43+
export async function verifyJWT(jwt: string) {
44+
const { payload, signature } = decodeJWT(jwt);
45+
46+
console.log("payload", payload);
47+
console.log("signature", signature);
48+
// verify the signature
49+
const verified = await verifyEOASignature({
50+
message: stringify(payload),
51+
signature,
52+
address: serverAccount.address,
53+
});
54+
if (!verified) {
55+
throw new Error("Invalid JWT signature");
56+
}
57+
return payload;
58+
}

0 commit comments

Comments
 (0)