Skip to content

Commit

Permalink
adds pkce and token methods to public api
Browse files Browse the repository at this point in the history
OKTA-353961
<<<Jenkins Check-In of Tested SHA: ad4a380 for [email protected]>>>
Artifact: okta-auth-js
  • Loading branch information
aarongranick-okta authored and eng-prod-CI-bot-okta committed Dec 17, 2020
1 parent 45d83aa commit d93cfc6
Show file tree
Hide file tree
Showing 32 changed files with 658 additions and 192 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ node_modules
/build/types
/build/cjs
/samples/templates
/samples/generated/webpack-spa/public/*-bundle.*
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## 4.5.0

### Features

- [#567](https://github.com/okta/okta-auth-js/pull/567) Adds new methods:
- `token.prepareTokenParams`
- `token.exchangeCodeForTokens`
- `pkce.generateVerifier`
- `pkce.computeChallenge`
and constant:
- `pkce.DEFAULT_CODE_CHALLENGE_METHOD`
This API allows more control over the `PKCE` authorization flow and is enabled for both browser and nodeJS.

## 4.4.0

### Features
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ var config = {
```

## API Reference

<!-- no toc -->
* [signIn](#signinoptions)
* [signInWithCredentials](#signinwithcredentialsoptions)
* [signInWithRedirect](#signinwithredirectoptions)
Expand Down Expand Up @@ -676,6 +676,8 @@ var config = {
* [token.getUserInfo](#tokengetuserinfoaccesstokenobject-idtokenobject)
* [token.verify](#tokenverifyidtokenobject)
* [token.isLoginRedirect](#tokenisloginredirect)
* [token.prepareTokenParams](#tokenpreparetokenparams)
* [token.exchangeCodeForTokens](#tokenexchangecodefortokens)
* [tokenManager](#tokenmanager)
* [tokenManager.add](#tokenmanageraddkey-token)
* [tokenManager.get](#tokenmanagergetkey)
Expand Down Expand Up @@ -2210,6 +2212,15 @@ authClient.token.verify(idTokenObject, validationOptions)

> :warning: Deprecated, this method will be removed in next major release, use [sdk.isLoginRedirect](#isloginredirect) instead.

#### `token.prepareTokenParams`

Returns a `TokenParams` object. If `PKCE` is enabled, this object will contain values for `codeVerifier`, `codeChallenge` and `codeChallengeMethod`.

#### `token.exchangeCodeForTokens`

Used internally to perform the final step of the `PKCE` authorization code flow. Accepts a `TokenParams` object which should contain a `codeVerifier` and an `authorizationCode`.

### `tokenManager`

#### `tokenManager.add(key, token)`
Expand Down
3 changes: 2 additions & 1 deletion jest.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ module.exports = {
'^@okta/okta-auth-js$': OktaAuth
},
'setupFiles': [
'<rootDir>/test/support/nodeExceptions.js'
'<rootDir>/test/support/nodeExceptions.js',
'<rootDir>/jest.setup.js'
],
'testMatch': [
'**/test/spec/*.{js,ts}'
Expand Down
3 changes: 3 additions & 0 deletions jest.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ module.exports = {
'moduleNameMapper': {
'^@okta/okta-auth-js$': OktaAuth
},
'setupFiles': [
'<rootDir>/jest.setup.js'
],
'testMatch': [
'**/test/spec/*.js'
],
Expand Down
8 changes: 8 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// crypto to mimic browser environment
const Crypto = require('@peculiar/webcrypto').Crypto;
global.crypto = new Crypto();

// TextEncoder
const TextEncoder = require('util').TextEncoder;
// eslint-disable-next-line node/no-unsupported-features/node-builtins
global.TextEncoder = TextEncoder;
21 changes: 18 additions & 3 deletions lib/OktaAuthBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
postToTransaction,
AuthTransaction
} from './tx';
import PKCE from './pkce';
import {
OktaAuth,
OktaAuthOptions,
Expand All @@ -27,26 +28,35 @@ import {
VerifyRecoveryTokenOptions,
TransactionAPI,
SessionAPI,
SigninAPI
SigninAPI,
PkceAPI,
} from './types';

export default class OktaAuthBase implements OktaAuth, SigninAPI {
options: OktaAuthOptions;
tx: TransactionAPI;
userAgent: string;
session: SessionAPI;
pkce: PkceAPI;

constructor(args: OktaAuthOptions) {
assertValidConfig(args);
this.options = {
issuer: removeTrailingSlash(args.issuer),
tokenUrl: removeTrailingSlash(args.tokenUrl),
httpRequestClient: args.httpRequestClient,
transformErrorXHR: args.transformErrorXHR,
storageUtil: args.storageUtil,
headers: args.headers,
devMode: args.devMode || false
devMode: args.devMode || false,
clientId: args.clientId,
redirectUri: args.redirectUri,
pkce: args.pkce
};

// Give the developer the ability to disable token signature validation.
this.options.ignoreSignature = !!args.ignoreSignature;

this.tx = {
status: transactionStatus.bind(null, this),
resume: resumeTransaction.bind(null, this),
Expand All @@ -58,7 +68,12 @@ export default class OktaAuthBase implements OktaAuth, SigninAPI {
}),
introspect: introspect.bind(null, this)
};


this.pkce = {
DEFAULT_CODE_CHALLENGE_METHOD: PKCE.DEFAULT_CODE_CHALLENGE_METHOD,
generateVerifier: PKCE.generateVerifier,
computeChallenge: PKCE.computeChallenge
};
}

// { username, password, (relayState), (context) }
Expand Down
11 changes: 5 additions & 6 deletions lib/browser/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ import {
renewToken,
renewTokens,
getUserInfo,
verifyToken
verifyToken,
prepareTokenParams,
exchangeCodeForTokens
} from '../token';
import { TokenManager } from '../TokenManager';
import {
Expand Down Expand Up @@ -132,7 +134,6 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI {
clientId: args.clientId,
authorizeUrl: removeTrailingSlash(args.authorizeUrl),
userinfoUrl: removeTrailingSlash(args.userinfoUrl),
tokenUrl: removeTrailingSlash(args.tokenUrl),
revokeUrl: removeTrailingSlash(args.revokeUrl),
logoutUrl: removeTrailingSlash(args.logoutUrl),
pkce: args.pkce === false ? false : true,
Expand Down Expand Up @@ -161,10 +162,6 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI {
} else {
this.options.maxClockSkew = args.maxClockSkew;
}

// Give the developer the ability to disable token signature
// validation.
this.options.ignoreSignature = !!args.ignoreSignature;

this.session = {
close: closeSession.bind(null, this),
Expand All @@ -176,6 +173,8 @@ class OktaAuthBrowser extends OktaAuthBase implements OktaAuth, SignoutAPI {

this._tokenQueue = new PromiseQueue();
this.token = {
prepareTokenParams: prepareTokenParams.bind(null, this),
exchangeCodeForTokens: exchangeCodeForTokens.bind(null, this),
getWithoutPrompt: getWithoutPrompt.bind(null, this),
getWithPopup: getWithPopup.bind(null, this),
getWithRedirect: getWithRedirect.bind(null, this),
Expand Down
6 changes: 6 additions & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ export const ACCESS_TOKEN_STORAGE_KEY = 'accessToken';
export const ID_TOKEN_STORAGE_KEY = 'idToken';
export const REFRESH_TOKEN_STORAGE_KEY = 'refreshToken';
export const REFERRER_PATH_STORAGE_KEY = 'referrerPath';

// Code verifier: Random URL-safe string with a minimum length of 43 characters.
// Code challenge: Base64 URL-encoded SHA-256 hash of the code verifier.
export const MIN_VERIFIER_LENGTH = 43;
export const MAX_VERIFIER_LENGTH = 128;
export const DEFAULT_CODE_CHALLENGE_METHOD = 'S256';
45 changes: 23 additions & 22 deletions lib/pkce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,8 @@
import AuthSdkError from './errors/AuthSdkError';
import http from './http';
import { warn, stringToBase64Url, removeNils, toQueryString } from './util';
import { TokenParams, CustomUrls, PKCEMeta, OAuthResponse } from './types';

// Code verifier: Random URL-safe string with a minimum length of 43 characters.
// Code challenge: Base64 URL-encoded SHA-256 hash of the code verifier.
var MIN_VERIFIER_LENGTH = 43;
var MAX_VERIFIER_LENGTH = 128;
var DEFAULT_CODE_CHALLENGE_METHOD = 'S256';
import { TokenParams, CustomUrls, PKCEMeta, OAuthResponse, OAuthParams } from './types';
import { MIN_VERIFIER_LENGTH, MAX_VERIFIER_LENGTH, DEFAULT_CODE_CHALLENGE_METHOD } from './constants';

function dec2hex (dec) {
return ('0' + dec.toString(16)).substr(-2);
Expand All @@ -34,7 +29,7 @@ function getRandomString(length) {
return str.slice(0, length);
}

function generateVerifier(prefix) {
function generateVerifier(prefix?: string): string {
var verifier = prefix || '';
if (verifier.length < MIN_VERIFIER_LENGTH) {
verifier = verifier + getRandomString(MIN_VERIFIER_LENGTH - verifier.length);
Expand Down Expand Up @@ -99,7 +94,7 @@ function clearMeta(sdk) {
storage.clearStorage();
}

function computeChallenge(str) {
function computeChallenge(str: string): PromiseLike<any> {
var buffer = new TextEncoder().encode(str);
return crypto.subtle.digest('SHA-256', buffer).then(function(arrayBuffer) {
var hash = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer));
Expand All @@ -119,7 +114,7 @@ function validateOptions(options: TokenParams) {
throw new AuthSdkError('The redirectUri passed to /authorize must also be passed to /token');
}

if (!options.authorizationCode) {
if (!options.authorizationCode && !options.interactionCode) {
throw new AuthSdkError('An authorization code (returned from /authorize) must be passed to /token');
}

Expand All @@ -129,20 +124,26 @@ function validateOptions(options: TokenParams) {
}

function getPostData(options: TokenParams): string {
// Convert options to OAuth params
var params = removeNils({
// Convert Token params to OAuth params, sent to the /token endpoint
var params: OAuthParams = removeNils({
'client_id': options.clientId,
'redirect_uri': options.redirectUri,
'grant_type': 'authorization_code',
'code': options.authorizationCode,
'grant_type': options.interactionCode ? 'interaction_code' : 'authorization_code',
'code_verifier': options.codeVerifier
});

if (options.interactionCode) {
params['interaction_code'] = options.interactionCode;
} else if (options.authorizationCode) {
params.code = options.authorizationCode;
}

// Encode as URL string
return toQueryString(params).slice(1);
}

// exchange authorization code for an access token
function getToken(sdk, options: TokenParams, urls: CustomUrls): Promise<OAuthResponse> {
function exchangeCodeForTokens(sdk, options: TokenParams, urls: CustomUrls): Promise<OAuthResponse> {
validateOptions(options);
var data = getPostData(options);

Expand All @@ -158,11 +159,11 @@ function getToken(sdk, options: TokenParams, urls: CustomUrls): Promise<OAuthRes
}

export default {
DEFAULT_CODE_CHALLENGE_METHOD: DEFAULT_CODE_CHALLENGE_METHOD,
generateVerifier: generateVerifier,
clearMeta: clearMeta,
saveMeta: saveMeta,
loadMeta: loadMeta,
computeChallenge: computeChallenge,
getToken: getToken
DEFAULT_CODE_CHALLENGE_METHOD,
generateVerifier,
clearMeta,
saveMeta,
loadMeta,
computeChallenge,
exchangeCodeForTokens
};
43 changes: 43 additions & 0 deletions lib/server/features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*!
* Copyright (c) 2015-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.
*/

/* global crypto */

export function isFingerprintSupported() {
return false;
}

export function isPopupPostMessageSupported() {
return false;
}

export function isTokenVerifySupported() {
return typeof crypto !== 'undefined' && crypto.subtle && typeof Uint8Array !== 'undefined';
}

export function hasTextEncoder() {
// eslint-disable-next-line node/no-unsupported-features/node-builtins
return typeof TextEncoder !== 'undefined';
}

export function isPKCESupported() {
return isTokenVerifySupported() && hasTextEncoder();
}

export function isHTTPS() {
return false;
}

export function isLocalhost() {
return false;
}

20 changes: 19 additions & 1 deletion lib/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,18 @@ import OktaAuthBase from '../OktaAuthBase';
import fetchRequest from '../fetch/fetchRequest';
import { getUserAgent } from '../builderUtil';
import serverStorage from './serverStorage';
import * as features from './features';
import { BaseTokenAPI, FeaturesAPI } from '../types';
import { prepareTokenParams, exchangeCodeForTokens, decodeToken } from '../token';

const PACKAGE_JSON = require('../../package.json');

const SDK_VERSION = PACKAGE_JSON.version;

export default class OktaAuthNode extends OktaAuthBase {
class OktaAuthNode extends OktaAuthBase {
static features: FeaturesAPI;
features: FeaturesAPI;
token: BaseTokenAPI;
constructor(args) {
args = Object.assign({
httpRequestClient: fetchRequest,
Expand All @@ -29,5 +36,16 @@ export default class OktaAuthNode extends OktaAuthBase {
super(args);

this.userAgent = getUserAgent(args, `okta-auth-js-server/${SDK_VERSION}`);

this.token = {
decode: decodeToken,
prepareTokenParams: prepareTokenParams.bind(null, this),
exchangeCodeForTokens: exchangeCodeForTokens.bind(null, this)
};
}
}

// Hoist feature detection functions to static type
OktaAuthNode.features = OktaAuthNode.prototype.features = features;

export default OktaAuthNode;
Loading

0 comments on commit d93cfc6

Please sign in to comment.