/*! * @license * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import assert from 'assert'; import { AlfrescoApi, ContentApi, Oauth2Auth } from '../src'; import { EcmAuthMock, OAuthMock } from './mockObjects'; import jsdom from 'jsdom'; const { JSDOM } = jsdom; const globalAny: any = global; describe('Oauth2 test', () => { let alfrescoJsApi: AlfrescoApi; let oauth2Mock: OAuthMock; let authResponseMock: EcmAuthMock; beforeEach(() => { const hostOauth2 = 'https://myOauthUrl:30081'; const mockStorage = { getItem: () => {}, setItem: () => {} }; oauth2Mock = new OAuthMock(hostOauth2); authResponseMock = new EcmAuthMock(hostOauth2); alfrescoJsApi = new AlfrescoApi({ hostEcm: 'myecm' }); alfrescoJsApi.storage.setStorage(mockStorage); }); describe('Discovery urls', () => { const authType = 'OAUTH'; const host = 'https://dummy/auth'; const clientId = 'dummy'; const scope = 'openid'; const redirectUri = '/'; it('should have default urls', async () => { const oauth2Auth = new Oauth2Auth( { oauth2: { host, clientId, scope, redirectUri }, authType }, alfrescoJsApi ); assert.equal(oauth2Auth.discovery.loginUrl, host + Oauth2Auth.DEFAULT_AUTHORIZATION_URL); assert.equal(oauth2Auth.discovery.tokenEndpoint, host + Oauth2Auth.DEFAULT_TOKEN_URL); assert.equal(oauth2Auth.discovery.logoutUrl, host + Oauth2Auth.DEFAULT_LOGOUT_URL); }); it('should be possible to override the default urls', async () => { const authorizationUrl = '/custom-login'; const logoutUrl = '/custom-logout'; const tokenUrl = '/custom-token'; const oauth2Auth = new Oauth2Auth( { oauth2: { host, authorizationUrl, logoutUrl, tokenUrl, clientId, scope, redirectUri }, authType }, alfrescoJsApi ); assert.equal(oauth2Auth.discovery.loginUrl, authorizationUrl); assert.equal(oauth2Auth.discovery.tokenEndpoint, tokenUrl); assert.equal(oauth2Auth.discovery.logoutUrl, logoutUrl); }); }); describe('With Authentication', () => { it('should be possible have different user login in different instance of the oauth2Auth class', async () => { const oauth2AuthInstanceOne = new Oauth2Auth( { oauth2: { host: 'https://myOauthUrl:30081/auth/realms/springboot', clientId: 'activiti', scope: 'openid', secret: '', redirectUri: '/', redirectUriLogout: '/logout' }, authType: 'OAUTH' }, alfrescoJsApi ); const oauth2AuthInstanceTwo = new Oauth2Auth( { oauth2: { host: 'https://myOauthUrl:30081/auth/realms/springboot', clientId: 'activiti', scope: 'openid', secret: '', redirectUri: '/', redirectUriLogout: '/logout' }, authType: 'OAUTH' }, alfrescoJsApi ); const mock = new OAuthMock('https://myOauthUrl:30081'); mock.get200Response('superman-token'); const loginInstanceOne = await oauth2AuthInstanceOne.login('superman', 'crypto'); mock.get200Response('barman-token'); const loginInstanceTwo = await oauth2AuthInstanceTwo.login('barman', 'IamBarman'); assert.equal(loginInstanceOne.access_token, 'superman-token'); assert.equal(loginInstanceTwo.access_token, 'barman-token'); oauth2AuthInstanceOne.logOut(); oauth2AuthInstanceTwo.logOut(); }); it('login should return the Token if is ok', (done) => { oauth2Mock.get200Response(); const oauth2Auth = new Oauth2Auth( { oauth2: { host: 'https://myOauthUrl:30081/auth/realms/springboot', clientId: 'activiti', scope: 'openid', secret: '', redirectUri: '/', redirectUriLogout: '/logout' }, authType: 'OAUTH' }, alfrescoJsApi ); oauth2Auth.login('admin', 'admin').then((data) => { assert.equal(data.access_token, 'test-token'); oauth2Auth.logOut(); done(); }); }); it('should refresh token when the login not use the implicitFlow ', function (done) { this.timeout(3000); oauth2Mock.get200Response(); const oauth2Auth = new Oauth2Auth( { oauth2: { host: 'https://myOauthUrl:30081/auth/realms/springboot', clientId: 'activiti', scope: 'openid', secret: '', redirectUri: '/', redirectUriLogout: '/logout', implicitFlow: false, refreshTokenTimeout: 100 }, authType: 'OAUTH' }, alfrescoJsApi ); let calls = 0; oauth2Auth.refreshToken = () => { calls++; return Promise.resolve(); }; setTimeout(() => { assert.equal(calls > 2, true); oauth2Auth.logOut(); done(); }, 600); oauth2Auth.login('admin', 'admin'); }); it('should not hang the app also if teh logout is missing', function (done) { this.timeout(3000); oauth2Mock.get200Response(); const oauth2Auth = new Oauth2Auth( { oauth2: { host: 'https://myOauthUrl:30081/auth/realms/springboot', clientId: 'activiti', scope: 'openid', secret: '', redirectUri: '/', redirectUriLogout: '/logout', implicitFlow: false, refreshTokenTimeout: 100 }, authType: 'OAUTH' }, alfrescoJsApi ); let calls = 0; oauth2Auth.refreshToken = () => { calls++; return Promise.resolve(); }; setTimeout(() => { assert.equal(calls > 2, true); done(); }, 600); oauth2Auth.login('admin', 'admin'); }); it('should emit a token_issued event if login is ok ', (done) => { oauth2Mock.get200Response(); const oauth2Auth = new Oauth2Auth( { oauth2: { host: 'https://myOauthUrl:30081/auth/realms/springboot', clientId: 'activiti', scope: 'openid', secret: '', redirectUri: '/', redirectUriLogout: '/logout' }, authType: 'OAUTH' }, alfrescoJsApi ); oauth2Auth.once('token_issued', () => { oauth2Auth.logOut(); done(); }); oauth2Auth.login('admin', 'admin'); }); it('should not emit a token_issued event if setToken is null ', (done) => { oauth2Mock.get200Response(); const oauth2Auth = new Oauth2Auth( { oauth2: { host: 'https://myOauthUrl:30081/auth/realms/springboot', clientId: 'activiti', scope: 'openid', secret: '', redirectUri: '/', redirectUriLogout: '/logout' }, authType: 'OAUTH' }, alfrescoJsApi ); let counterCallEvent = 0; oauth2Auth.once('token_issued', () => { counterCallEvent++; }); oauth2Auth.setToken(null, null); oauth2Auth.setToken('200', null); oauth2Auth.setToken(null, null); assert.equal(counterCallEvent, 1); done(); }); it('should emit a token_issued if provider is ECM', (done) => { oauth2Mock.get200Response(); authResponseMock.get200ValidTicket(); const oauth2Auth = new Oauth2Auth( { provider: 'ECM', oauth2: { host: 'https://myOauthUrl:30081/auth/realms/springboot', clientId: 'activiti', scope: 'openid', secret: '', redirectUri: '/', redirectUriLogout: '/logout' }, authType: 'OAUTH' }, alfrescoJsApi ); oauth2Auth.once('token_issued', () => { oauth2Auth.logOut(); done(); }); oauth2Auth.login('admin', 'admin'); }); it('should emit a token_issued if provider is ALL', (done) => { oauth2Mock.get200Response(); authResponseMock.get200ValidTicket(); const oauth2Auth = new Oauth2Auth( { provider: 'ALL', oauth2: { host: 'https://myOauthUrl:30081/auth/realms/springboot', clientId: 'activiti', scope: 'openid', secret: '', redirectUri: '/', redirectUriLogout: '/logout' }, authType: 'OAUTH' }, alfrescoJsApi ); oauth2Auth.once('token_issued', () => { oauth2Auth.logOut(); done(); }); oauth2Auth.login('admin', 'admin'); }); it('should after token_issued event exchange the access_token for the alf_ticket', (done) => { oauth2Mock.get200Response(); authResponseMock.get200ValidTicket(); const alfrescoApi = new AlfrescoApi({ hostEcm: 'https://myOauthUrl:30081', oauth2: { host: 'https://myOauthUrl:30081/auth/realms/springboot', clientId: 'activiti', scope: 'openid', secret: '', redirectUri: '/', redirectUriLogout: '/logout' }, authType: 'OAUTH' }); alfrescoApi.oauth2Auth.on('ticket_exchanged', () => { assert.equal(alfrescoApi.config.ticketEcm, 'TICKET_4479f4d3bb155195879bfbb8d5206f433488a1b1'); assert.equal(alfrescoApi.contentClient.config.ticketEcm, 'TICKET_4479f4d3bb155195879bfbb8d5206f433488a1b1'); const content = new ContentApi(alfrescoApi); const URL = content.getContentUrl('FAKE-NODE-ID'); assert.equal( URL, 'https://myOauthUrl:30081/alfresco/api/-default-/public/alfresco/versions/1/nodes/FAKE-NODE-ID/content?attachment=false&alf_ticket=TICKET_4479f4d3bb155195879bfbb8d5206f433488a1b1' ); alfrescoApi.oauth2Auth.logOut(); done(); }); alfrescoApi.login('admin', 'admin'); }); it('should after token_issued event exchange the access_token for the alf_ticket with the compatibility layer', (done) => { oauth2Mock.get200Response(); authResponseMock.get200ValidTicket(); const alfrescoApi = new AlfrescoApi({ hostEcm: 'https://myOauthUrl:30081', oauth2: { host: 'https://myOauthUrl:30081/auth/realms/springboot', clientId: 'activiti', scope: 'openid', secret: '', redirectUri: '/', redirectUriLogout: '/logout' }, authType: 'OAUTH' }); const contentApi = new ContentApi(alfrescoApi); alfrescoApi.oauth2Auth.on('ticket_exchanged', () => { assert.equal(alfrescoApi.config.ticketEcm, 'TICKET_4479f4d3bb155195879bfbb8d5206f433488a1b1'); assert.equal(alfrescoApi.contentClient.config.ticketEcm, 'TICKET_4479f4d3bb155195879bfbb8d5206f433488a1b1'); const URL = contentApi.getContentUrl('FAKE-NODE-ID'); assert.equal( URL, 'https://myOauthUrl:30081/alfresco/api/-default-/public/alfresco/versions/1/nodes/FAKE-NODE-ID/content?attachment=false&alf_ticket=TICKET_4479f4d3bb155195879bfbb8d5206f433488a1b1' ); alfrescoApi.oauth2Auth.logOut(); done(); }); alfrescoApi.login('admin', 'admin'); }); // TODO: very flaky test, fails on different machines if running slow, might relate to `this.timeout` // eslint-disable-next-line ban/ban xit('should extend content session after oauth token refresh', function (done) { this.timeout(3000); oauth2Mock.get200Response(); authResponseMock.get200ValidTicket(); const alfrescoApi = new AlfrescoApi({ hostEcm: 'https://myOauthUrl:30081', oauth2: { host: 'https://myOauthUrl:30081/auth/realms/springboot', clientId: 'activiti', scope: 'openid', secret: '', redirectUri: '/', redirectUriLogout: '/logout' }, authType: 'OAUTH' }); let counterCallEvent = 0; alfrescoApi.oauth2Auth.on('ticket_exchanged', () => { assert.equal(alfrescoApi.config.ticketEcm, 'TICKET_4479f4d3bb155195879bfbb8d5206f433488a1b1'); assert.equal(alfrescoApi.contentClient.config.ticketEcm, 'TICKET_4479f4d3bb155195879bfbb8d5206f433488a1b1'); const content = new ContentApi(alfrescoApi); const URL = content.getContentUrl('FAKE-NODE-ID'); assert.equal( URL, 'https://myOauthUrl:30081/alfresco/api/-default-/public/alfresco/versions/1/nodes/FAKE-NODE-ID/content?attachment=false&alf_ticket=TICKET_4479f4d3bb155195879bfbb8d5206f433488a1b1' ); counterCallEvent++; if (counterCallEvent === 2) { done(); } }); alfrescoApi.login('admin', 'admin'); this.timeout(3000); alfrescoApi.refreshToken(); }); it('isLoggedIn should return true if the api is logged in', (done) => { oauth2Mock.get200Response(); const oauth2Auth = new Oauth2Auth( { oauth2: { host: 'https://myOauthUrl:30081/auth/realms/springboot', clientId: 'activiti', scope: 'openid', secret: '', redirectUri: '/', redirectUriLogout: '/logout' }, authType: 'OAUTH' }, alfrescoJsApi ); oauth2Auth.login('admin', 'admin').then(() => { assert.equal(oauth2Auth.isLoggedIn(), true); oauth2Auth.logOut(); done(); }); }); it('login password should be removed after login', (done) => { oauth2Mock.get200Response(); const oauth2Auth = new Oauth2Auth( { oauth2: { host: 'https://myOauthUrl:30081/auth/realms/springboot', clientId: 'activiti', scope: 'openid', secret: '', redirectUri: '/', redirectUriLogout: '/logout' }, authType: 'OAUTH' }, alfrescoJsApi ); oauth2Auth.login('admin', 'admin').then(() => { assert.notEqual(oauth2Auth.authentications.basicAuth.password, 'admin'); oauth2Auth.logOut(); done(); }); }); describe('With mocked DOM', () => { beforeEach(() => { const dom = new JSDOM('', { url: 'https://localhost' }); globalAny.window = dom.window; globalAny.document = dom.window.document; }); it('a failed hash check calls the logout', () => { const oauth2Auth = new Oauth2Auth( { oauth2: { host: 'https://myOauthUrl:30081/auth/realms/springboot', clientId: 'activiti', scope: 'openid', secret: '', redirectUri: '/', redirectUriLogout: '/logout' }, authType: 'OAUTH' }, alfrescoJsApi ); oauth2Auth.createIframe(); const iframe = document.getElementById('silent_refresh_token_iframe'); iframe.contentWindow.location.hash = 'invalid'; let logoutCalled = false; oauth2Auth.logOut = () => { logoutCalled = true; return Promise.resolve(); }; // invalid hash location leads to a reject which leads to a logout oauth2Auth.iFrameHashListener(); setTimeout(() => { assert.equal(logoutCalled, true); }, 500); }); afterEach(() => { globalAny.window = undefined; }); }); describe('public urls', () => { let oauth2Auth: Oauth2Auth; beforeEach(() => { oauth2Auth = new Oauth2Auth( { oauth2: { host: 'https://myOauthUrl:30081/auth/realms/springboot', clientId: 'activiti', scope: 'openid', secret: '', redirectUri: '/', redirectUriLogout: '/logout' }, authType: 'OAUTH' }, alfrescoJsApi ); }); it('should return true if PathMatcher.match returns true for matching url', () => { globalAny.window = { location: { href: 'public-url' } }; oauth2Auth.config.oauth2.publicUrls = ['public-url']; oauth2Auth.pathMatcher = { match: () => true }; assert.equal(oauth2Auth.isPublicUrl(), true); }); it('should return false if PathMatcher.match returns false for matching url', () => { globalAny.window = { location: { href: 'some-public-url' } }; oauth2Auth.config.oauth2.publicUrls = ['public-url']; oauth2Auth.pathMatcher = { match: () => false }; assert.equal(oauth2Auth.isPublicUrl(), false); }); it('should return false if publicUrls property is not defined', () => { assert.equal(oauth2Auth.isPublicUrl(), false); }); it('should return false if public urls is not set as an array list', () => { globalAny.window = { location: { href: 'public-url-string' } }; oauth2Auth.config.oauth2.publicUrls = null; assert.equal(oauth2Auth.isPublicUrl(), false); }); it('should not call `implicitLogin`', async () => { globalAny.window = { location: { href: 'public-url' } }; oauth2Auth.config.oauth2.silentLogin = true; oauth2Auth.config.oauth2.publicUrls = ['public-url']; oauth2Auth.pathMatcher = { match: () => true }; let implicitLoginCalled = false; oauth2Auth.implicitLogin = () => { implicitLoginCalled = true; }; await oauth2Auth.checkFragment(); assert.equal(implicitLoginCalled, false); }); }); }); });