Skip to content
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
8 changes: 8 additions & 0 deletions packages/access-token-jwt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@
_This package is not published_

Verfies and decodes Access Token JWTs loosley following [draft-ietf-oauth-access-token-jwt-12](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-12)

## Features

- JWT verification with JWKS discovery
- Support for direct public key verification (no JWKS required)
- Symmetric algorithm support (HS256, HS384, HS512)
- Asymmetric algorithm support (RS256, RS384, etc.)
- Customizable claim validation
13 changes: 11 additions & 2 deletions packages/access-token-jwt/src/get-key-fn.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createSecretKey } from 'crypto';
import { createRemoteJWKSet } from 'jose';
import { createRemoteJWKSet, KeyLike } from 'jose';
import { JwtVerifierOptions } from './jwt-verifier';

type GetKeyFn = ReturnType<typeof createRemoteJWKSet>;
Expand All @@ -22,7 +22,16 @@ export default ({
let getKeyFn: GetKeyFn;
let prevjwksUri: string;

const secretKey = secret && createSecretKey(Buffer.from(secret));
// If secret is a KeyLike object (public key), use it directly
if (secret && typeof secret !== 'string') {
const publicKey = secret as KeyLike;
return () => () => publicKey;
}

// Otherwise, handle string secret as before
const secretKey = typeof secret === 'string' && secret
? createSecretKey(Buffer.from(secret))
: undefined;

return (jwksUri: string) => {
if (secretKey) return () => secretKey;
Expand Down
9 changes: 5 additions & 4 deletions packages/access-token-jwt/src/jwt-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { strict as assert } from 'assert';
import { Agent as HttpAgent } from 'http';
import { Agent as HttpsAgent } from 'https';
import { jwtVerify } from 'jose';
import type { JWTPayload, JWSHeaderParameters } from 'jose';
import type { JWTPayload, JWSHeaderParameters, KeyLike } from 'jose';
import { InvalidTokenError } from 'oauth2-bearer';
import discovery from './discovery';
import getKeyFn from './get-key-fn';
Expand Down Expand Up @@ -115,7 +115,7 @@ export interface JwtVerifierOptions {
* Secret to verify an Access Token JWT signed with a symmetric algorithm.
* By default this SDK validates tokens signed with asymmetric algorithms.
*/
secret?: string;
secret?: string | KeyLike;

/**
* You must provide this if your tokens are signed with symmetric algorithms
Expand Down Expand Up @@ -187,8 +187,9 @@ const jwtVerifier = ({
"You must not provide both a 'secret' and 'jwksUri'"
);
assert(audience, "An 'audience' is required to validate the 'aud' claim");
// Only require tokenSigningAlg for string secrets (symmetric keys)
assert(
!secret || (secret && tokenSigningAlg),
!secret || typeof secret !== 'string' || (typeof secret === 'string' && tokenSigningAlg),
"You must provide a 'tokenSigningAlg' for validating symmetric algorithms"
);
assert(
Expand All @@ -198,7 +199,7 @@ const jwtVerifier = ({
)} for 'tokenSigningAlg' to validate asymmetrically signed tokens`
);
assert(
!secret || (tokenSigningAlg && SYMMETRIC_ALGS.includes(tokenSigningAlg)),
!secret || typeof secret !== 'string' || (tokenSigningAlg && SYMMETRIC_ALGS.includes(tokenSigningAlg)),
`You must supply one of ${SYMMETRIC_ALGS.join(
', '
)} for 'tokenSigningAlg' to validate symmetrically signed tokens`
Expand Down
48 changes: 30 additions & 18 deletions packages/access-token-jwt/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface CreateJWTOptions {
discoverSpy?: jest.Mock;
delay?: number;
secret?: string;
privateKey?: any; // Allow passing a specific private key
}

export const createJwt = async ({
Expand All @@ -35,26 +36,37 @@ export const createJwt = async ({
jwksSpy = jest.fn(),
discoverSpy = jest.fn(),
secret,
privateKey: customPrivateKey,
}: CreateJWTOptions = {}): Promise<string> => {
const { publicKey, privateKey } = await generateKeyPair('RS256');
const publicJwk = await exportJWK(publicKey);
nock(issuer)
.persist()
.get(jwksUri)
.reply(200, (...args) => {
jwksSpy(...args);
return { keys: [{ kid, ...publicJwk }] };
})
.get(discoveryUri)
.reply(200, (...args) => {
discoverSpy(...args);
return {
issuer,
jwks_uri: (issuer + jwksUri).replace('//.well-known', '/.well-known'),
};
});
// Generate key pair if not provided
const { publicKey, privateKey: generatedPrivateKey } = customPrivateKey
? { publicKey: null, privateKey: customPrivateKey }
: await generateKeyPair('RS256');

const finalPrivateKey = customPrivateKey || generatedPrivateKey;

// Only set up mocks if not using custom keys
if (!customPrivateKey) {
const publicJwk = await exportJWK(publicKey);
nock(issuer)
.persist()
.get(jwksUri)
.reply(200, (...args) => {
jwksSpy(...args);
return { keys: [{ kid, ...publicJwk }] };
})
.get(discoveryUri)
.reply(200, (...args) => {
discoverSpy(...args);
return {
issuer,
jwks_uri: (issuer + jwksUri).replace('//.well-known', '/.well-known'),
};
});
}

const secretKey = secret && createSecretKey(Buffer.from(secret));
const signingKey = secretKey || finalPrivateKey;

return new SignJWT(payload)
.setProtectedHeader({
Expand All @@ -67,5 +79,5 @@ export const createJwt = async ({
.setAudience(audience)
.setIssuedAt(iat)
.setExpirationTime(exp)
.sign(secretKey || privateKey);
.sign(signingKey);
};
76 changes: 76 additions & 0 deletions packages/access-token-jwt/test/public-key.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { exportJWK, generateKeyPair } from 'jose';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note test

Unused import exportJWK.

Copilot Autofix

AI 4 months ago

To fix the problem, the unused exportJWK import should be removed from the file. This involves editing the import statement on line 1 to exclude exportJWK. The rest of the code remains unchanged, as this modification does not affect any functionality.

Suggested changeset 1
packages/access-token-jwt/test/public-key.test.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/access-token-jwt/test/public-key.test.ts b/packages/access-token-jwt/test/public-key.test.ts
--- a/packages/access-token-jwt/test/public-key.test.ts
+++ b/packages/access-token-jwt/test/public-key.test.ts
@@ -1,2 +1,2 @@
-import { exportJWK, generateKeyPair } from 'jose';
+import { generateKeyPair } from 'jose';
 import nock from 'nock';
EOF
@@ -1,2 +1,2 @@
import { exportJWK, generateKeyPair } from 'jose';
import { generateKeyPair } from 'jose';
import nock from 'nock';
Copilot is powered by AI and may make mistakes. Always verify output.
import nock from 'nock';
import { jwtVerifier } from '../src';
import { createJwt } from './helpers';

describe('public-key verification', () => {
beforeEach(() => {
nock.cleanAll();
});

it('should verify a token with a directly provided public key', async () => {
// Generate a key pair for testing
const { publicKey, privateKey } = await generateKeyPair('RS256');

// Create a JWT signed with the private key
const tokenPayload = { foo: 'bar' };
const jwt = await createJwt({
payload: tokenPayload,
privateKey: privateKey
});

// Verify the JWT using the public key directly
const verify = jwtVerifier({
issuer: 'https://issuer.example.com/',
audience: 'https://api/',
secret: publicKey
});

const result = await verify(jwt);
expect(result.payload.foo).toBe('bar');
});

it('should verify a token with directly provided public key when tokenSigningAlg is specified', async () => {
// Generate a key pair for testing
const { publicKey, privateKey } = await generateKeyPair('RS256');

// Create a JWT signed with the private key
const tokenPayload = { foo: 'bar' };
const jwt = await createJwt({
payload: tokenPayload,
privateKey: privateKey
});

// Verify the JWT using the public key directly with explicit alg
const verify = jwtVerifier({
issuer: 'https://issuer.example.com/',
audience: 'https://api/',
secret: publicKey,
tokenSigningAlg: 'RS256'
});

const result = await verify(jwt);
expect(result.payload.foo).toBe('bar');
});

it('should fail to verify when using mismatched keys', async () => {
// Generate two different key pairs
const keyPair1 = await generateKeyPair('RS256');
const keyPair2 = await generateKeyPair('RS256');

// Create JWT with first private key
const jwt = await createJwt({
payload: { test: 'data' },
privateKey: keyPair1.privateKey
});

// Try to verify with second key's public key (should fail)
const verify = jwtVerifier({
issuer: 'https://issuer.example.com/',
audience: 'https://api/',
secret: keyPair2.publicKey
});

await expect(verify(jwt)).rejects.toThrow();
});
});
24 changes: 24 additions & 0 deletions packages/express-oauth2-jwt-bearer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,30 @@ app.use(
);
```

#### Using direct public key verification (without JWKS)

If you already have the public key for verifying tokens and want to bypass JWKS discovery:

```js
const { auth } = require('express-oauth2-jwt-bearer');
const fs = require('fs');
const { createPublicKey } = require('crypto');

// Load your public key (this is just an example)
const publicKeyData = fs.readFileSync('./public-key.pem');
const publicKey = createPublicKey(publicKeyData);

app.use(
auth({
issuer: 'https://YOUR_ISSUER_DOMAIN',
audience: 'https://my-api.com',
secret: publicKey
})
);
```

This approach allows you to verify tokens without requiring internet access to a JWKS endpoint.

With this configuration, your api will require a valid Access Token JWT bearer token for all routes.

Successful requests will have the following properties added to them:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Direct Public Key Verification Example

Below is an example of how to use the library to verify JWTs with a directly provided public key (no JWKS or discovery):

```js
const { jwtVerifier } = require('access-token-jwt');
// or: import { jwtVerifier } from 'access-token-jwt';

// You could load your public key from a file, environment variable, etc.
// This is just an example of how you'd use it once you have the key
// (could be a CryptoKey, KeyObject, etc.)
const publicKey = getPublicKeyFromSomewhere();

// Set up the verifier with the public key
const verify = jwtVerifier({
issuer: 'https://your-issuer.example.com/',
audience: 'https://your-api/',
secret: publicKey // Pass the public key directly
});

// Verify a token
try {
const { payload, header } = await verify(token);
console.log('Token verified!', payload);
} catch (err) {
console.error('Token verification failed:', err.message);
}
```

With this approach you can bypass JWKS discovery and validation while still properly verifying tokens signed with asymmetric algorithms.
Loading