Skip to content

Commit

Permalink
detect error and error_description in redirect uri
Browse files Browse the repository at this point in the history
OKTA-348505
<<<Jenkins Check-In of Tested SHA: ff58d51 for eng_productivity_ci_bot_okta@okta.com>>>
Artifact: okta-auth-js
  • Loading branch information
aarongranick-okta authored and eng-prod-CI-bot-okta committed Jan 8, 2021
1 parent 3fdc52e commit 56489f9
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 55 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 4.6.0

### Other

- [#583](https://github.com/okta/okta-auth-js/pull/583) Better error handling for redirect flows: if redirect URI contains `error` or `error_description` then `isLoginRedirect` will return true and `parseFromUrl` will throw `OAuthError`

## 4.5.0

### Features
Expand Down
33 changes: 24 additions & 9 deletions lib/oauthUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,25 +231,40 @@ function hasCodeInUrl(hashOrSearch: string): boolean {
return /(code=)/i.test(hashOrSearch);
}

function hasErrorInUrl(hashOrSearch: string): boolean {
return /(error=)/i.test(hashOrSearch) || /(error_description)/i.test(hashOrSearch);
}

function isRedirectUri(uri: string, sdk: OktaAuth): boolean {
var authParams = sdk.options;
return uri && uri.indexOf(authParams.redirectUri) == 0;
return uri && uri.indexOf(authParams.redirectUri) === 0;
}

/**
* Check if tokens or a code have been passed back into the url, which happens in
* the social auth IDP redirect flow.
* the OIDC (including social auth IDP) redirect flow.
*/
function isLoginRedirect (sdk: OktaAuth) {
// First check, is this a redirect URI?
if (!isRedirectUri(window.location.href, sdk)){
return false;
}

// The location contains either a code, token, or an error&error_description
var authParams = sdk.options;
if (authParams.pkce || authParams.responseType === 'code' || authParams.responseMode === 'query') {
// Look for code
var hasCode = authParams.responseMode === 'fragment' ?
hasCodeInUrl(window.location.hash) :
hasCodeInUrl(window.location.search);
return isRedirectUri(window.location.href, sdk) && hasCode;
var codeFlow = authParams.pkce || authParams.responseType === 'code' || authParams.responseMode === 'query';
var useQuery = codeFlow && authParams.responseMode !== 'fragment';

if (hasErrorInUrl(useQuery ? window.location.search : window.location.hash)) {
return true;
}

if (codeFlow) {
var hasCode = useQuery ? hasCodeInUrl(window.location.search) : hasCodeInUrl(window.location.hash);
return hasCode;
}
// Look for tokens (Implicit OIDC flow)

// implicit flow, will always be hash fragment
return hasTokensInHash(window.location.hash);
}

Expand Down
150 changes: 104 additions & 46 deletions test/spec/oauthUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,10 @@ describe('validateClaims', function () {
});

describe('isLoginRedirect', function() {
const redirectUri = 'http://fake/login/callback';
const issuer = 'https://auth-js-test.okta.com';
const clientId = 'foo';

let sdk;
let originalLocation;
beforeEach(() => {
Expand All @@ -922,108 +926,162 @@ describe('isLoginRedirect', function() {
window.location = originalLocation;
});


function mockHash(hash) {
delete window.location;
window.location = {
hash: `#${hash}`,
href: `${redirectUri}#${hash}`
};
}

function mockSearch(search) {
delete window.location;
window.location = {
search: `?${search}`,
href: `${redirectUri}?${search}`
};
}

describe('Implicit OIDC flow', () => {
beforeEach(() => {
sdk = new OktaAuth({
pkce: false,
issuer: 'https://auth-js-test.okta.com',
clientId: 'foo'
issuer,
clientId,
redirectUri
});
});

it('should return true if there is id_token in hash', () => {
delete window.location;
window.location = {
hash: '#id_token=fakeidtoken'
};
mockHash('id_token=fakeidtoken');
const result = oauthUtil.isLoginRedirect(sdk);
expect(result).toBe(true);
});

it('should return true if there is access_token in hash', () => {
delete window.location;
window.location = {
hash: '#access_token=fakeaccesstoken'
};
mockHash('access_token=fakeaccesstoken');
const result = oauthUtil.isLoginRedirect(sdk);
expect(result).toBe(true);
});

it('should return false if there is no id or access token in hash', () => {
delete window.location;
window.location = {
hash: '#random_token=fakerandomtoken'
};
mockHash('random_token=fakerandomtoken');
const result = oauthUtil.isLoginRedirect(sdk);
expect(result).toBe(false);
});

it('should return false if current URI is not redirect URI', () => {
mockHash('id_token=fakeidtoken');
window.location.href = 'http://fake/product/search' + window.location.hash;
const result = oauthUtil.isLoginRedirect(sdk);
expect(result).toBe(false);
});
});

describe('PKCE', () => {
it('there should be code in hash when responseMode is fragment', () => {
delete window.location;
window.location = {
hash: '#code=fakecode',
href: 'https://exmple.com/implicit/callback#code=fakecode'
};
sdk = new OktaAuth({
pkce: true,
responseMode: 'fragment',
issuer: 'https://auth-js-test.okta.com',
clientId: 'foo',
redirectUri: 'https://exmple.com/implicit/callback'
issuer,
clientId,
redirectUri
});
mockHash('code=fakecode');
const result = oauthUtil.isLoginRedirect(sdk);
expect(result).toBe(true);
});

it('there should be code in query when use default responseMode', () => {
delete window.location;
window.location = {
search: '?code=fakecode',
href: 'https://exmple.com/implicit/callback?code=fakecode'
};
sdk = new OktaAuth({
pkce: true,
issuer: 'https://auth-js-test.okta.com',
clientId: 'foo',
redirectUri: 'https://exmple.com/implicit/callback'
issuer,
clientId,
redirectUri
});
mockSearch('code=fakecode');
const result = oauthUtil.isLoginRedirect(sdk);
expect(result).toBe(true);
});

it('there should be code in query when responseMode is query', () => {
delete window.location;
window.location = {
search: '?code=fakecode',
href: 'https://exmple.com/implicit/callback?code=fakecode'
};
sdk = new OktaAuth({
pkce: true,
responseMode: 'query',
issuer: 'https://auth-js-test.okta.com',
clientId: 'foo',
redirectUri: 'https://exmple.com/implicit/callback'
issuer,
clientId,
redirectUri
});
mockSearch('code=fakecode');
const result = oauthUtil.isLoginRedirect(sdk);
expect(result).toBe(true);
});

it('should return false if current URI is not redirect URI', () => {
delete window.location;
window.location = {
search: '?code=somecode',
href: 'https://exmple.com/products/search?code=somecode'
};
sdk = new OktaAuth({
pkce: true,
issuer: 'https://auth-js-test.okta.com',
clientId: 'foo',
redirectUri: 'https://exmple.com/implicit/callback'
issuer,
clientId,
redirectUri
});
mockSearch('code=fakecode');
window.location.href = 'https://exmple.com/products/search?code=somecode';
const result = oauthUtil.isLoginRedirect(sdk);
expect(result).toBe(false);
});
});

describe('OAuth errors', () => {
it('PKCE: should recognize error', () => {
sdk = new OktaAuth({
pkce: true,
issuer,
clientId,
redirectUri
});
mockSearch('error=fakeerror&error_description=fakedescription');
const result = oauthUtil.isLoginRedirect(sdk);
expect(result).toBe(true);
});

it('PKCE (fragment): should recognize error', () => {
sdk = new OktaAuth({
pkce: true,
responseMode: 'fragment',
issuer,
clientId,
redirectUri
});
mockHash('error=fakeerror&error_description=fakedescription');
const result = oauthUtil.isLoginRedirect(sdk);
expect(result).toBe(true);
});

it('implicit flow: should recognize error', () => {
sdk = new OktaAuth({
pkce: false,
issuer,
clientId,
redirectUri
});
mockHash('error=fakeerror&error_description=fakedescription');
const result = oauthUtil.isLoginRedirect(sdk);
expect(result).toBe(true);
});

it('should return false if current URI is not redirect URI', () => {
sdk = new OktaAuth({
pkce: true,
issuer,
clientId,
redirectUri
});
mockSearch('error=fakeerror&error_description=fakedescription');
window.location.href = 'http://fake/product/search?error=fakeerror&error_description=fakedescription';
const result = oauthUtil.isLoginRedirect(sdk);
expect(result).toBe(false);
});

});
});
32 changes: 32 additions & 0 deletions test/spec/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2847,6 +2847,38 @@ describe('token.parseFromUrl', function() {
util.expectErrorToEqual(e, error);
});
});

it('throws an OAuthError error if "error" and "error_description" in the url', () => {
const error = {
name: 'OAuthError',
message: 'fake_description',
errorCode: 'fake_error',
errorSummary: 'fake_description',
errorId: 'INTERNAL',
errorCauses: []
};
return oauthUtil.setupParseUrl({
willFail: true,
hashMock: '#error=fake_error&error_description=fake_description',
oauthParams: JSON.stringify({
responseType: ['id_token', 'token'],
state: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
nonce: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
scopes: ['openid', 'email'],
urls: {
issuer: 'https://auth-js-test.okta.com',
tokenUrl: 'https://auth-js-test.okta.com/oauth2/v1/token',
authorizeUrl: 'https://auth-js-test.okta.com/oauth2/v1/authorize',
userinfoUrl: 'https://auth-js-test.okta.com/oauth2/v1/userinfo'
}
})
})
.catch(function(e) {
util.expectErrorToEqual(e, error);
});

});

});

describe('token.renew', function() {
Expand Down

0 comments on commit 56489f9

Please sign in to comment.