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

authpal

v1.5.11

Published

An authentication and authorization library for express applications and clients

Downloads

328

Readme

Authpal

authpal

NPM Version NPM Install Size NPM Downloads

authpal-client

NPM Version NPM Install Size NPM Downloads

A node package to handle user authentication and authorization securely on both client and server.

Built on top of express, passport and jwt.

Its goal is to be simple to use yet up to security standards. And be reusable across different apps so you don't have to rewrite the same thing every time you build a web app.

It uses the accessToken & refreshToken combo. The latter is stored in cookies and the former should be stored in memory (let accessToken, not localStorage.accessToken).

Server

For quick setup follow 1️⃣ and 2️⃣.
Your greens 🥦 are pretty good to have but you don't necessarily have to read.

Client

Setup your project following 3️⃣ then 4️⃣ explains the configuration for the client side better.

Server Side Usage

1️⃣ Setup

Prerequisites:

Your express app should use the following

import * as bodyParser from 'body-parser'
import * as cookieParser from 'cookie-parser'

//...

app.use(bodyParser.json())
app.use(cookieParser())

/*
  cors should also be set.
  those are required for secure httpOnly cookies by browsers
*/

Install the package:

npm install authpal

Import the package and istantiate it:

import { Authpal } from 'authpal'

//...

let authpal = new Authpal({
  //AuthpalConfigs are mandatory, read the 'Configs (concretely)' paragraph below
})

//...

Create your routes using the prebuilt middlewares:

//retrieve accessToken and set-cookie refreshToken
app.post('/login', authpal.loginMiddleWare) //no need to setup response

//generate a new accessToken via refreshToken cookie
app.get('/resume', authpal.resumeMiddleware) //no need to setup response

//verify headers have 'Bearer <accessToken>'
app.get('/secure', authpal.authorizationMiddleware, (req, res) => {
  let user = req.user

  //DO YOUR THINGS HERE

  res.sendStatus(200)
})

🥦 Configs


The way the interface is setup requires you to define callbacks to:

  • find your user by username/id/refreshToken
  • verify password
  • store refreshToken

This allows you to handle your user data however you prefer.

The configs type looks like this:

{
  jwtSecret: string //A secret used to encrypt the JWTs (usually in process.env.JWT_SECRET)

  /*
  By default authpal will look in the /login request body for 'username' and 'password'.
  These can be changed if you'd rather call them something else
  */
  usernameField?: string //Overrides 'username'
  passwordField?: string //Overrides 'password'

  refreshTokenExpiration?: number //How many seconds before refresh token expires (default 14 days)


  //A callback that must return the User Payload based on the username
  findUserByUsernameCallback(
    username: string
  ): Promise<AuthpalJWTPayload | null> | AuthpalJWTPayload | null

  //A callback that must return the User Payload based on the user ID
  findUserByIDCallback(
    userid: string | number
  ): Promise<AuthpalJWTPayload | null> | AuthpalJWTPayload | null

  //A callback that must return the User Payload based on the token
  findUserByRefreshToken(
    refreshToken: string
  ): Promise<AuthpalJWTPayload | null> | AuthpalJWTPayload | null

  //A callback that must return a boolean after verifying that password matches the user
  verifyPasswordCallback(
    username: string,
    password: string
  ): Promise<boolean> | boolean

  /*
  A callback that returns the refresh token object as well as the associated User Payload.
  Use this to store the token in your database.
  */
  tokenRefreshedCallback(
    jwtPayload: AuthpalJWTPayload,
    token: RefreshToken
  ): Promise<void> | void

  /*
  A callback that returns the refresh token object as well as the associated User Payload.
  Use this to delete the token from your database when user logs out.
  */
  tokenDeletedCallback(
    jwtPayload: AuthpalJWTPayload,
    token: RefreshToken
  ): Promise<void> | void
}

🥦 Understand the User Payload (AuthpalJWTPayload)


AuthpalJWTPayload is defined as

{
  userid?: string | number
}

This is the object that will be passed around the middlewares and put into a JWT on the client's cookies.

If you don't understand what this is, your best bet is to just leave it as is, but this is passed as a generic and can therefore be extended.

For example if you require to send some more data you can do it this way:

interface MyCustomPayload extends AuthpalJWTPayload {
  mayTheForce: 'Be with you. No Kathleen, not you...'
}

let authpal = new Authpal<MyCustomPayload>({
  //configs
})

//at this point if you need to extract it out of a secured route you can access
app.get('/secure', authpal.authorizationMiddleware, (req, res) => {
  let user = req.user.MayTheForce //'Be with you. No Kathleen, not you...'
  res.sendStatus(200)
})

2️⃣ Configs (concretely)

If you skipped the previous two paragraphs, it doesn't really matter, all you need to know is that you need to setup at least the basic configs in a similar fashion

let authpal = new Authpal({
  jwtSecret: 'myJWTsecret', //please don't hardcode it but process.env.JWT_SECRET or something,

  //These examples are with mongo & mongoose but obviously you need to implement your own fetch callbacks
  findUserByUsernameCallback: async (username) => {
    return await UsersModel.findOne({ username })
  },
  findUserByIDCallback: async (userid) => {
    return await UsersModel.findOne({ _id: userid })
  },
  findUserByRefreshToken: async (token) => {
    let session = await SessionsModel.findOne({ token }) //You can save the tokens wherever you want, even straight up in the users documents.
    return {
      userid: session.user,
    }
  },
  tokenRefreshedCallback: async (jwtPayload, token) => {
    UsersModel.findOne({ _id: jwtPayload.userid }).then((user) => {
      //Delete existing or update them to your discretion
      await SessionsModel.create({
        user: jwtPayload.userid,
        token: token.token,
        expiration: token.expiration,
      })
    })
  },
  tokenDeletedCallback: async (jwtPayload, token) => {
    UsersModel.findOne({ _id: jwtPayload.userid }).then((user) => {
      //Delete the token on logout
      await SessionsModel.deleteMany({
        user: jwtPayload.userid,
        token: token.token,
      })
    })
  },

  //Example with bcrypt but you can implement your own
  verifyPasswordCallback: (username, password) => {
    let user = await UsersModel.findOne({ username })
    return bcrypt.compareSync(password, user.hash)
  },
})

Client Side Usage

3️⃣ Setup

Install the package:

npm install authpal-client

Import the package and istantiate it:

import { AuthpalClient } from 'authpal'

//...

let authpalClient = new AuthpalClient({
  //AuthpalClientConfigs are mandatory, read the 'Configs' paragraph below
})

//...

Here's how to use the library

/*
  RESUME SESSION

  As soon as your application start attempt to resume to revalidate the refresh cookie.
  If it exists and the server validates it will receive a new access token and be logged in again.
*/
authpalClient.attemptResume()

/*
  LOGIN

  This method takes a credentials object as a parameter.
  These have to match what you selected on the server side as overrides.
  You you didn't change anything they'll be 'username' and 'password'.
*/
authpalClient.login({
  username: 'myusername',
  password: 'asupersecretpassword',
})

/*
  LOGOUT
*/
authpalClient.logout()

All of these methods are async Promises and can be awaited for.

The best method to catch the events is through the emitter that you can pass via the ClientConfigs:

userChangesEmitter.subscribe((changes) => {
  //This fires with every event and change in login status.
})

/*
    resomeDoneEmitter is a Subject<void> (from 'rxjs').
    This is marked as .complete() whenever the attemptResume() is done.

    This is particulary useful when you need to wait until the resume 
    process is over before doing other things like rendering or requesting data
  */
await resumeDoneEmitter.toPromise() //Will only continue when is alredy or gets completed

/*
    Sometimes you wanna do more stuff before the resume process is over.
    You can provide a middleware function that gets fired right before completing succcessfully.
  */
{
  //... your client configs
  resumeDoneMiddleware: async (changes) => {
    //User requests are now authenticated
    //Do whatever you need to do (ask for user data or ...)
  }
}

Once you're authenticated, you can pass the authorization token to your request library of choice like so:

//This example is with axios but it should work with any library

axios
  .get({
    method: 'get',
    url: 'https://example.com/api/v1/secure/private/get-out/please-no',
    headers: {
      //Your other custom headers go here

      //Add Auth headers to the others
      ...this.authPalClient.getAuthorizationHeader(),
    },
  })
  .then(({ data }) => {
    console.log(data)
  })

Whenever you receive a userChangesEvent it's defined like so:

/*
  The changes fired in your
  userChangesEmitter.subscribe((changes) => {})
*/
{
  type: string //this can be 'login', 'resume' or 'logout'
  authenticated: boolean //is user authenticated after this event?
}

4️⃣ Configs

The AuthpalClientConfigs object is defined this way:

/*
  You wanna keep an outside reference to these so you can subscribe 
  and listen to events, or await for the resume process to be done
*/
let userChangesEmitter = new UserChangesEmitter()
let resumeDoneEmitter = new Subject()

let authpalClient = new AuthpalClient({
  //The POST endpoint for logging in on your server
  loginPostUrl: 'https://example.com/api/v1/login',
  //The GET endpoint for resuming the session in on your server
  loginPostUrl: 'https://example.com/api/v1/login',
  //The GET endpoint for logging out on your server
  //If you don't call this the session will not be closed, hence resumed on page refresh
  logoutGetURL: 'https://example.com/api/v1/logout',

  //The custom subject that emits changes to the user (As defined above)
  userChangesEmitter: userChangesEmitter,

  //A Subject<void> that gets completed when the resume attempt is over (See 3️⃣)
  resumeDoneEmitter: resumeDoneEmitter,

  //(optional) A middleware callback to call right before a resume request succeeds
  resumeDoneMiddleware: async (changes) => {
    //Do your things... You're already authenticated at this point
  },
})