Skip to content

Commit

Permalink
feat: consolidation of security levels undertaken
Browse files Browse the repository at this point in the history
Add a refresh token mechanism to extend the duration of the login state.
Intercept interface request token dynamic verification.
  • Loading branch information
AprilNEA committed Dec 12, 2023
1 parent 7b08808 commit fae6cec
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 47 deletions.
27 changes: 20 additions & 7 deletions packages/backend/src/libs/jwt/jwt.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,16 @@ export type JWTPayload = JWTPayloadDefault & {
export class JwtService {
constructor(private configService: ConfigService) {}

async getKey(type: 'public' | 'private') {
async getKey(type: 'public' | 'private', refresh = false) {
switch (this.configService.get('jwt').algorithm) {
case 'HS256':
if (refresh) {
const refreshSecret = this.configService.get('jwt')?.refreshSecret;
if (refreshSecret) {
return new TextEncoder().encode(refreshSecret);
}
}
return new TextEncoder().encode(this.configService.get('jwt').secret);
case 'ES256':
if (type === 'private') {
return await importJWK(
Expand All @@ -32,23 +40,28 @@ export class JwtService {
JSON.parse(this.configService.get('jwt').publicKey),
'ES256',
);
case 'HS256':
return new TextEncoder().encode(this.configService.get('jwt').secret);
}
}

async sign(payload: JWTPayload): Promise<string> {
async sign(
payload: JWTPayload,
duration = 7 * 24 * 60 * 60,
refresh = false,
): Promise<string> {
const iat = Math.floor(Date.now() / 1000); // Not before: Now
const exp = iat + 7 * 24 * 60 * 60; // One week
return await new SignJWT({ ...payload })
const exp = iat + duration; // One week
return await new SignJWT({
...payload,
...(refresh ? { sub: 'refresh' } : {}),
})
.setProtectedHeader({
alg: this.configService.get('jwt').algorithm,
typ: 'JWT',
})
.setExpirationTime(exp)
.setIssuedAt(iat)
.setNotBefore(iat)
.sign(await this.getKey('private'));
.sign(await this.getKey('private', refresh));
}

async verify(token: string): Promise<JWTPayload> {
Expand Down
9 changes: 9 additions & 0 deletions packages/backend/src/modules/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ConfigService } from '@/common/config';
import { BizException } from '@/common/exceptions/biz.exception';
import { Payload, Public } from '@/common/guards/auth.guard';
import { ZodValidationPipe } from '@/common/pipes/zod';
import { JWTPayload } from '@/libs/jwt/jwt.service';
import { WechatService } from '@/modules/auth/wechat.service';

import { AuthDTO, ErrorCodeEnum } from 'shared';
Expand Down Expand Up @@ -185,4 +186,12 @@ export class AuthController {
data: roles,
};
}

@Get('refresh')
async refresh(@Payload() payload: JWTPayload) {
return {
success: true,
...(await this.authService.refresh(payload.id, payload.role)),
};
}
}
15 changes: 10 additions & 5 deletions packages/backend/src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,19 @@ export class AuthService {
* 1. 检查是否绑定账户
* 2. 检查是否设置密码
*/
async #signWithCheck(user: any): Promise<{
token: string;
status: IAccountStatus;
}> {
async #signWithCheck(user: any) {
let status: IAccountStatus = 'ok';
if (!user.email && !user.phone) {
status = 'bind';
} else if (!user.newPassword) {
status = 'password';
}
return {
token: await this.jwt.sign({ id: user.id, role: user.role }),
sessionToken: await this.jwt.sign({ id: user.id, role: user.role }),
refreshToken: await this.jwt.sign(
{ id: user.id, role: user.role },
30 * 24 * 60 * 60,
),
status,
};
}
Expand Down Expand Up @@ -341,4 +342,8 @@ export class AuthService {
},
});
}

async refresh(userId: number, userRole: string) {
return this.#signWithCheck({ id: userId, role: userRole });
}
}
5 changes: 2 additions & 3 deletions packages/frontend/src/app/(admin-end)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import useSWR from 'swr';

import { Box, Grid, Text } from '@radix-ui/themes';
import { Card, LineChart, Title } from '@tremor/react';

import { Loading } from '@/components/loading';
import { useStore } from '@/store';
Expand Down Expand Up @@ -32,14 +31,14 @@ export default function DashboardIndex() {
return (
<Grid columns="6" gap="3" width="auto">
{analytics.map((item) => (
<Card key={item.key}>
<Box key={item.key}>
<Text as="p" size="2" weight="medium">
{item.label}
</Text>
<Text as="p" size="6" weight="bold">
{data[item.key]}
</Text>
</Card>
</Box>
))}
</Grid>
);
Expand Down
3 changes: 2 additions & 1 deletion packages/frontend/src/app/(admin-end)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import '@radix-ui/themes/styles.css';
import '@/styles/dashboard.css';

export const metadata = {
title: 'Dashboard | ChatGPT Admin ',
title: 'Dashboard | ChatGPT Admin Web',
description: 'Manage Dashboard for ChatGPT Admin Web',
};

export default function AdminEndLayout({
Expand Down
8 changes: 4 additions & 4 deletions packages/frontend/src/app/auth/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const weChatOauthRedirectUrl =
/* 验证码登录/注册 */
function ValidateCodeLogin() {
const router = useRouter();
const { fetcher, setSessionToken } = useStore();
const { fetcher, setAuthToken } = useStore();
const [identity, setIdentity] = useState('');
const [ifCodeSent, setIfCodeSent] = useState(false);
const [validateCode, setValidateCode] = useState('');
Expand Down Expand Up @@ -65,7 +65,7 @@ function ValidateCodeLogin() {
.then((res) => res.json())
.then((res) => {
if (res.success) {
setSessionToken(res.token);
setAuthToken(res.sessionToken, res.refreshToken);
return router.push('/');
} else {
showToast(res.message);
Expand Down Expand Up @@ -151,7 +151,7 @@ function ValidateCodeLogin() {
/* 密码登录 */
const PasswordLogin: React.FC = () => {
const router = useRouter();
const { fetcher, setSessionToken } = useStore();
const { fetcher, setAuthToken } = useStore();
const [identity, setIdentity] = useState('');
const [password, setPassword] = useState('');
const [isSubmitting, handleSubmit] = usePreventFormSubmit();
Expand All @@ -165,7 +165,7 @@ const PasswordLogin: React.FC = () => {
.then((res) => res.json())
.then((res) => {
if (res.success) {
setSessionToken(res.token);
setAuthToken(res.sessionToken, res.refreshToken);
router.push('/');
} else {
router.refresh();
Expand Down
25 changes: 25 additions & 0 deletions packages/frontend/src/app/provider/auth-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useRouter } from 'next/navigation';
import { Middleware, SWRHook } from 'swr';

import { useStore } from '@/store';

import { ErrorCodeEnum } from 'shared';

const authMiddleware: Middleware =
(useSWRNext: SWRHook) => (key, fetcher, config) => {
// Handle the next middleware, or the `useSWR` hook if this is the last one.
const swr = useSWRNext(key, fetcher, config);

// After hook runs...
// @ts-ignore
if (swr.data?.success === false) {
// @ts-ignore
if (swr.data?.code === ErrorCodeEnum.AuthFail) {
useStore().clearAuthToken();
useRouter().push('/auth');
throw new Error('Auth failed');
}
}
return swr;
};
export default authMiddleware;
63 changes: 50 additions & 13 deletions packages/frontend/src/app/provider/client-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { usePathname, useRouter } from 'next/navigation';
import React, { useCallback, useEffect, useState } from 'react';
import { SWRConfig } from 'swr';

import authMiddleware from '@/app/provider/auth-middleware';
import { Loading } from '@/components/loading';
import { useStore } from '@/store';

Expand All @@ -14,7 +15,8 @@ export function AuthProvider({
children: React.ReactNode;
admin?: boolean;
}) {
const { sessionToken, setSessionToken } = useStore();
const { sessionToken, refreshToken, setAuthToken, clearAuthToken } =
useStore();

const pathname = usePathname();
const router = useRouter();
Expand All @@ -25,12 +27,36 @@ export function AuthProvider({
if (!sessionToken) return false;

try {
const payload = JSON.parse(atob(sessionToken.split('.')[1]));
if (admin) {
const payload = JSON.parse(atob(sessionToken.split('.')[1]));
if (payload.role !== 'Admin') {
return false;
}
}
return payload.exp >= Date.now() / 1000;
} catch (e) {
return false;
}
}

function refreshSession() {
if (!refreshToken) return false;

try {
const payload = JSON.parse(atob(refreshToken.split('.')[1]));
if (payload.exp <= Date.now() / 1000) {
return false;
}
fetch('/api/auth/refresh', {
method: 'GET',
headers: {
Authorization: `Bearer ${refreshToken}`,
},
})
.then((res) => res.json())
.then((data) => {
setAuthToken(data.sessionToken, data.refreshSession);
});
return true;
} catch (e) {
return false;
Expand All @@ -40,21 +66,30 @@ export function AuthProvider({
const validate = useCallback(() => {
const isValid = validateSession();
if (!isValid) {
if (admin) {
return router.push('/');
}
return router.push('/auth');
} else {
setIsValidating(false);
if (pathname.startsWith('/auth')) {
return router.push('/');
if (!refreshSession()) {
clearAuthToken();
if (admin) {
return router.push('/');
}
return router.push('/auth');
}
}
}, [pathname, validateSession, router, admin]);
setIsValidating(false);
if (pathname.startsWith('/auth')) {
return router.push('/');
}
}, [
router,
pathname,
clearAuthToken,
validateSession,
refreshSession,
admin,
]);

useEffect(() => {
validate();
}, [pathname, validate]);
}, [pathname, sessionToken, refreshToken, validate]);

if (isValidating) {
return <Loading />;
Expand All @@ -65,6 +100,8 @@ export function AuthProvider({

export function SWRProvider({ children }: { children: React.ReactNode }) {
return (
<SWRConfig value={{ provider: () => new Map() }}>{children}</SWRConfig>
<SWRConfig value={{ use: [authMiddleware], provider: () => new Map() }}>
{children}
</SWRConfig>
);
}
1 change: 0 additions & 1 deletion packages/frontend/src/components/radix-ui-lib.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import useInstallStore from '@/store/install';
import styles from '@/styles/module/radix-ui-lib.module.scss';

import {
ISettingResult,
ISettingSchema,
MultiInputSettingSchema,
SelectSettingSchema,
Expand Down
22 changes: 12 additions & 10 deletions packages/frontend/src/components/sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import clsx from 'clsx';
import Link from 'next/link';
import Router from 'next/router';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import useSWR from 'swr';
Expand Down Expand Up @@ -107,15 +107,17 @@ function bindPassword() {
}

export function Sidebar({ children }: { children: React.ReactNode }) {
const router = useRouter();
const {
setSessionToken,
fetcher,
clearAuthToken,
showSideBar,
setShowSideBar,
fetcher,
latestAnnouncementId,
setLatestAnnouncementId,
config,
} = useStore();
// componentState
const [newbtnExpanded, setNewbtnExpanded] = useState<boolean>(false);
const [morebtnExpanded, setMorebtnExpanded] = useState<boolean>(false);

Expand All @@ -133,6 +135,7 @@ export function Sidebar({ children }: { children: React.ReactNode }) {
}
}),
);

const { data: userData, isLoading: isUserDataLoading } = useSWR<IUserData>(
'/user/info',
(url) =>
Expand Down Expand Up @@ -160,6 +163,11 @@ export function Sidebar({ children }: { children: React.ReactNode }) {
},
);

function logout() {
clearAuthToken();
router.push('/auth');
}

const loading = !useHasHydrated();

useSwitchTheme();
Expand Down Expand Up @@ -329,13 +337,7 @@ export function Sidebar({ children }: { children: React.ReactNode }) {
</Link>
<div
className={styles['sidebar-account-ext-item']}
onClick={() => {
setSessionToken(undefined);
setShowSideBar(false);
setNewbtnExpanded(false);
setMorebtnExpanded(false);
Router.reload();
}}
onClick={logout}
>
<div className={styles['icon']}>
<LogoutIcon />
Expand Down
Loading

0 comments on commit fae6cec

Please sign in to comment.