This is work in progress and only suitable for internal use.
This package solves JWT-based authentication by saving the refresh token in an http-only cookie (accessible only server-side) and the access-token + a session indicator with the expiration date in client-acessible cookies.
- server-side api routes that handles all session cookies
- server-side route restrictions
- server-side token refresh
- client-side token refresh (interval + window focus)
- client-side access token access (e.g.: for client-side API calls)
-
Generate a new github PAT (Classic Personal Access Token). Grant
read:packages
Download packages from GitHub Package Registry. -
Run
npm login --scope=@significa --registry=https://npm.pkg.github.com
. In the interactive CLI set your GitHub handle as the username and the newly generated PAT as the password (email can be anything). -
npm install @significa/auth-next
More info: Working with the GitHub npm registry.
Create a lib/auth.ts
file to create your auth's config.
This package exposes a main Auth
class that should be initialized with your project's configuration:
// lib/auth.ts
import { Auth } from '@significa/auth-next'
import { API_URL } from 'common/constants'
export const auth = new Auth({
accessTokenKey: 'project_token',
sessionIndicatorKey: 'project_session',
refreshTokenKey: 'project_refresh_token',
/* configuration for the handler in Next's API Routes */
serverHandlers: {
login: {
fetch: (email, password) => {
return fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
password,
}),
})
},
parseResponse: async (res) => {
const { data } = await res.json()
return {
accessToken: data.access_token,
expires: data.expires,
refreshToken: data.refresh_token,
}
},
},
refresh: {
fetch: async (refreshToken: string) => {
return fetch(`${API_URL}/auth/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refresh_token: refreshToken }),
})
},
parseResponse: async (res) => {
const { data } = await res.json()
return {
accessToken: data.access_token,
expires: data.expires,
refreshToken: data.refresh_token,
}
},
},
logout: {
fetch: async (refreshToken: string) => {
return fetch(`${API_URL}/auth/logout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refresh_token: refreshToken }),
})
},
},
},
})
If you're using Directus, you can use createDirectusHandlers
instead:
// lib/auth.ts
import { Auth, createDirectusHandlers } from '@significa/auth-next'
import { API_URL } from 'common/constants'
export const auth = new Auth({
accessTokenKey: 'project_token',
sessionIndicatorKey: 'project_session',
refreshTokenKey: 'project_refresh_token',
serverHandlers: createDirectusHandlers({
url: API_URL,
}),
})
Finally, you can create some aliases for page restrictions:
// still in lib/auth.ts
export const withRestriction = auth.restrictions.withRestriction
export const withSessionRefresh = auth.restrictions.withSessionRefresh
export const withGuestRestriction = withRestriction.bind(null, (isAuthed) =>
isAuthed ? '/app' : false
)
export const withAuthRestriction = withRestriction.bind(null, (isAuthed) =>
isAuthed ? false : '/login'
)
Create a pages/api/auth/[path].ts
file.
If you passed basePath
in your serverHandlers
config, make sure you create the file in the appropriate path
import { auth } from 'lib/auth'
export default auth.server.handler
- To login, just do a POST request to
auth.server.paths.login
. - To logout, do a GET request to
auth.server.paths.logout
.
You can create some hooks to centralize all the login/logout logic:
// useLogin.tsx
import { useRouter } from 'next/router'
import { useState } from 'react'
import { auth } from 'lib/auth'
export const useLogin = ({
onSuccess,
onError,
}: {
onSuccess?: () => void
onError?: () => void
} = {}) => {
const { push, query } = useRouter()
const [loading, setLoading] = useState(false)
const [error, setError] = useState(false)
const login = async ({
email,
password,
}: {
email: string
password: string
}) => {
setLoading(true)
try {
const res = await fetch(auth.server.paths.login, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
})
if (!res.ok) throw new Error()
if (typeof onSuccess === 'function') {
onSuccess()
} else {
// push to app by default
push(typeof query.returnTo === 'string' ? query.returnTo : '/app')
}
} catch (error) {
setError(true)
onError?.()
} finally {
setLoading(false)
}
}
const resetError = () => {
if (error) setError(false)
}
return { login, loading, error, resetError }
}
// useLogout.tsx
import { useRouter } from 'next/router'
import { useState } from 'react'
import { auth } from 'lib/auth'
export const useLogout = () => {
const { push } = useRouter()
const [loading, setLoading] = useState(false)
const logout = async () => {
setLoading(true)
try {
const res = await fetch(auth.server.paths.logout)
if (!res.ok) throw new Error()
} catch (error) {
// at least clear client-side cookies
auth.client.clearAccessToken()
auth.client.clearSessionIndicator()
} finally {
// redirect anyway
push('/')
setLoading(false)
}
}
return { logout, loading }
}
Finally, you can use the aliases in 'lib/auth' to lock routes:
// pages/app/index.tsx
import { withAuthRestriction } from 'lib/auth'
const AppHomepage = () => <div>Hello from App</div>
export const getServerSideProps = withAuthRestriction()
export default AppHomepage
withRestriction
already refreshes the session if necessary but, if you need, you can trigger a session refresh server-side by using withSessionRefresh
:
// pages/index.tsx
import { withSessionRefresh } from 'lib/auth'
...
export const getServerSideProps = withSessionRefresh()
This package also exports a useRefreshSession
hook that can be used to make client-side refreshes at a certain interval or whenever the window gains focus:
// _app.tsx
import { useRefreshSession, getDateDistance } from '@significa/auth-next'
import { auth } from 'lib/auth'
function MyApp({ Component, pageProps }: AppProps) {
useRefreshSession({
refreshPath: auth.server.paths.refresh,
shouldRefresh: () => {
const expiryDate = auth.client.getSessionIndicator()
if (!expiryDate) return false
return getDateDistance(new Date(expiryDate)) <= 30
},
onRefresh: () => {
queryClient.invalidateQueries(useMeQuery.getKey())
},
})
})
return ...
}