Skip to content
This repository has been archived by the owner on Oct 24, 2024. It is now read-only.

Commit

Permalink
feat[jwt-verifier]: Add verifyIdToken()
Browse files Browse the repository at this point in the history
Add verifyIdToken() functionality to @okta/jwt-verifier (OKTA-234446)
Verifier will throw error "No KID specified" if no KID is present in the JWT header
  • Loading branch information
denysoblohin-okta committed Jan 11, 2021
1 parent 8327d9f commit a0c9023
Show file tree
Hide file tree
Showing 6 changed files with 825 additions and 322 deletions.
52 changes: 44 additions & 8 deletions packages/jwt-verifier/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@
[![npm version](https://img.shields.io/npm/v/@okta/jwt-verifier.svg?style=flat-square)](https://www.npmjs.com/package/@okta/jwt-verifier)
[![build status](https://img.shields.io/travis/okta/okta-oidc-js/master.svg?style=flat-square)](https://travis-ci.org/okta/okta-oidc-js)

This library verifies Okta access tokens (issued by [Okta Custom Authorization servers](https://developer.okta.com/docs/concepts/auth-servers/) by fetching the public keys from the JWKS endpoint of the authorization server. If the access token is valid it will be converted to a JSON object and returned to your code.
This library verifies Okta access and ID tokens by fetching the public keys from the JWKS endpoint of the authorization server.

This library does not yet verify id tokens. You can learn about [access tokens](https://developer.okta.com/docs/reference/api/oidc/#access-token) and [id tokens](https://developer.okta.com/docs/reference/api/oidc/#id-token) in our [OIDC and OAuth 2.0 API Referece](https://developer.okta.com/docs/reference/api/oidc/).
> This library is for Node.js applications and will not compile into a front-end application. If you need to work with tokens in front-end applications, please see [okta-auth-js](https://github.com/okta/okta-auth-js).
Using Express? Our [Express Resource Server Example](https://github.com/okta/samples-nodejs-express-4/tree/master/resource-server) will show you how to use this library in your Express application.

## Access Tokens

> Okta Custom Authorization Servers require the API Access Management license. If you are using Okta Org Authorization Servers (which don’t require API Access Management) you can manually validate against the /introspect endpoint ( https://developer.okta.com/docs/reference/api/oidc/#introspect ).
This library verifies Okta access tokens (issued by [Okta Custom Authorization servers](https://developer.okta.com/docs/concepts/auth-servers/#custom-authorization-server)) by fetching the public keys from the JWKS endpoint of the authorization server. If the access token is valid it will be converted to a JSON object and returned to your code.

You can learn about [access tokens](https://developer.okta.com/docs/reference/api/oidc/#access-token), [scopes](https://developer.okta.com/docs/reference/api/oidc/#scopes) and [claims](https://developer.okta.com/docs/reference/api/oidc/#claims) in our [OIDC and OAuth 2.0 API Referece](https://developer.okta.com/docs/reference/api/oidc/).

> Okta Custom Authorization Servers require the [API Access Management](https://developer.okta.com/docs/concepts/api-access-management/) license. If you are using Okta Org Authorization Servers (which don’t require API Access Management) you can manually validate against the /introspect endpoint ( https://developer.okta.com/docs/reference/api/oidc/#introspect ).
For any access token to be valid, the following are asserted:
* Signature is valid (the token was signed by a private key which has a corresponding public key in the JWKS response from the authorization server).
Expand All @@ -16,11 +24,23 @@ For any access token to be valid, the following are asserted:
* The `iss` claim matches the issuer the verifier is constructed with.
* Any custom claim assertions that have been configured.

> This library is for Node.js applications and will not compile into a front-end application. If you need to work with tokens in front-end applications, please see [okta-auth-js](https://github.com/okta/okta-auth-js).
To learn more about verification cases and Okta's tokens please read [Validate Access Tokens](https://developer.okta.com/docs/guides/validate-access-tokens/go/overview/).

Using Express? Our [Express Resource Server Example](https://github.com/okta/samples-nodejs-express-4/tree/master/resource-server) will show you how to use this library in your Express application.
## ID Tokens

This library verifies Okta ID tokens (issued by [Okta Custom Authorization servers](https://developer.okta.com/docs/concepts/auth-servers/#custom-authorization-server) or [Okta Org Authorization Server](https://developer.okta.com/docs/concepts/auth-servers/#org-authorization-server)) by fetching the public keys from the JWKS endpoint of the authorization server. If the token is valid it will be converted to a JSON object and returned to your code.

You can learn about [ID tokens](https://developer.okta.com/docs/reference/api/oidc/#id-token), [scopes](https://developer.okta.com/docs/reference/api/oidc/#scopes) and [claims](https://developer.okta.com/docs/reference/api/oidc/#claims) in our [OIDC and OAuth 2.0 API Referece](https://developer.okta.com/docs/reference/api/oidc/).

For any ID token to be valid, the following are asserted:
* Signature is valid (the token was signed by a private key which has a corresponding public key in the JWKS response from the authorization server).
* ID token is not expired (requires local system time to be in sync with Okta, checks the `exp` claim of the ID token).
* The `aud` claim matches the expected client ID passed to `verifyIdToken()`.
* The `iss` claim matches the issuer the verifier is constructed with.
* The `nonce` claim matches the expected nonce.
* Any custom claim assertions that have been configured.

To learn more about verification cases and Okta's tokens please read [Working With OAuth 2.0 Tokens](https://developer.okta.com/authentication-guide/tokens/)
To learn more about verification cases and Okta's tokens please read [Validate ID Tokens](https://developer.okta.com/docs/guides/validate-id-tokens/overview/).

## Upgrading

Expand All @@ -42,7 +62,7 @@ const oktaJwtVerifier = new OktaJwtVerifier({
});
```

With a verifier, you can now verify access tokens:
### Verify access tokens

```javascript
oktaJwtVerifier.verifyAccessToken(accessTokenString, expectedAud)
Expand All @@ -68,6 +88,22 @@ oktaJwtVerifier.verifyAccessToken(accessTokenString, [ 'api://special', 'api://d
.catch(err => console.warn('token failed validation') );
```

### Verify ID tokens

```javascript
oktaJwtVerifier.verifyIdToken(idTokenString, expectedClientId, expectedNonce)
.then(jwt => {
// the token is valid (per definition of 'valid' above)
console.log(jwt.claims);
})
.catch(err => {
// a validation failed, inspect the error
});
```

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.


## Custom Claims 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:
Expand All @@ -82,7 +118,7 @@ const verifier = new OktaJwtVerifier({
});
```

Validation fails and an error is returned if an access token does not have the configured claim.
Validation fails and an error is returned if the token does not have the configured claim.

For more complex use cases, you can ask the verifier to assert that a claim includes one or more values. This is useful for array type claims as well as claims that have space-separated values in a string.

Expand Down
56 changes: 51 additions & 5 deletions packages/jwt-verifier/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,36 @@ function verifyAudience(expected, aud) {
}
}

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

assertClientId(expected);

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

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

function verifyNonce(expected, nonce) {
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 ) {
throw new Error(`nonce claim ${nonce} does not match expected nonce: ${expected}`);
}
}

class OktaJwtVerifier {
constructor(options = {}) {
// Assert configuration options exist and are well-formed (not necessarily correct!)
Expand All @@ -114,16 +138,20 @@ class OktaJwtVerifier {
rateLimit: true
});
this.verifier = nJwt.createVerifier().setSigningAlgorithm('RS256').withKeyResolver((kid, cb) => {
this.jwksClient.getSigningKey(kid, (err, key) => {
cb(err, key && (key.publicKey || key.rsaPublicKey));
});
if (kid) {
this.jwksClient.getSigningKey(kid, (err, key) => {
cb(err, key && (key.publicKey || key.rsaPublicKey));
});
} else {
cb("No KID specified", null);
}
});
}

async verifyAsPromise(accessTokenString) {
async verifyAsPromise(tokenString) {
return new Promise((resolve, reject) => {
// Convert to a promise
this.verifier.verify(accessTokenString, (err, jwt) => {
this.verifier.verify(tokenString, (err, jwt) => {
if (err) {
return reject(err);
}
Expand Down Expand Up @@ -151,6 +179,24 @@ class OktaJwtVerifier {

return jwt;
}

async verifyIdToken(idTokenString, expectedClientId, expectedNonce) {
// njwt verifies expiration and signature.
// We require RS256 in the base verifier.
// Remaining to verify:
// - audience claim (must match client id)
// - issuer claim
// - nonce claim (if present)
// - any custom claims passed in

const jwt = await this.verifyAsPromise(idTokenString);
verifyClientId(expectedClientId, jwt.claims.aud);
verifyIssuer(this.issuer, jwt.claims.iss);
verifyNonce(expectedNonce, jwt.claims.nonce);
verifyAssertedClaims(this, jwt.claims);

return jwt;
}
}

module.exports = OktaJwtVerifier;
35 changes: 28 additions & 7 deletions packages/jwt-verifier/test/internal-ci/token.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,25 @@ const constants = require('../constants')
const LONG_TIMEOUT = 15000;

const OktaJwtVerifier = require('../../lib');
const getAccessToken = require('../util').getAccessToken;
const { getAccessToken, getIdToken } = require('../util');

// These need to be exported in the environment, from a working Okta org
const ISSUER = constants.ISSUER;
const CLIENT_ID = constants.CLIENT_ID;
const USERNAME = constants.USERNAME;
const PASSWORD = constants.PASSWORD;
const REDIRECT_URI = constants.REDIRECT_URI
const OKTA_TESTING_DISABLEHTTPSCHECK = constants.OKTA_TESTING_DISABLEHTTPSCHECK
const REDIRECT_URI = constants.REDIRECT_URI;
const OKTA_TESTING_DISABLEHTTPSCHECK = constants.OKTA_TESTING_DISABLEHTTPSCHECK;
const NONCE = 'foo';

// Used to get an access token from the AS
const issuer1AccessTokenParams = {
// Used to get an access token and id token from the AS
const issuer1TokenParams = {
ISSUER,
CLIENT_ID,
USERNAME,
PASSWORD,
REDIRECT_URI
REDIRECT_URI,
NONCE
};

describe('Access token test with api call', () => {
Expand All @@ -44,10 +46,29 @@ describe('Access token test with api call', () => {
});

it('should allow me to verify Okta access tokens', () => {
return getAccessToken(issuer1AccessTokenParams)
return getAccessToken(issuer1TokenParams)
.then(accessToken => verifier.verifyAccessToken(accessToken, expectedAud))
.then(jwt => {
expect(jwt.claims.iss).toBe(ISSUER);
});
}, LONG_TIMEOUT);
});

describe('ID token test with api call', () => {
const expectedClientId = CLIENT_ID;
const verifier = new OktaJwtVerifier({
issuer: ISSUER,
testing: {
disableHttpsCheck: OKTA_TESTING_DISABLEHTTPSCHECK
}
});

it('should allow me to verify Okta ID tokens', () => {
return getIdToken(issuer1TokenParams)
.then(idToken => verifier.verifyIdToken(idToken, expectedClientId, NONCE))
.then(jwt => {
expect(jwt.claims.iss).toBe(ISSUER);
});
}, LONG_TIMEOUT);
});

Loading

0 comments on commit a0c9023

Please sign in to comment.