npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@significa/auth-next

v1.4.1-test-ci-2

Published

Auth-related functions to handle access and refresh tokens in NextJS projects

Downloads

1

Readme

Significa's auth methods to handle JWT sessions on NextJS projects

This is work in progress and only suitable for internal use.

Description

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)

Using the package

  1. Generate a new github PAT (Classic Personal Access Token). Grant read:packages Download packages from GitHub Package Registry.

  2. 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).

  3. npm install @significa/auth-next

More info: Working with the GitHub npm registry.

Configuration

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'
)

Use

1. Create API Routes

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

2. Login / Logout

  • To login, just do a POST request to auth.server.paths.login.
  • To logout, do a GET request to auth.server.paths.logout.

Example useLogin and useLogout hooks

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 }
}

3. Page restrictions / Session refresh

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()

useRefreshSession

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 ...
}