├── .github └── workflows │ ├── main.yml │ └── size.yml ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src ├── checkBasicAuth.ts ├── index.ts ├── types.ts └── utils │ ├── authHeaderToBase64.ts │ └── findAndCheckUser.ts ├── test ├── authHeaderToBase64.test.ts ├── findAndCheckUser.test.ts └── init.test.ts ├── tsconfig.json └── yarn.lock /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['10.x', '12.x', '14.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test --ci --coverage --maxWorkers=2 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 J.C. Hiatt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NextJS Basic Auth 2 | 3 | This package makes it simple to add Basic HTTP Auth to your Next.js application. Note that it **requires** you to opt out of static generation for any routes you want to protect, due to the fact that it needs to be called inside `getServerSideProps`. 4 | 5 | ## Installation 6 | 7 | ``` 8 | yarn add nextjs-basic-auth 9 | ``` 10 | 11 | ## Setup 12 | 13 | Initialize by importing and passing an object containing containing a `users` key, the value of which should be an array of users (use `user` and `password` as the keys for each respective user object): 14 | 15 | ``` 16 | import initializeBasicAuth from 'nextjs-basic-auth' 17 | const users = [ 18 | { user: 'user1', password: 'toocool' }, 19 | { user: 'admin', password: 'password' }, 20 | ] 21 | const basicAuthCheck = initializeBasicAuth({ 22 | users: users 23 | }) 24 | ``` 25 | 26 | This will return a function to you that you can call with two arguments: `req` and `res`. 27 | 28 | ## Usage 29 | 30 | Provided you have initialized the package already, you can await the returned function inside `getServerSideProps` of any route(s). 31 | 32 | ``` 33 | // some-route.js 34 | export async function getServerSideProps(ctx) { 35 | const {req, res} = ctx 36 | 37 | await basicAuthCheck(req, res) 38 | 39 | return { 40 | props: {} 41 | } 42 | } 43 | ``` 44 | 45 | If the user input cannot be authenticated, the user will receive a 401 and a prompt to re-enter credentials. 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.3", 3 | "description": "Simple Basic Auth implemention for Next.js applications.", 4 | "license": "MIT", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "files": [ 8 | "dist", 9 | "src" 10 | ], 11 | "repository": "jchiatt/nextjs-basic-auth", 12 | "homepage": "https://github.com/jchiatt/nextjs-basic-auth", 13 | "bugs": "https://github.com/jchiatt/nextjs-basic-auth/issues", 14 | "keywords": [ 15 | "next", 16 | "nextjs", 17 | "basic auth", 18 | "auth" 19 | ], 20 | "engines": { 21 | "node": ">=10" 22 | }, 23 | "scripts": { 24 | "start": "tsdx watch", 25 | "build": "tsdx build", 26 | "test": "tsdx test", 27 | "lint": "tsdx lint", 28 | "prepare": "tsdx build", 29 | "size": "size-limit", 30 | "analyze": "size-limit --why" 31 | }, 32 | "peerDependencies": {}, 33 | "husky": { 34 | "hooks": { 35 | "pre-commit": "tsdx lint" 36 | } 37 | }, 38 | "prettier": { 39 | "printWidth": 80, 40 | "semi": false, 41 | "singleQuote": false, 42 | "trailingComma": "es5" 43 | }, 44 | "name": "nextjs-basic-auth", 45 | "author": "J.C. Hiatt", 46 | "module": "dist/nextjs-basic-auth.esm.js", 47 | "size-limit": [ 48 | { 49 | "path": "dist/nextjs-basic-auth.cjs.production.min.js", 50 | "limit": "10 KB" 51 | }, 52 | { 53 | "path": "dist/nextjs-basic-auth.esm.js", 54 | "limit": "10 KB" 55 | } 56 | ], 57 | "devDependencies": { 58 | "@size-limit/preset-small-lib": "^4.9.1", 59 | "@types/basic-auth": "^1.1.3", 60 | "@types/node": "^14.14.10", 61 | "husky": "^4.3.5", 62 | "size-limit": "^4.9.1", 63 | "tsdx": "^0.14.1", 64 | "tslib": "^2.0.3", 65 | "typescript": "^4.1.2" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/checkBasicAuth.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse } from "http" 2 | import { User } from "./types" 3 | import authHeaderToBase64 from "./utils/authHeaderToBase64" 4 | import findAndCheckUser from "./utils/findAndCheckUser" 5 | 6 | async function checkBasicAuth( 7 | req: IncomingMessage, 8 | res: ServerResponse, 9 | users: User[] 10 | ) { 11 | if (!req.headers.authorization) { 12 | res.setHeader("WWW-Authenticate", 'Basic realm="Protected"') 13 | res.statusCode = 401 14 | res.end("Unauthorized") 15 | } else { 16 | const [user, password] = authHeaderToBase64(req.headers.authorization) 17 | 18 | if (!findAndCheckUser(user, password, users)) { 19 | res.setHeader("WWW-Authenticate", 'Basic realm="Protected"') 20 | res.statusCode = 401 21 | } 22 | } 23 | } 24 | 25 | export default checkBasicAuth 26 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse } from "http" 2 | import { User } from "./types" 3 | import checkBasicAuth from "./checkBasicAuth" 4 | 5 | interface Options { 6 | users?: User[] 7 | } 8 | 9 | function init(options: Options = {}) { 10 | const { users } = options 11 | if (!users) { 12 | throw new Error( 13 | "You must supply an array of user/password combinations in the config." 14 | ) 15 | } 16 | 17 | return (req: IncomingMessage, res: ServerResponse) => { 18 | checkBasicAuth(req, res, users) 19 | } 20 | } 21 | 22 | export default init 23 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | user: string 3 | password: string 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/authHeaderToBase64.ts: -------------------------------------------------------------------------------- 1 | const headerToBase64 = (header: string) => { 2 | const b64auth = header.split(" ")[1] 3 | const [user, password] = Buffer.from(b64auth, "base64") 4 | .toString() 5 | .split(":") 6 | 7 | return [user, password] 8 | } 9 | 10 | export default headerToBase64 11 | -------------------------------------------------------------------------------- /src/utils/findAndCheckUser.ts: -------------------------------------------------------------------------------- 1 | import { User } from "../types" 2 | 3 | type findAndCheckUserReturn = User | false 4 | 5 | const findAndCheckUser = ( 6 | user: string, 7 | password: string, 8 | users: User[] 9 | ): findAndCheckUserReturn => { 10 | const foundUser = users.find( 11 | acct => acct.user === user && acct.password === password 12 | ) 13 | if (!foundUser) return false 14 | return foundUser 15 | } 16 | 17 | export default findAndCheckUser 18 | -------------------------------------------------------------------------------- /test/authHeaderToBase64.test.ts: -------------------------------------------------------------------------------- 1 | import authHeaderToBase64 from "../src/utils/authHeaderToBase64" 2 | 3 | const authHeader = "Basic YWRtaW46cGFzc3dvcmQ=" 4 | 5 | describe("authHeaderToBase64", () => { 6 | it("returns a username and password tuple when called with an Authorization header", () => { 7 | const result = authHeaderToBase64(authHeader) 8 | expect(Array.isArray(result)).toBe(true) 9 | expect(result.length).toBe(2) 10 | expect(result[0]).toBe("admin") 11 | expect(result[1]).toBe("password") 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /test/findAndCheckUser.test.ts: -------------------------------------------------------------------------------- 1 | import findAndCheckUser from "../src/utils/findAndCheckUser" 2 | 3 | const users = [ 4 | { user: "foo", password: "bar" }, 5 | { user: "admin", password: "password" }, 6 | ] 7 | 8 | describe("authHeaderToBase64", () => { 9 | it("returns false if no user can be found with supplied credentials", () => { 10 | expect(findAndCheckUser("doesnot", "exist", users)).toBe(false) 11 | }) 12 | 13 | it("returns the matched user object if one is found matching supplied credentials", () => { 14 | const match = findAndCheckUser("admin", "password", users) 15 | const expected = { user: "admin", password: "password" } 16 | expect(match).toBeTruthy() 17 | expect(typeof match).toBe("object") 18 | expect(match).toStrictEqual(expected) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/init.test.ts: -------------------------------------------------------------------------------- 1 | import init from "../src" 2 | 3 | describe("init function", () => { 4 | it("throws if a users array is not supplied", () => { 5 | expect(() => init()).toThrowError( 6 | "You must supply an array of user/password combinations in the config." 7 | ) 8 | }) 9 | 10 | it("returns a function when called", () => { 11 | const users = [{ user: "foo", password: "bar" }] 12 | const options = { users } 13 | const fn = init(options) 14 | expect(typeof fn).toBe("function") 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | } 35 | } 36 | --------------------------------------------------------------------------------