-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit e69e300
Showing
11 changed files
with
2,006 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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_ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
generated | ||
pnpm-lock.yaml | ||
CHANGELOG.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
Oops, something went wrong.