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

@lwtw/identity

v0.4.2

Published

A simple library for managing users, user groups, and sessions

Downloads

7

Readme

README

A simple library for managing users, user groups, and sessions

Note: This package is still in alpha. API stability is not guaranteed, and it would be unwise to use it in production.

Introduction

This package helps you to quickly add user accounts, user groups, and sessions in your APIs/WebApps.

Change log

v0.4.1

  • Added feature: User names can be listed with paging functionality
  • Added feature: Group names can be listed with paging functionality
  • Added feature: Users can be searched for by their indexed data, with paging functionality

v0.4.0

  • Breaking change: Moved all static methods from the Group, Session, and User classes to their new respective *Admin classes
  • Breaking change: New table schema & setup incompatible with previous one
  • Introduced new Identity class that allows for multiple operations within one transaction

v0.3.1

  • Fixed issue: group.removeMember() now only removes the user from the specified group instead of all groups

v0.3.0

  • Fixed inconsistency: group.removeMember() now throws a GroupNotAMemberError instead of GroupHasMemberError

v0.2.2

  • Added middleware: requireAnyGroup()
  • Added middleware: requireAllGroups()
  • Added middleware: requireCondition()
  • Fixed issue: Exceptions in the middleware are now passed on to express error handlers, instead of causing a crash

v0.2.1

  • Added feature: User groups

v0.2.0

  • Added feature: Expired sessions can now be renewed using renewal tokens
  • Introduced breaking change to signature of method session.updateLifetime()
  • Introduced breaking change to interface RequireGuardOptions

v0.1.4

  • Added feature: Middleware can now automatically update the lifetime of valid sessions
  • Added feature: Session lifetime can be updated
  • Cleaned up leaking sessions from tests

v0.1.3

  • Added tests for route options

v0.1.2

  • Added feature: Data in the from of key-value pairs can be added to users

v0.1.1

  • Added this README file.

Installation

npm install @lwtw/identity

Usage

Connect to your database, create schema

import knex from "knex"
import { Identity } from "@lwtw/identity"

const db = knex(/* Your database connection parameters go here */)
const identity = new Identity(db) // Create a new instance of the Identity class

await identity.user.schema.build() // Create the required user tables
await identity.group.schema.build() // Create the required group tables - these reference the user_accounts table, so always build the user tables first
await identity.session.schema.build() // Create the required session tables - these reference the user_accounts table, so always build the user tables first

Create a new session and get the session token

const session = await identity.session.create() // Create new session that expires after 30 days
const sessionToken = session.getToken() // Get session token
const longLivedSession = await identity.session.create({ months: 3, days: 15 }) // Create a new session that expires after 3 months and 15 days

Open an existing session

const session = await identity.session.open(sessionToken) // Open an existing session - This will throw if the session token is invalid, or the session has expired

Renew a session

const renewedSession = await identity.session.renew(expiredSessionToken, renewalTokenForExpiredSession) // Renew a session - this will generate a new session- and renewal token

Set a timer to purge sessions with expired renewal periods from the database once a day

setInterval(() => {
  identity.session.purge() // Purges all sessions with expired renewal periods
}, 24 * 60 * 60 * 1000 /* One day in milliseconds */)

Destroy a session

const session = await identity.session.open(sessionToken) // Open an existing session
await session.destroy() // Destroy the session you just opened

Create a new user and set their password

const user = await identity.user.create("my-user") // Create a new user
await user.setPassword("my secret password") // Set the password for the user you just created

Get a user by username

const user = await identity.user.get("my-user") // Get a user by their username

Check if a user exists

await userExists = await identity.user.exists("my-user") // true if the user exists, false otherwise

Get user data

const user = await identity.user.get("my-user") // Get a user
const username = user.getUsername() // Get their username
const authenticated = user.isAuthenticated() // Check whether they are currently authenticated

Check if a password is correct

const user = await identity.user.get("my-user") // Get a user
const passwordIsCorrect = await user.verifyPassword("my secret password") // true if the password is correct, false otherwise

Remove a user

await identity.user.remove("my-user")

Authenticate a user

const user = await identity.user.get("my-user") // Get a user
await user.authenticate("my secret password") // Throws an error if the password is wrong
const userIsAutheticated = user.isAuthenticated() // Will be true if authentication succeeded

Log in a user to a session

const session = await identity.session.create() // Create a new session (you could also open an existing one)
const user = await identity.user.get("my-user") // Get the user
user.login(session, "my secret password") // Log in the user - This will throw if the password is wrong
const userIsAutheticated = user.isAuthenticated() // Will be true if login succeeded

Get a user from a session

const session = await identity.session.open(sessionToken) // Open an existing session
const user = await identity.user.fromSession(session) // Get the session user - this will throw if the session has no user logged in

Log out a user from a session

const session = await identity.session.open(sessionToken) // Open an existing session
const user = await identity.user.fromSession(session) // Get the session user
await user.logout(session) // Log out the user from this session

Deal with user data

const user = await identity.user.get("my-user") // Get a user
await user.setItems({ firstName: "Jane", lastName: "Doe", age: 28 }) // set values for custom keys 'firstName', 'lastName', and 'age'
const { firstName, age } = await user.getItems(["firstName", "age"]) // Get values for keys 'firstName' and 'age'
await user.removeItems(["firstName", "lastName"]) // Remove items 'firstName' and 'lastName'

List usernames

const firstTenUsernames = await identity.user.list(0, 10) // get the first 10 usernames in alphabetical order
const secondTenUsernames = await identity.user.list(10, 10) // get the next 10 usernames in alphabetical order

Index user data so it can be matched in a search

const user = await identity.user.get("my-user") // Get a user
await user.setItems(
  { firstName: "Jane", lastName: "Doe", age: 28 }, // set values for custom keys 'firstName', 'lastName', and 'age'
  true // Tell the backend to create an index of all 3 items, so they can be matched during a search operation
) 

Search for users

Note that the search only looks at user data that was added using the setItems() method with indexing explicitly enabled.

const firstTenUsernames = await identity.user.list(0, 10, "jane doe") // get the first 10 usernames with indexed data matching the search string, in order of relevance
const secondTenUsernames = await identity.user.list(10, 10, "[email protected]") // get the next 10 usernames with indexed data matching the search string, in order of relevance

Get names of groups the user is a member of

const user = await identity.user.get("my-user") // Get an existing user
const groupNames = user.listGroups() // Get a list of all group names the user is a member of

Create a group

const newGroup = await identity.group.create("my-group") // Create a new group
const groupName = newGroup.getName() // Get the name of the newly created group

Check whether a group exists

await groupExists = await identity.group.exists("my-group") // Resolves to true if the group exists, to false otherwise

Get an existing group

const existingGroup = await identity.group.get("my-group") // Get an existing group named 'my-group'

Add a user to a group

const group = await identity.group.get("my-group") // Get an existing group
const user = await identity.user.get("my-user") // Get an existing user
await group.addMember(user) // Add the user to the group

Get usernames of group members

const group = await identity.group.get("my-group") // Get an existing group
const usernames = await group.listMembers() // Gets a list of all usernames of users that are a member of the group

Check if user is a member of a group

const group = await identity.group.get("my-group") // Get an existing group
const user = await identity.user.get("my-user") // Get an existing user
const isMember = await group.hasMember(user) // Resolves to true if the user is a member of the group, to false otherwise

Remove a user from a group

const group = await identity.group.get("my-group") // Get an existing group
const user = await identity.user.get("my-user") // Get an existing user
await group.removeMember(user) // Removes the user from the group

List group names

const firstTenGroupNames = await identity.group.list(0, 10) // Get the first ten group names in alphabetical order
const secondTenGroupNames = await identity.group.list(10, 10) // Get the next ten group names in alphabetical order

Remove a group

await identity.group.remove("my-group") // Remove a group

Perform multiple operations as one atomic operation

await identity.atomicOperation(async (atomic) => {
    const user = await atomic.user.create("my-user")
    await user.setPassword("123")
    await user.setItems({
      foo: "bar",
      lorem: "ipsum",
    })

    const group = await atomic.group.create(
      "my-group"
    )
    await group.addMember(user)

    const session = await atomic.session.create()
    await user.login(session, "123")
  }) // Uses the same database transaction for all operations inside the callback
     // If an error is thrown inside the callback, all the previous operations in the callback are rolled back

Express middleware

For the middleware to do its job, the session token needs to be provided as Bearer token in the Authorization header.

Make a valid session required for a route

import express, { Request, Response } from "express"
import { Identity, requireSession, RequestWithIdentity } from "@lwtw/identity"
import knex from "knex"

const db = knex(/* Your database connection parameters go here */)
const identity = new Identity(db) // Create a new instance of the Identity class

app = express()
app.use("/session-required/", requireSession(identity)) // This will respond with an error if the request includes no valid session token

app.get("/session-required", (req: Request, res: Response) => {
  const { session } = req as RequestWithIdentity // Get the session from the request
})

Make a login required for a route

import express, { Request, Response } from "express"
import { Identity, requireLogin, RequestWithIdentity } from "@lwtw/identity"
import knex from "knex"

const db = knex(/* Your database connection parameters go here */)
const identity = new Identity(db) // Create a new instance of the Identity class

app = express()
app.use("/login-required/", requireLogin(identity)) // This will respond with an error if the request includes no valid session token with a logged in user

app.get("/login-required", (req: Request, res: Response) => {
  const { session, user } = req as RequestWithIdentity // Get the session and user from the request
})

Make it a requirement for the user to be in at least one group in a list

import express, { Request, Response } from "express"
import { Identity, requireAnyGroup, RequestWithIdentity } from "@lwtw/identity"
import knex from "knex"

const db = knex(/* Your database connection parameters go here */)
const identity = new Identity(db) // Create a new instance of the Identity class

app = express()
app.use(
    "/require-any-group",
    requireAnyGroup(identity, ["this-group", "or-that-group", "or-even-this-group"])
  ) // Responds with an error if the request comes from a user who is not in at least one of these groups

app.get("/require-any-group", (req: Request, res: Response) => {
  const { session, user } = req as RequestWithIdentity // Get the session and user from the request
})

Make it a requirement for the user to be in all groups in a list

import express, { Request, Response } from "express"
import { Identity, requireAllGroups, RequestWithIdentity } from "@lwtw/identity"
import knex from "knex"

const db = knex(/* Your database connection parameters go here */)
const identity = new Identity(db) // Create a new instance of the Identity class

app = express()
app.use(
    "/require-all-groups",
    requireAllGroups(identity, ["this-group", "and-this-group", "this-group-as-well"])
  ) // Responds with an error if the request comes from a user who is not in all of these groups

app.get("/require-all-groups", (req: Request, res: Response) => {
  const { session, user } = req as RequestWithIdentity // Get the session and user from the request
})

Make a custom requirement

import express, { Request, Response } from "express"
import { Identity, User, Session, requireCondition, RequestWithIdentity, UserInvalidError } from "@lwtw/identity"
import knex from "knex"

const db = knex(/* Your database connection parameters go here */)
const identity = new Identity(db) // Create a new instance of the Identity class

app = express()
app.use(
    "/require-condition",
    requireCondition(identity, (req: Request, res: Response) => {
      const { session, user } = req as RequestWithIdentity // Get the session and user from the request, if they are available
      const username = user instanceof User ? user.getUsername() : ""
      const sessionId = session instanceof Session ? session.getId() : ""

      if (username !== "admin") {
        console.warn(`User '${username}' tried to access the admin area (sessionId: ${sessionId})!`)
        throw new UserInvalidError("Only the administrator is allowed here!") // Prevents access
      }
    })
  ) // Responds with an error if the callback throws or rejects with a Group*, User* or Session* error - all other errors are forwarded to express
    // You can handle other kinds of errors as well by providing a responseCallback (see 'Middleware options' below)

app.get("/require-condition", (req: Request, res: Response) => {
  const { session, user } = req as RequestWithIdentity // Get the session and user from the request
})

Middleware options

You can pass an object of type RequireGuardOptions as the last argument to any require* middleware function to change its behavior.

export interface RequireGuardOptions {
  responseCode?: number // The HTTP-response code to send on error
  responseData?: { [key: string]: any } | string | Buffer // The response data to send on error
  headers?: { [key: string]: string } // The response headers to send on error
  responseCallback?: (
    req: Request,
    res: Response,
    error: Error
  ) => Promise<void> | void // A callback responsible for sending a response on error - if provided, the above options will have no effect
  update?:{
    lifetime: Lifetime
    renewalPeriod: Lifetime
  } // The new lifetime and renewal period for the session if a valid token was provided - by default, lifetime and renewal period remain unchanged
}

Notes on caching and access control security

When you get or create a user, or open or create a session, some data is cached in the resulting object. You can assume that any synchronous method returning data for a session or user (e.g. user.getUsername(), session.getExpirationDate()) may return stale information. Consider the following two examples:

const originalSession = await identity.session.create({ days: 1 })
const sessionCopy = await identity.session.open(originalSession.getToken())
await sessionCopy.updateLifetime({ days: 10 })

originalSession.getExpirationDate() // Will return a (stale) expiration date one day in the future
sessionCopy.getExpirationDate() // Will return an accurate expiration date 10 days in the future
const user = await identity.user.create("my-user") // Create a new user
const group = await identity.group.create("my-group") // Create a new, empty group
await group.addMember(user) // Add user to group

const groupList = user.listGroups() // will be a stale (and thus empty) array
const memberList = await identity.group.listMembers() // will correctly contain "my-user"

For this reason, it is recommended to keep session and user objects alive for as short as possible. Short lifespans tend to be the natural case for REST endpoints using the middleware provided, since the middleware checks access against a fresh instance of a session and/or user, and adds the session and user to the Request object express passes to route handlers. Once the route handler returns, the objects will go out of scope, and there is no risk of accessing stale data at some much later point.

WebSockets however may keep connections alive for long periods of time. In this case, it is recommended to create a new User or Session instance for each request coming in on the WebSocket, instead of keeping them around from when the connection was first initiated. You can however cache the session token at the beginning of the connection, and reopen the session for each request, so the client does not have to re-authenticate with every single request after the initial connection.

If you need to make sure your data is as recent as possible, you can simply re-get a user or group, or re-open a session:

let user = await identity.user.create("my-user") // Create a new user
const group = await identity.group.create("my-group") // Create a new, empty group
await group.addMember(user) // Add user to group

user = await identity.user.get(user.getUsername()) // Get a fresh instance of the user

const groupList = user.listGroups() // will correctly contain 'my-group'
const memberList = await identity.group.listMembers() // will still correctly contain 'my-user'

ToDos

  • Add tests to all methods that update or delete from the database to make sure only the desired row is affected