Skip to content

Commit

Permalink
feat: ID-1238 Update logoutDeviceFlow to return endSessionEndpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
haydenfowler authored Nov 30, 2023
1 parent 8bb064f commit e9017bf
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 102 deletions.
6 changes: 3 additions & 3 deletions packages/game-bridge/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { gameBridgeVersionCheck } from '@imtbl/version-check';
const scope = 'openid offline_access profile email transact';
const audience = 'platform_api';
const redirectUri = 'https://localhost:3000/'; // Not required
const logoutRedirectUri = 'https://localhost:3000/'; // Not required

const keyFunctionName = 'fxName';
const keyRequestId = 'requestId';
Expand Down Expand Up @@ -134,7 +133,7 @@ window.callFunction = async (jsonData: string) => { // eslint-disable-line no-un
audience,
scope,
redirectUri: (redirect ?? redirectUri),
logoutRedirectUri,
logoutRedirectUri: request?.logoutRedirectUri,
crossSdkBridgeEnabled: true,
};
passportClient = new passport.Passport(passportConfig);
Expand Down Expand Up @@ -266,11 +265,12 @@ window.callFunction = async (jsonData: string) => { // eslint-disable-line no-un
break;
}
case PASSPORT_FUNCTIONS.logout: {
await passportClient?.logoutDeviceFlow();
const deviceFlowEndSessionEndpoint = await passportClient?.logoutDeviceFlow();
callbackToGame({
responseFor: fxName,
requestId,
success: true,
result: deviceFlowEndSessionEndpoint,
});
break;
}
Expand Down
22 changes: 21 additions & 1 deletion packages/passport/sdk/src/Passport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ describe('Passport', () => {
let authLoginMock: jest.Mock;
let loginCallbackMock: jest.Mock;
let logoutMock: jest.Mock;
let removeUserMock: jest.Mock;
let getDeviceFlowEndSessionEndpointMock: jest.Mock;
let magicLoginMock: jest.Mock;
let magicLogoutMock: jest.Mock;
let confirmationLogoutMock: jest.Mock;
Expand All @@ -44,6 +46,8 @@ describe('Passport', () => {
confirmationLogoutMock = jest.fn();
magicLogoutMock = jest.fn();
logoutMock = jest.fn();
removeUserMock = jest.fn();
getDeviceFlowEndSessionEndpointMock = jest.fn();
getUserMock = jest.fn();
requestRefreshTokenMock = jest.fn();
getProviderMock = jest.fn();
Expand All @@ -53,6 +57,8 @@ describe('Passport', () => {
login: authLoginMock,
loginCallback: loginCallbackMock,
logout: logoutMock,
removeUser: removeUserMock,
getDeviceFlowEndSessionEndpoint: getDeviceFlowEndSessionEndpointMock,
getUser: getUserMock,
requestRefreshTokenAfterRegistration: requestRefreshTokenMock,
});
Expand Down Expand Up @@ -162,8 +168,22 @@ describe('Passport', () => {
await passport.logout();

expect(logoutMock).toBeCalledTimes(1);
expect(confirmationLogoutMock).toBeCalledTimes(1);
expect(magicLogoutMock).toBeCalledTimes(1);
expect(confirmationLogoutMock).toBeCalledTimes(1);
});
});

describe('logoutDeviceFlow', () => {
it('should execute logoutDeviceFlow without error and return the device flow end session endpoint', async () => {
const endSessionEndpoint = 'https://test.com/logout';
getDeviceFlowEndSessionEndpointMock.mockReturnValue(endSessionEndpoint);

const result = await passport.logoutDeviceFlow();

expect(result).toEqual('https://test.com/logout');
expect(removeUserMock).toHaveBeenCalledTimes(1);
expect(magicLogoutMock).toHaveBeenCalledTimes(1);
expect(getDeviceFlowEndSessionEndpointMock).toHaveBeenCalledTimes(1);
});
});

Expand Down
18 changes: 13 additions & 5 deletions packages/passport/sdk/src/Passport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ export class Passport {
return user ? user.profile : null;
}

public async loginCallback(): Promise<void> {
return this.authManager.loginCallback();
}

public async loginWithDeviceFlow(): Promise<DeviceConnectResponse> {
return this.authManager.loginWithDeviceFlow();
}
Expand All @@ -134,10 +138,6 @@ export class Passport {
return user.profile;
}

public async loginCallback(): Promise<void> {
return this.authManager.loginCallback();
}

public async logout(): Promise<void> {
await this.confirmationScreen.logout();
await this.authManager.logout();
Expand All @@ -146,10 +146,18 @@ export class Passport {
this.passportEventEmitter.emit(PassportEvents.LOGGED_OUT);
}

public async logoutDeviceFlow(): Promise<void> {
/**
* Logs the user out of Passport when using device flow authentication.
*
* @returns {Promise<string>} The device flow end session endpoint. Consumers are responsible for
* opening this URL in the same browser that was used to log the user in.
*/
public async logoutDeviceFlow(): Promise<string> {
await this.authManager.removeUser();
await this.magicAdapter.logout();
this.passportEventEmitter.emit(PassportEvents.LOGGED_OUT);

return this.authManager.getDeviceFlowEndSessionEndpoint();
}

/**
Expand Down
156 changes: 87 additions & 69 deletions packages/passport/sdk/src/authManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { PassportError, PassportErrorType } from './errors/passportError';
import { PassportConfiguration } from './config';
import { mockUser, mockUserImx, mockUserZkEvm } from './test/mocks';
import { isTokenExpired } from './utils/token';
import { PassportModuleConfiguration } from './types';

jest.mock('jwt-decode');
jest.mock('./utils/token');
Expand All @@ -15,15 +16,14 @@ jest.mock('oidc-client-ts', () => ({
WebStorageStateStore: jest.fn(),
}));

const baseConfig = new ImmutableConfiguration({
environment: Environment.SANDBOX,
});
const config = new PassportConfiguration({
baseConfig,
logoutRedirectUri: 'https://test.com',
const getConfig = (values?: Partial<PassportModuleConfiguration>) => new PassportConfiguration({
baseConfig: new ImmutableConfiguration({
environment: Environment.SANDBOX,
}),
clientId: '11111',
redirectUri: 'https://test.com',
scope: 'email profile',
...values,
});

const commonOidcUser: OidcUser = {
Expand Down Expand Up @@ -98,74 +98,62 @@ describe('AuthManager', () => {
signinSilent: mockSigninSilent,
storeUser: mockStoreUser,
});
authManager = new AuthManager(config);
authManager = new AuthManager(getConfig());
});

describe('constructor', () => {
it('should initialise AuthManager with a configuration containing audience params', () => {
const configWithAudience = new PassportConfiguration({
baseConfig,
logoutRedirectUri: 'https://test.com',
clientId: '11111',
redirectUri: 'https://test.com',
scope: 'email profile',
audience: 'audience',
});

// to work around new being used as a side effect, which would cause a lint failure
const am = new AuthManager(configWithAudience);
it('should initialise AuthManager with the correct default configuration', () => {
const config = getConfig();
const am = new AuthManager(config);
expect(am).toBeDefined();
expect(UserManager).toBeCalledWith({
authority: configWithAudience.authenticationDomain,
client_id: configWithAudience.oidcConfiguration.clientId,
authority: config.authenticationDomain,
client_id: config.oidcConfiguration.clientId,
loadUserInfo: true,
mergeClaims: true,
metadata: {
authorization_endpoint: `${configWithAudience.authenticationDomain}/authorize`,
token_endpoint: `${configWithAudience.authenticationDomain}/oauth/token`,
userinfo_endpoint: `${configWithAudience.authenticationDomain}/userinfo`,
end_session_endpoint:
`${configWithAudience.authenticationDomain}/v2/logout`
+ `?returnTo=${encodeURIComponent(
configWithAudience.oidcConfiguration.logoutRedirectUri,
)}`
+ `&client_id=${configWithAudience.oidcConfiguration.clientId}`,
authorization_endpoint: `${config.authenticationDomain}/authorize`,
token_endpoint: `${config.authenticationDomain}/oauth/token`,
userinfo_endpoint: `${config.authenticationDomain}/userinfo`,
end_session_endpoint: `${config.authenticationDomain}/v2/logout`
+ `?client_id=${config.oidcConfiguration.clientId}`,
},
popup_redirect_uri: configWithAudience.oidcConfiguration.redirectUri,
redirect_uri: configWithAudience.oidcConfiguration.redirectUri,
scope: configWithAudience.oidcConfiguration.scope,
popup_redirect_uri: config.oidcConfiguration.redirectUri,
redirect_uri: config.oidcConfiguration.redirectUri,
scope: config.oidcConfiguration.scope,
userStore: expect.any(WebStorageStateStore),
extraQueryParams: {
audience: configWithAudience.oidcConfiguration.audience,
},
});
});
});

it('should initialise AuthManager with the correct default configuration', () => {
// to work around new being used as a side effect, which would cause a lint failure
const am = new AuthManager(config);
expect(am).toBeDefined();
expect(UserManager).toBeCalledWith({
authority: config.authenticationDomain,
client_id: config.oidcConfiguration.clientId,
loadUserInfo: true,
mergeClaims: true,
metadata: {
authorization_endpoint: `${config.authenticationDomain}/authorize`,
token_endpoint: `${config.authenticationDomain}/oauth/token`,
userinfo_endpoint: `${config.authenticationDomain}/userinfo`,
end_session_endpoint:
`${config.authenticationDomain}/v2/logout`
+ `?returnTo=${encodeURIComponent(
config.oidcConfiguration.logoutRedirectUri,
)}`
+ `&client_id=${config.oidcConfiguration.clientId}`,
},
popup_redirect_uri: config.oidcConfiguration.redirectUri,
redirect_uri: config.oidcConfiguration.redirectUri,
scope: config.oidcConfiguration.scope,
userStore: expect.any(WebStorageStateStore),
describe('when an audience is specified', () => {
it('should initialise AuthManager with a configuration containing audience params', () => {
const configWithAudience = getConfig({
audience: 'audience',
});
const am = new AuthManager(configWithAudience);
expect(am).toBeDefined();
expect(UserManager).toBeCalledWith(expect.objectContaining({
extraQueryParams: {
audience: configWithAudience.oidcConfiguration.audience,
},
}));
});
});

describe('when a logoutRedirectUri is specified', () => {
it('should set the endSessionEndpoint `returnTo` and `client_id` query string params', () => {
const configWithLogoutRedirectUri = getConfig({
logoutRedirectUri: 'https://test.com/logout/callback',
});

const am = new AuthManager(configWithLogoutRedirectUri);
expect(am).toBeDefined();
expect(UserManager).toBeCalledWith(expect.objectContaining({
metadata: expect.objectContaining({
end_session_endpoint: 'https://auth.immutable.com/v2/logout?client_id=11111&returnTo=https%3A%2F%2Ftest.com%2Flogout%2Fcallback',
}),
}));
});
});
});

Expand Down Expand Up @@ -268,8 +256,9 @@ describe('AuthManager', () => {

describe('logout', () => {
it('should call redirect logout if logout mode is redirect', async () => {
const configuration = { ...config };
configuration.oidcConfiguration.logoutMode = 'redirect';
const configuration = getConfig({
logoutMode: 'redirect',
});
const manager = new AuthManager(configuration);

await manager.logout();
Expand All @@ -278,8 +267,9 @@ describe('AuthManager', () => {
});

it('should call redirect logout if logout mode is not set', async () => {
const configuration = { ...config };
configuration.oidcConfiguration.logoutMode = undefined;
const configuration = getConfig({
logoutMode: undefined,
});
const manager = new AuthManager(configuration);

await manager.logout();
Expand All @@ -288,8 +278,9 @@ describe('AuthManager', () => {
});

it('should call silent logout if logout mode is silent', async () => {
const configuration = { ...config };
configuration.oidcConfiguration.logoutMode = 'silent';
const configuration = getConfig({
logoutMode: 'silent',
});
const manager = new AuthManager(configuration);

await manager.logout();
Expand All @@ -298,8 +289,9 @@ describe('AuthManager', () => {
});

it('should throw an error if user is failed to logout', async () => {
const configuration = { ...config };
configuration.oidcConfiguration.logoutMode = 'redirect';
const configuration = getConfig({
logoutMode: 'redirect',
});
const manager = new AuthManager(configuration);

mockSignoutRedirect.mockRejectedValue(new Error(mockErrorMsg));
Expand Down Expand Up @@ -402,4 +394,30 @@ describe('AuthManager', () => {
});
});
});

describe('getDeviceFlowEndSessionEndpoint', () => {
describe('when a logoutRedirectUri is specified', () => {
it('should set the endSessionEndpoint `returnTo` and `client_id` query string params', () => {
const am = new AuthManager(getConfig({
logoutRedirectUri: 'https://test.com/logout/callback',
}));

const result = am.getDeviceFlowEndSessionEndpoint();

expect(result).toEqual(
'https://auth.immutable.com/v2/logout?client_id=11111&returnTo=https%3A%2F%2Ftest.com%2Flogout%2Fcallback',
);
});
});

describe('when no logoutRedirectUri is specified', () => {
it('should return the endSessionEndpoint without a `returnTo` or `client_id` query string params', () => {
const am = new AuthManager(getConfig());

const result = am.getDeviceFlowEndSessionEndpoint();

expect(result).toEqual('https://auth.immutable.com/v2/logout');
});
});
});
});
Loading

0 comments on commit e9017bf

Please sign in to comment.