mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-10 05:40:06 +00:00
chore(git): Merge branch 'master' into auto-tests
This commit is contained in:
@@ -1,11 +1,18 @@
|
||||
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'
|
||||
|
||||
export class AuthManager {
|
||||
public userName = ''
|
||||
private loginUrl: string
|
||||
private logoutUrl: string
|
||||
private redirectedLoginUrl = `/SASLogon/home`
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private serverType: ServerType,
|
||||
@@ -16,68 +23,175 @@ export class AuthManager {
|
||||
this.logoutUrl =
|
||||
this.serverType === ServerType.Sas9
|
||||
? '/SASLogon/logout?'
|
||||
: '/SASLogon/logout.do?'
|
||||
: this.serverType === ServerType.SasViya
|
||||
? '/SASLogon/logout.do?'
|
||||
: '/SASjsApi/auth/logout'
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens Pop up window to SAS Login screen.
|
||||
* And checks if user has finished login process.
|
||||
*/
|
||||
public async redirectedLogIn({
|
||||
onLoggedOut
|
||||
}: LoginOptions): Promise<LoginResult> {
|
||||
const { isLoggedIn: isLoggedInAlready, userName: currentSessionUsername } =
|
||||
await this.fetchUserName()
|
||||
|
||||
if (isLoggedInAlready) {
|
||||
await this.loginCallback()
|
||||
|
||||
return {
|
||||
isLoggedIn: true,
|
||||
userName: currentSessionUsername
|
||||
}
|
||||
}
|
||||
|
||||
const loginPopup = await openWebPage(
|
||||
this.redirectedLoginUrl,
|
||||
'SASLogon',
|
||||
{
|
||||
width: 500,
|
||||
height: 600
|
||||
},
|
||||
onLoggedOut
|
||||
)
|
||||
|
||||
if (!loginPopup) {
|
||||
return { isLoggedIn: false, userName: '' }
|
||||
}
|
||||
|
||||
const { isLoggedIn } =
|
||||
this.serverType === ServerType.SasViya
|
||||
? await verifySasViyaLogin(loginPopup)
|
||||
: await verifySas9Login(loginPopup)
|
||||
|
||||
loginPopup.close()
|
||||
|
||||
if (isLoggedIn) {
|
||||
if (this.serverType === ServerType.Sas9) {
|
||||
await this.performCASSecurityCheck()
|
||||
}
|
||||
|
||||
const { userName } = await this.fetchUserName()
|
||||
|
||||
await this.loginCallback()
|
||||
|
||||
return { isLoggedIn: true, userName }
|
||||
}
|
||||
|
||||
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<LoginResult> {
|
||||
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.
|
||||
* @param password - a string representing the password.
|
||||
* @returns - a boolean `isLoggedin` and a string `username`
|
||||
*/
|
||||
public async logIn(username: string, password: string) {
|
||||
const loginParams: any = {
|
||||
public async logIn(username: string, password: string): Promise<LoginResult> {
|
||||
const loginParams = {
|
||||
_service: 'default',
|
||||
username,
|
||||
password
|
||||
}
|
||||
|
||||
this.userName = loginParams.username
|
||||
let {
|
||||
isLoggedIn: isLoggedInAlready,
|
||||
loginForm,
|
||||
userName: currentSessionUsername
|
||||
} = await this.checkSession()
|
||||
|
||||
const { isLoggedIn, loginForm } = await this.checkSession()
|
||||
if (isLoggedInAlready) {
|
||||
if (currentSessionUsername === loginParams.username) {
|
||||
await this.loginCallback()
|
||||
|
||||
if (isLoggedIn) {
|
||||
await this.loginCallback()
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
userName: this.userName
|
||||
this.userName = currentSessionUsername!
|
||||
return {
|
||||
isLoggedIn: true,
|
||||
userName: this.userName
|
||||
}
|
||||
} else {
|
||||
await this.logOut()
|
||||
loginForm = await this.getNewLoginForm()
|
||||
}
|
||||
}
|
||||
} else this.userName = ''
|
||||
|
||||
let loginResponse = await this.sendLoginRequest(loginForm, loginParams)
|
||||
|
||||
let loggedIn = isLogInSuccess(loginResponse)
|
||||
let isLoggedIn = isLogInSuccess(loginResponse)
|
||||
|
||||
if (!loggedIn) {
|
||||
if (!isLoggedIn) {
|
||||
if (isCredentialsVerifyError(loginResponse)) {
|
||||
const newLoginForm = await this.getLoginForm(loginResponse)
|
||||
|
||||
loginResponse = await this.sendLoginRequest(newLoginForm, loginParams)
|
||||
}
|
||||
|
||||
const currentSession = await this.checkSession()
|
||||
loggedIn = currentSession.isLoggedIn
|
||||
const res = await this.checkSession()
|
||||
isLoggedIn = res.isLoggedIn
|
||||
|
||||
if (isLoggedIn) this.userName = res.userName
|
||||
} else {
|
||||
this.userName = loginParams.username
|
||||
}
|
||||
|
||||
if (loggedIn) {
|
||||
if (isLoggedIn) {
|
||||
if (this.serverType === ServerType.Sas9) {
|
||||
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
||||
|
||||
await this.requestClient.get<string>(
|
||||
`/SASLogon/login?service=${casAuthenticationUrl}`,
|
||||
undefined
|
||||
)
|
||||
await this.performCASSecurityCheck()
|
||||
}
|
||||
|
||||
this.loginCallback()
|
||||
}
|
||||
} else this.userName = ''
|
||||
|
||||
return {
|
||||
isLoggedIn: !!loggedIn,
|
||||
isLoggedIn,
|
||||
userName: this.userName
|
||||
}
|
||||
}
|
||||
|
||||
private async performCASSecurityCheck() {
|
||||
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
||||
|
||||
await this.requestClient.get<string>(
|
||||
`/SASLogon/login?service=${casAuthenticationUrl}`,
|
||||
undefined
|
||||
)
|
||||
}
|
||||
|
||||
private async sendLoginRequest(
|
||||
loginForm: { [key: string]: any },
|
||||
loginParams: { [key: string]: any }
|
||||
@@ -101,17 +215,70 @@ 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.
|
||||
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
|
||||
* @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() {
|
||||
//For VIYA we will send request on API endpoint. Which is faster then pinging SASJobExecution.
|
||||
//For SAS9 we will send request on SASStoredProcess
|
||||
public async checkSession(): Promise<{
|
||||
isLoggedIn: boolean
|
||||
userName: string
|
||||
loginForm?: any
|
||||
}> {
|
||||
const { isLoggedIn, userName } = await this.fetchUserName()
|
||||
let loginForm = null
|
||||
|
||||
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()
|
||||
|
||||
if (this.serverType !== ServerType.Sasjs)
|
||||
loginForm = await this.getNewLoginForm()
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
isLoggedIn,
|
||||
userName: userName.toLowerCase(),
|
||||
loginForm
|
||||
})
|
||||
}
|
||||
|
||||
private async getNewLoginForm() {
|
||||
const { result: formResponse } = await this.requestClient.get<string>(
|
||||
this.loginUrl.replace('.do', ''),
|
||||
undefined,
|
||||
'text/plain'
|
||||
)
|
||||
|
||||
return await this.getLoginForm(formResponse)
|
||||
}
|
||||
|
||||
private async fetchUserName(): Promise<{
|
||||
isLoggedIn: boolean
|
||||
userName: string
|
||||
}> {
|
||||
const url =
|
||||
this.serverType === 'SASVIYA'
|
||||
? `${this.serverUrl}/identities`
|
||||
: `${this.serverUrl}/SASStoredProcess`
|
||||
this.serverType === ServerType.SasViya
|
||||
? `${this.serverUrl}/identities/users/@currentUser`
|
||||
: this.serverType === ServerType.Sas9
|
||||
? `${this.serverUrl}/SASStoredProcess`
|
||||
: `${this.serverUrl}/SASjsApi/session`
|
||||
|
||||
const { result: loginResponse } = await this.requestClient
|
||||
.get<string>(url, undefined, 'text/plain')
|
||||
@@ -120,27 +287,40 @@ export class AuthManager {
|
||||
})
|
||||
|
||||
const isLoggedIn = loginResponse !== 'authErr'
|
||||
let loginForm = null
|
||||
const userName = isLoggedIn ? this.extractUserName(loginResponse) : ''
|
||||
|
||||
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()
|
||||
|
||||
const { result: formResponse } = await this.requestClient.get<string>(
|
||||
this.loginUrl.replace('.do', ''),
|
||||
undefined,
|
||||
'text/plain'
|
||||
)
|
||||
|
||||
loginForm = await this.getLoginForm(formResponse)
|
||||
}
|
||||
|
||||
return { isLoggedIn, userName }
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
isLoggedIn,
|
||||
userName: this.userName,
|
||||
loginForm
|
||||
})
|
||||
private extractUserName = (response: any): string => {
|
||||
switch (this.serverType) {
|
||||
case ServerType.SasViya:
|
||||
return response?.id
|
||||
|
||||
case ServerType.Sas9:
|
||||
const matched = response?.match(/"title":"Log Off [0-1a-zA-Z ]*"/)
|
||||
const username = matched?.[0].slice(17, -1)
|
||||
|
||||
if (!username.includes(' ')) return username
|
||||
|
||||
return username
|
||||
.split(' ')
|
||||
.map((name: string) => name.slice(0, 3).toLowerCase())
|
||||
.join('')
|
||||
|
||||
case ServerType.Sasjs:
|
||||
return response?.username
|
||||
|
||||
default:
|
||||
console.error('Server Type not found in extractUserName function')
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
private getLoginForm(response: any) {
|
||||
@@ -186,9 +366,21 @@ export class AuthManager {
|
||||
|
||||
/**
|
||||
* Logs out of the configured SAS server.
|
||||
* @param accessToken - an optional access token is required for SASjs server type.
|
||||
*/
|
||||
public logOut() {
|
||||
public async logOut() {
|
||||
if (this.serverType === ServerType.Sasjs) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
36
src/auth/getAccessTokenForSasjs.ts
Normal file
36
src/auth/getAccessTokenForSasjs.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
|
||||
/**
|
||||
* Exchanges the auth code for an access token for the given client.
|
||||
* @param requestClient - the pre-configured HTTP request client
|
||||
* @param clientId - the client ID to authenticate with.
|
||||
* @param authCode - the auth code received from the server.
|
||||
*/
|
||||
export async function getAccessTokenForSasjs(
|
||||
requestClient: RequestClient,
|
||||
clientId: string,
|
||||
authCode: string
|
||||
) {
|
||||
const url = '/SASjsApi/auth/token'
|
||||
const data = {
|
||||
clientId,
|
||||
code: authCode
|
||||
}
|
||||
|
||||
return await requestClient
|
||||
.post(url, data, undefined)
|
||||
.then((res) => {
|
||||
const sasAuth = res.result as {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
}
|
||||
return {
|
||||
access_token: sasAuth.accessToken,
|
||||
refresh_token: sasAuth.refreshToken
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting access token. ')
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SasAuthResponse } from '@sasjs/utils/types'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import * as NodeFormData from 'form-data'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { CertificateError } from '../types/errors'
|
||||
|
||||
/**
|
||||
* Exchanges the auth code for an access token for the given client.
|
||||
@@ -10,7 +10,7 @@ import { RequestClient } from '../request/RequestClient'
|
||||
* @param clientSecret - the client secret to authenticate with.
|
||||
* @param authCode - the auth code received from the server.
|
||||
*/
|
||||
export async function getAccessToken(
|
||||
export async function getAccessTokenForViya(
|
||||
requestClient: RequestClient,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
@@ -24,29 +24,21 @@ export async function getAccessToken(
|
||||
token = Buffer.from(clientId + ':' + clientSecret).toString('base64')
|
||||
}
|
||||
const headers = {
|
||||
Authorization: 'Basic ' + token
|
||||
Authorization: 'Basic ' + token,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
|
||||
let formData
|
||||
if (typeof FormData === 'undefined') {
|
||||
formData = new NodeFormData()
|
||||
} else {
|
||||
formData = new FormData()
|
||||
}
|
||||
formData.append('grant_type', 'authorization_code')
|
||||
formData.append('code', authCode)
|
||||
const data = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: authCode
|
||||
})
|
||||
|
||||
const authResponse = await requestClient
|
||||
.post(
|
||||
url,
|
||||
formData,
|
||||
undefined,
|
||||
'multipart/form-data; boundary=' + (formData as any)._boundary,
|
||||
headers
|
||||
)
|
||||
.post(url, data, undefined, 'application/x-www-form-urlencoded', headers)
|
||||
.then((res) => res.result as SasAuthResponse)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting access token')
|
||||
if (err instanceof CertificateError) throw err
|
||||
throw prefixMessage(err, 'Error while getting access token. ')
|
||||
})
|
||||
|
||||
return authResponse
|
||||
31
src/auth/getAuthCodeForSasjs.ts
Normal file
31
src/auth/getAuthCodeForSasjs.ts
Normal file
@@ -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
|
||||
}
|
||||
@@ -3,18 +3,21 @@ import {
|
||||
isRefreshTokenExpiring,
|
||||
hasTokenExpired
|
||||
} from '@sasjs/utils/auth'
|
||||
import { AuthConfig } from '@sasjs/utils/types'
|
||||
import { AuthConfig, ServerType } from '@sasjs/utils/types'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { refreshTokens } from './refreshTokens'
|
||||
import { refreshTokensForViya } from './refreshTokensForViya'
|
||||
import { refreshTokensForSasjs } from './refreshTokensForSasjs'
|
||||
|
||||
/**
|
||||
* Returns the auth configuration, refreshing the tokens if necessary.
|
||||
* @param requestClient - the pre-configured HTTP request client
|
||||
* @param authConfig - an object containing a client ID, secret, access token and refresh token
|
||||
* @param serverType - server type for which refreshing the tokens, defaults to SASVIYA
|
||||
*/
|
||||
export async function getTokens(
|
||||
requestClient: RequestClient,
|
||||
authConfig: AuthConfig
|
||||
authConfig: AuthConfig,
|
||||
serverType: ServerType = ServerType.SasViya
|
||||
): Promise<AuthConfig> {
|
||||
const logger = process.logger || console
|
||||
let { access_token, refresh_token, client, secret } = authConfig
|
||||
@@ -29,12 +32,16 @@ export async function getTokens(
|
||||
throw new Error(error)
|
||||
}
|
||||
logger.info('Refreshing access and refresh tokens.')
|
||||
;({ access_token, refresh_token } = await refreshTokens(
|
||||
requestClient,
|
||||
client,
|
||||
secret,
|
||||
refresh_token
|
||||
))
|
||||
const tokens =
|
||||
serverType === ServerType.SasViya
|
||||
? await refreshTokensForViya(
|
||||
requestClient,
|
||||
client,
|
||||
secret,
|
||||
refresh_token
|
||||
)
|
||||
: await refreshTokensForSasjs(requestClient, refresh_token)
|
||||
;({ access_token, refresh_token } = tokens)
|
||||
}
|
||||
return { access_token, refresh_token, client, secret }
|
||||
}
|
||||
|
||||
40
src/auth/openWebPage.ts
Normal file
40
src/auth/openWebPage.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { openLoginPrompt } from '../utils/loginPrompt'
|
||||
|
||||
interface WindowFeatures {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
const defaultWindowFeatures: WindowFeatures = { width: 500, height: 600 }
|
||||
|
||||
export async function openWebPage(
|
||||
url: string,
|
||||
windowName: string = '',
|
||||
WindowFeatures: WindowFeatures = defaultWindowFeatures,
|
||||
onLoggedOut?: () => Promise<Boolean>
|
||||
): Promise<Window | null> {
|
||||
const { width, height } = WindowFeatures
|
||||
const left = screen.width / 2 - width / 2
|
||||
const top = screen.height / 2 - height / 2
|
||||
|
||||
const loginPopup = window.open(
|
||||
url,
|
||||
windowName,
|
||||
`toolbar=0,location=0,menubar=0,width=${width},height=${height},left=${left},top=${top}`
|
||||
)
|
||||
|
||||
if (!loginPopup) {
|
||||
const getUserAction: () => Promise<Boolean> = onLoggedOut ?? openLoginPrompt
|
||||
|
||||
const doLogin = await getUserAction()
|
||||
return doLogin
|
||||
? window.open(
|
||||
url,
|
||||
windowName,
|
||||
`toolbar=0,location=0,menubar=0,width=${width},height=${height},left=${left},top=${top}`
|
||||
)
|
||||
: null
|
||||
}
|
||||
|
||||
return loginPopup
|
||||
}
|
||||
35
src/auth/refreshTokensForSasjs.ts
Normal file
35
src/auth/refreshTokensForSasjs.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
|
||||
/**
|
||||
* Exchanges the refresh token for an access token for the given client.
|
||||
* @param requestClient - the pre-configured HTTP request client
|
||||
* @param refreshToken - the refresh token received from the server.
|
||||
*/
|
||||
export async function refreshTokensForSasjs(
|
||||
requestClient: RequestClient,
|
||||
refreshToken: string
|
||||
) {
|
||||
const url = '/SASjsApi/auth/refresh'
|
||||
const headers = {
|
||||
Authorization: 'Bearer ' + refreshToken
|
||||
}
|
||||
|
||||
const authResponse = await requestClient
|
||||
.post(url, undefined, undefined, undefined, headers)
|
||||
.then((res) => {
|
||||
const sasAuth = res.result as {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
}
|
||||
return {
|
||||
access_token: sasAuth.accessToken,
|
||||
refresh_token: sasAuth.refreshToken
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while refreshing tokens')
|
||||
})
|
||||
|
||||
return authResponse
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import { RequestClient } from '../request/RequestClient'
|
||||
* @param requestClient - the pre-configured HTTP request client
|
||||
* @param clientId - the client ID to authenticate with.
|
||||
* @param clientSecret - the client secret to authenticate with.
|
||||
* @param authCode - the refresh token received from the server.
|
||||
* @param refreshToken - the refresh token received from the server.
|
||||
*/
|
||||
export async function refreshTokens(
|
||||
export async function refreshTokensForViya(
|
||||
requestClient: RequestClient,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
@@ -3,10 +3,14 @@ import * as dotenv from 'dotenv'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
mockedCurrentUserApi,
|
||||
mockLoginAuthoriseRequiredResponse,
|
||||
mockLoginSuccessResponse
|
||||
} from './mockResponses'
|
||||
import { serialize } from '../../utils'
|
||||
import * as openWebPageModule from '../openWebPage'
|
||||
import * as verifySasViyaLoginModule from '../verifySasViyaLogin'
|
||||
import * as verifySas9LoginModule from '../verifySas9Login'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
@@ -57,134 +61,614 @@ describe('AuthManager', () => {
|
||||
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout?')
|
||||
})
|
||||
|
||||
it('should call the auth callback and return when already logged in', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: true,
|
||||
userName: 'test',
|
||||
loginForm: 'test'
|
||||
})
|
||||
)
|
||||
describe('login - default mechanism', () => {
|
||||
it('should call the auth callback and return when already logged in', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: true,
|
||||
userName,
|
||||
loginForm: 'test'
|
||||
})
|
||||
)
|
||||
|
||||
const loginResponse = await authManager.logIn(userName, password)
|
||||
const loginResponse = await authManager.logIn(userName, password)
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeTruthy()
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should post a login request to the server if not logged in', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: 'test',
|
||||
loginForm: { name: 'test' }
|
||||
})
|
||||
)
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: mockLoginSuccessResponse })
|
||||
)
|
||||
|
||||
const loginResponse = await authManager.logIn(userName, password)
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeTruthy()
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
|
||||
const loginParams = serialize({
|
||||
_service: 'default',
|
||||
username: userName,
|
||||
password,
|
||||
name: 'test'
|
||||
expect(loginResponse.isLoggedIn).toBeTruthy()
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
`/SASLogon/login`,
|
||||
loginParams,
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: '*/*'
|
||||
|
||||
it('should post a login request to the server when already logged in with other username', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: true,
|
||||
userName: 'someOtherUsername',
|
||||
loginForm: null
|
||||
})
|
||||
)
|
||||
jest
|
||||
.spyOn(authManager, 'logOut')
|
||||
.mockImplementation(() => Promise.resolve(true))
|
||||
|
||||
jest
|
||||
.spyOn<any, any>(authManager, 'getNewLoginForm')
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
name: 'test'
|
||||
})
|
||||
)
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: mockLoginSuccessResponse })
|
||||
)
|
||||
|
||||
const loginResponse = await authManager.logIn(userName, password)
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeTruthy()
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
|
||||
const loginParams = serialize({
|
||||
_service: 'default',
|
||||
username: userName,
|
||||
password,
|
||||
name: 'test'
|
||||
})
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
expect(authManager.logOut).toHaveBeenCalledTimes(1)
|
||||
expect(authManager['getNewLoginForm']).toHaveBeenCalledTimes(1)
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
`/SASLogon/login`,
|
||||
loginParams,
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: '*/*'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
)
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should post a login request to the server when not logged in', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: '',
|
||||
loginForm: { name: 'test' }
|
||||
})
|
||||
)
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: mockLoginSuccessResponse })
|
||||
)
|
||||
|
||||
const loginResponse = await authManager.logIn(userName, password)
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeTruthy()
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
|
||||
const loginParams = serialize({
|
||||
_service: 'default',
|
||||
username: userName,
|
||||
password,
|
||||
name: 'test'
|
||||
})
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
`/SASLogon/login`,
|
||||
loginParams,
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: '*/*'
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should post a login & a cas_security request to the SAS9 server when not logged in', async () => {
|
||||
const serverType = ServerType.Sas9
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: '',
|
||||
loginForm: { name: 'test' }
|
||||
})
|
||||
)
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: mockLoginSuccessResponse })
|
||||
)
|
||||
mockedAxios.get.mockImplementation(() => Promise.resolve({ status: 200 }))
|
||||
|
||||
const loginResponse = await authManager.logIn(userName, password)
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeTruthy()
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
|
||||
const loginParams = serialize({
|
||||
_service: 'default',
|
||||
username: userName,
|
||||
password,
|
||||
name: 'test'
|
||||
})
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
`/SASLogon/login`,
|
||||
loginParams,
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: '*/*'
|
||||
}
|
||||
}
|
||||
)
|
||||
const casAuthenticationUrl = `${serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(
|
||||
`/SASLogon/login?service=${casAuthenticationUrl}`,
|
||||
getHeadersJson
|
||||
)
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should return empty username if unable to logged in', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: '',
|
||||
loginForm: { name: 'test' }
|
||||
})
|
||||
)
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: 'Not Signed in' })
|
||||
)
|
||||
|
||||
const loginResponse = await authManager.logIn(userName, password)
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeFalsy()
|
||||
expect(loginResponse.userName).toEqual('')
|
||||
|
||||
const loginParams = serialize({
|
||||
_service: 'default',
|
||||
username: userName,
|
||||
password,
|
||||
name: 'test'
|
||||
})
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
`/SASLogon/login`,
|
||||
loginParams,
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: '*/*'
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should parse and submit the authorisation form when necessary', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest
|
||||
.spyOn(requestClient, 'authorize')
|
||||
.mockImplementation(() => Promise.resolve())
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: 'test',
|
||||
loginForm: { name: 'test' }
|
||||
})
|
||||
)
|
||||
mockedAxios.post.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
data: mockLoginAuthoriseRequiredResponse,
|
||||
config: { url: 'https://test.com/SASLogon/login' },
|
||||
request: { responseURL: 'https://test.com/OAuth/authorize' }
|
||||
})
|
||||
)
|
||||
|
||||
mockedAxios.get.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
data: mockLoginAuthoriseRequiredResponse
|
||||
})
|
||||
)
|
||||
|
||||
await authManager.logIn(userName, password)
|
||||
|
||||
expect(requestClient.authorize).toHaveBeenCalledWith(
|
||||
mockLoginAuthoriseRequiredResponse
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse and submit the authorisation form when necessary', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest
|
||||
.spyOn(requestClient, 'authorize')
|
||||
.mockImplementation(() => Promise.resolve())
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: 'test',
|
||||
loginForm: { name: 'test' }
|
||||
})
|
||||
)
|
||||
mockedAxios.post.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
data: mockLoginAuthoriseRequiredResponse,
|
||||
config: { url: 'https://test.com/SASLogon/login' },
|
||||
request: { responseURL: 'https://test.com/OAuth/authorize' }
|
||||
})
|
||||
)
|
||||
describe('login - redirect mechanism', () => {
|
||||
beforeAll(() => {
|
||||
jest.mock('../openWebPage')
|
||||
jest
|
||||
.spyOn(openWebPageModule, 'openWebPage')
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({ close: jest.fn() } as unknown as Window)
|
||||
)
|
||||
jest.mock('../verifySasViyaLogin')
|
||||
jest
|
||||
.spyOn(verifySasViyaLoginModule, 'verifySasViyaLogin')
|
||||
.mockImplementation(() => Promise.resolve({ isLoggedIn: true }))
|
||||
jest.mock('../verifySas9Login')
|
||||
jest
|
||||
.spyOn(verifySas9LoginModule, 'verifySas9Login')
|
||||
.mockImplementation(() => Promise.resolve({ isLoggedIn: true }))
|
||||
})
|
||||
|
||||
mockedAxios.get.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
data: mockLoginAuthoriseRequiredResponse
|
||||
})
|
||||
)
|
||||
it('should call the auth callback and return when already logged in', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest
|
||||
.spyOn<any, any>(authManager, 'fetchUserName')
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: true,
|
||||
userName
|
||||
})
|
||||
)
|
||||
|
||||
await authManager.logIn(userName, password)
|
||||
const loginResponse = await authManager.redirectedLogIn({})
|
||||
|
||||
expect(requestClient.authorize).toHaveBeenCalledWith(
|
||||
mockLoginAuthoriseRequiredResponse
|
||||
)
|
||||
expect(loginResponse.isLoggedIn).toBeTruthy()
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should perform login via pop up if not logged in', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest
|
||||
.spyOn<any, any>(authManager, 'fetchUserName')
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: ''
|
||||
})
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: true,
|
||||
userName
|
||||
})
|
||||
)
|
||||
|
||||
const loginResponse = await authManager.redirectedLogIn({})
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeTruthy()
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
|
||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||
`/SASLogon/home`,
|
||||
'SASLogon',
|
||||
{
|
||||
width: 500,
|
||||
height: 600
|
||||
},
|
||||
undefined
|
||||
)
|
||||
expect(authManager['fetchUserName']).toHaveBeenCalledTimes(2)
|
||||
expect(verifySasViyaLoginModule.verifySasViyaLogin).toHaveBeenCalledTimes(
|
||||
1
|
||||
)
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should perform login via pop up if not logged in with server sas9', async () => {
|
||||
const serverType = ServerType.Sas9
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest
|
||||
.spyOn<any, any>(authManager, 'fetchUserName')
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: ''
|
||||
})
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: true,
|
||||
userName
|
||||
})
|
||||
)
|
||||
|
||||
const loginResponse = await authManager.redirectedLogIn({})
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeTruthy()
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
|
||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||
`/SASLogon/home`,
|
||||
'SASLogon',
|
||||
{
|
||||
width: 500,
|
||||
height: 600
|
||||
},
|
||||
undefined
|
||||
)
|
||||
expect(authManager['fetchUserName']).toHaveBeenCalledTimes(2)
|
||||
expect(verifySas9LoginModule.verifySas9Login).toHaveBeenCalledTimes(1)
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should return empty username if user unable to re-login via pop up', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest
|
||||
.spyOn<any, any>(authManager, 'fetchUserName')
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: ''
|
||||
})
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: true,
|
||||
userName
|
||||
})
|
||||
)
|
||||
jest
|
||||
.spyOn(verifySasViyaLoginModule, 'verifySasViyaLogin')
|
||||
.mockImplementation(() => Promise.resolve({ isLoggedIn: false }))
|
||||
|
||||
const loginResponse = await authManager.redirectedLogIn({})
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeFalsy()
|
||||
expect(loginResponse.userName).toEqual('')
|
||||
|
||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||
`/SASLogon/home`,
|
||||
'SASLogon',
|
||||
{
|
||||
width: 500,
|
||||
height: 600
|
||||
},
|
||||
undefined
|
||||
)
|
||||
expect(authManager['fetchUserName']).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(authCallback).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it('should return empty username if user rejects to re-login', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest
|
||||
.spyOn<any, any>(authManager, 'fetchUserName')
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: ''
|
||||
})
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: true,
|
||||
userName
|
||||
})
|
||||
)
|
||||
jest
|
||||
.spyOn(openWebPageModule, 'openWebPage')
|
||||
.mockImplementation(() => Promise.resolve(null))
|
||||
|
||||
const loginResponse = await authManager.redirectedLogIn({})
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeFalsy()
|
||||
expect(loginResponse.userName).toEqual('')
|
||||
|
||||
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||
`/SASLogon/home`,
|
||||
'SASLogon',
|
||||
{
|
||||
width: 500,
|
||||
height: 600
|
||||
},
|
||||
undefined
|
||||
)
|
||||
expect(authManager['fetchUserName']).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(authCallback).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
|
||||
it('should check and return session information if logged in', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: '<button onClick="logout">' })
|
||||
)
|
||||
describe('checkSession', () => {
|
||||
it('return session information when logged in', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: mockedCurrentUserApi(userName) })
|
||||
)
|
||||
|
||||
const response = await authManager.checkSession()
|
||||
expect(response.isLoggedIn).toBeTruthy()
|
||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
`http://test-server.com/identities`,
|
||||
{
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
transformResponse: undefined,
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
const response = await authManager.checkSession()
|
||||
expect(response.isLoggedIn).toBeTruthy()
|
||||
expect(response.userName).toEqual(userName)
|
||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
`http://test-server.com/identities/users/@currentUser`,
|
||||
{
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
transformResponse: undefined,
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('return session information when logged in - SAS9', async () => {
|
||||
// username cannot have `-` and cannot be uppercased
|
||||
const username = 'testusername'
|
||||
const serverType = ServerType.Sas9
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
data: `"title":"Log Off ${username}","url":"javascript: clearFrame(\"/SASStoredProcess/do?_action=logoff\")"' })`
|
||||
})
|
||||
)
|
||||
|
||||
const response = await authManager.checkSession()
|
||||
expect(response.isLoggedIn).toBeTruthy()
|
||||
expect(response.userName).toEqual(username)
|
||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
`http://test-server.com/SASStoredProcess`,
|
||||
{
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
transformResponse: undefined,
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('return session information when logged in - SAS9 - having full name in html', async () => {
|
||||
const fullname = 'FirstName LastName'
|
||||
const username = 'firlas'
|
||||
const serverType = ServerType.Sas9
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
data: `"title":"Log Off ${fullname}","url":"javascript: clearFrame(\"/SASStoredProcess/do?_action=logoff\")"' })`
|
||||
})
|
||||
)
|
||||
|
||||
const response = await authManager.checkSession()
|
||||
expect(response.isLoggedIn).toBeTruthy()
|
||||
expect(response.userName).toEqual(username)
|
||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
`http://test-server.com/SASStoredProcess`,
|
||||
{
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
transformResponse: undefined,
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('perform logout when not logged in', async () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
mockedAxios.get
|
||||
.mockImplementationOnce(() => Promise.resolve({ status: 401 }))
|
||||
.mockImplementation(() => Promise.resolve({}))
|
||||
|
||||
const response = await authManager.checkSession()
|
||||
expect(response.isLoggedIn).toBeFalsy()
|
||||
expect(response.userName).toEqual('')
|
||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
`http://test-server.com/identities/users/@currentUser`,
|
||||
{
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
transformResponse: undefined,
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
`/SASLogon/logout.do?`,
|
||||
getHeadersJson
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const getHeadersJson = {
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
responseType: 'json'
|
||||
}
|
||||
|
||||
65
src/auth/spec/getAccessTokenForSasjs.spec.ts
Normal file
65
src/auth/spec/getAccessTokenForSasjs.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { AuthConfig } from '@sasjs/utils'
|
||||
import { generateToken, mockSasjsAuthResponse } from './mockResponses'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import { getAccessTokenForSasjs } from '../getAccessTokenForSasjs'
|
||||
|
||||
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
||||
|
||||
describe('getAccessTokenForSasjs', () => {
|
||||
it('should attempt to refresh tokens', async () => {
|
||||
setupMocks()
|
||||
const access_token = generateToken(30)
|
||||
const refresh_token = generateToken(30)
|
||||
const authConfig: AuthConfig = {
|
||||
access_token,
|
||||
refresh_token,
|
||||
client: 'cl13nt',
|
||||
secret: 's3cr3t'
|
||||
}
|
||||
jest
|
||||
.spyOn(requestClient, 'post')
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({ result: mockSasjsAuthResponse, etag: '' })
|
||||
)
|
||||
|
||||
await getAccessTokenForSasjs(
|
||||
requestClient,
|
||||
authConfig.client,
|
||||
authConfig.refresh_token
|
||||
)
|
||||
|
||||
expect(requestClient.post).toHaveBeenCalledWith(
|
||||
'/SASjsApi/auth/token',
|
||||
{ clientId: authConfig.client, code: authConfig.refresh_token },
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle errors while refreshing tokens', async () => {
|
||||
setupMocks()
|
||||
const access_token = generateToken(30)
|
||||
const refresh_token = generateToken(30)
|
||||
const authConfig: AuthConfig = {
|
||||
access_token,
|
||||
refresh_token,
|
||||
client: 'cl13nt',
|
||||
secret: 's3cr3t'
|
||||
}
|
||||
jest
|
||||
.spyOn(requestClient, 'post')
|
||||
.mockImplementation(() => Promise.reject('Token Error'))
|
||||
|
||||
const error = await getAccessTokenForSasjs(
|
||||
requestClient,
|
||||
authConfig.client,
|
||||
authConfig.refresh_token
|
||||
).catch((e) => e)
|
||||
|
||||
expect(error).toContain('Error while getting access token')
|
||||
})
|
||||
})
|
||||
|
||||
const setupMocks = () => {
|
||||
jest.restoreAllMocks()
|
||||
jest.mock('../../request/RequestClient')
|
||||
}
|
||||
@@ -2,11 +2,11 @@ import { AuthConfig } from '@sasjs/utils'
|
||||
import * as NodeFormData from 'form-data'
|
||||
import { generateToken, mockAuthResponse } from './mockResponses'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import { getAccessToken } from '../getAccessToken'
|
||||
import { getAccessTokenForViya } from '../getAccessTokenForViya'
|
||||
|
||||
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
||||
|
||||
describe('getAccessToken', () => {
|
||||
describe('getAccessTokenForViya', () => {
|
||||
it('should attempt to refresh tokens', async () => {
|
||||
setupMocks()
|
||||
const access_token = generateToken(30)
|
||||
@@ -26,7 +26,7 @@ describe('getAccessToken', () => {
|
||||
authConfig.client + ':' + authConfig.secret
|
||||
).toString('base64')
|
||||
|
||||
await getAccessToken(
|
||||
await getAccessTokenForViya(
|
||||
requestClient,
|
||||
authConfig.client,
|
||||
authConfig.secret,
|
||||
@@ -35,11 +35,12 @@ describe('getAccessToken', () => {
|
||||
|
||||
expect(requestClient.post).toHaveBeenCalledWith(
|
||||
'/SASLogon/oauth/token',
|
||||
expect.any(NodeFormData),
|
||||
expect.any(URLSearchParams),
|
||||
undefined,
|
||||
expect.stringContaining('multipart/form-data; boundary='),
|
||||
'application/x-www-form-urlencoded',
|
||||
{
|
||||
Authorization: 'Basic ' + token
|
||||
Authorization: 'Basic ' + token,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -58,7 +59,7 @@ describe('getAccessToken', () => {
|
||||
.spyOn(requestClient, 'post')
|
||||
.mockImplementation(() => Promise.reject('Token Error'))
|
||||
|
||||
const error = await getAccessToken(
|
||||
const error = await getAccessTokenForViya(
|
||||
requestClient,
|
||||
authConfig.client,
|
||||
authConfig.secret,
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AuthConfig } from '@sasjs/utils'
|
||||
import * as refreshTokensModule from '../refreshTokens'
|
||||
import * as refreshTokensModule from '../refreshTokensForViya'
|
||||
import { generateToken, mockAuthResponse } from './mockResponses'
|
||||
import { getTokens } from '../getTokens'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
@@ -20,7 +20,7 @@ describe('getTokens', () => {
|
||||
|
||||
await getTokens(requestClient, authConfig)
|
||||
|
||||
expect(refreshTokensModule.refreshTokens).toHaveBeenCalledWith(
|
||||
expect(refreshTokensModule.refreshTokensForViya).toHaveBeenCalledWith(
|
||||
requestClient,
|
||||
authConfig.client,
|
||||
authConfig.secret,
|
||||
@@ -41,7 +41,7 @@ describe('getTokens', () => {
|
||||
|
||||
await getTokens(requestClient, authConfig)
|
||||
|
||||
expect(refreshTokensModule.refreshTokens).toHaveBeenCalledWith(
|
||||
expect(refreshTokensModule.refreshTokensForViya).toHaveBeenCalledWith(
|
||||
requestClient,
|
||||
authConfig.client,
|
||||
authConfig.secret,
|
||||
@@ -71,9 +71,9 @@ describe('getTokens', () => {
|
||||
const setupMocks = () => {
|
||||
jest.restoreAllMocks()
|
||||
jest.mock('../../request/RequestClient')
|
||||
jest.mock('../refreshTokens')
|
||||
jest.mock('../refreshTokensForViya')
|
||||
|
||||
jest
|
||||
.spyOn(refreshTokensModule, 'refreshTokens')
|
||||
.spyOn(refreshTokensModule, 'refreshTokensForViya')
|
||||
.mockImplementation(() => Promise.resolve(mockAuthResponse))
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ export const mockAuthResponse: SasAuthResponse = {
|
||||
jti: 'test'
|
||||
}
|
||||
|
||||
export const mockSasjsAuthResponse = {
|
||||
access_token: 'acc355',
|
||||
refresh_token: 'r3fr35h'
|
||||
}
|
||||
|
||||
export const generateToken = (timeToLiveSeconds: number): string => {
|
||||
const exp =
|
||||
new Date(new Date().getTime() + timeToLiveSeconds * 1000).getTime() / 1000
|
||||
@@ -22,3 +27,28 @@ export const generateToken = (timeToLiveSeconds: number): string => {
|
||||
const token = `${header}.${payload}.${signature}`
|
||||
return token
|
||||
}
|
||||
|
||||
export const mockedCurrentUserApi = (username: string) => ({
|
||||
creationTimeStamp: '2021-04-17T14:13:14.000Z',
|
||||
modifiedTimeStamp: '2021-08-31T22:08:07.000Z',
|
||||
id: username,
|
||||
type: 'user',
|
||||
name: 'Full User Name',
|
||||
links: [
|
||||
{
|
||||
method: 'GET',
|
||||
rel: 'self',
|
||||
href: `/identities/users/${username}`,
|
||||
uri: `/identities/users/${username}`,
|
||||
type: 'user'
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
rel: 'alternate',
|
||||
href: `/identities/users/${username}`,
|
||||
uri: `/identities/users/${username}`,
|
||||
type: 'application/vnd.sas.summary'
|
||||
}
|
||||
],
|
||||
version: 2
|
||||
})
|
||||
|
||||
64
src/auth/spec/openWebPage.spec.ts
Normal file
64
src/auth/spec/openWebPage.spec.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import { openWebPage } from '../openWebPage'
|
||||
import * as loginPromptModule from '../../utils/loginPrompt'
|
||||
|
||||
describe('openWebPage', () => {
|
||||
const serverUrl = 'http://test-server.com'
|
||||
|
||||
describe('window.open is not blocked', () => {
|
||||
const mockedOpen = jest
|
||||
.fn()
|
||||
.mockImplementation(() => ({} as unknown as Window))
|
||||
const originalOpen = window.open
|
||||
|
||||
beforeAll(() => {
|
||||
window.open = mockedOpen
|
||||
})
|
||||
afterAll(() => {
|
||||
window.open = originalOpen
|
||||
})
|
||||
|
||||
it(`should return new Window popup - using default adapter's dialog`, async () => {
|
||||
await expect(openWebPage(serverUrl)).resolves.toBeDefined()
|
||||
|
||||
expect(mockedOpen).toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('window.open is blocked', () => {
|
||||
const mockedOpen = jest.fn().mockImplementation(() => null)
|
||||
const originalOpen = window.open
|
||||
|
||||
beforeAll(() => {
|
||||
window.open = mockedOpen
|
||||
})
|
||||
afterAll(() => {
|
||||
window.open = originalOpen
|
||||
})
|
||||
|
||||
it(`should return new Window popup - using default adapter's dialog`, async () => {
|
||||
jest.mock('../../utils/loginPrompt')
|
||||
jest
|
||||
.spyOn(loginPromptModule, 'openLoginPrompt')
|
||||
.mockImplementation(() => Promise.resolve(true))
|
||||
|
||||
await expect(openWebPage(serverUrl)).resolves.toBeDefined()
|
||||
expect(loginPromptModule.openLoginPrompt).toBeCalled()
|
||||
expect(mockedOpen).toBeCalled()
|
||||
})
|
||||
|
||||
it(`should return new Window popup - using frontend's provided onloggedOut`, async () => {
|
||||
const onLoggedOut = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve(true))
|
||||
|
||||
await expect(
|
||||
openWebPage(serverUrl, undefined, undefined, onLoggedOut)
|
||||
).resolves.toBeDefined()
|
||||
expect(onLoggedOut).toBeCalled()
|
||||
expect(mockedOpen).toBeCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
47
src/auth/spec/refreshTokensForSasjs.spec.ts
Normal file
47
src/auth/spec/refreshTokensForSasjs.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { generateToken, mockAuthResponse } from './mockResponses'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import { refreshTokensForSasjs } from '../refreshTokensForSasjs'
|
||||
|
||||
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
||||
|
||||
describe('refreshTokensForSasjs', () => {
|
||||
it('should attempt to refresh tokens', async () => {
|
||||
setupMocks()
|
||||
const refresh_token = generateToken(30)
|
||||
jest
|
||||
.spyOn(requestClient, 'post')
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({ result: mockAuthResponse, etag: '' })
|
||||
)
|
||||
|
||||
await refreshTokensForSasjs(requestClient, refresh_token)
|
||||
|
||||
expect(requestClient.post).toHaveBeenCalledWith(
|
||||
'/SASjsApi/auth/refresh',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ Authorization: `Bearer ${refresh_token}` }
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle errors while refreshing tokens', async () => {
|
||||
setupMocks()
|
||||
const refresh_token = generateToken(30)
|
||||
jest
|
||||
.spyOn(requestClient, 'post')
|
||||
.mockImplementation(() => Promise.reject('Token Error'))
|
||||
|
||||
const error = await refreshTokensForSasjs(
|
||||
requestClient,
|
||||
refresh_token
|
||||
).catch((e) => e)
|
||||
|
||||
expect(error).toContain('Error while refreshing tokens')
|
||||
})
|
||||
})
|
||||
|
||||
const setupMocks = () => {
|
||||
jest.restoreAllMocks()
|
||||
jest.mock('../../request/RequestClient')
|
||||
}
|
||||
@@ -2,11 +2,11 @@ import { AuthConfig } from '@sasjs/utils'
|
||||
import * as NodeFormData from 'form-data'
|
||||
import { generateToken, mockAuthResponse } from './mockResponses'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
import { refreshTokens } from '../refreshTokens'
|
||||
import { refreshTokensForViya } from '../refreshTokensForViya'
|
||||
|
||||
const requestClient = new (<jest.Mock<RequestClient>>RequestClient)()
|
||||
|
||||
describe('refreshTokens', () => {
|
||||
describe('refreshTokensForViya', () => {
|
||||
it('should attempt to refresh tokens', async () => {
|
||||
setupMocks()
|
||||
const access_token = generateToken(30)
|
||||
@@ -26,7 +26,7 @@ describe('refreshTokens', () => {
|
||||
authConfig.client + ':' + authConfig.secret
|
||||
).toString('base64')
|
||||
|
||||
await refreshTokens(
|
||||
await refreshTokensForViya(
|
||||
requestClient,
|
||||
authConfig.client,
|
||||
authConfig.secret,
|
||||
@@ -58,7 +58,7 @@ describe('refreshTokens', () => {
|
||||
.spyOn(requestClient, 'post')
|
||||
.mockImplementation(() => Promise.reject('Token Error'))
|
||||
|
||||
const error = await refreshTokens(
|
||||
const error = await refreshTokensForViya(
|
||||
requestClient,
|
||||
authConfig.client,
|
||||
authConfig.secret,
|
||||
37
src/auth/spec/verifySas9Login.spec.ts
Normal file
37
src/auth/spec/verifySas9Login.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import { verifySas9Login } from '../verifySas9Login'
|
||||
import * as delayModule from '../../utils/delay'
|
||||
|
||||
describe('verifySas9Login', () => {
|
||||
const serverUrl = 'http://test-server.com'
|
||||
|
||||
beforeAll(() => {
|
||||
jest.mock('../../utils')
|
||||
jest
|
||||
.spyOn(delayModule, 'delay')
|
||||
.mockImplementation(() => Promise.resolve({}))
|
||||
})
|
||||
|
||||
it('should return isLoggedIn true by checking state of popup', async () => {
|
||||
const popup = {
|
||||
window: {
|
||||
location: { href: serverUrl + `/SASLogon/home` },
|
||||
document: { body: { innerText: '<h3>You have signed in.</h3>' } }
|
||||
}
|
||||
} as unknown as Window
|
||||
|
||||
await expect(verifySas9Login(popup)).resolves.toEqual({
|
||||
isLoggedIn: true
|
||||
})
|
||||
})
|
||||
|
||||
it('should return isLoggedIn false if user closed popup, already', async () => {
|
||||
const popup: Window = { closed: true } as unknown as Window
|
||||
|
||||
await expect(verifySas9Login(popup)).resolves.toEqual({
|
||||
isLoggedIn: false
|
||||
})
|
||||
})
|
||||
})
|
||||
38
src/auth/spec/verifySasViyaLogin.spec.ts
Normal file
38
src/auth/spec/verifySasViyaLogin.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import { verifySasViyaLogin } from '../verifySasViyaLogin'
|
||||
import * as delayModule from '../../utils/delay'
|
||||
|
||||
describe('verifySasViyaLogin', () => {
|
||||
const serverUrl = 'http://test-server.com'
|
||||
|
||||
beforeAll(() => {
|
||||
jest.mock('../../utils')
|
||||
jest
|
||||
.spyOn(delayModule, 'delay')
|
||||
.mockImplementation(() => Promise.resolve({}))
|
||||
document.cookie = encodeURIComponent('Current-User={"userId":"user-hash"}')
|
||||
})
|
||||
|
||||
it('should return isLoggedIn true by checking state of popup', async () => {
|
||||
const popup = {
|
||||
window: {
|
||||
location: { href: serverUrl + `/SASLogon/home` },
|
||||
document: { body: { innerText: '<h3>You have signed in.</h3>' } }
|
||||
}
|
||||
} as unknown as Window
|
||||
|
||||
await expect(verifySasViyaLogin(popup)).resolves.toEqual({
|
||||
isLoggedIn: true
|
||||
})
|
||||
})
|
||||
|
||||
it('should return isLoggedIn false if user closed popup, already', async () => {
|
||||
const popup: Window = { closed: true } as unknown as Window
|
||||
|
||||
await expect(verifySasViyaLogin(popup)).resolves.toEqual({
|
||||
isLoggedIn: false
|
||||
})
|
||||
})
|
||||
})
|
||||
20
src/auth/verifySas9Login.ts
Normal file
20
src/auth/verifySas9Login.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { delay } from '../utils'
|
||||
|
||||
export async function verifySas9Login(loginPopup: Window): Promise<{
|
||||
isLoggedIn: boolean
|
||||
}> {
|
||||
let isLoggedIn = false
|
||||
let startTime = new Date()
|
||||
let elapsedSeconds = 0
|
||||
do {
|
||||
await delay(1000)
|
||||
if (loginPopup.closed) break
|
||||
|
||||
isLoggedIn =
|
||||
loginPopup.window.location.href.includes('SASLogon') &&
|
||||
loginPopup.window.document.body.innerText.includes('You have signed in.')
|
||||
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
|
||||
} while (!isLoggedIn && elapsedSeconds < 5 * 60)
|
||||
|
||||
return { isLoggedIn }
|
||||
}
|
||||
33
src/auth/verifySasViyaLogin.ts
Normal file
33
src/auth/verifySasViyaLogin.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { delay } from '../utils'
|
||||
|
||||
export async function verifySasViyaLogin(loginPopup: Window): Promise<{
|
||||
isLoggedIn: boolean
|
||||
}> {
|
||||
let isLoggedIn = false
|
||||
let startTime = new Date()
|
||||
let elapsedSeconds = 0
|
||||
do {
|
||||
await delay(1000)
|
||||
if (loginPopup.closed) break
|
||||
isLoggedIn = isLoggedInSASVIYA()
|
||||
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
|
||||
} while (!isLoggedIn && elapsedSeconds < 5 * 60)
|
||||
|
||||
let isAuthorized = false
|
||||
startTime = new Date()
|
||||
do {
|
||||
await delay(1000)
|
||||
if (loginPopup.closed) break
|
||||
isAuthorized =
|
||||
loginPopup.window.location.href.includes('SASLogon') ||
|
||||
loginPopup.window.document.body?.innerText?.includes(
|
||||
'You have signed in.'
|
||||
)
|
||||
elapsedSeconds = (new Date().valueOf() - startTime.valueOf()) / 1000
|
||||
} while (!isAuthorized && elapsedSeconds < 5 * 60)
|
||||
|
||||
return { isLoggedIn: isLoggedIn && isAuthorized }
|
||||
}
|
||||
|
||||
export const isLoggedInSASVIYA = () =>
|
||||
document.cookie.includes('Current-User') && document.cookie.includes('userId')
|
||||
Reference in New Issue
Block a user