@shajib729/auth-kit
v1.0.0
Published
A unified, strategy-based authentication middleware system for Express. Supporting Google OAuth, Credentials, and custom authentication strategies with a single onAuthenticate callback interface.
Maintainers
Readme
auth-kit
A unified, strategy-based auth system for Express. Google OAuth, credentials, GitHub, anything else — every strategy resolves to the same result shape, and you decide what happens next in one place.
npm install @shajib729/auth-kitThe core idea
Authenticating with Google and authenticating with email/password are fundamentally different processes — but what your server does after either succeeds is usually identical: look up or create a user, issue a session, redirect or respond. Most auth libraries blur this line and you end up with provider-specific glue code scattered across routes.
auth-kit splits it cleanly into two layers:
| Layer | Responsibility | Where it lives |
|---|---|---|
| Strategy | Talk to the provider, return a normalized user | strategies/*.js |
| AuthKit | Wire strategies to routes, call your hook | core/AuthKit.js |
| Your hook | Decide what "logged in" means for your app | your code |
Every strategy, no matter how different internally, resolves to exactly this:
{
provider: "google", // or "credentials", "github", ...
user: {
id, email, name, picture, emailVerified
},
raw: { /* untouched provider payload, for anything custom */ }
}Your onAuthenticate hook receives this shape every time, regardless of
which strategy ran. Add a GitHub strategy next month — your hook doesn't
change at all.
Quick start
import express from "express"
import jwt from "jsonwebtoken"
import { AuthKit, GoogleStrategy, CredentialsStrategy } from "auth-kit"
const google = new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
redirectURI: "http://localhost:8000/auth/google/callback",
})
const credentials = new CredentialsStrategy({
// YOU control persistence — Postgres, Mongo, anything
verify: async ({ email, password }) => {
const row = await db.users.findByEmail(email)
if (!row) return null
const valid = await CredentialsStrategy.compare(password, row.passwordHash)
return valid ? row : null
},
})
const authKit = new AuthKit({
strategies: { google, credentials },
// Called after ANY strategy succeeds — same shape every time
onAuthenticate: async (result, req, res) => {
const { provider, user } = result
const token = jwt.sign({ id: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: "1d" })
if (provider === "google") {
return res.redirect(`http://localhost:3000/auth/callback?token=${token}`)
}
res.json({ token, user })
},
onError: (err, req, res) => {
res.status(err.status ?? 500).json({ error: err.message, code: err.code })
},
})
const app = express()
app.use(express.json())
app.get("/auth/google", authKit.initiate("google"))
app.get("/auth/google/callback", authKit.complete("google"))
app.post("/auth/login", authKit.complete("credentials"))Notice: initiate() and complete() are the same two calls for every
strategy. The only thing that changes per-provider is which string you pass.
Strategies included
GoogleStrategy
new GoogleStrategy({
clientID, // required
clientSecret, // required
redirectURI, // required
scopes, // default: ["email", "profile"]
accessType, // "offline" | "online", default "offline"
prompt, // default "consent"
})result.raw contains { profile, tokens } — the full Google profile and
token response, in case you need fields beyond the normalized set (e.g.
locale, or the Google refresh_token to call Google APIs later).
CredentialsStrategy
new CredentialsStrategy({
verify: async ({ email, password }, req) => {
// Return a user object, or null/undefined if invalid.
// Throwing here is treated as a 500 (DB error), not a 401 (bad password) —
// keeps your error codes honest.
},
emailField: "email", // body field name, if you ever need to change it
passwordField: "password",
})This strategy deliberately does not touch your database — you provide
verify. That's what makes "production grade, your choice of persistence"
actually true: swap Postgres for Mongo for an in-memory Map without touching
anything in this package.
Password helpers (shared so signup and login use identical hashing):
const hash = await CredentialsStrategy.hash("plaintext") // for signup
const valid = await CredentialsStrategy.compare("plaintext", hash) // used internally by verify()Signup itself is intentionally not abstracted — creating a user is a
write to your specific schema, so write that route yourself (see
server.example.js) using CredentialsStrategy.hash() for consistency.
Adding your own strategy (e.g. GitHub)
Implement two methods. That's the entire contract:
import { BaseStrategy, AuthError } from "auth-kit"
export class GitHubStrategy extends BaseStrategy {
name = "github"
constructor({ clientID, clientSecret, redirectURI }) {
super()
this.clientID = clientID
this.clientSecret = clientSecret
this.redirectURI = redirectURI
}
async initiate(_req, res) {
res.redirect(`https://github.com/login/oauth/authorize?client_id=${this.clientID}&redirect_uri=${this.redirectURI}`)
}
async authenticate(req) {
const code = req.query.code
if (!code) throw new AuthError("Missing code", { status: 400 })
// ... exchange code, fetch profile via GitHub's API ...
return {
provider: this.name,
user: { id, email, name, picture, emailVerified },
raw: { profile, tokens },
}
}
}Register it and it slots into the exact same onAuthenticate hook:
authKit.use("github", new GitHubStrategy({ ... }))
app.get("/auth/github", authKit.initiate("github"))
app.get("/auth/github/callback", authKit.complete("github"))Nothing else in your app needs to know GitHub exists.
Error handling
Every strategy should throw AuthError (not a plain Error) for known
failure cases:
throw new AuthError("Invalid email or password", { status: 401, code: "INVALID_CREDENTIALS" })Unexpected errors (network failures, bugs) get auto-wrapped by AuthKit
into an AuthError with status: 500, code: "STRATEGY_ERROR", so
onError always receives a consistent shape:
err.message // human-readable
err.status // HTTP status to respond with
err.code // machine-readable, e.g. "INVALID_CREDENTIALS"
err.cause // original error, if any (useful for logging)What this package intentionally does NOT do
- No database.
CredentialsStrategy.verifyand youronAuthenticatehook are where persistence happens — bring your own ORM/driver. - No JWT signing.
onAuthenticategets a normalized user; signing tokens, setting cookies, and session management are yours to control. - No signup flow. Creating a user is schema-specific — write that
route directly, using
CredentialsStrategy.hash()for consistency.
This is by design: the moment a library starts making decisions about your schema or session strategy, it stops being reusable across projects. This package's only job is "run a strategy, hand back the same shape every time."
File structure
auth-kit/
├── core/
│ ├── AuthKit.js ← orchestrator: initiate(), complete(), use()
│ ├── BaseStrategy.js ← interface every strategy follows
│ └── errors.js ← AuthError class
├── strategies/
│ ├── google.js
│ └── credentials.js
├── index.js ← public exports
└── server.example.js ← full working example (Google + credentials)License
MIT
