Skip to content
This repository has been archived by the owner on Dec 16, 2024. It is now read-only.

Commit

Permalink
fix: modify jwt refresh logic
Browse files Browse the repository at this point in the history
  • Loading branch information
jspark2000 committed Feb 27, 2024
1 parent fdb7fb4 commit 2b4122e
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 85 deletions.
1 change: 1 addition & 0 deletions frontend/next.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
reactStrictMode: process.env !== 'production',
env: {
NEXTAUTH_URL: process.env.NEXTAUTH_URL
}
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/app/(public)/login/_components/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Input } from '@/components/ui/input'
import { LoginFormSchema } from '@/lib/forms'
import { zodResolver } from '@hookform/resolvers/zod'
import { signIn } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { useRouter, useSearchParams } from 'next/navigation'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
Expand All @@ -22,6 +22,7 @@ import type { z } from 'zod'
export default function LoginForm() {
const [isFetching, setIsFetching] = useState(false)
const router = useRouter()
const searchParams = useSearchParams()

const form = useForm<z.infer<typeof LoginFormSchema>>({
resolver: zodResolver(LoginFormSchema),
Expand All @@ -40,7 +41,7 @@ export default function LoginForm() {
})

if (!res?.error) {
router.push('/')
router.replace(searchParams.get('callbackUrl') ?? '/console/dashboard')
} else {
toast.error('로그인 실패')
}
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/app/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use client'

import { Button } from '@/components/ui/button'
import { useRouter } from 'next/navigation'

interface Props {
error: Error & { digest?: string }
reset: () => void
}

export default function Error({ error }: Props) {
const router = useRouter()

return (
<div className="flex h-full w-full flex-1 flex-col items-center justify-center gap-3 py-12">
<p className="mt-8 text-2xl font-extrabold">Something Went Wrong!</p>
<p className="mb-4 max-w-[36rem] text-lg font-semibold">
{error.message || 'Unknown Error'}
</p>
<Button variant="outline" onClick={router.refresh}>
Reload
</Button>
</div>
)
}
36 changes: 33 additions & 3 deletions frontend/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ const getAuthToken = (res: Response) => {
return {
accessToken: Authorization,
refreshToken,
accessTokenExpires: Date.now() + ACCESS_TOKEN_EXPIRE_TIME - 30 * 1000, // 29 minutes 30 seconds
refreshTokenExpires: Date.parse(refreshTokenExpires) - 30 * 1000 // 23 hours 59 minutes 30 seconds
accessTokenExpires: Date.now() + ACCESS_TOKEN_EXPIRE_TIME - 30 * 1000,
refreshTokenExpires: Date.parse(refreshTokenExpires) - 30 * 1000
}
}

Expand Down Expand Up @@ -75,7 +75,7 @@ export const authOptions: NextAuthOptions = {
],
session: {
strategy: 'jwt',
maxAge: 24 * 60 * 60 // 24 hours
maxAge: 7 * 24 * 60 * 60
},
callbacks: {
jwt: async ({ token, user }: { token: JWT; user?: User }) => {
Expand All @@ -86,6 +86,32 @@ export const authOptions: NextAuthOptions = {
token.refreshToken = user.refreshToken
token.accessTokenExpires = user.accessTokenExpires
token.refreshTokenExpires = user.refreshTokenExpires
} else if (token.accessTokenExpires <= Date.now()) {
const reissueRes = await fetch(API_BASE_URL + '/auth/reissue', {
headers: {
cookie: `refresh_token=${token.refreshToken}`
},
cache: 'no-store'
})

if (reissueRes.ok) {
const {
accessToken,
refreshToken,
accessTokenExpires,
refreshTokenExpires
} = getAuthToken(reissueRes)

token.accessToken = accessToken
token.refreshToken = refreshToken
token.accessTokenExpires = accessTokenExpires
token.refreshTokenExpires = refreshTokenExpires
} else {
return {
...token,
error: 'RefreshAccessTokenError'
}
}
}
return token
},
Expand All @@ -100,8 +126,12 @@ export const authOptions: NextAuthOptions = {
accessTokenExpires: token.accessTokenExpires,
refreshTokenExpires: token.refreshTokenExpires
}
session.error = token.error
return session
}
},
pages: {
signIn: '/login'
}
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const fetcher = {
if (!response.ok) {
if (response.status === HttpStatus.UNAUTHORIZED && !retry) {
const session = await auth()
if (session) {
if (session && !session.error) {
// retry request
return this.customFetch<T>(url, method, body, cache, true)
}
Expand Down
85 changes: 6 additions & 79 deletions frontend/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,9 @@
import { ACCESS_TOKEN_EXPIRE_TIME, API_BASE_URL } from '@/lib/vars'
import { encode, getToken } from 'next-auth/jwt'
import { parseCookie } from 'next/dist/compiled/@edge-runtime/cookies'
import { NextResponse, type NextRequest } from 'next/server'
import { withAuth } from 'next-auth/middleware'

const getAuthToken = (res: Response) => {
const Authorization = res.headers.get('authorization') as string
const parsedCookie = parseCookie(res.headers.get('set-cookie') || '')
const refreshToken = parsedCookie.get('refresh_token') as string
const refreshTokenExpires = parsedCookie.get('Expires') as string
return {
accessToken: Authorization,
refreshToken,
accessTokenExpires: Date.now() + ACCESS_TOKEN_EXPIRE_TIME - 30 * 1000,
refreshTokenExpires: Date.parse(refreshTokenExpires) - 30 * 1000
export default withAuth({
pages: {
signIn: '/login'
}
}
})

const sessionCookieName = process.env.NEXTAUTH_URL?.startsWith('https://')
? '__Secure-next-auth.session-token'
: 'next-auth.session-token'

export const middleware = async (req: NextRequest) => {
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET })
if (req.nextUrl.pathname.startsWith('/console') && !token)
return NextResponse.redirect(new URL('/', req.url))

if (token && token.accessTokenExpires <= Date.now()) {
const reissueRes = await fetch(API_BASE_URL + '/auth/reissue', {
headers: {
cookie: `refresh_token=${token.refreshToken}`
},
cache: 'no-store'
})
if (reissueRes.ok) {
const {
accessToken,
refreshToken,
accessTokenExpires,
refreshTokenExpires
} = getAuthToken(reissueRes)
const newToken = await encode({
secret: process.env.NEXTAUTH_SECRET as string,
token: {
...token,
accessToken,
refreshToken,
accessTokenExpires,
refreshTokenExpires
},
maxAge: 24 * 60 * 60 // 24 hours
})
req.cookies.set(sessionCookieName, newToken)
const res = NextResponse.next({
request: {
headers: req.headers
}
})
res.cookies.set(sessionCookieName, newToken, {
maxAge: 24 * 60 * 60,
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'lax'
})
return res
} else if (reissueRes.status == 401) {
req.cookies.delete(sessionCookieName)
const res = NextResponse.next({
request: {
headers: req.headers
}
})
res.cookies.delete(sessionCookieName)
return res
}
}
return NextResponse.next({
request: {
headers: req.headers
}
})
}
export const config = { matcher: ['/console/:path*'] }
2 changes: 2 additions & 0 deletions frontend/src/types/next-auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ interface Token {
refreshToken: string
accessTokenExpires: number
refreshTokenExpires: number
error?: string
}

declare module 'next-auth' {
interface User extends DefaultUser, UserData, Token {}
interface Session extends DefaultSession {
user: UserData
token: Token
error?: string
}
}

Expand Down

0 comments on commit 2b4122e

Please sign in to comment.