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

change: deprecate options.url, enforce options.issuer #316

Merged
merged 1 commit into from
Jan 22, 2020
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
- New method `closeSession` for XHR signout without redirect or reload.
- New method `revokeAccessToken`

-[#316](https://github.com/okta/okta-auth-js/pull/316) - Option `issuer` is required. Option `url` has been deprecated and is no longer used.

### Other

## 2.12.0
Expand Down
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,21 @@ You can also browse the full [API reference documentation](#api-reference).

## Configuration reference

If you are using this SDK to implement an OIDC flow, the only required configuration option is `issuer`:
Whether you are using this SDK to implement an OIDC flow or for communicating with the [Authentication API](https://developer.okta.com/docs/api/resources/authn), the only required configuration option is `issuer`, which is the URL to an Okta [Authorization Server](https://developer.okta.com/docs/guides/customize-authz-server/overview/)

### About the Issuer

You may use the URL for your Okta organization as the issuer. This will apply a default authorization policy and issue tokens scoped at the organization level.

```javascript
var config = {
issuer: 'https://{yourOktaDomain}'
};

var authClient = new OktaAuth(config);
```

Okta allows you to create multiple custom OAuth 2.0 authorization servers that you can use to protect your own resource servers. Within each authorization server you can define your own OAuth 2.0 scopes, claims, and access policies. Many organizations have a "default" authorization server.

```javascript
var config = {
Expand All @@ -130,12 +144,12 @@ var config = {
var authClient = new OktaAuth(config);
```

If you’re using this SDK only for communicating with the [Authentication API](https://developer.okta.com/docs/api/resources/authn), you instead need to set the `url` for your Okta Domain:
You may also create and customize additional authorization servers.

```javascript

var config = {
// The URL for your Okta organization
url: 'https://{yourOktaDomain}'
issuer: 'https://{yourOktaDomain}/oauth2/custom-auth-server-id'
};

var authClient = new OktaAuth(config);
Expand Down
7 changes: 3 additions & 4 deletions packages/okta-auth-js/lib/browser/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,10 @@ var util = require('../util');
function OktaAuthBuilder(args) {
var sdk = this;

var url = builderUtil.getValidUrl(args);
builderUtil.assertValidConfig(args);
// OKTA-242989: support for grantType will be removed in 3.0
var usePKCE = args.pkce || args.grantType === 'authorization_code';
this.options = {
url: util.removeTrailingSlash(url),
clientId: args.clientId,
issuer: util.removeTrailingSlash(args.issuer),
authorizeUrl: util.removeTrailingSlash(args.authorizeUrl),
Expand Down Expand Up @@ -335,7 +334,7 @@ proto.fingerprint = function(options) {
iframe.style.display = 'none';

listener = function listener(e) {
if (!e || !e.data || e.origin !== sdk.options.url) {
if (!e || !e.data || e.origin !== sdk.getIssuerOrigin()) {
return;
}

Expand All @@ -357,7 +356,7 @@ proto.fingerprint = function(options) {
};
oauthUtil.addListener(window, 'message', listener);

iframe.src = sdk.options.url + '/auth/services/devicefingerprint';
iframe.src = sdk.getIssuerOrigin() + '/auth/services/devicefingerprint';
document.body.appendChild(iframe);

timeout = setTimeout(function() {
Expand Down
36 changes: 20 additions & 16 deletions packages/okta-auth-js/lib/builderUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,37 @@ var AuthSdkError = require('./errors/AuthSdkError');
var tx = require('./tx');
var util = require('./util');

function getValidUrl(args) {
// TODO: use @okta/configuration-validation (move module to this monorepo?)
function assertValidConfig(args) {
if (!args) {
throw new AuthSdkError('No arguments passed to constructor. ' +
'Required usage: new OktaAuth(args)');
}

var url = args.url;
if (!url) {
var isUrlRegex = new RegExp('^http?s?://.+');
if (args.issuer && isUrlRegex.test(args.issuer)) {
// Infer the URL from the issuer URL, omitting the /oauth2/{authServerId}
url = args.issuer.split('/oauth2/')[0];
} else {
throw new AuthSdkError('No url passed to constructor. ' +
'Required usage: new OktaAuth({url: "https://{yourOktaDomain}.com"})');
}
var issuer = args.issuer;
if (!issuer) {
throw new AuthSdkError('No issuer passed to constructor. ' +
'Required usage: new OktaAuth({issuer: "https://{yourOktaDomain}.com/oauth2/{authServerId}"})');
}

if (url.indexOf('-admin.') !== -1) {
throw new AuthSdkError('URL passed to constructor contains "-admin" in subdomain. ' +
'Required usage: new OktaAuth({url: "https://{yourOktaDomain}.com})');
var isUrlRegex = new RegExp('^http?s?://.+');
if (!isUrlRegex.test(args.issuer)) {
throw new AuthSdkError('Issuer must be a valid URL. ' +
'Required usage: new OktaAuth({issuer: "https://{yourOktaDomain}.com/oauth2/{authServerId}"})');
}

return url;
if (issuer.indexOf('-admin.') !== -1) {
throw new AuthSdkError('Issuer URL passed to constructor contains "-admin" in subdomain. ' +
'Required usage: new OktaAuth({issuer: "https://{yourOktaDomain}.com})');
}
}

function addSharedPrototypes(proto) {
proto.getIssuerOrigin = function() {
// Infer the URL from the issuer URL, omitting the /oauth2/{authServerId}
return this.options.issuer.split('/oauth2/')[0];
};

// { username, (relayState) }
proto.forgotPassword = function (opts) {
return tx.postToTransaction(this, '/api/v1/authn/recovery/password', opts);
Expand Down Expand Up @@ -90,5 +94,5 @@ function buildOktaAuth(OktaAuthBuilder) {
module.exports = {
addSharedPrototypes: addSharedPrototypes,
buildOktaAuth: buildOktaAuth,
getValidUrl: getValidUrl
assertValidConfig: assertValidConfig
};
4 changes: 2 additions & 2 deletions packages/okta-auth-js/lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ function httpRequest(sdk, options) {
}

function get(sdk, url, options) {
url = util.isAbsoluteUrl(url) ? url : sdk.options.url + url;
url = util.isAbsoluteUrl(url) ? url : sdk.getIssuerOrigin() + url;
var getOptions = {
url: url,
method: 'GET'
Expand All @@ -121,7 +121,7 @@ function get(sdk, url, options) {
}

function post(sdk, url, args, options) {
url = util.isAbsoluteUrl(url) ? url : sdk.options.url + url;
url = util.isAbsoluteUrl(url) ? url : sdk.getIssuerOrigin() + url;
var postOptions = {
url: url,
method: 'POST',
Expand Down
50 changes: 2 additions & 48 deletions packages/okta-auth-js/lib/oauthUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function loadPopup(src, options) {
}

function getWellKnown(sdk, issuer) {
var authServerUri = (issuer || sdk.options.issuer || sdk.options.url);
var authServerUri = (issuer || sdk.options.issuer);
return http.get(sdk, authServerUri + '/.well-known/openid-configuration', {
cacheResponse: true
});
Expand Down Expand Up @@ -172,53 +172,7 @@ function getOAuthUrls(sdk, oauthParams, options) {
var logoutUrl = util.removeTrailingSlash(options.logoutUrl) || sdk.options.logoutUrl;
var revokeUrl = util.removeTrailingSlash(options.revokeUrl) || sdk.options.revokeUrl;

// If an issuer exists but it's not a url, assume it's an authServerId
if (issuer && !(/^https?:/.test(issuer))) {
// Make it a url
issuer = sdk.options.url + '/oauth2/' + issuer;
}

// If an authorizeUrl is supplied without an issuer, and an id_token is requested
if (!issuer && authorizeUrl &&
oauthParams.responseType.indexOf('id_token') !== -1) {
// The issuer is ambiguous, so we won't be able to validate the id_token jwt
throw new AuthSdkError('Cannot request idToken with an authorizeUrl without an issuer');
}

// If a token is requested without an issuer
if (!issuer && oauthParams && oauthParams.responseType.indexOf('token') !== -1) {
// If an authorizeUrl is supplied without a userinfoUrl
if (authorizeUrl && !userinfoUrl) {
// The userinfoUrl is ambiguous, so we won't be able to call getUserInfo
throw new AuthSdkError('Cannot request accessToken with an authorizeUrl without an issuer or userinfoUrl');
}

// If a userinfoUrl is supplied without a authorizeUrl
if (userinfoUrl && !authorizeUrl) {
// The authorizeUrl is ambiguous, so we won't be able to call the authorize endpoint
throw new AuthSdkError('Cannot request token with an userinfoUrl without an issuer or authorizeUrl');
}
}

// Default the issuer to our baseUrl
issuer = issuer || sdk.options.url;

// Trim trailing slashes
issuer = util.removeTrailingSlash(issuer);

var baseUrl = issuer;
// A custom auth server issuer looks like:
// https://example.okta.com/oauth2/aus8aus76q8iphupD0h7
//
// Most orgs have a "default" custom authorization server:
// https://example.okta.com/oauth2/default
var customAuthServerRegex = new RegExp('^https?://.*?/oauth2/.+');
if (!customAuthServerRegex.test(baseUrl)) {
// Append '/oauth2' if necessary
if (!baseUrl.endsWith('/oauth2')) {
baseUrl += '/oauth2';
}
}
var baseUrl = issuer.indexOf('/oauth2') > 0 ? issuer : issuer + '/oauth2';

authorizeUrl = authorizeUrl || baseUrl + '/v1/authorize';
userinfoUrl = userinfoUrl || baseUrl + '/v1/userinfo';
Expand Down
4 changes: 2 additions & 2 deletions packages/okta-auth-js/lib/server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ var util = require('../util');
function OktaAuthBuilder(args) {
var sdk = this;

var url = builderUtil.getValidUrl(args);
builderUtil.assertValidConfig(args);
this.options = {
url: util.removeTrailingSlash(url),
issuer: util.removeTrailingSlash(args.issuer),
httpRequestClient: args.httpRequestClient,
storageUtil: args.storageUtil,
headers: args.headers
Expand Down
4 changes: 2 additions & 2 deletions packages/okta-auth-js/lib/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function getSession(sdk) {

function closeSession(sdk) {
return http.httpRequest(sdk, {
url: sdk.options.url + '/api/v1/sessions/me',
url: sdk.getIssuerOrigin() + '/api/v1/sessions/me',
method: 'DELETE'
});
}
Expand All @@ -61,7 +61,7 @@ function refreshSession(sdk) {

function setCookieAndRedirect(sdk, sessionToken, redirectUrl) {
redirectUrl = redirectUrl || window.location.href;
window.location = sdk.options.url + '/login/sessionCookieRedirect' +
window.location = sdk.getIssuerOrigin() + '/login/sessionCookieRedirect' +
util.toQueryParams({
checkAccountSetupComplete: true,
token: sessionToken,
Expand Down
4 changes: 2 additions & 2 deletions packages/okta-auth-js/lib/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function verifyToken(sdk, token, validationParams) {

var validationOptions = {
clientId: sdk.options.clientId,
issuer: sdk.options.issuer || sdk.options.url,
issuer: sdk.options.issuer,
ignoreSignature: sdk.options.ignoreSignature
};

Expand Down Expand Up @@ -122,7 +122,7 @@ function addPostMessageListener(sdk, timeout, state) {
// This may happen if apps with different issuers are running on the same host url
// If they share the same storage key, they may read and write tokens in the same location.
// Common when developing against http://localhost
if (e.origin !== sdk.options.url) {
if (e.origin !== sdk.getIssuerOrigin()) {
return reject(new AuthSdkError('The request does not match client configuration'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense for the error message to be a bit more specific about what doesn't match

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vaguely recall some discussion around this back when the original code was put in. We want to be careful not to leak any sensitive information, since it does indicate a mismatch between token and app configuration (which is not expected, unless the app has a bug or there are multiple apps on the same domain, like with localhost) - so the error should not be seen by end-users. Welcome for suggestions on this, maybe there is a message which is more helpful but does not reveal anything too specific.

}

Expand Down
4 changes: 2 additions & 2 deletions packages/okta-auth-js/lib/tx.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function getStateToken(res) {

function transactionStatus(sdk, args) {
args = addStateToken(sdk, args);
return http.post(sdk, sdk.options.url + '/api/v1/authn', args);
return http.post(sdk, sdk.getIssuerOrigin() + '/api/v1/authn', args);
}

function resumeTransaction(sdk, args) {
Expand Down Expand Up @@ -75,7 +75,7 @@ function introspect (sdk, args) {

function transactionStep(sdk, args) {
args = addStateToken(sdk, args);
return http.post(sdk, sdk.options.url + '/idp/idx/introspect', args);
return http.post(sdk, sdk.getIssuerOrigin() + '/idp/idx/introspect', args);
}

function transactionExists(sdk) {
Expand Down
19 changes: 9 additions & 10 deletions packages/okta-auth-js/test/spec/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe('Browser', function() {
var issuer;
beforeEach(function() {
issuer = 'http://my-okta-domain';
auth = new OktaAuth({ url: issuer });
auth = new OktaAuth({ issuer });
});

it('is a valid constructor', function() {
Expand All @@ -21,7 +21,7 @@ describe('Browser', function() {
it('Listens to error events from TokenManager', function() {
jest.spyOn(Emitter.prototype, 'on');
jest.spyOn(OktaAuth.prototype, '_onTokenManagerError');
var auth = new OktaAuth({ url: 'http://localhost/fake' });
var auth = new OktaAuth({ issuer: 'http://localhost/fake' });
expect(Emitter.prototype.on).toHaveBeenCalledWith('error', auth._onTokenManagerError, auth);
var emitter = Emitter.prototype.on.mock.instances[0];
var error = { errorCode: 'anything'};
Expand All @@ -32,7 +32,7 @@ describe('Browser', function() {
it('error with errorCode "login_required" and accessToken: true will call option "onSessionExpired" function', function() {
var onSessionExpired = jest.fn();
jest.spyOn(Emitter.prototype, 'on');
new OktaAuth({ url: 'http://localhost/fake', onSessionExpired: onSessionExpired });
new OktaAuth({ issuer: 'http://localhost/fake', onSessionExpired: onSessionExpired });
var emitter = Emitter.prototype.on.mock.instances[0];
expect(onSessionExpired).not.toHaveBeenCalled();
var error = { errorCode: 'login_required', accessToken: true };
Expand All @@ -43,7 +43,7 @@ describe('Browser', function() {
it('error with errorCode "login_required" (not accessToken) does not call option "onSessionExpired" function', function() {
var onSessionExpired = jest.fn();
jest.spyOn(Emitter.prototype, 'on');
new OktaAuth({ url: 'http://localhost/fake', onSessionExpired: onSessionExpired });
new OktaAuth({ issuer: 'http://localhost/fake', onSessionExpired: onSessionExpired });
var emitter = Emitter.prototype.on.mock.instances[0];
expect(onSessionExpired).not.toHaveBeenCalled();
var error = { errorCode: 'login_required' };
Expand All @@ -54,7 +54,7 @@ describe('Browser', function() {
it('error with unknown errorCode does not call option "onSessionExpired" function', function() {
var onSessionExpired = jest.fn();
jest.spyOn(Emitter.prototype, 'on');
new OktaAuth({ url: 'http://localhost/fake', onSessionExpired: onSessionExpired });
new OktaAuth({ issuer: 'http://localhost/fake', onSessionExpired: onSessionExpired });
var emitter = Emitter.prototype.on.mock.instances[0];
expect(onSessionExpired).not.toHaveBeenCalled();
var error = { errorCode: 'unknown', accessToken: true };
Expand All @@ -73,20 +73,20 @@ describe('Browser', function() {

it('can be set by arg', function() {
spyOn(OktaAuth.features, 'isPKCESupported').and.returnValue(true);
auth = new OktaAuth({ pkce: true, url: 'http://my-okta-domain' });
auth = new OktaAuth({ pkce: true, issuer: 'http://my-okta-domain' });
expect(auth.options.pkce).toBe(true);
});

it('accepts alias "grantType"', function() {
spyOn(OktaAuth.features, 'isPKCESupported').and.returnValue(true);
auth = new OktaAuth({ grantType: "authorization_code", url: 'http://my-okta-domain' });
auth = new OktaAuth({ grantType: "authorization_code", issuer: 'http://my-okta-domain' });
expect(auth.options.pkce).toBe(true);
});

it('throws if PKCE is not supported', function() {
spyOn(OktaAuth.features, 'isPKCESupported').and.returnValue(false);
function fn() {
auth = new OktaAuth({ pkce: true, url: 'http://my-okta-domain' });
auth = new OktaAuth({ pkce: true, issuer: 'http://my-okta-domain' });
}
expect(fn).toThrowError(
'PKCE requires a modern browser with encryption support running in a secure context.\n' +
Expand Down Expand Up @@ -226,7 +226,6 @@ describe('Browser', function() {
it('supports custom authorization server', function() {
issuer = 'http://my-okta-domain/oauth2/custom-as';
auth = new OktaAuth({
url: 'http://my-okta-domain',
issuer
});
initSpies();
Expand Down Expand Up @@ -261,7 +260,7 @@ describe('Browser', function() {
const postLogoutRedirectUri = 'http://someother';
const encodedUri = encodeURIComponent(postLogoutRedirectUri);
auth = new OktaAuth({
url: issuer,
issuer,
postLogoutRedirectUri
});
initSpies();
Expand Down
Loading