[PRODENG-211] integrate JS-API with monorepo (part 1) (#9081)

* integrate JS-API with monorepo

* [ci:force] fix token issue

[ci:force] migrate docs folder

[ci:force] clean personal tokens

* [ci:force] gha workflow support

* [ci:force] npm publish target

* fix js-api test linting

* [ci:force] fix test linting, mocks, https scheme

* [ci:force] fix https scheme

* [ci:force] typescript mappings

* [ci:force] update scripts

* lint fixes

* linting fixes

* fix linting

* [ci:force] linting fixes

* linting fixes

* [ci:force] remove js-api upstream and corresponding scripts

* [ci:force] jsdoc fixes

* fix jsdoc linting

* [ci:force] jsdoc fixes

* [ci:force] jsdoc fixes

* jsdoc fixes

* jsdoc fixes

* jsdoc fixes

* [ci:force] fix jsdoc

* [ci:force] reduce code duplication

* replace 'chai' expect with node.js assert

* replace 'chai' expect with node.js assert

* [ci:force] remove chai and chai-spies for js-api testing

* [ci:force] cleanup and fix imports

* [ci:force] fix linting

* [ci:force] fix unit test

* [ci:force] fix sonar linting findings

* [ci:force] switch activiti api models to interfaces (-2.5% reduction of bundle)

* [ci:force] switch activiti api models to interfaces

* [ci:force] switch AGS api models to interfaces

* [ci:force] switch AGS api models to interfaces

* [ci:force] switch search api models to interfaces

* [ci:force] switch content api models to interfaces where applicable
This commit is contained in:
Denys Vuika
2023-11-21 10:27:51 +00:00
committed by GitHub
parent 804fa2ffd4
commit ea2c0ce229
1334 changed files with 82605 additions and 1068 deletions

View File

@@ -0,0 +1,26 @@
/*!
* @license
* Copyright © 2005-2023 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 { BasicAuth } from './basicAuth';
import { Oauth2 } from './oauth2';
export interface Authentication {
basicAuth?: BasicAuth;
oauth2?: Oauth2;
cookie?: string;
type?: string;
}

View File

@@ -0,0 +1,22 @@
/*!
* @license
* Copyright © 2005-2023 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.
*/
export interface BasicAuth {
username?: string;
password?: string;
ticket?: string;
}

View File

@@ -0,0 +1,215 @@
/*!
* @license
* Copyright © 2005-2023 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 ee from 'event-emitter';
import { AuthenticationApi, TicketBody } from '../api/auth-rest-api';
import { AlfrescoApiClient } from '../alfrescoApiClient';
import { AlfrescoApiConfig } from '../alfrescoApiConfig';
import { Authentication } from './authentication';
import { Storage } from '../storage';
import { AlfrescoApiType } from '../to-deprecate/alfresco-api-type';
import { HttpClient } from '../api-clients/http-client.interface';
export class ContentAuth extends AlfrescoApiClient {
ticketStorageLabel: string;
ticket: string;
authApi: AuthenticationApi;
constructor(config: AlfrescoApiConfig, alfrescoApi: AlfrescoApiType, httpClient?: HttpClient) {
super(undefined, httpClient);
this.className = 'ContentAuth';
this.storage = Storage.getInstance();
this.storage.setDomainPrefix(config.domainPrefix);
this.setConfig(config);
this.authApi = new AuthenticationApi(alfrescoApi);
}
setConfig(config: AlfrescoApiConfig) {
this.config = config;
this.basePath = this.config.hostEcm + '/' + this.config.contextRoot + '/api/-default-/public/authentication/versions/1'; //Auth Call
this.ticketStorageLabel = 'ticket-ECM';
if (this.config.ticketEcm) {
this.setTicket(config.ticketEcm);
} else if (this.storage.getItem(this.ticketStorageLabel)) {
this.setTicket(this.storage.getItem(this.ticketStorageLabel));
}
}
changeHost() {
this.basePath = this.config.hostEcm + '/' + this.config.contextRoot + '/api/-default-/public/authentication/versions/1'; //Auth Call
this.ticket = undefined;
}
saveUsername(username: string) {
if (this.storage.supportsStorage()) {
this.storage.setItem('ACS_USERNAME', username);
}
}
/**
* login Alfresco API
*
* @param username Username to login
* @param password Password to login
* @returns A promise that returns {new authentication ticket} if resolved and {error} if rejected.
*/
login(username: string, password: string): Promise<string> {
this.authentications.basicAuth.username = username;
this.authentications.basicAuth.password = password;
const loginRequest = new TicketBody({
userId: this.authentications.basicAuth.username,
password: this.authentications.basicAuth.password
});
const promise: any = new Promise<string>((resolve, reject) => {
this.authApi
.createTicket(loginRequest)
.then((data) => {
this.saveUsername(username);
this.setTicket(data.entry.id);
promise.emit('success');
this.emit('logged-in');
resolve(data.entry.id);
})
.catch((error) => {
this.saveUsername('');
if (error.status === 401) {
promise.emit('unauthorized');
} else if (error.status === 403) {
promise.emit('forbidden');
} else {
promise.emit('error');
}
reject(error);
});
});
ee(promise); // jshint ignore:line
return promise;
}
/**
* validate the ticket present in this.config.ticket against the server
*
* @returns A promise that returns if resolved and {error} if rejected.
*/
validateTicket(): Promise<string> {
this.setTicket(this.config.ticketEcm);
const promise: any = new Promise<string>((resolve, reject) => {
this.authApi.validateTicket().then(
(data) => {
this.setTicket(data.entry.id);
promise.emit('success');
this.emit('logged-in');
resolve(data.entry.id);
},
(error) => {
if (error.status === 401) {
promise.emit('unauthorized');
}
promise.emit('error');
reject(error);
}
);
});
ee(promise); // jshint ignore:line
return promise;
}
/**
* logout Alfresco API
*
* @returns A promise that returns { authentication ticket} if resolved and {error} if rejected.
*/
logout(): Promise<void> {
this.saveUsername('');
const promise: any = new Promise<void>((resolve, reject) => {
this.authApi.deleteTicket().then(
() => {
promise.emit('logout');
this.invalidateSession();
resolve();
},
(error) => {
if (error.status === 401) {
promise.emit('unauthorized');
}
promise.emit('error');
reject(error);
}
);
});
ee(promise); // jshint ignore:line
return promise;
}
/**
* Set the current Ticket
*/
setTicket(ticket: string) {
this.authentications.basicAuth.username = 'ROLE_TICKET';
this.authentications.basicAuth.password = ticket;
this.config.ticketEcm = ticket;
this.storage.setItem(this.ticketStorageLabel, ticket);
this.ticket = ticket;
}
/**
* Get the current Ticket
*
* @returns ticket value
*/
getTicket(): string {
return this.ticket;
}
invalidateSession() {
this.storage.removeItem(this.ticketStorageLabel);
this.authentications.basicAuth.username = null;
this.authentications.basicAuth.password = null;
this.config.ticketEcm = null;
this.ticket = null;
}
/**
* If the client is logged in return true
*
* @returns `true` if client is logged in, otherwise `false`
*/
isLoggedIn(): boolean {
return !!this.ticket;
}
/**
* return the Authentication
*
* @returns authentication object
*/
getAuthentication(): Authentication {
return this.authentications;
}
}

View File

@@ -0,0 +1,21 @@
/*!
* @license
* Copyright © 2005-2023 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.
*/
export interface Oauth2 {
refreshToken?: string;
accessToken?: string;
}

View File

@@ -0,0 +1,778 @@
/*!
* @license
* Copyright © 2005-2023 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 ee from 'event-emitter';
import { AlfrescoApiClient } from '../alfrescoApiClient';
import { AlfrescoApiConfig } from '../alfrescoApiConfig';
import { Authentication } from './authentication';
import { AuthenticationApi } from '../api/auth-rest-api';
import { AlfrescoApi } from '../alfrescoApi';
import { Storage } from '../storage';
import { HttpClient } from '../api-clients/http-client.interface';
import { PathMatcher } from '../utils/path-matcher';
declare const Buffer: any;
declare let window: Window;
interface Userinfo {
name: string;
given_name?: string;
family_name?: string;
preferred_username?: string;
email?: string;
picture?: string;
}
export class Oauth2Auth extends AlfrescoApiClient {
static readonly DEFAULT_AUTHORIZATION_URL = '/protocol/openid-connect/auth';
static readonly DEFAULT_TOKEN_URL = '/protocol/openid-connect/token';
static readonly DEFAULT_USER_INFO_ENDPOINT = '/protocol/openid-connect/userinfo';
static readonly DEFAULT_LOGOUT_URL = '/protocol/openid-connect/logout';
private refreshTokenIntervalPolling: any;
private refreshTokenTimeoutIframe: any;
private checkAccessToken = true;
hashFragmentParams: any;
token: string;
discovery: {
loginUrl?: string;
logoutUrl?: string;
tokenEndpoint?: string;
userinfoEndpoint?: string;
} = {
loginUrl: undefined,
logoutUrl: undefined,
tokenEndpoint: undefined,
userinfoEndpoint: undefined
};
authentications: Authentication = {
oauth2: { accessToken: '' },
type: 'oauth2',
basicAuth: {}
};
iFrameHashListener: any;
pathMatcher = new PathMatcher();
constructor(config: AlfrescoApiConfig, alfrescoApi: AlfrescoApi, httpClient?: HttpClient) {
super(undefined, httpClient);
this.storage = Storage.getInstance();
this.className = 'Oauth2Auth';
if (config) {
this.setConfig(config, alfrescoApi);
}
}
setConfig(config: AlfrescoApiConfig, alfrescoApi: AlfrescoApi) {
this.config = config;
this.storage.setDomainPrefix(config.domainPrefix);
if (this.config.oauth2) {
if (this.config.oauth2.host === undefined || this.config.oauth2.host === null) {
throw new Error('Missing the required oauth2 host parameter');
}
if (this.config.oauth2.clientId === undefined || this.config.oauth2.clientId === null) {
throw new Error('Missing the required oauth2 clientId parameter');
}
if (this.config.oauth2.scope === undefined || this.config.oauth2.scope === null) {
throw new Error('Missing the required oauth2 scope parameter');
}
if (this.config.oauth2.secret === undefined || this.config.oauth2.secret === null) {
this.config.oauth2.secret = '';
}
if ((this.config.oauth2.redirectUri === undefined || this.config.oauth2.redirectUri === null) && this.config.oauth2.implicitFlow) {
throw new Error('Missing redirectUri required parameter');
}
if (!this.config.oauth2.refreshTokenTimeout) {
this.config.oauth2.refreshTokenTimeout = 300000;
}
if (!this.config.oauth2.redirectSilentIframeUri) {
let context = '';
if (typeof window !== 'undefined') {
context = window.location.origin;
}
this.config.oauth2.redirectSilentIframeUri = context + '/assets/silent-refresh.html';
}
this.basePath = this.config.oauth2.host; //Auth Call
this.host = this.config.oauth2.host;
this.discoveryUrls();
if (this.hasContentProvider()) {
this.exchangeTicketListener(alfrescoApi);
}
this.initOauth();
}
}
initOauth() {
if (!this.config.oauth2.implicitFlow && this.isValidAccessToken()) {
const accessToken = this.storage.getItem('access_token');
this.setToken(accessToken, null);
}
if (this.config.oauth2.implicitFlow) {
this.checkFragment('nonce');
}
}
discoveryUrls() {
this.discovery.loginUrl = this.config.oauth2.authorizationUrl || this.host + Oauth2Auth.DEFAULT_AUTHORIZATION_URL;
this.discovery.logoutUrl = this.config.oauth2.logoutUrl || this.host + Oauth2Auth.DEFAULT_LOGOUT_URL;
this.discovery.tokenEndpoint = this.config.oauth2.tokenUrl || this.host + Oauth2Auth.DEFAULT_TOKEN_URL;
this.discovery.userinfoEndpoint = this.config.oauth2.userinfoEndpoint || this.host + Oauth2Auth.DEFAULT_USER_INFO_ENDPOINT;
}
getProfile(): Promise<Userinfo> {
const postBody = {};
const pathParams = {};
const queryParams = {};
const headerParams = {
'Content-Type': 'application/x-www-form-urlencoded'
};
const formParams = {};
const contentTypes = ['application/x-www-form-urlencoded'];
const accepts = ['application/json'];
return this.callCustomApi(
this.discovery.userinfoEndpoint,
'GET',
pathParams,
queryParams,
headerParams,
formParams,
postBody,
contentTypes,
accepts
);
}
hasContentProvider(): boolean {
return this.config.provider === 'ECM' || this.config.provider === 'ALL';
}
checkFragment(nonceKey?: string, externalHash?: any): any {
// jshint ignore:line
this.hashFragmentParams = this.getHashFragmentParams(externalHash);
if (this.hashFragmentParams && this.hashFragmentParams.error === undefined) {
this.useFragmentTimeLogin(nonceKey);
} else {
this.refreshBrowserLogin();
}
}
private refreshBrowserLogin() {
if (this.config.oauth2.silentLogin && !this.isPublicUrl()) {
this.implicitLogin();
}
if (this.isValidToken() && this.isValidAccessToken()) {
const accessToken = this.storage.getItem('access_token');
this.setToken(accessToken, null);
this.silentRefresh();
}
}
private useFragmentTimeLogin(nonceKey: string) {
const accessToken = this.hashFragmentParams.access_token;
const idToken = this.hashFragmentParams.id_token;
const sessionState = this.hashFragmentParams.session_state;
const expiresIn = this.hashFragmentParams.expires_in;
if (!sessionState) {
throw new Error('session state not present');
}
try {
const jwt = this.processJWTToken(idToken, nonceKey);
if (jwt) {
this.storeIdToken(idToken, jwt.payload.exp);
this.storeAccessToken(accessToken, expiresIn);
this.authentications.basicAuth.username = jwt.payload.preferred_username;
this.saveUsername(jwt.payload.preferred_username);
this.silentRefresh();
return accessToken;
}
} catch (error) {
throw new Error('Validation JWT error' + error);
}
}
isPublicUrl(): boolean {
const publicUrls = this.config.oauth2.publicUrls || [];
if (Array.isArray(publicUrls)) {
return publicUrls.length > 0 && publicUrls.some((urlPattern: string) => this.pathMatcher.match(window.location.href, urlPattern));
}
return false;
}
padBase64(base64data: any) {
while (base64data.length % 4 !== 0) {
base64data += '=';
}
return base64data;
}
processJWTToken(jwt: any, nonceKey: string): any {
if (jwt) {
const jwtArray = jwt.split('.');
const headerBase64 = this.padBase64(jwtArray[0]);
const headerJson = this.b64DecodeUnicode(headerBase64);
const header = JSON.parse(headerJson);
const payloadBase64 = this.padBase64(jwtArray[1]);
const payloadJson = this.b64DecodeUnicode(payloadBase64);
const payload = JSON.parse(payloadJson);
const savedNonce = this.storage.getItem(nonceKey);
if (!payload.sub) {
throw new Error('Missing sub in JWT');
}
if (payload.nonce !== savedNonce) {
return;
}
return {
idToken: jwt,
payload,
header
};
}
}
b64DecodeUnicode(b64string: string) {
const base64 = b64string.replace(/-/g, '+').replace(/_/g, '/');
return decodeURIComponent(
atob(base64)
.split('')
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
}
storeIdToken(idToken: string, exp: number) {
this.storage.setItem('id_token', idToken);
this.storage.setItem('id_token_expires_at', Number(exp * 1000).toString());
this.storage.setItem('id_token_stored_at', Date.now().toString());
}
storeAccessToken(accessToken: string, expiresIn: number, refreshToken?: string) {
this.storage.setItem('access_token', accessToken);
const expiresInMilliSeconds = expiresIn * 1000;
const now = new Date();
const expiresAt = now.getTime() + expiresInMilliSeconds;
this.storage.setItem('access_token_expires_in', expiresAt);
this.storage.setItem('access_token_stored_at', Date.now().toString());
this.setToken(accessToken, refreshToken);
}
saveUsername(username: string) {
if (this.storage.supportsStorage()) {
this.storage.setItem('USERNAME', username);
}
}
implicitLogin() {
if (!this.isValidToken() || !this.isValidAccessToken()) {
this.redirectLogin();
}
}
isValidToken(): boolean {
let validToken = false;
if (this.getIdToken()) {
const expiresAt = this.storage.getItem('id_token_expires_at');
const now = new Date();
if (expiresAt && parseInt(expiresAt, 10) >= now.getTime()) {
validToken = true;
}
}
return validToken;
}
isValidAccessToken(): boolean {
let validAccessToken = false;
if (this.getAccessToken()) {
const expiresAt = this.storage.getItem('access_token_expires_in');
const now = new Date();
if (expiresAt && parseInt(expiresAt, 10) >= now.getTime()) {
validAccessToken = true;
}
}
return validAccessToken;
}
getIdToken(): string {
return this.storage.getItem('id_token');
}
getAccessToken(): string {
return this.storage.getItem('access_token');
}
redirectLogin(): void {
if (this.config.oauth2.implicitFlow && typeof window !== 'undefined') {
const href = this.composeImplicitLoginUrl();
window.location.href = href;
this.emit('implicit_redirect', href);
}
}
isRedirectionUrl() {
return window.location.hash && window.location.hash.split('&')[0].indexOf('session_state') === -1;
}
genNonce(): string {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 40; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
composeImplicitLoginUrl(): string {
const nonce = this.genNonce();
this.storage.setItem('nonce', nonce);
const afterLoginUriSegment = this.isRedirectionUrl() ? window.location.hash : '';
if (afterLoginUriSegment && afterLoginUriSegment !== '/') {
this.storage.setItem('loginFragment', afterLoginUriSegment.replace('#', '').trim());
}
const separation = this.discovery.loginUrl.indexOf('?') > -1 ? '&' : '?';
return (
this.discovery.loginUrl +
separation +
'client_id=' +
encodeURIComponent(this.config.oauth2.clientId) +
'&redirect_uri=' +
encodeURIComponent(this.config.oauth2.redirectUri) +
'&scope=' +
encodeURIComponent(this.config.oauth2.scope) +
'&response_type=' +
encodeURIComponent('id_token token') +
'&nonce=' +
encodeURIComponent(nonce)
);
}
composeIframeLoginUrl(): string {
const nonce = this.genNonce();
this.storage.setItem('refresh_nonce', nonce);
const separation = this.discovery.loginUrl.indexOf('?') > -1 ? '&' : '?';
return (
this.discovery.loginUrl +
separation +
'client_id=' +
encodeURIComponent(this.config.oauth2.clientId) +
'&redirect_uri=' +
encodeURIComponent(this.config.oauth2.redirectSilentIframeUri) +
'&scope=' +
encodeURIComponent(this.config.oauth2.scope) +
'&response_type=' +
encodeURIComponent('id_token token') +
'&nonce=' +
encodeURIComponent(nonce) +
'&prompt=none'
);
}
hasHashCharacter(hash: string): boolean {
return hash.indexOf('#') === 0;
}
startWithHashRoute(hash: string) {
return hash.startsWith('#/');
}
getHashFragmentParams(externalHash: string): string {
let hashFragmentParams = null;
if (typeof window !== 'undefined') {
let hash: string;
if (!externalHash) {
hash = decodeURIComponent(window.location.hash);
if (!this.startWithHashRoute(hash)) {
window.location.hash = '';
}
} else {
hash = decodeURIComponent(externalHash);
this.removeHashFromSilentIframe();
this.destroyIframe();
}
if (this.hasHashCharacter(hash) && !this.startWithHashRoute(hash)) {
const questionMarkPosition = hash.indexOf('?');
if (questionMarkPosition > -1) {
hash = hash.substring(questionMarkPosition + 1);
} else {
hash = hash.substring(1);
}
hashFragmentParams = this.parseQueryString(hash);
}
}
return hashFragmentParams;
}
parseQueryString(queryString: string): any {
const data: { [key: string]: any } = {};
let pairs: string[];
let pair: string;
let separatorIndex: number;
let escapedKey: string;
let escapedValue: string;
let key: string;
let value: string;
if (queryString !== null) {
pairs = queryString.split('&');
for (const item of pairs) {
pair = item;
separatorIndex = pair.indexOf('=');
if (separatorIndex === -1) {
escapedKey = pair;
escapedValue = null;
} else {
escapedKey = pair.substring(0, separatorIndex);
escapedValue = pair.substring(separatorIndex + 1);
}
key = decodeURIComponent(escapedKey);
value = decodeURIComponent(escapedValue);
if (key.startsWith('/')) {
key = key.substring(1);
}
data[key] = value;
}
}
return data;
}
silentRefresh(): void {
if (typeof document === 'undefined') {
this.pollingRefreshToken();
return;
}
if (this.checkAccessToken) {
this.destroyIframe();
this.createIframe();
this.checkAccessToken = false;
return;
}
this.refreshTokenTimeoutIframe = setTimeout(() => {
this.destroyIframe();
this.createIframe();
}, this.config.oauth2.refreshTokenTimeout);
}
removeHashFromSilentIframe() {
const iframe = document.getElementById('silent_refresh_token_iframe') as HTMLIFrameElement;
if (iframe?.contentWindow.location.hash) {
iframe.contentWindow.location.hash = '';
}
}
createIframe() {
const iframe = document.createElement('iframe');
iframe.id = 'silent_refresh_token_iframe';
const loginUrl = this.composeIframeLoginUrl();
iframe.setAttribute('src', loginUrl);
iframe.style.display = 'none';
document.body.appendChild(iframe);
this.iFrameHashListener = () => {
const silentRefreshTokenIframe: any = document.getElementById('silent_refresh_token_iframe');
const hash = silentRefreshTokenIframe.contentWindow.location.hash;
try {
this.checkFragment('refresh_nonce', hash);
} catch {
this.logOut();
}
};
iframe.addEventListener('load', this.iFrameHashListener);
}
destroyIframe() {
const iframe = document.getElementById('silent_refresh_token_iframe');
if (iframe) {
iframe.removeEventListener('load', this.iFrameHashListener);
document.body.removeChild(iframe);
}
}
/**
* login Alfresco API
*
* @returns A promise that returns {new authentication token} if resolved and {error} if rejected.
*/
login(username: string, password: string): Promise<any> {
return new Promise((resolve, reject) => {
this.grantPasswordLogin(username, password, resolve, reject);
});
}
grantPasswordLogin(username: string, password: string, resolve: any, reject: any) {
this.invalidateSession();
const headerParams = {
'Content-Type': 'application/x-www-form-urlencoded'
};
const formParams = {
username,
password,
grant_type: 'password',
client_id: this.config.oauth2.clientId,
client_secret: this.config.oauth2.secret
};
const contentTypes = ['application/x-www-form-urlencoded'];
const accepts = ['application/json'];
const promise = this.callCustomApi(this.discovery.tokenEndpoint, 'POST', {}, {}, headerParams, formParams, {}, contentTypes, accepts).then(
(data: any) => {
this.saveUsername(username);
this.silentRefresh();
this.storeAccessToken(data.access_token, data.expires_in, data.refresh_token);
resolve(data);
},
(error) => {
if ((error.error && error.error.status === 401) || error.status === 401) {
this.emit('unauthorized');
}
this.emit('error');
reject(error.error);
}
);
ee(promise); // jshint ignore:line
}
pollingRefreshToken() {
this.refreshTokenIntervalPolling = setInterval(async () => {
try {
await this.refreshToken();
} catch {
/* continue regardless of error */
}
}, this.config.oauth2.refreshTokenTimeout);
this.refreshTokenIntervalPolling.unref();
}
/**
* Refresh the Token
*
* @returns promise of void
*/
refreshToken(): Promise<any> {
const auth = 'Basic ' + this.universalBtoa(this.config.oauth2.clientId + ':' + this.config.oauth2.secret);
const headerParams = {
'Content-Type': 'application/x-www-form-urlencoded',
'Cache-Control': 'no-cache',
Authorization: auth
};
const formParams = {
grant_type: 'refresh_token',
refresh_token: this.authentications.oauth2.refreshToken
};
const contentTypes = ['application/x-www-form-urlencoded'];
const accepts = ['application/json'];
const promise = new Promise((resolve, reject) => {
this.callCustomApi(this.discovery.tokenEndpoint, 'POST', {}, {}, headerParams, formParams, {}, contentTypes, accepts).then(
(data: any) => {
this.setToken(data.access_token, data.refresh_token);
resolve(data);
},
(error) => {
if (error.error && error.error.status === 401) {
this.emit('unauthorized');
}
this.emit('error');
reject(error.error);
}
);
});
ee(promise); // jshint ignore:line
return promise;
}
universalBtoa(stringToConvert: string) {
try {
return btoa(stringToConvert);
} catch (err) {
return Buffer.from(stringToConvert).toString('base64');
}
}
/**
* Set the current Token
*/
setToken(token: string, refreshToken: string) {
this.authentications.oauth2.accessToken = token;
this.authentications.oauth2.refreshToken = refreshToken;
this.authentications.basicAuth.password = null;
this.token = token;
if (token) {
this.emit('token_issued');
this.emit('logged-in');
}
}
/**
* Get the current Token
*
* @returns token value
*/
getToken(): string {
return this.token;
}
/**
* return the Authentication
*
* @returns authentications
*/
getAuthentication(): Authentication {
return this.authentications;
}
/**
* Change the Host
*/
changeHost(host: string) {
this.config.hostOauth2 = host;
}
/**
* If the client is logged in return true
*
* @returns is logged in
*/
isLoggedIn(): boolean {
return !!this.authentications.oauth2.accessToken;
}
/**
* Logout
*/
async logOut() {
this.checkAccessToken = true;
const id_token = this.getIdToken();
this.invalidateSession();
this.setToken(null, null);
const separation = this.discovery.logoutUrl.indexOf('?') > -1 ? '&' : '?';
const redirectLogout = this.config.oauth2.redirectUriLogout || this.config.oauth2.redirectUri;
const logoutUrl =
this.discovery.logoutUrl +
separation +
'post_logout_redirect_uri=' +
encodeURIComponent(redirectLogout) +
'&id_token_hint=' +
encodeURIComponent(id_token);
if (id_token != null && this.config.oauth2.implicitFlow && typeof window !== 'undefined') {
window.location.href = logoutUrl;
}
}
invalidateSession() {
clearTimeout(this.refreshTokenTimeoutIframe);
clearInterval(this.refreshTokenIntervalPolling);
this.storage.removeItem('access_token');
this.storage.removeItem('access_token_expires_in');
this.storage.removeItem('access_token_stored_at');
this.storage.removeItem('id_token');
this.storage.removeItem('id_token');
this.storage.removeItem('id_token_claims_obj');
this.storage.removeItem('id_token_expires_at');
this.storage.removeItem('id_token_stored_at');
this.storage.removeItem('nonce');
}
exchangeTicketListener(alfrescoApi: AlfrescoApi) {
this.on('token_issued', async () => {
const authContentApi: AuthenticationApi = new AuthenticationApi(alfrescoApi);
// Legacy stuff, needs deprecation, can not be more specific here because of circular dependency
(authContentApi.apiClient as unknown as any).authentications = this.authentications;
try {
const ticketEntry = await authContentApi.getTicket();
this.config.ticketEcm = ticketEntry.entry.id;
this.emit('ticket_exchanged');
} catch (e) {
// continue regardless of error
}
});
}
}

View File

@@ -0,0 +1,34 @@
/*!
* @license
* Copyright © 2005-2023 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.
*/
export interface Oauth2Config {
clientId: string;
secret?: string;
host: string;
authorizationUrl?: string;
tokenUrl?: string;
userinfoEndpoint?: string;
logoutUrl?: string;
scope: string;
implicitFlow?: boolean;
redirectUri: string;
refreshTokenTimeout?: number;
silentLogin?: boolean;
redirectUriLogout?: string;
redirectSilentIframeUri?: string;
publicUrls?: string[];
}

View File

@@ -0,0 +1,211 @@
/*!
* @license
* Copyright © 2005-2023 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 ee from 'event-emitter';
import { AlfrescoApiClient, AlfrescoApiClientPromise } from '../alfrescoApiClient';
import { AlfrescoApiConfig } from '../alfrescoApiConfig';
import { Authentication } from './authentication';
import { Storage } from '../storage';
import { HttpClient } from '../api-clients/http-client.interface';
import { isBrowser } from '../utils';
export class ProcessAuth extends AlfrescoApiClient {
ticket: string;
authentications: Authentication = {
basicAuth: { ticket: '' },
type: 'activiti'
};
constructor(config: AlfrescoApiConfig, httpClient?: HttpClient) {
super(undefined, httpClient);
this.storage = Storage.getInstance();
this.storage.setDomainPrefix(config.domainPrefix);
this.className = 'ProcessAuth';
if (!isBrowser()) {
this.defaultHeaders = {
'user-agent': 'alfresco-js-api'
};
}
this.setConfig(config);
}
setConfig(config: AlfrescoApiConfig) {
this.config = config;
this.ticket = undefined;
this.basePath = config.hostBpm + '/' + this.config.contextRootBpm; //Activiti Call
if (this.config.ticketBpm) {
this.setTicket(config.ticketBpm);
} else if (this.storage.getItem('ticket-BPM')) {
this.setTicket(this.storage.getItem('ticket-BPM'));
}
}
changeHost() {
this.basePath = this.config.hostBpm + '/' + this.config.contextRootBpm; //Activiti Call
this.ticket = undefined;
}
changeCsrfConfig(disableCsrf: boolean) {
this.config.disableCsrf = disableCsrf;
}
saveUsername(username: string) {
if (this.storage.supportsStorage()) {
this.storage.setItem('APS_USERNAME', username);
}
}
/**
* login Activiti API
*
* @param username Username to login
* @param password Password to login
* @returns A promise that returns {new authentication ticket} if resolved and {error} if rejected.
*/
login(username: string, password: string): AlfrescoApiClientPromise<string> {
this.authentications.basicAuth.username = username;
this.authentications.basicAuth.password = password;
const headerParams = {
'Content-Type': 'application/x-www-form-urlencoded',
'Cache-Control': 'no-cache'
};
const formParams = {
j_username: this.authentications.basicAuth.username,
j_password: this.authentications.basicAuth.password,
_spring_security_remember_me: true,
submit: 'Login'
};
const contentTypes = ['application/x-www-form-urlencoded'];
const accepts = ['application/json'];
const promise: any = new Promise<string>((resolve, reject) => {
this.callApi('/app/authentication', 'POST', {}, {}, headerParams, formParams, {}, contentTypes, accepts).then(
() => {
this.saveUsername(username);
const ticket = this.basicAuth(this.authentications.basicAuth.username, this.authentications.basicAuth.password);
this.setTicket(ticket);
promise.emit('success');
this.emit('logged-in');
resolve(ticket);
},
(error) => {
this.saveUsername('');
if (error.status === 401) {
promise.emit('unauthorized');
} else if (error.status === 403) {
promise.emit('forbidden');
} else {
promise.emit('error');
}
reject(error);
}
);
});
ee(promise); // jshint ignore:line
return promise;
}
/**
* logout Alfresco API
*
* @returns A promise that returns {new authentication ticket} if resolved and {error} if rejected.
*/
logout(): AlfrescoApiClientPromise<void> {
this.saveUsername('');
const contentTypes = ['application/json'];
const accepts = ['application/json'];
const promise: any = new Promise<void>((resolve, reject) => {
this.callApi('/app/logout', 'GET', {}, {}, {}, {}, {}, contentTypes, accepts).then(
() => {
this.invalidateSession();
promise.emit('logout');
resolve();
},
(error) => {
if (error.status === 401) {
promise.emit('unauthorized');
}
promise.emit('error');
reject(error);
}
);
});
ee(promise);
return promise;
}
/**
* Set the current Ticket
*
* @param ticket Ticket value
*/
setTicket(ticket: string) {
this.authentications.basicAuth.ticket = ticket;
this.authentications.basicAuth.password = null;
this.config.ticketBpm = ticket;
this.storage.setItem('ticket-BPM', ticket);
this.ticket = ticket;
}
invalidateSession() {
this.storage.removeItem('ticket-BPM');
this.authentications.basicAuth.ticket = null;
this.authentications.basicAuth.password = null;
this.authentications.basicAuth.username = null;
this.config.ticketBpm = null;
this.ticket = null;
}
/**
* Get the current Ticket
*
* @returns ticket
*/
getTicket(): string {
return this.ticket;
}
/**
* If the client is logged in return true
*
* @returns `true` if logged in, otherwise `false`
*/
isLoggedIn(): boolean {
return !!this.ticket;
}
/**
* return the Authentication
*
* @returns authentication object
*/
getAuthentication(): Authentication {
return this.authentications;
}
}