From c56874fe002e149cfb5f75d4d6ed1378ac811237 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Thu, 9 Dec 2021 17:13:47 +0500 Subject: [PATCH] feat: login for web with server type SASJS --- src/SASjs.ts | 30 +++++++++--- src/SASjsApiClient.ts | 15 ++++++ src/auth/AuthManager.ts | 75 +++++++++++++++++++++++++---- src/auth/getAuthCodeForSasjs.ts | 31 ++++++++++++ src/job-execution/WebJobExecutor.ts | 1 - src/request/RequestClient.ts | 11 +++++ src/request/Sas9RequestClient.ts | 2 +- src/request/SasjsRequestClient.ts | 23 +++++++++ 8 files changed, 168 insertions(+), 20 deletions(-) create mode 100644 src/auth/getAuthCodeForSasjs.ts create mode 100644 src/request/SasjsRequestClient.ts diff --git a/src/SASjs.ts b/src/SASjs.ts index 0027f5f..3103457 100644 --- a/src/SASjs.ts +++ b/src/SASjs.ts @@ -21,6 +21,7 @@ import { SasAuthResponse } from '@sasjs/utils/types' import { RequestClient } from './request/RequestClient' +import { SasjsRequestClient } from './request/SasjsRequestClient' import { JobExecutor, WebJobExecutor, @@ -547,29 +548,39 @@ export default class SASjs { /** * Checks whether a session is active, or login is required. - * @param accessToken - an optional access token is required for SASjs server type. * @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`. */ - public async checkSession(accessToken?: string) { - return this.authManager!.checkSession(accessToken) + public async checkSession() { + return this.authManager!.checkSession() } /** * Logs into the SAS server with the supplied credentials. * @param username - a string representing the username. * @param password - a string representing the password. + * @param clientId - a string representing the client ID. */ public async logIn( username?: string, password?: string, + clientId?: string, options: LoginOptions = {} ): Promise { if (this.sasjsConfig.loginMechanism === LoginMechanism.Default) { - if (!username || !password) { + if (!username || !password) throw new Error( 'A username and password are required when using the default login mechanism.' ) + + if (this.sasjsConfig.serverType === ServerType.Sasjs) { + if (!clientId) + throw new Error( + 'A username, password and clientId are required when using the default login mechanism with server type SASJS.' + ) + + return this.authManager!.logInSasjs(username, password, clientId) } + return this.authManager!.logIn(username, password) } @@ -584,10 +595,9 @@ export default class SASjs { /** * Logs out of the configured SAS server. - * @param accessToken - an optional access token is required for SASjs server type. */ - public logOut(accessToken?: string) { - return this.authManager!.logOut(accessToken) + public logOut() { + return this.authManager!.logOut() } /** @@ -983,7 +993,11 @@ export default class SASjs { } if (!this.requestClient) { - this.requestClient = new RequestClient( + const RequestClientClass = + this.sasjsConfig.serverType === ServerType.Sasjs + ? SasjsRequestClient + : RequestClient + this.requestClient = new RequestClientClass( this.sasjsConfig.serverUrl, this.sasjsConfig.httpsAgentOptions ) diff --git a/src/SASjsApiClient.ts b/src/SASjsApiClient.ts index 62980c8..73ed694 100644 --- a/src/SASjsApiClient.ts +++ b/src/SASjsApiClient.ts @@ -2,6 +2,7 @@ import { FolderMember, ServiceMember, ExecutionQuery } from './types' import { RequestClient } from './request/RequestClient' import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs' import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs' +import { getAuthCodeForSasjs } from './auth/getAuthCodeForSasjs' export class SASjsApiClient { constructor( @@ -58,6 +59,20 @@ export class SASjsApiClient { public async refreshTokens(refreshToken: string): Promise { return refreshTokensForSasjs(this.requestClient, refreshToken) } + + /** + * Performs a login authenticate and returns an auth code for the given client. + * @param username - a string representing the username. + * @param password - a string representing the password. + * @param clientId - the client ID to authenticate with. + */ + public async getAuthCode( + username: string, + password: string, + clientId: string + ) { + return getAuthCodeForSasjs(this.requestClient, username, password, clientId) + } } // todo move to sasjs/utils diff --git a/src/auth/AuthManager.ts b/src/auth/AuthManager.ts index a0bddcc..5d69c94 100644 --- a/src/auth/AuthManager.ts +++ b/src/auth/AuthManager.ts @@ -2,6 +2,8 @@ import { ServerType } from '@sasjs/utils/types' import { RequestClient } from '../request/RequestClient' import { LoginOptions, LoginResult } from '../types/Login' import { serialize } from '../utils' +import { getAccessTokenForSasjs } from './getAccessTokenForSasjs' +import { getAuthCodeForSasjs } from './getAuthCodeForSasjs' import { openWebPage } from './openWebPage' import { verifySas9Login } from './verifySas9Login' import { verifySasViyaLogin } from './verifySasViyaLogin' @@ -81,6 +83,39 @@ export class AuthManager { return { isLoggedIn: false, userName: '' } } + /** + * Logs into the SAS server with the supplied credentials. + * @param userName - a string representing the username. + * @param password - a string representing the password. + * @param clientId - a string representing the client ID. + * @returns - a boolean `isLoggedin` and a string `username` + */ + public async logInSasjs( + username: string, + password: string, + clientId: string + ): Promise { + const isLoggedIn = await this.sendLoginRequestSasjs( + username, + password, + clientId + ) + .then((res) => { + this.userName = username + this.requestClient.saveLocalStorageToken( + res.access_token, + res.refresh_token + ) + return true + }) + .catch(() => false) + + return { + isLoggedIn, + userName: this.userName + } + } + /** * Logs into the SAS server with the supplied credentials. * @param username - a string representing the username. @@ -180,28 +215,41 @@ export class AuthManager { return loginResponse } + private async sendLoginRequestSasjs( + username: string, + password: string, + clientId: string + ) { + const authCode = await getAuthCodeForSasjs( + this.requestClient, + username, + password, + clientId + ) + return getAccessTokenForSasjs(this.requestClient, clientId, authCode) + } /** * Checks whether a session is active, or login is required. - * @param accessToken - an optional access token is required for SASjs server type. * @returns - a promise which resolves with an object containing three values * - a boolean `isLoggedIn` * - a string `userName` and * - a form `loginForm` if not loggedin. */ - public async checkSession(accessToken?: string): Promise<{ + public async checkSession(): Promise<{ isLoggedIn: boolean userName: string loginForm?: any }> { - const { isLoggedIn, userName } = await this.fetchUserName(accessToken) + const { isLoggedIn, userName } = await this.fetchUserName() let loginForm = null - if (!isLoggedIn && this.serverType !== ServerType.Sasjs) { + if (!isLoggedIn) { //We will logout to make sure cookies are removed and login form is presented //Residue can happen in case of session expiration await this.logOut() - loginForm = await this.getNewLoginForm() + if (this.serverType !== ServerType.Sasjs) + loginForm = await this.getNewLoginForm() } return Promise.resolve({ @@ -221,7 +269,7 @@ export class AuthManager { return await this.getLoginForm(formResponse) } - private async fetchUserName(accessToken?: string): Promise<{ + private async fetchUserName(): Promise<{ isLoggedIn: boolean userName: string }> { @@ -232,9 +280,8 @@ export class AuthManager { ? `${this.serverUrl}/SASStoredProcess` : `${this.serverUrl}/SASjsApi/session` - // Access token is required for server type `SASjs` const { result: loginResponse } = await this.requestClient - .get(url, accessToken, 'text/plain') + .get(url, undefined, 'text/plain') .catch((err: any) => { return { result: 'authErr' } }) @@ -315,11 +362,19 @@ export class AuthManager { * Logs out of the configured SAS server. * @param accessToken - an optional access token is required for SASjs server type. */ - public logOut(accessToken?: string) { + public async logOut() { if (this.serverType === ServerType.Sasjs) { - return this.requestClient.post(this.logoutUrl, undefined, accessToken) + return this.requestClient + .delete(this.logoutUrl) + .catch(() => true) + .finally(() => { + this.requestClient.clearLocalStorageTokens() + return true + }) } + this.requestClient.clearCsrfTokens() + return this.requestClient.get(this.logoutUrl, undefined).then(() => true) } } diff --git a/src/auth/getAuthCodeForSasjs.ts b/src/auth/getAuthCodeForSasjs.ts new file mode 100644 index 0000000..aa6d2b2 --- /dev/null +++ b/src/auth/getAuthCodeForSasjs.ts @@ -0,0 +1,31 @@ +import { prefixMessage } from '@sasjs/utils/error' +import { RequestClient } from '../request/RequestClient' + +/** + * Performs a login authenticate and returns an auth code for the given client. + * @param requestClient - the pre-configured HTTP request client + * @param username - a string representing the username. + * @param password - a string representing the password. + * @param clientId - the client ID to authenticate with. + */ +export const getAuthCodeForSasjs = async ( + requestClient: RequestClient, + username: string, + password: string, + clientId: string +) => { + const url = '/SASjsApi/auth/authorize' + const data = { username, password, clientId } + + const { code: authCode } = await requestClient + .post<{ code: string }>(url, data, undefined) + .then((res) => res.result) + .catch((err) => { + throw prefixMessage( + err, + 'Error while authenticating with provided username, password and clientId. ' + ) + }) + + return authCode +} diff --git a/src/job-execution/WebJobExecutor.ts b/src/job-execution/WebJobExecutor.ts index 9f91ab3..de6a23b 100644 --- a/src/job-execution/WebJobExecutor.ts +++ b/src/job-execution/WebJobExecutor.ts @@ -144,7 +144,6 @@ export class WebJobExecutor extends BaseJobExecutor { } const requestPromise = new Promise((resolve, reject) => { - // Access token is required for server type `SASjs` this.requestClient!.post(apiUrl, formData, authConfig?.access_token) .then(async (res: any) => { const resObj = diff --git a/src/request/RequestClient.ts b/src/request/RequestClient.ts index f7caedb..4084525 100644 --- a/src/request/RequestClient.ts +++ b/src/request/RequestClient.ts @@ -48,7 +48,9 @@ export interface HttpClient { ): Promise<{ result: T; etag: string }> getCsrfToken(type: 'general' | 'file'): CsrfToken | undefined + saveLocalStorageToken(accessToken: string, refreshToken: string): void clearCsrfTokens(): void + clearLocalStorageTokens(): void getBaseUrl(): string } @@ -70,6 +72,11 @@ export class RequestClient implements HttpClient { this.createHttpClient(baseUrl, httpsAgentOptions) } + public saveLocalStorageToken(accessToken: string, refreshToken: string) { + localStorage.setItem('accessToken', accessToken) + localStorage.setItem('refreshToken', refreshToken) + } + public getCsrfToken(type: 'general' | 'file' = 'general') { return type === 'file' ? this.fileUploadCsrfToken : this.csrfToken } @@ -78,6 +85,10 @@ export class RequestClient implements HttpClient { this.csrfToken = { headerName: '', value: '' } this.fileUploadCsrfToken = { headerName: '', value: '' } } + public clearLocalStorageTokens() { + localStorage.setItem('accessToken', '') + localStorage.setItem('refreshToken', '') + } public getBaseUrl() { return this.httpClient.defaults.baseURL || '' diff --git a/src/request/Sas9RequestClient.ts b/src/request/Sas9RequestClient.ts index f5575c5..80a6d9e 100644 --- a/src/request/Sas9RequestClient.ts +++ b/src/request/Sas9RequestClient.ts @@ -87,7 +87,7 @@ export class Sas9RequestClient extends RequestClient { }) } - public post( + public async post( url: string, data: any, accessToken: string | undefined, diff --git a/src/request/SasjsRequestClient.ts b/src/request/SasjsRequestClient.ts new file mode 100644 index 0000000..8bbbb59 --- /dev/null +++ b/src/request/SasjsRequestClient.ts @@ -0,0 +1,23 @@ +import { RequestClient } from './RequestClient' + +/** + * Specific request client for SASJS. + * Append tokens in headers. + */ +export class SasjsRequestClient extends RequestClient { + getHeaders = (accessToken: string | undefined, contentType: string) => { + const headers: any = {} + + if (contentType !== 'application/x-www-form-urlencoded') + headers['Content-Type'] = contentType + + headers.Accept = contentType === 'application/json' ? contentType : '*/*' + + if (!accessToken) + accessToken = localStorage.getItem('accessToken') ?? undefined + + if (accessToken) headers.Authorization = `Bearer ${accessToken}` + + return headers + } +}