Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: jose asymmetric, closes #21 #22

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"release": "npm run build && npm run test && npm publish --access public"
},
"dependencies": {
"jose": "^4.14.4"
"jose": "^5.2.3"
},
"devDependencies": {
"@elysiajs/cookie": "^0.3.0",
Expand Down
41 changes: 36 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,19 @@ export interface JWTOption<
name?: Name
/**
* JWT Secret
* Only `secret` or both `privateKey`, `publicKey` must be set
*/
secret: string | Uint8Array | KeyLike
secret?: string | Uint8Array | KeyLike
/**
* JWT Private Key
* Only `secret` or both `privateKey`, `publicKey` must be set
*/
privateKey?: Uint8Array | KeyLike
/**
* JWT Public Key
* Only `secret` or both `privateKey`, `publicKey` must be set
*/
publicKey?: Uint8Array | KeyLike
/**
* Type strict validation for JWT payload
*/
Expand All @@ -80,6 +91,8 @@ export const jwt = <
>({
name = 'jwt' as Name,
secret,
publicKey,
privateKey,
// Start JWT Header
alg = 'HS256',
crit,
Expand All @@ -91,11 +104,29 @@ export const jwt = <
...payload
}: // End JWT Payload
JWTOption<Name, Schema>) => {
if (!secret) throw new Error("Secret can't be empty")

const key =
typeof secret === 'string' ? new TextEncoder().encode(secret) : secret

let asymmetric = false

if (secret && (privateKey || publicKey)) {
throw new Error("When using asymmetric algorithm, only `privateKey` and `publicKey` is accepted")
}

if (privateKey && !publicKey) {
throw new Error("When using asymmetric algorithm, both `privateKey` and `publicKey` must be set. Public key is missing")
}

if (publicKey && !privateKey) {
throw new Error("When using asymmetric algorithm, both `privateKey` and `publicKey` must be set. Private key is missing")
}

if (privateKey && privateKey) {
asymmetric = true
} else if (!secret) {
throw new Error("Secret can't be empty")
}

const validator = schema
? getSchemaValidator(
t.Intersect([
Expand Down Expand Up @@ -146,7 +177,7 @@ JWTOption<Name, Schema>) => {
if (nbf) jwt = jwt.setNotBefore(nbf)
if (exp) jwt = jwt.setExpirationTime(exp)

return jwt.sign(key)
return jwt.sign(asymmetric ? privateKey! : key!)
},
verify: async (
jwt?: string
Expand All @@ -158,7 +189,7 @@ JWTOption<Name, Schema>) => {
if (!jwt) return false

try {
const data: any = (await jwtVerify(jwt, key)).payload
const data: any = (await jwtVerify(jwt, asymmetric ? publicKey! : key!)).payload

if (validator && !validator!.Check(data))
throw new ValidationError('JWT', validator, data)
Expand Down
53 changes: 41 additions & 12 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Elysia, t } from 'elysia'
import { importJWK } from 'jose'
import { jwt } from '../src'

import { describe, expect, it } from 'bun:test'

const req = (path: string) => new Request(`http://localhost${path}`)
const post = (path: string, body = {}) =>
new Request(`http://localhost${path}`, {
method: 'POST',
Expand All @@ -14,8 +14,22 @@ const post = (path: string, body = {}) =>
})

describe('Static Plugin', () => {
async function signTest() {
const name = 'Shirokami'

const _sign = post('/sign', { name })
await _sign.text()

const _verified = post('/verify', { name })
const signed = (await _verified.json()) as {
name: string
}

expect(name).toBe(signed.name)
}

it('sign JWT', async () => {
const app = new Elysia()
new Elysia()
.use(
jwt({
name: 'jwt',
Expand All @@ -31,16 +45,31 @@ describe('Static Plugin', () => {
body: t.Object({ name: t.String() })
})

const name = 'Shirokami'

const _sign = post('/sign', { name })
const token = await _sign.text()
await signTest()
})
it('sign JWT (asymmetric)', async () => {
const crv = 'Ed25519'
const d = 'N3cOzsFZwiIbtNiBYQP9bcbcTIdkITC8a4iRslrbW7Q'
const x = 'RjnTe-mqZcVls6SQ5CgW0X__jRaa-Quj5HBDREzVLhc'
const kty = 'OKP'

const _verified = post('/verify', { name })
const signed = (await _verified.json()) as {
name: string
}
new Elysia()
.use(
jwt({
name: 'jwt',
privateKey: await importJWK({ crv, d, x, kty }, 'EdDSA'),
publicKey: await importJWK({ crv, x, kty }, 'EdDSA')
})
)
.post('/validate', ({ jwt, body }) => jwt.sign(body), {
body: t.Object({
name: t.String()
})
})
.post('/validate', ({ jwt, body: { name } }) => jwt.verify(name), {
body: t.Object({ name: t.String() })
})

expect(name).toBe(signed.name)
})
await signTest()
})
})