mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-14 13:58:46 +00:00
Refresh Token 구현 변경
This commit is contained in:
@@ -25,6 +25,7 @@
|
|||||||
"@types/better-sqlite3": "^7.6.11",
|
"@types/better-sqlite3": "^7.6.11",
|
||||||
"@types/jsonwebtoken": "^9.0.7",
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
"@types/ms": "^0.7.34",
|
"@types/ms": "^0.7.34",
|
||||||
|
"@types/node-schedule": "^2.1.7",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"dexie": "^4.0.10",
|
"dexie": "^4.0.10",
|
||||||
"drizzle-kit": "^0.22.0",
|
"drizzle-kit": "^0.22.0",
|
||||||
@@ -50,6 +51,8 @@
|
|||||||
"drizzle-orm": "^0.33.0",
|
"drizzle-orm": "^0.33.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"ms": "^2.1.3",
|
"ms": "^2.1.3",
|
||||||
|
"node-schedule": "^2.1.1",
|
||||||
|
"uuid": "^11.0.3",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
pnpm-lock.yaml
generated
49
pnpm-lock.yaml
generated
@@ -20,6 +20,12 @@ dependencies:
|
|||||||
ms:
|
ms:
|
||||||
specifier: ^2.1.3
|
specifier: ^2.1.3
|
||||||
version: 2.1.3
|
version: 2.1.3
|
||||||
|
node-schedule:
|
||||||
|
specifier: ^2.1.1
|
||||||
|
version: 2.1.1
|
||||||
|
uuid:
|
||||||
|
specifier: ^11.0.3
|
||||||
|
version: 11.0.3
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.24.1
|
specifier: ^3.24.1
|
||||||
version: 3.24.1
|
version: 3.24.1
|
||||||
@@ -49,6 +55,9 @@ devDependencies:
|
|||||||
'@types/ms':
|
'@types/ms':
|
||||||
specifier: ^0.7.34
|
specifier: ^0.7.34
|
||||||
version: 0.7.34
|
version: 0.7.34
|
||||||
|
'@types/node-schedule':
|
||||||
|
specifier: ^2.1.7
|
||||||
|
version: 2.1.7
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.20
|
specifier: ^10.4.20
|
||||||
version: 10.4.20(postcss@8.4.49)
|
version: 10.4.20(postcss@8.4.49)
|
||||||
@@ -1293,6 +1302,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
|
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/node-schedule@2.1.7:
|
||||||
|
resolution: {integrity: sha512-G7Z3R9H7r3TowoH6D2pkzUHPhcJrDF4Jz1JOQ80AX0K2DWTHoN9VC94XzFAPNMdbW9TBzMZ3LjpFi7RYdbxtXA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 22.10.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/node@22.10.2:
|
/@types/node@22.10.2:
|
||||||
resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==}
|
resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1694,6 +1709,13 @@ packages:
|
|||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/cron-parser@4.9.0:
|
||||||
|
resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
dependencies:
|
||||||
|
luxon: 3.5.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/cross-spawn@7.0.6:
|
/cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -2621,10 +2643,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/long-timeout@0.1.1:
|
||||||
|
resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lru-cache@10.4.3:
|
/lru-cache@10.4.3:
|
||||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/luxon@3.5.0:
|
||||||
|
resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/magic-string@0.30.17:
|
/magic-string@0.30.17:
|
||||||
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2740,6 +2771,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/node-schedule@2.1.1:
|
||||||
|
resolution: {integrity: sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dependencies:
|
||||||
|
cron-parser: 4.9.0
|
||||||
|
long-timeout: 0.1.1
|
||||||
|
sorted-array-functions: 1.3.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/normalize-path@3.0.0:
|
/normalize-path@3.0.0:
|
||||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -3239,6 +3279,10 @@ packages:
|
|||||||
totalist: 3.0.1
|
totalist: 3.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/sorted-array-functions@1.3.0:
|
||||||
|
resolution: {integrity: sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/source-map-js@1.2.1:
|
/source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -3591,6 +3635,11 @@ packages:
|
|||||||
/util-deprecate@1.0.2:
|
/util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
|
/uuid@11.0.3:
|
||||||
|
resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/vite@5.4.11:
|
/vite@5.4.11:
|
||||||
resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==}
|
resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
|||||||
9
src/hooks.server.ts
Normal file
9
src/hooks.server.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { ServerInit } from "@sveltejs/kit";
|
||||||
|
import schedule from "node-schedule";
|
||||||
|
import { cleanupExpiredRefreshTokens } from "$lib/server/db/token";
|
||||||
|
|
||||||
|
export const init: ServerInit = () => {
|
||||||
|
schedule.scheduleJob("0 * * * *", () => {
|
||||||
|
cleanupExpiredRefreshTokens();
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -5,8 +5,10 @@ import { client, userClient } from "./schema";
|
|||||||
export const createClient = async (pubKey: string, userId: number) => {
|
export const createClient = async (pubKey: string, userId: number) => {
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
const insertRes = await tx.insert(client).values({ pubKey }).returning({ id: client.id });
|
const insertRes = await tx.insert(client).values({ pubKey }).returning({ id: client.id });
|
||||||
const { id: clientId } = insertRes[0]!;
|
await tx.insert(userClient).values({
|
||||||
await tx.insert(userClient).values({ userId, clientId });
|
userId,
|
||||||
|
clientId: insertRes[0]!.id,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./client";
|
export * from "./client";
|
||||||
|
export * from "./token";
|
||||||
export * from "./user";
|
export * from "./user";
|
||||||
|
|||||||
18
src/lib/server/db/schema/token.ts
Normal file
18
src/lib/server/db/schema/token.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { sqliteTable, text, integer, unique } from "drizzle-orm/sqlite-core";
|
||||||
|
import { client } from "./client";
|
||||||
|
import { user } from "./user";
|
||||||
|
|
||||||
|
export const refreshToken = sqliteTable(
|
||||||
|
"refresh_token",
|
||||||
|
{
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
userId: integer("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id),
|
||||||
|
clientId: integer("client_id").references(() => client.id),
|
||||||
|
expiresAt: integer("expires_at").notNull(), // Only used for cleanup
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
unq: unique().on(t.userId, t.clientId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -5,9 +5,3 @@ export const user = sqliteTable("user", {
|
|||||||
email: text("email").notNull().unique(),
|
email: text("email").notNull().unique(),
|
||||||
password: text("password").notNull(),
|
password: text("password").notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const revokedToken = sqliteTable("revoked_token", {
|
|
||||||
id: integer("id").primaryKey(),
|
|
||||||
token: text("token").notNull().unique(),
|
|
||||||
revokedAt: integer("revoked_at").notNull(),
|
|
||||||
});
|
|
||||||
|
|||||||
75
src/lib/server/db/token.ts
Normal file
75
src/lib/server/db/token.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { SqliteError } from "better-sqlite3";
|
||||||
|
import { eq, lte } from "drizzle-orm";
|
||||||
|
import ms from "ms";
|
||||||
|
import env from "$lib/server/loadenv";
|
||||||
|
import db from "./drizzle";
|
||||||
|
import { refreshToken } from "./schema";
|
||||||
|
|
||||||
|
const expiresIn = ms(env.jwt.refreshExp);
|
||||||
|
const expiresAt = () => Date.now() + expiresIn;
|
||||||
|
|
||||||
|
export const registerRefreshToken = async (
|
||||||
|
userId: number,
|
||||||
|
clientId: number | null,
|
||||||
|
tokenId: string,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.insert(refreshToken)
|
||||||
|
.values({
|
||||||
|
id: tokenId,
|
||||||
|
userId,
|
||||||
|
clientId,
|
||||||
|
expiresAt: expiresAt(),
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRefreshToken = async (tokenId: string) => {
|
||||||
|
const tokens = await db.select().from(refreshToken).where(eq(refreshToken.id, tokenId)).execute();
|
||||||
|
return tokens[0] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rotateRefreshToken = async (oldTokenId: string, newTokenId: string) => {
|
||||||
|
const res = await db
|
||||||
|
.update(refreshToken)
|
||||||
|
.set({
|
||||||
|
id: newTokenId,
|
||||||
|
expiresAt: expiresAt(),
|
||||||
|
})
|
||||||
|
.where(eq(refreshToken.id, oldTokenId))
|
||||||
|
.execute();
|
||||||
|
return res.changes > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const upgradeRefreshToken = async (
|
||||||
|
oldTokenId: string,
|
||||||
|
newTokenId: string,
|
||||||
|
clientId: number,
|
||||||
|
) => {
|
||||||
|
const res = await db
|
||||||
|
.update(refreshToken)
|
||||||
|
.set({
|
||||||
|
id: newTokenId,
|
||||||
|
clientId,
|
||||||
|
expiresAt: expiresAt(),
|
||||||
|
})
|
||||||
|
.where(eq(refreshToken.id, oldTokenId))
|
||||||
|
.execute();
|
||||||
|
return res.changes > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const revokeRefreshToken = async (tokenId: string) => {
|
||||||
|
await db.delete(refreshToken).where(eq(refreshToken.id, tokenId)).execute();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cleanupExpiredRefreshTokens = async () => {
|
||||||
|
await db.delete(refreshToken).where(lte(refreshToken.expiresAt, Date.now())).execute();
|
||||||
|
};
|
||||||
@@ -1,27 +1,8 @@
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import db from "./drizzle";
|
import db from "./drizzle";
|
||||||
import { user, revokedToken } from "./schema";
|
import { user } from "./schema";
|
||||||
|
|
||||||
export const getUserByEmail = async (email: string) => {
|
export const getUserByEmail = async (email: string) => {
|
||||||
const users = await db.select().from(user).where(eq(user.email, email)).execute();
|
const users = await db.select().from(user).where(eq(user.email, email)).execute();
|
||||||
return users[0] ?? null;
|
return users[0] ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const revokeToken = async (token: string) => {
|
|
||||||
await db
|
|
||||||
.insert(revokedToken)
|
|
||||||
.values({
|
|
||||||
token,
|
|
||||||
revokedAt: Date.now(),
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isTokenRevoked = async (token: string) => {
|
|
||||||
const tokens = await db
|
|
||||||
.select()
|
|
||||||
.from(revokedToken)
|
|
||||||
.where(eq(revokedToken.token, token))
|
|
||||||
.execute();
|
|
||||||
return tokens.length > 0;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -2,34 +2,31 @@ import { error } from "@sveltejs/kit";
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import env from "$lib/server/loadenv";
|
import env from "$lib/server/loadenv";
|
||||||
|
|
||||||
interface TokenData {
|
type TokenPayload =
|
||||||
type: "access" | "refresh";
|
| {
|
||||||
userId: number;
|
type: "access";
|
||||||
clientId?: number;
|
userId: number;
|
||||||
}
|
clientId?: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "refresh";
|
||||||
|
jti: string;
|
||||||
|
};
|
||||||
|
|
||||||
export enum TokenError {
|
export enum TokenError {
|
||||||
EXPIRED,
|
EXPIRED,
|
||||||
INVALID,
|
INVALID,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const issueToken = (type: "access" | "refresh", userId: number, clientId?: number) => {
|
export const issueToken = (payload: TokenPayload) => {
|
||||||
return jwt.sign(
|
return jwt.sign(payload, env.jwt.secret, {
|
||||||
{
|
expiresIn: payload.type === "access" ? env.jwt.accessExp : env.jwt.refreshExp,
|
||||||
type,
|
});
|
||||||
userId,
|
|
||||||
clientId,
|
|
||||||
} satisfies TokenData,
|
|
||||||
env.jwt.secret,
|
|
||||||
{
|
|
||||||
expiresIn: type === "access" ? env.jwt.accessExp : env.jwt.refreshExp,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyToken = (token: string) => {
|
export const verifyToken = (token: string) => {
|
||||||
try {
|
try {
|
||||||
return jwt.verify(token, env.jwt.secret) as TokenData;
|
return jwt.verify(token, env.jwt.secret) as TokenPayload;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof jwt.TokenExpiredError) {
|
if (error instanceof jwt.TokenExpiredError) {
|
||||||
return TokenError.EXPIRED;
|
return TokenError.EXPIRED;
|
||||||
@@ -41,18 +38,18 @@ export const verifyToken = (token: string) => {
|
|||||||
export const authenticate = (request: Request) => {
|
export const authenticate = (request: Request) => {
|
||||||
const accessToken = request.headers.get("Authorization");
|
const accessToken = request.headers.get("Authorization");
|
||||||
if (!accessToken?.startsWith("Bearer ")) {
|
if (!accessToken?.startsWith("Bearer ")) {
|
||||||
error(401, "Token required");
|
error(401, "Access token required");
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenData = verifyToken(accessToken.slice(7));
|
const tokenPayload = verifyToken(accessToken.slice(7));
|
||||||
if (tokenData === TokenError.EXPIRED) {
|
if (tokenPayload === TokenError.EXPIRED) {
|
||||||
error(401, "Token expired");
|
error(401, "Access token expired");
|
||||||
} else if (tokenData === TokenError.INVALID || tokenData.type !== "access") {
|
} else if (tokenPayload === TokenError.INVALID || tokenPayload.type !== "access") {
|
||||||
error(401, "Invalid token");
|
error(401, "Invalid access token");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userId: tokenData.userId,
|
userId: tokenPayload.userId,
|
||||||
clientId: tokenData.clientId,
|
clientId: tokenPayload.clientId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,37 @@
|
|||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
import argon2 from "argon2";
|
import argon2 from "argon2";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { getClientByPubKey } from "$lib/server/db/client";
|
import { getClientByPubKey } from "$lib/server/db/client";
|
||||||
import { getUserByEmail, revokeToken, isTokenRevoked } from "$lib/server/db/user";
|
import { getUserByEmail } from "$lib/server/db/user";
|
||||||
|
import {
|
||||||
|
getRefreshToken,
|
||||||
|
registerRefreshToken,
|
||||||
|
rotateRefreshToken,
|
||||||
|
revokeRefreshToken,
|
||||||
|
} from "$lib/server/db/token";
|
||||||
import { issueToken, verifyToken, TokenError } from "$lib/server/modules/auth";
|
import { issueToken, verifyToken, TokenError } from "$lib/server/modules/auth";
|
||||||
|
|
||||||
const verifyPassword = async (hash: string, password: string) => {
|
const verifyPassword = async (hash: string, password: string) => {
|
||||||
return await argon2.verify(hash, password);
|
return await argon2.verify(hash, password);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const issueAccessToken = (userId: number, clientId?: number) => {
|
||||||
|
return issueToken({ type: "access", userId, clientId });
|
||||||
|
};
|
||||||
|
|
||||||
|
const issueRefreshToken = async (userId: number, clientId?: number) => {
|
||||||
|
const jti = uuidv4();
|
||||||
|
const token = issueToken({ type: "refresh", jti });
|
||||||
|
|
||||||
|
if (!(await registerRefreshToken(userId, clientId ?? null, jti))) {
|
||||||
|
error(403, "Already logged in");
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
export const login = async (email: string, password: string, pubKey?: string) => {
|
export const login = async (email: string, password: string, pubKey?: string) => {
|
||||||
const user = await getUserByEmail(email);
|
const user = await getUserByEmail(email);
|
||||||
if (!user) {
|
if (!user || !(await verifyPassword(user.password, password))) {
|
||||||
error(401, "Invalid email or password");
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = await verifyPassword(user.password, password);
|
|
||||||
if (!isValid) {
|
|
||||||
error(401, "Invalid email or password");
|
error(401, "Invalid email or password");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,36 +41,45 @@ export const login = async (email: string, password: string, pubKey?: string) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken: issueToken("access", user.id, client?.id),
|
accessToken: issueAccessToken(user.id, client?.id),
|
||||||
refreshToken: issueToken("refresh", user.id, client?.id),
|
refreshToken: await issueRefreshToken(user.id, client?.id),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const verifyRefreshToken = async (refreshToken: string) => {
|
const verifyRefreshToken = async (refreshToken: string) => {
|
||||||
const tokenData = verifyToken(refreshToken);
|
const tokenPayload = verifyToken(refreshToken);
|
||||||
if (tokenData === TokenError.EXPIRED) {
|
if (tokenPayload === TokenError.EXPIRED) {
|
||||||
error(401, "Token expired");
|
error(401, "Refresh token expired");
|
||||||
} else if (
|
} else if (tokenPayload === TokenError.INVALID || tokenPayload.type !== "refresh") {
|
||||||
tokenData === TokenError.INVALID ||
|
error(401, "Invalid refresh token");
|
||||||
tokenData.type !== "refresh" ||
|
|
||||||
(await isTokenRevoked(refreshToken))
|
|
||||||
) {
|
|
||||||
error(401, "Invalid token");
|
|
||||||
}
|
}
|
||||||
return tokenData;
|
|
||||||
|
const tokenData = await getRefreshToken(tokenPayload.jti);
|
||||||
|
if (!tokenData) {
|
||||||
|
error(500, "Refresh token not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
jti: tokenPayload.jti,
|
||||||
|
userId: tokenData.userId,
|
||||||
|
clientId: tokenData.clientId ?? undefined,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const logout = async (refreshToken: string) => {
|
export const logout = async (refreshToken: string) => {
|
||||||
await verifyRefreshToken(refreshToken);
|
const { jti } = await verifyRefreshToken(refreshToken);
|
||||||
await revokeToken(refreshToken);
|
await revokeRefreshToken(jti);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const refreshToken = async (refreshToken: string) => {
|
export const refreshToken = async (refreshToken: string) => {
|
||||||
const tokenData = await verifyRefreshToken(refreshToken);
|
const { jti: oldJti, userId, clientId } = await verifyRefreshToken(refreshToken);
|
||||||
|
const newJti = uuidv4();
|
||||||
|
|
||||||
await revokeToken(refreshToken);
|
if (!(await rotateRefreshToken(oldJti, newJti))) {
|
||||||
|
error(500, "Refresh token not found");
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
accessToken: issueToken("access", tokenData.userId, tokenData.clientId),
|
accessToken: issueAccessToken(userId, clientId),
|
||||||
refreshToken: issueToken("refresh", tokenData.userId, tokenData.clientId),
|
refreshToken: issueToken({ type: "refresh", jti: newJti }),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user