Skip to content
This repository was archived by the owner on Nov 25, 2020. It is now read-only.

Commit

Permalink
feat(authService): add logout handling for openid connect
Browse files Browse the repository at this point in the history
OIDC providers like IdentityService and OpenIddict handle logout very similarly to authenticate. Feature adds logoutEndpoint to oAuth2 configuration and handles calling redirection to this endpoint very similarly to authenticate.
  • Loading branch information
MartinMason committed Aug 30, 2016
1 parent e165bda commit 91e9217
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 12 deletions.
24 changes: 19 additions & 5 deletions src/authService.js
Original file line number Diff line number Diff line change
Expand Up @@ -415,10 +415,12 @@ export class AuthService {
* logout locally and redirect to redirectUri (if set) or redirectUri of config. Sends logout request first, if set in config
*
* @param {[String]} [redirectUri] [optional redirectUri overwrite]
* @param {[String]} [query] [optional query]
* @param {[String]} [name] [optional name Name of the provider]
*
* @return {Promise<>|Promise<Object>|Promise<Error>} Server response as Object
*/
logout(redirectUri, query) {
logout(redirectUri, query, name) {
let localLogout = response => new Promise(resolve => {
this.setResponseObject(null);

Expand All @@ -427,13 +429,25 @@ export class AuthService {
if (typeof this.onLogout === 'function') {
this.onLogout(response);
}

resolve(response);
});

return (this.config.logoutUrl
? this.client.request(this.config.logoutMethod, this.config.joinBase(this.config.logoutUrl)).then(localLogout)
: localLogout());
if (name) {
if (this.config.providers[name].logoutEndpoint) {
return this.authentication.logout(name)
.then(logoutResponse => {
let stateValue = this.authentication.storage.get(name + '_state');
if (logoutResponse.state !== stateValue) {
return Promise.reject('OAuth2 response state value differs');
}
return localLogout(logoutResponse);
});
}
} else {
return (this.config.logoutUrl
? this.client.request(this.config.logoutMethod, this.config.joinBase(this.config.logoutUrl)).then(localLogout)
: localLogout());
}
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/authentication.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference path="../test/oAuth2.spec.js" />
import {PLATFORM} from 'aurelia-pal';
import {buildQueryString} from 'aurelia-path';
import {inject} from 'aurelia-dependency-injection';
Expand Down Expand Up @@ -261,6 +262,14 @@ export class Authentication {
return providerLogin.open(this.config.providers[name], userData);
}

logout(name) {
let rtnValue = Promise.resolve('Not Applicable');
if (this.config.providers[name].oauthType !== '2.0' || !this.config.providers[name].logoutEndpoint) {
return rtnValue;
}
return this.oAuth2.close(this.config.providers[name]);
}

redirect(redirectUrl, defaultRedirectUrl, query) {
// stupid rule to keep it BC
if (redirectUrl === true) {
Expand Down
31 changes: 31 additions & 0 deletions src/oAuth2.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,37 @@ export class OAuth2 {
});
return query;
}

close(options) {
const provider = extend(true, {}, this.defaults, options);
const url = provider.logoutEndpoint + '?'
+ buildQueryString(this.buildLogoutQuery(provider));
const popup = this.popup.open(url, provider.name, provider.popupOptions);
const openPopup = (this.config.platform === 'mobile')
? popup.eventListener(provider.postLogoutRedirectUri)
: popup.pollPopup();

return openPopup
.then(response => {
return response;
});
}

buildLogoutQuery(provider) {
let query = {};
let authResponse = this.storage.get(this.config.storageKey);

if (provider.postLogoutRedirectUri) {
query.post_logout_redirect_uri = provider.postLogoutRedirectUri;
}
if (this.storage.get(provider.name + '_state')) {
query.state = this.storage.get(provider.name + '_state');
}
if (JSON.parse(authResponse).id_token) {
query.id_token_hint = JSON.parse(authResponse).id_token;
}
return query;
}
}

const camelCase = function(name) {
Expand Down
50 changes: 47 additions & 3 deletions test/authService.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ function getContainer() {
return container;
}

let oidcProviderConfig = {
providers: {
oidcProvider: {
name: 'oidcProvider',
oauthType: '2.0',
postLogoutRedirectUri: 'http://localhost:1927/',
logoutEndpoint: 'http://localhost:54540/connect/logout',
popupOptions: { width: 1028, height: 529 }
}
}
};

describe('AuthService', () => {
describe('.constructor()', () => {
Expand Down Expand Up @@ -794,6 +805,7 @@ describe('AuthService', () => {
beforeEach(() => {
authService.setResponseObject({token: 'some', refresh_token: 'another'});
authService.config.logoutRedirect = 'nowhere';
authService.config.configure(oidcProviderConfig);
});

afterEach(() => {
Expand All @@ -806,6 +818,8 @@ describe('AuthService', () => {
.then(() => {
expect(authService.isAuthenticated()).toBe(false);

done();
}, err => {
done();
});
});
Expand All @@ -819,11 +833,41 @@ describe('AuthService', () => {
expect(response.path).toBe('/auth/logout');
expect(response.method).toBe('GET');

done();
}, err => {
done();
});
});
});

it('should call oAuth2.close() if logoutEndpoint defined', done => {
spyOn(authService.authentication.oAuth2, 'close').and.returnValue(Promise.resolve({ state: 'ThisIsTheState' }));
authService.config.logoutRedirect = false;
authService.authentication.storage.set('oidcProvider_state', 'ThisIsTheState');
authService.logout(0, undefined, 'oidcProvider')
.then(response => {
expect(authService.authentication.oAuth2.close).toHaveBeenCalled();
done();
}, err => {
expect(err).toBeUndefined();
done();
});
});

it('return reject Promise if states differ', done => {
spyOn(authService.authentication.oAuth2, 'close').and.callFake( () => {
return Promise.resolve({ state: 'ThisIsTheState' });
});
authService.authentication.storage.set('oidcProvider_state', 'ThisIsNotTheState');
authService.logout(0, undefined, 'oidcProvider')
.then(response => {
expect(authService.authentication.oAuth2.close).toHaveBeenCalled();
done();
}, err => {
expect(err).toBe('OAuth2 response state value differs');
done();
});
});
});

describe('.authenticate()', () => {
const container = getContainer();
Expand Down Expand Up @@ -852,7 +896,7 @@ describe('AuthService', () => {

authService.authenticate('twitter', 0, {data: 'some'})
.then(response => {
expect(response.provider).toBe(authService.config.providers['twitter']);
expect(response.provider).toBe(authService.config.providers.twitter);
expect(response.userData.data).toBe('some');
expect(response.access_token).toBe('oauth1');

Expand All @@ -868,7 +912,7 @@ describe('AuthService', () => {

authService.authenticate('facebook', null, {data: 'some'})
.then(response => {
expect(response.provider).toBe(authService.config.providers['facebook']);
expect(response.provider).toBe(authService.config.providers.facebook);
expect(response.userData.data).toBe('some');
expect(response.access_token).toBe('oauth2');

Expand Down
61 changes: 61 additions & 0 deletions test/authentication.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ const tokenFuture = {
jwt: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidG9rZW5GdXR1cmUiLCJhZG1pbiI6dHJ1ZSwiZXhwIjoiMjQ2MDAxNzE1NCJ9.iHXLzWGY5U9WwVT4IVRLuKTf65XpgrA1Qq_Jlynv6bc'
};

const oidcProviderConfig = {
providers: {
oidcProvider: {
name: 'oidcProvider',
oauthType: '2.0',
postLogoutRedirectUri: 'http://localhost:1927/',
logoutEndpoint: 'http://localhost:54540/connect/logout',
popupOptions: { width: 1028, height: 529 }
}
}
};

describe('Authentication', () => {
describe('.getResponseObject', () => {
Expand Down Expand Up @@ -402,6 +413,56 @@ describe('Authentication', () => {
});
});

describe('.logout', () => {
const container = new Container();
const authentication = container.get(Authentication);

afterEach(() => {
authentication.setResponseObject(null);
});

it('should return Not Applicable when logoutEndpoint not defined', done => {
authentication.config.configure(oidcProviderConfig);
authentication.config.providers.oidcProvider.logoutEndpoint = null;
authentication.logout('oidcProvider')
.then( (value) => {
expect(value).toBe('Not Applicable');
done();
})
.catch( err => {
done();
});
});

it('should return Not Applicable when oauthType not equal to 2.0', done => {
authentication.config.configure(oidcProviderConfig);
authentication.config.providers.oidcProvider.oauthType = '1.0';
authentication.logout('oidcProvider')
.then( (value) => {
expect(value).toBe('Not Applicable');
done();
})
.catch( err => {
done();
});
});

it('should return state', done => {
let stateValue = '123456789';
authentication.config.configure(oidcProviderConfig);
spyOn(authentication.oAuth2, 'close').and.callFake(() => {
return Promise.resolve(stateValue);
});
authentication.logout('oidcProvider')
.then( state => {
expect(state).toBe(stateValue);
done();
})
.catch( err => {
done();
});
});
});

describe('.redirect', () => {
const container = new Container();
Expand Down
59 changes: 55 additions & 4 deletions test/oAuth2.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ function getContainer() {
return container;
}

let oidcProviderConfig = {
providers: {
oidcProvider: {
name: 'oidcProvider',
postLogoutRedirectUri: 'http://localhost:1927/',
logoutEndpoint: 'http://localhost:54540/connect/logout',
popupOptions: { width: 1028, height: 529 }
}
}
};

describe('OAuth2', () => {
const container = getContainer();
const storage = container.get(Storage);
Expand All @@ -51,7 +62,7 @@ describe('OAuth2', () => {
oAuth2.exchangeForToken(
{access_token: 'someToken'},
{userData: 'some'},
baseConfig.providers['facebook']
baseConfig.providers.facebook
).then(res => {
expect(res).toBeUndefined();
expect(false).toBe(true);
Expand All @@ -68,7 +79,7 @@ describe('OAuth2', () => {
oAuth2.exchangeForToken(
{access_token: 'someToken'},
{userData: 'some'},
baseConfig.providers['facebook']
baseConfig.providers.facebook
).then(res => {
expect(res).toBeDefined();
expect(res.path).toBe('/auth/facebook');
Expand All @@ -87,7 +98,7 @@ describe('OAuth2', () => {
describe('.open()', () => {
it('not fails with withCredentials = false', done => {
baseConfig.withCredentials = false;
oAuth2.open(baseConfig.providers['facebook'], {userData: 'some'})
oAuth2.open(baseConfig.providers.facebook, {userData: 'some'})
.then(res=>{
expect(res).toBeDefined();
expect(res.path).toBe('/auth/facebook');
Expand All @@ -107,9 +118,49 @@ describe('OAuth2', () => {

describe('.buildQuery()', () => {
it('return query', () => {
const query = oAuth2.buildQuery(baseConfig.providers['facebook']);
const query = oAuth2.buildQuery(baseConfig.providers.facebook);
expect(query.display).toBe('popup');
expect(query.scope).toBe('email');
});
});

describe('.close()', () => {
it('logout popup url correct', done => {
const expectedIdToken = 'Some Id Token';
const expectedLogoutRedirect = 'http://localhost:1927/';
const expectedState = '1234567890';

spyOn(storage, 'get').and.callFake((key) => {
if (key === 'oidcProvider_state') {
return expectedState;
}
if (key === oAuth2.config.storageKey) {
return `{ "id_token": "${expectedIdToken}" }`;
}
});
oAuth2.close(oidcProviderConfig.providers.oidcProvider)
.then(res => {
const expectedUrl = `http://localhost:54540/connect/logout?id_token_hint=${encodeURIComponent(expectedIdToken)}&post_logout_redirect_uri=${encodeURIComponent(expectedLogoutRedirect)}&state=${encodeURIComponent(expectedState)}`;
expect(popup.url).toBe(expectedUrl);
done();
});
});
});

describe('.buildLogoutQuery()', () => {
it('return query parameters', () => {
spyOn(storage, 'get').and.callFake((key) => {
if (key === 'oidcProvider_state') {
return '123456789';
}
if (key === oAuth2.config.storageKey) {
return '{ "id_token": "IdTokenHere" }';
}
});
const query = oAuth2.buildLogoutQuery(oidcProviderConfig.providers.oidcProvider);
expect(query.post_logout_redirect_uri).toBe('http://localhost:1927/');
expect(query.state).toBe('123456789');
expect(query.id_token_hint).toBe('IdTokenHere');
});
});
});

0 comments on commit 91e9217

Please sign in to comment.