Skip to content

Commit

Permalink
DCJ-654: Sign-in via OIDC (#2667)
Browse files Browse the repository at this point in the history
  • Loading branch information
rushtong authored Sep 19, 2024
1 parent 62ad448 commit 1652274
Show file tree
Hide file tree
Showing 18 changed files with 323 additions and 317 deletions.
1 change: 0 additions & 1 deletion config/alpha.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"hash": "alpha",
"apiUrl": "https://consent.dsde-alpha.broadinstitute.org",
"ontologyApiUrl": "https://consent-ontology.dsde-alpha.broadinstitute.org/",
"clientId": "1020846292598-hd801vsmmbhh97vaf6aar17lu0q2evfj.apps.googleusercontent.com",
"errorApiKey": "1234567890abcdefghijklmnop",
"gaId": "",
"profileUrl": "https://profile-dot-broad-shibboleth-prod.appspot.com/dev",
Expand Down
1 change: 0 additions & 1 deletion config/base_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"ontologyApiUrl": "",
"terraUrl": "",
"tdrApiUrl": "",
"clientId": "",
"errorApiKey": "",
"nihUrl": "",
"gaId": "",
Expand Down
38 changes: 29 additions & 9 deletions cypress/component/Auth/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,48 @@
/* eslint-disable no-undef */

import {OidcBroker} from '../../../src/libs/auth/oidcBroker';
import {Auth} from '../../../src/libs/auth/auth';
import {Config} from '../../../src/libs/config';
import {GoogleIS} from '../../../src/libs/googleIS';
import {OAuth2} from '../../../src/libs/ajax/OAuth2';
import {Storage} from '../../../src/libs/storage';
import { v4 as uuid } from 'uuid';
import {v4 as uuid} from 'uuid';
import {mockOidcUser} from './mockOidcUser';

describe('Auth', function () {
describe('Auth Failure', function () {
it('Sign In error throws expected message', async function () {
cy.stub(OidcBroker, 'signIn').returns(null);
cy.on('fail', (err) => {
return err.message !== Auth.signInError();
});
Auth.signIn().then(() => {
expect(Storage.getOidcUser()).to.be.null;
expect(Storage.userIsLogged()).to.be.false;
});
});
});

describe('Auth Success', function () {
// Intercept configuration calls
beforeEach(async () => {
beforeEach(() => {
cy.intercept({
method: 'GET',
url: '/config.json',
hostname: 'localhost',
}, {'env': 'ci'});
cy.stub(OAuth2, 'getConfig').returns({
'authorityEndpoint': 'authorityEndpoint',
'authorityEndpoint': Cypress.config().baseUrl,
'clientId': 'clientId'
});
await Auth.initialize();
Auth.initialize();
});

it('Sign In stores the current user', async function () {
cy.stub(OidcBroker, 'signIn').returns(mockOidcUser);
await Auth.signIn();
expect(Storage.getOidcUser()).to.not.be.empty;
expect(Storage.userIsLogged()).to.be.true;
});

it('Sign Out Clears the session when called', async function () {
cy.stub(Config, 'getGoogleClientId').returns('12345');
cy.stub(GoogleIS, 'revokeAccessToken');
Storage.setUserIsLogged(true);
Storage.setAnonymousId(uuid());
Storage.setData('key', 'val');
Expand All @@ -38,4 +57,5 @@ describe('Auth', function () {
expect(Storage.getData('key')).to.be.null;
expect(Storage.getEnv()).to.be.null;
});

});
29 changes: 29 additions & 0 deletions cypress/component/Auth/mockOidcUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {OidcUser} from "../../../src/libs/auth/oidcBroker";

export const mockOidcUser: OidcUser = {
access_token: '',
get expires_in(): number | undefined {
return undefined;
},
session_state: undefined,
state: undefined,
token_type: '',
get expired(): boolean | undefined {
return undefined;
},
get scopes(): string[] {
return [];
},
toStorageString(): string {
return '';
},
profile: {
jti: undefined,
nbf: undefined,
sub: undefined,
iss: '',
aud: '',
exp: 0,
iat: 0
}
};
46 changes: 40 additions & 6 deletions cypress/component/Auth/oidcBroker.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
/* eslint-disable no-undef */

import {Config} from '../../../src/libs/config';
import {GoogleIS} from '../../../src/libs/googleIS';
import {OAuth2} from '../../../src/libs/ajax/OAuth2';
import {OidcBroker} from '../../../src/libs/auth/oidcBroker';

describe('OidcBroker', function () {
describe('OidcBroker Failure', function () {

it('Get User Manager Fails without initialization', function () {
cy.on('fail', (err) => {
return !err.message.includes('initialized');
});
OidcBroker.getUserManager();
});

it('Get User Manager Settings Fails without initialization', function () {
cy.on('fail', (err) => {
return !err.message.includes('initialized');
});
OidcBroker.getUserManagerSettings();
});

});

describe('OidcBroker Success', function () {
// Intercept configuration calls
beforeEach(() => {
cy.intercept({
Expand All @@ -14,13 +30,30 @@ describe('OidcBroker', function () {
hostname: 'localhost',
}, {'env': 'ci'});
cy.stub(OAuth2, 'getConfig').returns({
'authorityEndpoint': 'authorityEndpoint',
'authorityEndpoint': Cypress.config().baseUrl,
'clientId': 'clientId'
});
});

it('Initialization Succeeds', async function () {
await OidcBroker.initialize();
expect(OidcBroker.getUserManager()).to.not.be.null;
expect(OidcBroker.getUserManagerSettings()).to.not.be.null;
});

it('Sign In calls Oidc Broker UserManager sign-in popup function', async function () {
await OidcBroker.initialize();
const um = OidcBroker.getUserManager();
cy.spy(um, 'signinPopup').as('signinPopup');
// Since we are not calling a real sign-in url, we expect oidc-client errors when doing so
cy.on('uncaught:exception', (err) => {
return !(err.message.includes('Invalid URL'))
});
OidcBroker.signIn();
expect(um.signinPopup).to.be.called;
});

it('Sign Out calls Oidc UserManager sign-out functions', async function () {
cy.stub(Config, 'getGoogleClientId').returns('12345');
cy.stub(GoogleIS, 'revokeAccessToken');
await OidcBroker.initialize();
const um = OidcBroker.getUserManager();
cy.spy(um, 'removeUser').as('removeUser');
Expand All @@ -29,4 +62,5 @@ describe('OidcBroker', function () {
expect(um.removeUser).to.be.called;
expect(um.clearStaleState).to.be.called;
});

});
48 changes: 0 additions & 48 deletions cypress/component/SignIn/google_is.spec.js

This file was deleted.

119 changes: 101 additions & 18 deletions cypress/component/SignIn/sign_in_button.spec.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,113 @@
/* eslint-disable no-undef */

import React from 'react';
import { mount } from 'cypress/react';
import {mount} from 'cypress/react18';
import SignInButton from '../../../src/components/SignInButton';
import { Config } from '../../../src/libs/config';
import {User} from '../../../src/libs/ajax/User';
import {Auth} from '../../../src/libs/auth/auth';
import {Storage} from '../../../src/libs/storage';
import {Metrics} from '../../../src/libs/ajax/Metrics';
import {StackdriverReporter} from '../../../src/libs/stackdriverReporter';
import {ToS} from '../../../src/libs/ajax/ToS';
import {mockOidcUser} from '../Auth/mockOidcUser';

const signInText = 'Sign-in';
const signInText = 'Sign In';

// Note that we do not want to click the signin button
// in tests as that would trigger an auth-flow we cannot
// replicate in a test environment.
describe('Sign In Component', function() {
it('Sign In Button Loads when client id is valid', function () {
const duosUser = {
displayName: 'display name',
email: '[email protected]',
roles: [{
name: 'Admin'
}]
};

const userStatus = {
'adminEnabled': true,
'enabled': true,
'inAllUsersGroup': true,
'inGoogleProxyGroup': true,
'tosAccepted': true
};

const notAcceptedUserStatus = Object.assign({}, userStatus, {'tosAccepted': false});

describe('Sign In: Component Loads', function () {

it('Sign In Button Loads', function () {
cy.viewport(600, 300);
mount(<SignInButton history={undefined}/>);
cy.contains(signInText).should('exist');
});

it('Sign In: On Success', function () {
cy.viewport(600, 300);
cy.stub(Auth, 'signIn').returns(Promise.resolve(mockOidcUser));
cy.stub(User, 'getMe').returns(duosUser);
cy.stub(StackdriverReporter, 'report');
cy.stub(Metrics, 'identify');
cy.stub(Metrics, 'syncProfile');
cy.stub(Metrics, 'captureEvent');
cy.stub(ToS, 'getStatus').returns(userStatus);
mount(<SignInButton history={[]}/>);
cy.get('button').click().then(() => {
expect(Storage.getCurrentUser()).to.deep.equal(duosUser);
expect(Storage.getAnonymousId()).to.not.be.null;
expect(StackdriverReporter.report).to.not.be.called;
expect(Metrics.identify).to.be.called;
expect(Metrics.syncProfile).to.be.called;
expect(Metrics.captureEvent).to.be.called;
});
});

it('Sign In: No Roles Error Reporter Is Called', function () {
const bareUser = {email: '[email protected]'};
cy.viewport(600, 300);
// Load the client id from perf so we can have a valid button
cy.readFile('config/alpha.json').then((config) => {
const clientId = config.clientId;
cy.stub(Config, 'getGoogleClientId').returns(clientId);
mount(<SignInButton />);
cy.contains(signInText).should('exist');
cy.stub(Auth, 'signIn').returns(Promise.resolve(mockOidcUser));
cy.stub(User, 'getMe').returns(bareUser);
cy.stub(StackdriverReporter, 'report');
cy.stub(Metrics, 'identify');
cy.stub(Metrics, 'syncProfile');
cy.stub(Metrics, 'captureEvent');
cy.stub(ToS, 'getStatus').returns(userStatus);
mount(<SignInButton history={[]}/>);
cy.get('button').click().then(() => {
expect(StackdriverReporter.report).to.be.called;
});
});
it('Spinner loads when client id is empty', function () {

it('Sign In: Redirects to ToS if not accepted', function () {
cy.viewport(600, 300);
cy.stub(Config, 'getGoogleClientId').returns('');
mount(<SignInButton />);
cy.contains(signInText).should('not.exist');
cy.stub(Auth, 'signIn').returns(Promise.resolve(mockOidcUser));
cy.stub(User, 'getMe').returns(duosUser);
cy.stub(ToS, 'getStatus').returns(notAcceptedUserStatus);
cy.stub(Metrics, 'identify');
cy.stub(Metrics, 'syncProfile');
cy.stub(Metrics, 'captureEvent');
let history = [];
mount(<SignInButton history={history}/>);
cy.get('button').click().then(() => {
expect(history).to.not.be.empty;
expect(history[0].includes('tos_acceptance')).to.be.true;
});
});

it('Sign In: Registers user if not found and redirects to ToS', function () {
cy.viewport(600, 300);
cy.stub(Auth, 'signIn').returns(Promise.resolve(mockOidcUser));
// Simulate user not found
cy.stub(User, 'getMe').throws();
cy.stub(User, 'registerUser').returns(duosUser);
cy.stub(ToS, 'getStatus').returns(notAcceptedUserStatus);
cy.stub(Metrics, 'identify');
cy.stub(Metrics, 'syncProfile');
cy.stub(Metrics, 'captureEvent');
let history = [];
mount(<SignInButton history={history}/>);
cy.get('button').click().then(() => {
expect(User.registerUser).to.be.called;
expect(history).to.not.be.empty;
expect(history[0].includes('tos_acceptance')).to.be.true;
});
});

});
1 change: 0 additions & 1 deletion public/config-example.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"ontologyApiUrl": "https://ontologyURL.org/",
"terraUrl": "https://terraURL.org/",
"tdrApiUrl": "https://tdrApiUrl.org/",
"clientId": "111111111111-11111111111111111111111111111111.apps.googleusercontent.com",
"errorApiKey": "example",
"gaId": "",
"profileUrl": "https://profile-dot-broad-shibboleth-prod.appspot.com/dev",
Expand Down
9 changes: 2 additions & 7 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,13 @@ function App() {
setUserIsLogged();
});

const signIn = async () => {
await Storage.setUserIsLogged(true);
await setIsLoggedIn(true);
};

return (
<div className="body">
<div className="wrap">
<div className="main">
<DuosHeader onSignIn={signIn} />
<DuosHeader/>
<Spinner name="mainSpinner" group="duos" loadingImage={loadingImage} />
<Routes onSignIn={signIn} isLogged={isLoggedIn} env={env} />
<Routes isLogged={isLoggedIn} env={env} />
</div>
</div>
<DuosFooter />
Expand Down
Loading

0 comments on commit 1652274

Please sign in to comment.