Skip to content

Commit

Permalink
feat: add token validation
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielRivers committed May 1, 2024
0 parents commit e69e300
Show file tree
Hide file tree
Showing 11 changed files with 2,006 additions and 0 deletions.
41 changes: 41 additions & 0 deletions .github/workflows/build-test-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Build and test

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
main:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v3
with:
version: 8
- name: Setting up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
- name: Enabling pre-post scripts
run: pnpm config set enable-pre-post-scripts true
- run: pnpm install
- run: pnpm lint
- name: Cache pnpm modules
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- run: pnpm build
- run: pnpm test:coverage
- name: Upload coverage reports to Codecov
uses: codecov/[email protected]
with:
token: ${{ secrets.CODECOV_TOKEN }}
27 changes: 27 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
coverage

api_
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
generated
pnpm-lock.yaml
CHANGELOG.md
16 changes: 16 additions & 0 deletions .release-it.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"git": {
"requireBranch": "main",
"commitMessage": "chore: release v${version}"
},
"hooks": {
"before:init": ["git pull", "pnpm run lint"],
"after:bump": "npx changelogen@latest --output CHANGELOG.md && npm run build"
},
"github": {
"release": true
},
"npm": {
"publish": true
}
}
100 changes: 100 additions & 0 deletions lib/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// setToken.test.ts
import { describe, it, expect } from "vitest";
import { createHmac } from "crypto";
import { validateToken } from "./main";

function base64UrlEncode(str: string) {
return Buffer.from(str)
.toString("base64")
.replace("+", "-")
.replace("/", "_")
.replace(/=+$/, "");
}

export function jwtSign({
header,
payload,
secret,
}: {
header: any;
payload: any;
secret: string;
}) {
const encodedHeader = base64UrlEncode(JSON.stringify(header));
const encodedPayload = base64UrlEncode(JSON.stringify(payload));

const signature = createHmac("sha256", secret)
.update(encodedHeader + "." + encodedPayload)
.digest("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");

return encodedHeader + "." + encodedPayload + "." + signature;
}

describe("Validate token", () => {
it("no token supplied", async () => {
const isTokenValid = await validateToken({});
expect(isTokenValid).toEqual({
valid: false,
message: "Token is required",
});
});

it("no domain supplied", async () => {
const token = jwtSign({
header: { alg: "HS256", typ: "JWT" },
payload: { sub: "1234567890", name: "John Doe", iat: 1516239022 },
secret: "your-256-bit-secret",
});
const isTokenValid = await validateToken({ token });
expect(isTokenValid).toEqual({
valid: false,
message: "Domain is required",
});
});

it("token is valid", async () => {
const token =
"eyJhbGciOiJSUzI1NiIsImtpZCI6IjRjOmZhOjllOmQ2OjQ3OjIzOmI3OjM5OmM3OjhmOjk3OjI4OjQ1OmExOjg0OjM1IiwidHlwIjoiSldUIn0.eyJkYXRhIjp7InVzZXIiOnsiZW1haWwiOiJkYW5pZWxAa2luZGUuY29tIiwiZmlyc3RfbmFtZSI6IkRhbmllbCIsImlkIjoia3BfNjViMjhkNzFiYmExNGZhMzgwZDU2ZDJkOGQzNTAzZGEiLCJpc19wYXNzd29yZF9yZXNldF9yZXF1ZXN0ZWQiOmZhbHNlLCJpc19zdXNwZW5kZWQiOmZhbHNlLCJsYXN0X25hbWUiOiJSaXZlcnMiLCJvcmdhbml6YXRpb25zIjpbeyJjb2RlIjoib3JnXzU5MGQ3ZjFhODZhIiwicGVybWlzc2lvbnMiOm51bGwsInJvbGVzIjpudWxsfV0sInBob25lIjpudWxsLCJ1c2VybmFtZSI6ImRhbmllbCJ9fSwiZXZlbnRfaWQiOiJldmVudF8wMThmMzMxYTMxNzhmN2ZlZjI4NGI5NWZlNjc3MDM4NCIsInNvdXJjZSI6ImFkbWluIiwidGltZXN0YW1wIjoiMjAyNC0wNS0wMVQxNzo0MTo0NS41OTIxNDUrMTA6MDAiLCJ0eXBlIjoidXNlci51cGRhdGVkIn0.hAxfcxDNnzN8_U7sovti71NElh5pqVe6UEFKgVD1ZygVJUdEhmjYQOOSr6Aixj2ySs_hujZBvCRWeqG6jNPYbHRiV5kx0XaL6g3cW1DCoqpTpkxXtjf18HNYHCJmsUqMiSwfYpmVcI7kaIDfd0XwhWWH5gRdjAAMDneEwMKANklTzR_g_kIl5cVW5eVWntC4rFsSjRVvGSNb-OMsy2GJLWXUF8fc8Qru56VkJImeOE6ZOMi6wBhtx7HhOZEcEFgQjRvHeoQKdVmEE3BRUnO_LXTMMSjvP_kyfrS4JMaGWHc6mc8k1hZo_maASLSuXMF8882LZnr96cJFMHj8irRAug";
const isTokenValid = await validateToken({
token,
domain: "https://danielkinde.kinde.com",
});
expect(isTokenValid).toEqual({
valid: true,
message: "Token is valid",
});
});

it("token has invalid signature", async () => {
const token =
"eyJhbGciOiJSUzI1NiIsImtpZCI6IjRjOmZhOjllOmQ2OjQ3OjIzOmI3OjM5OmM3OjhmOjk3OjI4OjQ1OmExOjg0OjM1IiwidHlwIjoiSldUIn0.eyJkYXRhIjp7InVzZXIiOnsiZW1haWwiOiJtZSt3ZWJvb2tAZGFuaWVscml2ZXJzLmNvbSIsImZpcnN0X25hbWUiOiJhYSIsImlkIjoia3BfYmM2YjI4MTczZDZkNGRmYWI1NjU3NTg4NWIwMjE0YjEiLCJpc19wYXNzd29yZF9yZXNldF9yZXF1ZXN0ZWQiOmZhbHNlLCJpc19zdXNwZW5kZWQiOmZhbHNlLCJsYXN0X25hbWUiOiJhaGEiLCJvcmdhbml6YXRpb25zIjpbeyJjb2RlIjoib3JnXzU5MGQ3ZjFhODZhIiwicGVybWlzc2lvbnMiOm51bGwsInJvbGVzIjpudWxsfV0sInBob25lIjpudWxsLCJ1c2VybmFtZSI6bnVsbH19LCJldmVudF9pZCI6ImV2ZW50XzAxOGYyYTllNTkyZWNjZjUyMzI5MTgzYTQ1Y2QxOTU2Iiwic291cmNlIjoiYWRtaW4iLCJ0aW1lc3RhbXAiOiIyMDI0LTA0LTMwVDAyOjA5OjMxLjY0OTE2MisxMDowMCIsInR5cGUiOiJ1c2VyLnVwZGF0ZWQifQ.YIFd21Ek7R_hfpfEpAcwW5ebaDSDsT7TMYF5HTbg70CfWw36IDqKqQWKR6T1_vP0lI5s0xJlDptbjykvWfSm44fkz0LgjCWQhM_ENzTZiAa89pa2X1prjKH4vyS7lTqSCNXvCeYiAaFZSlr2X3s2aztASB4jGBDETziGCh_klNh4Gun3AcbkWOXz_QPm3YGNqgc3hYSBsLdOQbCQ_BxS2Wc60D3NAShVaodPrtOLC1bvY1vn_HucZHT9l-KuTKgY1st6D4er2K6DuHZaFBMMdvTaFQX5zN8OZltxeiucja4sg2vbtexryMdSdHY3y5Cz70dKWW6Ph2kHucK6xScQoQs";
const isTokenValid = await validateToken({
token,
domain: "https://danielkinde.kinde.com",
});
expect(isTokenValid).toEqual({
valid: false,
message: "signature verification failed",
});
});

it("token is present but not valid", async () => {
const token = jwtSign({
header: { alg: "HS256", typ: "JWT" },
payload: { sub: "1234567890", name: "John Doe", iat: 1516239022 },
secret: "your-256-bit-secret",
});

const isTokenValid = await validateToken({
token,
domain: "https://danielkinde.kinde.com",
});
expect(isTokenValid).toEqual({
valid: false,
message: 'Unsupported "alg" value for a JSON Web Key Set',
});
});
});
38 changes: 38 additions & 0 deletions lib/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createRemoteJWKSet, jwtVerify } from "jose";

export type jwtValidationResponse = {
valid: boolean;
message: string;
};

async function verifyJwt(
token: string,
domain: string,
): Promise<jwtValidationResponse> {
const JWKS = createRemoteJWKSet(new URL(`${domain}/.well-known/jwks.json`));

try {
await jwtVerify(token, JWKS);
return { valid: true, message: "Token is valid" };
} catch (error) {
return {
valid: false,
message: error instanceof Error ? error.message : "Unknown Error",
};
}
}

export const validateToken = async (validateOptions: {
token?: string;
domain?: string;
}): Promise<jwtValidationResponse> => {
if (!validateOptions.token) {
return { valid: false, message: "Token is required" };
}

if (!validateOptions.domain) {
return { valid: false, message: "Domain is required" };
}

return await verifyJwt(validateOptions.token, validateOptions.domain);
};
36 changes: 36 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@kinde/jwt-validator",
"private": false,
"type": "module",
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"version": "0.1.0-0",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest",
"test:coverage": "vitest --coverage",
"lint": "prettier --check .",
"lint:fix": "prettier --write ."
},
"module": "dist/jwt-validator.js",
"main": "dist/jwt-validator.cjs",
"types": "dist/main.d.ts",
"devDependencies": {
"@types/node": "^20.12.7",
"@vitest/coverage-v8": "^1.5.2",
"prettier": "^3.2.5",
"typescript": "^5.4.5",
"vite": "^5.2.10",
"vite-plugin-dts": "^3.9.0",
"vitest": "^1.5.2"
},
"dependencies": {
"jose": "^5.2.4"
}
}
Loading

0 comments on commit e69e300

Please sign in to comment.