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

Pkce2 #210

Merged
merged 6 commits into from
Apr 24, 2019
Merged

Pkce2 #210

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
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ tokenManager: {
| `redirectUri` | The url that is redirected to when using `token.getWithRedirect`. This must be pre-registered as part of client registration. If no `redirectUri` is provided, defaults to the current origin. |
| `authorizeUrl` | Specify a custom authorizeUrl to perform the OIDC flow. Defaults to the issuer plus "/v1/authorize". |
| `userinfoUrl` | Specify a custom userinfoUrl. Defaults to the issuer plus "/v1/userinfo". |
| `tokenUrl` | Specify a custom tokenUrl. Defaults to the issuer plus "/v1/token". |
| `ignoreSignature` | ID token signatures are validated by default when `token.getWithoutPrompt`, `token.getWithPopup`, `token.getWithRedirect`, and `token.verify` are called. To disable ID token signature validation for these methods, set this value to `true`. |
| | This option should be used only for browser support and testing purposes. |

Expand Down Expand Up @@ -1365,6 +1366,7 @@ The following configuration options can **only** be included in `token.getWithou

| Options | Description |
| :-------: | ----------|
| `grantType` | Specify grantType for this Application. Supported types are "implicit" and "authorization_code". Defaults to "implicit" |
| `sessionToken` | Specify an Okta sessionToken to skip reauthentication when the user already authenticated using the Authentication Flow. |
| `responseMode` | Specify how the authorization response should be returned. You will generally not need to set this unless you want to override the default values for `token.getWithRedirect`. See [Parameter Details](https://developer.okta.com/docs/api/resources/oidc#parameter-details) for a list of available modes. |
| `responseType` | Specify the [response type](https://developer.okta.com/docs/api/resources/oidc#request-parameters) for OIDC authentication. Defaults to `id_token`. |
Expand Down Expand Up @@ -1440,12 +1442,20 @@ Create token using a redirect.
* `oauthOptions` - See [Extended OpenID Connect options](#extended-openid-connect-options)

```javascript
authClient.token.getWithRedirect(oauthOptions);
authClient.token.getWithRedirect({
grantType: 'authorization_code',
responseType: ['id_token', 'token'])
})
```

#### `token.parseFromUrl(options)`

Parses the access or ID Tokens from the url after a successful authentication redirect. If an ID token is present, it will be [verified and validated](https://github.com/okta/okta-auth-js/blob/master/lib/token.js#L186-L190) before available for use.
Parses the authorization code, access, or ID Tokens from the URL after a successful authentication redirect.

If an authorization code is present, it will be exchanged for token(s) by posting to the `tokenUrl` endpoint.

The ID token will be [verified and validated](https://github.com/okta/okta-auth-js/blob/master/lib/token.js#L186-L190) before available for use.


```javascript
authClient.token.parseFromUrl()
Expand Down
4 changes: 2 additions & 2 deletions fetch/fetchRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ function fetchRequest(method, url, args) {
var body = args.data;

// JSON encode body (if appropriate)
if (body && args.headers['Content-Type'] === 'application/json' && typeof body !== 'string') {
if (body && args.headers && args.headers['Content-Type'] === 'application/json' && typeof body !== 'string') {
body = JSON.stringify(body);
}

var fetchPromise = fetch(url, {
method: method,
headers: args.headers,
body: body,
credentials: !args.withCredentials ? 'omit' : 'include'
credentials: args.withCredentials === false ? 'omit' : 'include'
})
.then(function(response) {
var error = !response.ok;
Expand Down
5 changes: 4 additions & 1 deletion lib/browser/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,11 @@ function OktaAuthBuilder(args) {

sdk.pkce = {
generateVerifier: pkce.generateVerifier,
clearMeta: util.bind(pkce.clearMeta, null, sdk),
saveMeta: util.bind(pkce.saveMeta, null, sdk),
loadMeta: util.bind(pkce.loadMeta, null, sdk),
computeChallenge: pkce.computeChallenge,
exchangeForToken: util.bind(pkce.exchangeForToken, null, sdk)
getToken: util.bind(pkce.getToken, null, sdk)
};

sdk.token = {
Expand Down
10 changes: 10 additions & 0 deletions lib/browser/browserStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ storageUtil.browserHasSessionStorage = function() {
}
};

storageUtil.getPKCEStorage = function() {
if (storageUtil.browserHasLocalStorage()) {
return storageBuilder(storageUtil.getLocalStorage(), config.PKCE_STORAGE_NAME);
} else if (storageUtil.browserHasSessionStorage()) {
return storageBuilder(storageUtil.getSessionStorage(), config.PKCE_STORAGE_NAME);
} else {
return storageBuilder(storageUtil.getCookieStorage(), config.PKCE_STORAGE_NAME);
}
};

storageUtil.getHttpCache = function() {
if (storageUtil.browserHasLocalStorage()) {
return storageBuilder(storageUtil.getLocalStorage(), config.CACHE_STORAGE_NAME);
Expand Down
71 changes: 41 additions & 30 deletions lib/pkce.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,35 @@
/* eslint-disable complexity, max-statements */
var http = require('./http');
var util = require('./util');
var oauthUtil = require('./oauthUtil');
var AuthSdkError = require('./errors/AuthSdkError');
var token = require('./token');

// 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;

function generateVerifier(prefix) {
var verifier = prefix || '';
if (verifier.length < MIN_VERIFIER_LENGTH) {
verifier = verifier + util.genRandomString(MIN_VERIFIER_LENGTH - verifier.length);
}
return encodeURIComponent(verifier);
return encodeURIComponent(verifier).slice(0, MAX_VERIFIER_LENGTH);
}

function saveMeta(sdk, meta) {
var storage = sdk.options.storageUtil.getPKCEStorage();
storage.setStorage(meta);
}

function loadMeta(sdk) {
var storage = sdk.options.storageUtil.getPKCEStorage();
var obj = storage.getStorage();
return obj;
}

function clearMeta(sdk) {
var storage = sdk.options.storageUtil.getPKCEStorage();
storage.clearStorage();
}

/* global Uint8Array, TextEncoder */
Expand All @@ -39,23 +55,28 @@ function computeChallenge(str) {
});
}

// for /v1/token endpoint
function setDefaultOptions(sdk, options) {
options = util.clone(options) || {};
var defaults = {
clientId: sdk.options.clientId,
redirectUri: sdk.options.redirectUri || window.location.href,
grantType: 'authorization_code'
};
util.extend(defaults, options);
return defaults;
}

function validateOptions(oauthOptions) {
// Quick validation
if (!oauthOptions.clientId) {
throw new AuthSdkError('A clientId must be specified in the OktaAuth constructor to get a token');
}

if (!oauthOptions.redirectUri) {
throw new AuthSdkError('The redirectUri passed to /authorize must also be passed to /token');
}

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

if (!oauthOptions.codeVerifier) {
throw new AuthSdkError('The "codeVerifier" (generated and saved by your app) must be passed to /token');
}

if (oauthOptions.grantType !== 'authorization_code') {
throw new AuthSdkError('Expecting "grantType" to equal "authorization_code"');
}
}

function getPostData(options) {
Expand All @@ -64,22 +85,17 @@ function getPostData(options) {
'client_id': options.clientId,
'redirect_uri': options.redirectUri,
'grant_type': options.grantType,
'code': options.code,
'code': options.authorizationCode,
'code_verifier': options.codeVerifier
});
// Encode as URL string
return util.toQueryParams(params).slice(1);
}

// exchange authorization code for an access token
function exchangeForToken(sdk, oauthOptions, options) {
oauthOptions = setDefaultOptions(sdk, oauthOptions || {});
options = options || {};

function getToken(sdk, oauthOptions, urls) {
validateOptions(oauthOptions);

var data = getPostData(oauthOptions);
var urls = oauthUtil.getOAuthUrls(sdk, oauthOptions, options);

return http.httpRequest(sdk, {
url: urls.tokenUrl,
Expand All @@ -89,19 +105,14 @@ function exchangeForToken(sdk, oauthOptions, options) {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
.then(function(res) {
if (!oauthOptions.responseType) {
oauthOptions.responseType = ['id_token', 'token'];
}
// scopes were passed in original /authorize call and are returned to us in this response
oauthOptions.scopes = res.scope.split(' ');
return token.handleOAuthResponse(sdk, oauthOptions, res, urls);
});
}

module.exports = {
generateVerifier: generateVerifier,
clearMeta: clearMeta,
saveMeta: saveMeta,
loadMeta: loadMeta,
computeChallenge: computeChallenge,
exchangeForToken: exchangeForToken
getToken: getToken
};
127 changes: 85 additions & 42 deletions lib/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ function handleOAuthResponse(sdk, oauthParams, res, urls) {

var tokenDict = {};


if (res['code']) {
tokenDict['code'] = res['code'];
return tokenDict;
}

if (res['access_token']) {
tokenDict['token'] = {
accessToken: res['access_token'],
Expand All @@ -157,12 +163,6 @@ function handleOAuthResponse(sdk, oauthParams, res, urls) {
};
}

if (res['code']) {
tokenDict['code'] = {
authorizationCode: res['code']
};
}

if (res['id_token']) {
var jwt = sdk.token.decode(res['id_token']);

Expand Down Expand Up @@ -221,10 +221,11 @@ function getDefaultOAuthParams(sdk, oauthOptions) {
oauthOptions = util.clone(oauthOptions) || {};

var defaults = {
grantType: 'implicit',
clientId: sdk.options.clientId,
redirectUri: sdk.options.redirectUri || window.location.href,
responseType: 'id_token',
responseMode: 'okta_post_message',
responseMode: 'fragment',
state: util.genRandomString(64),
nonce: util.genRandomString(64),
scopes: ['openid', 'email'],
Expand Down Expand Up @@ -482,48 +483,64 @@ function getWithPopup(sdk, oauthOptions, options) {
return getToken(sdk, oauthParams, options);
}

function getWithRedirect(sdk, oauthOptions, options) {
oauthOptions = util.clone(oauthOptions) || {};
function prepareOauthParams(sdk, oauthOptions) {
var oauthParams = getDefaultOAuthParams(sdk, oauthOptions);
// If the user didn't specify a responseMode
if (!oauthOptions.responseMode) {
// And it's only an auth code request (responseType could be an array)
var respType = oauthParams.responseType;
if (respType.indexOf('code') !== -1 &&
(util.isString(respType) || (Array.isArray(respType) && respType.length === 1))) {
// Default the responseMode to query
util.extend(oauthParams, {
responseMode: 'query'
});
// Otherwise, default to fragment
} else {
util.extend(oauthParams, {
responseMode: 'fragment'
});
}

if (oauthParams.grantType !== 'authorization_code') {
return Q.resolve(oauthParams);
}

var urls = oauthUtil.getOAuthUrls(sdk, oauthParams, options);
var requestUrl = urls.authorizeUrl + buildAuthorizeParams(oauthParams);
// PKCE authorization_code flow
var codeVerifier = sdk.pkce.generateVerifier(oauthParams.codeVerifier || '');

// Set session cookie to store the oauthParams
cookies.set(config.REDIRECT_OAUTH_PARAMS_COOKIE_NAME, JSON.stringify({
// We will need these values after redirect when we call /token
var meta = {
codeVerifier: codeVerifier,
responseType: oauthParams.responseType,
state: oauthParams.state,
nonce: oauthParams.nonce,
scopes: oauthParams.scopes,
clientId: oauthParams.clientId,
urls: urls,
ignoreSignature: oauthParams.ignoreSignature
}));
redirectUri: oauthParams.redirectUri
};
sdk.pkce.saveMeta(meta);

// Set nonce cookie for servers to validate nonce in id_token
cookies.set(config.REDIRECT_NONCE_COOKIE_NAME, oauthParams.nonce);
return sdk.pkce.computeChallenge(codeVerifier)
.then(function(codeChallenge) {

// Set state cookie for servers to validate state
cookies.set(config.REDIRECT_STATE_COOKIE_NAME, oauthParams.state);
// Clone/copy the params. Set codeChallenge and responseType for authorization_code
var clonedParams = util.clone(oauthParams) || {};
util.extend(clonedParams, oauthParams, {
codeChallenge: codeChallenge,
responseType: 'code'
});
return clonedParams;
});
}

function getWithRedirect(sdk, oauthOptions, options) {
oauthOptions = util.clone(oauthOptions) || {};

sdk.token.getWithRedirect._setLocation(requestUrl);
return prepareOauthParams(sdk, oauthOptions)
.then(function(oauthParams) {
var urls = oauthUtil.getOAuthUrls(sdk, oauthParams, options);
var requestUrl = urls.authorizeUrl + buildAuthorizeParams(oauthParams);

// Set session cookie to store the oauthParams
cookies.set(config.REDIRECT_OAUTH_PARAMS_COOKIE_NAME, JSON.stringify({
responseType: oauthParams.responseType,
state: oauthParams.state,
nonce: oauthParams.nonce,
scopes: oauthParams.scopes,
clientId: oauthParams.clientId,
urls: urls,
ignoreSignature: oauthParams.ignoreSignature
}));

// Set nonce cookie for servers to validate nonce in id_token
cookies.set(config.REDIRECT_NONCE_COOKIE_NAME, oauthParams.nonce);

// Set state cookie for servers to validate state
cookies.set(config.REDIRECT_STATE_COOKIE_NAME, oauthParams.state);

sdk.token.getWithRedirect._setLocation(requestUrl);
});
}

function renewToken(sdk, token) {
Expand Down Expand Up @@ -591,7 +608,33 @@ function parseFromUrl(sdk, url) {
// Remove the hash from the url
removeHash(sdk);
}
return handleOAuthResponse(sdk, oauthParams, res, urls);
return handleOAuthResponse(sdk, oauthParams, res, urls)
.then(function(res) {
if (oauthParams.responseType !== 'code') {
return res;
}

// PKCE authorization_code flow
// Retreive saved values and build oauthParans for call to /token
var meta = sdk.pkce.loadMeta();
var clonedParams = util.clone(oauthParams) || {};
util.extend(clonedParams, {
grantType: 'authorization_code',
authorizationCode: res,
codeVerifier: meta.codeVerifier,
responseType: meta.responseType,
redirectUri: meta.redirectUri
});
delete clonedParams.state;
return sdk.pkce.getToken(clonedParams, urls)
.then(function(res) {
return handleOAuthResponse(sdk, clonedParams, res, urls);
})
.fin(function() {
sdk.pkce.clearMeta();
});

});
});
}

Expand Down
Loading