Skip to content
This repository has been archived by the owner on Jul 26, 2022. It is now read-only.

feat[jwt-verifier]: Add type definitions for jwt-verifier (copy) #979

Merged
merged 6 commits into from
Mar 24, 2021
Merged
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
31 changes: 31 additions & 0 deletions packages/jwt-verifier/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"root": true,
"env": {
"node": true
},
"ignorePatterns": [
"test/**/*"
],
"extends": [
"eslint:recommended",
"plugin:node/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"node",
"@typescript-eslint"
],
"rules": {
"max-len": ["error", { "code": 120, "comments": 120 }],
"max-classes-per-file": ["error", 2],
"@typescript-eslint/no-var-requires": 0,
"node/no-unsupported-features/es-syntax": ["error", { "ignores": ["modules"] }],
"@typescript-eslint/no-unused-vars": 0
}
}
6 changes: 6 additions & 0 deletions packages/jwt-verifier/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 2.1.0

### Other

- [#979](https:/okta/okta-oidc-js/pull/979) - Adds TypeScript type declaration file. Configured eslint and tsd

# 2.0.1

### Other
Expand Down
142 changes: 142 additions & 0 deletions packages/jwt-verifier/lib.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*!
* Copyright (c) 2017-Present, Okta, Inc. and/or its affiliates. All rights reserved.
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
*
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and limitations under the License.
*/
export = OktaJwtVerifier;

declare class OktaJwtVerifier {
constructor(options: OktaJwtVerifier.VerifierOptions);

/**
* Verify an access token
*
* The expected audience passed to verifyAccessToken() is required, and can be
* either a string (direct match) or an array of strings (the actual aud claim
* in the token must match one of the strings).
*/
verifyAccessToken(
accessTokenString: string,
expectedAudience: string | string[]
): Promise<OktaJwtVerifier.Jwt>;

/**
* Verify ID Tokens
*
* The expected client ID passed to verifyIdToken() is required. Expected nonce
* value is optional and required if the claim is present in the token body.
*/
verifyIdToken(
idTokenString: string,
expectedClientId: string,
expectedNonce: string
): Promise<OktaJwtVerifier.Jwt>;

private verifyAsPromise(tokenString: string): Promise<OktaJwtVerifier.Jwt>;
}

declare namespace OktaJwtVerifier {
interface VerifierOptions {
/**
* Issuer/Authorization server URL
*
* @example
* "https://{yourOktaDomain}/oauth2/default"
*/
issuer: string;
/**
* Client ID
*/
clientId?: string;
/**
* Custom claim assertions
*
* For basic use cases, you can ask the verifier to assert a custom set of
* claims. For example, if you need to assert that this JWT was issued for a
* given client id:
*
* @example
* ```js
* const verifier = new OktaJwtVerifier({
* issuer: 'https://{yourOktaDomain}/oauth2/default',
* clientId: '{clientId}'
* assertClaims: {
* cid: '{clientId}'
* }
* });
* ```
* Validation fails and an error is returned if the token does not have the configured claim.
*
* Read more: https:/okta/okta-oidc-js/tree/master/packages/jwt-verifier#custom-claims-assertions
*/
assertClaims?: Record<string, unknown>;
/**
* Cache time in milliseconds
*
* By default, found keys are cached by key ID for one hour. This can be
* configured with the cacheMaxAge option for cache entries.
*
* Read more: https:/okta/okta-oidc-js/tree/master/packages/jwt-verifier#caching--rate-limiting
*/
cacheMaxAge?: number;
/**
* Rate limit in requests per minute
*
* If a key ID is not found in the cache, the JWKs endpoint will be requested.
* To prevent a DoS if many not-found keys are requested, a rate limit of 10
* JWKs requests per minute is enforced. This is configurable with the
* jwksRequestsPerMinute option.
*
* Read more: https:/okta/okta-oidc-js/tree/master/packages/jwt-verifier#caching--rate-limiting
*/
jwksRequestsPerMinute?: number;
}

type Algorithm =
'HS256'
| 'HS384'
| 'HS512'
| 'RS256'
| 'RS384'
| 'RS512'
| 'ES256'
| 'ES384'
| 'ES512'
| 'none';

interface JwtHeader {
alg: Algorithm;
typ: string;
kid?: string;
jku?: string;
x5u?: string;
x5t?: string;
}

interface JwtClaims {
iss: string;
sub: string;
aud: string;
exp: number;
nbf?: number;
iat?: number;
jti?: string;
nonce?: string;
scp?: string[];
[key: string]: unknown;
}

interface Jwt {
claims: JwtClaims;
header: JwtHeader;
toString(): string;
isExpired(): boolean;
isNotBefore(): boolean;
}
}
38 changes: 19 additions & 19 deletions packages/jwt-verifier/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const nJwt = require('njwt');

const {
assertIssuer,
assertClientId
assertClientId,
} = require('@okta/configuration-validation');

class AssertedClaimsVerifier {
Expand All @@ -41,18 +41,18 @@ class AssertedClaimsVerifier {

isValidOperator(operator) {
// may support more operators in the future
return !operator || operator === 'includes'
return !operator || operator === 'includes';
}

checkAssertions(op, claim, expectedValue, actualValue) {
if (!op && actualValue !== expectedValue) {
this.errors.push(`claim '${claim}' value '${actualValue}' does not match expected value '${expectedValue}'`);
} else if (op === 'includes' && Array.isArray(expectedValue)) {
expectedValue.forEach(value => {
expectedValue.forEach((value) => {
if (!actualValue || !actualValue.includes(value)) {
this.errors.push(`claim '${claim}' value '${actualValue}' does not include expected value '${value}'`);
}
})
});
} else if (op === 'includes' && (!actualValue || !actualValue.includes(expectedValue))) {
this.errors.push(`claim '${claim}' value '${actualValue}' does not include expected value '${expectedValue}'`);
}
Expand All @@ -61,60 +61,60 @@ class AssertedClaimsVerifier {

function verifyAssertedClaims(verifier, claims) {
const assertedClaimsVerifier = new AssertedClaimsVerifier();
for (let [claimName, expectedValue] of Object.entries(verifier.claimsToAssert)) {
for (const [claimName, expectedValue] of Object.entries(verifier.claimsToAssert)) {
const operator = assertedClaimsVerifier.extractOperator(claimName);
if (!assertedClaimsVerifier.isValidOperator(operator)) {
throw new Error(`operator: '${operator}' invalid. Supported operators: 'includes'.`);
}
const claim = assertedClaimsVerifier.extractClaim(claimName);
const actualValue = claims[claim];
assertedClaimsVerifier.checkAssertions(operator, claim, expectedValue, actualValue)
assertedClaimsVerifier.checkAssertions(operator, claim, expectedValue, actualValue);
}
if (assertedClaimsVerifier.errors.length) {
throw new Error(assertedClaimsVerifier.errors.join(', '));
}
}

function verifyAudience(expected, aud) {
if( !expected ) {
if (!expected) {
throw new Error('expected audience is required');
}

if ( Array.isArray(expected) && !expected.includes(aud) ) {
throw new Error(`audience claim ${aud} does not match one of the expected audiences: ${ expected.join(', ') }`);
if (Array.isArray(expected) && !expected.includes(aud)) {
throw new Error(`audience claim ${aud} does not match one of the expected audiences: ${expected.join(', ')}`);
}

if ( !Array.isArray(expected) && aud !== expected ) {
if (!Array.isArray(expected) && aud !== expected) {
throw new Error(`audience claim ${aud} does not match expected audience: ${expected}`);
}
}

function verifyClientId(expected, aud) {
if( !expected ) {
if (!expected) {
throw new Error('expected client id is required');
}

assertClientId(expected);

if ( aud !== expected ) {
if (aud !== expected) {
throw new Error(`audience claim ${aud} does not match expected client id: ${expected}`);
}
}

function verifyIssuer(expected, issuer) {
if( issuer !== expected ) {
if (issuer !== expected) {
throw new Error(`issuer ${issuer} does not match expected issuer: ${expected}`);
}
}

function verifyNonce(expected, nonce) {
if( nonce && !expected ) {
if (nonce && !expected) {
throw new Error('expected nonce is required');
}
if (!nonce && expected) {
throw new Error(`nonce claim is missing but expected: ${expected}`);
}
if( nonce && expected && nonce !== expected ) {
if (nonce && expected && nonce !== expected) {
throw new Error(`nonce claim ${nonce} does not match expected nonce: ${expected}`);
}
}
Expand All @@ -123,27 +123,27 @@ class OktaJwtVerifier {
constructor(options = {}) {
// Assert configuration options exist and are well-formed (not necessarily correct!)
assertIssuer(options.issuer, options.testing);
if( options.clientId ) {
if (options.clientId) {
assertClientId(options.clientId);
}

this.claimsToAssert = options.assertClaims || {};
this.issuer = options.issuer;
this.jwksClient = jwksClient({
jwksUri: options.issuer + '/v1/keys',
jwksUri: `${options.issuer}/v1/keys`,
cache: true,
cacheMaxAge: options.cacheMaxAge || (60 * 60 * 1000),
cacheMaxEntries: 3,
jwksRequestsPerMinute: options.jwksRequestsPerMinute || 10,
rateLimit: true
rateLimit: true,
});
this.verifier = nJwt.createVerifier().setSigningAlgorithm('RS256').withKeyResolver((kid, cb) => {
if (kid) {
this.jwksClient.getSigningKey(kid, (err, key) => {
cb(err, key && (key.publicKey || key.rsaPublicKey));
});
} else {
cb("No KID specified", null);
cb('No KID specified', null);
}
});
}
Expand Down
33 changes: 27 additions & 6 deletions packages/jwt-verifier/package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
{
"name": "@okta/jwt-verifier",
"version": "2.0.1",
"version": "2.1.0",
"description": "Easily validate Okta access tokens",
"repository": "https:/okta/okta-oidc-js",
"homepage": "https:/okta/okta-oidc-js/tree/master/packages/jwt-verifier",
"main": "lib.js",
"types": "lib.d.ts",
"files": [
"src"
"lib.js",
"lib.d.ts"
],
"scripts": {
"test": "yarn test:unit && yarn test:integration",
"test": "yarn test:unit && yarn test:integration && yarn lint",
"test:integration": "../../scripts/tck.sh",
"test:unit": "JEST_JUNIT_OUTPUT=./reports/junit/results.xml jest --runInBand test/spec",
"test:ci": "JEST_JUNIT_OUTPUT=./reports/junit/results.xml jest test/internal-ci"
"test:ci": "JEST_JUNIT_OUTPUT=./reports/junit/results.xml jest test/internal-ci",
"lint": "eslint . --ext .js --ext .ts && tsd"
},
"keywords": [
"okta",
Expand All @@ -23,7 +26,7 @@
"jwt"
],
"engines": {
"node": ">=6"
"node": ">=7.6"
},
"jest": {
"reporters": [
Expand All @@ -39,13 +42,31 @@
"njwt": "^1.0.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"cors": "^2.8.4",
"cross-env": "^5.1.1",
"eslint": "^7.22.0",
"eslint-plugin-node": "^11.1.0",
"express": "^4.15.4",
"jest": "^24.8.0",
"jest-junit": "^6.4.0",
"nock": "^9.0.14",
"node-fetch": "^2.6.0",
"timekeeper": "^1.0.0"
"timekeeper": "^1.0.0",
"tsd": "^0.14.0",
"typescript": "^4.1.5"
},
"tsd": {
"directory": "test/types",
"compilerOptions": {
"skipLibCheck": true,
"esModuleInterop": true,
"paths": {
"@okta/jwt-verifier": [
"."
]
}
}
}
}
Loading