From 4b6445d524842de7b113a2bdffc531b1883a7af0 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Thu, 25 May 2023 10:27:54 +0300 Subject: [PATCH] feat: improved error message for requests related to tokens operations --- src/auth/getAccessTokenForSasjs.ts | 14 +++- src/auth/getAccessTokenForViya.ts | 32 +++++++--- src/auth/getTokenRequestErrorPrefix.ts | 88 ++++++++++++++++++++++++++ src/auth/getTokens.ts | 3 + src/auth/refreshTokensForSasjs.ts | 12 +++- src/auth/refreshTokensForViya.ts | 17 ++++- 6 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 src/auth/getTokenRequestErrorPrefix.ts diff --git a/src/auth/getAccessTokenForSasjs.ts b/src/auth/getAccessTokenForSasjs.ts index 7b080bf..f91d9ad 100644 --- a/src/auth/getAccessTokenForSasjs.ts +++ b/src/auth/getAccessTokenForSasjs.ts @@ -1,5 +1,7 @@ import { prefixMessage } from '@sasjs/utils/error' import { RequestClient } from '../request/RequestClient' +import { getTokenRequestErrorPrefix } from './getTokenRequestErrorPrefix' +import { ServerType } from '@sasjs/utils' /** * Exchanges the auth code for an access token for the given client. @@ -31,6 +33,16 @@ export async function getAccessTokenForSasjs( } }) .catch((err) => { - throw prefixMessage(err, 'Error while getting access token. ') + throw prefixMessage( + err, + getTokenRequestErrorPrefix( + 'fetching access token', + 'getAccessTokenForSasjs', + ServerType.Sasjs, + url, + data, + clientId + ) + ) }) } diff --git a/src/auth/getAccessTokenForViya.ts b/src/auth/getAccessTokenForViya.ts index 508c767..c1d4243 100644 --- a/src/auth/getAccessTokenForViya.ts +++ b/src/auth/getAccessTokenForViya.ts @@ -1,11 +1,13 @@ -import { SasAuthResponse } from '@sasjs/utils/types' +import { SasAuthResponse, ServerType } from '@sasjs/utils/types' import { prefixMessage } from '@sasjs/utils/error' import { RequestClient } from '../request/RequestClient' import { CertificateError } from '../types/errors' +import { getTokenRequestErrorPrefix } from './getTokenRequestErrorPrefix' +// TODO: update func docs /** - * Exchanges the auth code for an access token for the given client. - * @param requestClient - the pre-configured HTTP request client + * Exchange the auth code for access / refresh tokens for the given client / secret pair. + * @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 auth code received from the server. @@ -16,29 +18,43 @@ export async function getAccessTokenForViya( clientSecret: string, authCode: string ): Promise { - const url = '/SASLogon/oauth/token' let token + if (typeof Buffer === 'undefined') { token = btoa(clientId + ':' + clientSecret) } else { token = Buffer.from(clientId + ':' + clientSecret).toString('base64') } + + const url = '/SASLogon/oauth/token' const headers = { Authorization: 'Basic ' + token, Accept: 'application/json' } - - const data = new URLSearchParams({ + const dataJson = { grant_type: 'authorization_code', code: authCode - }) + } + const data = new URLSearchParams(dataJson) const authResponse = await requestClient .post(url, data, undefined, 'application/x-www-form-urlencoded', headers) .then((res) => res.result as SasAuthResponse) .catch((err) => { if (err instanceof CertificateError) throw err - throw prefixMessage(err, 'Error while getting access token. ') + throw prefixMessage( + err, + getTokenRequestErrorPrefix( + 'fetching access token', + 'getAccessTokenForViya', + ServerType.SasViya, + url, + dataJson, + headers, + clientId, + clientSecret + ) + ) }) return authResponse diff --git a/src/auth/getTokenRequestErrorPrefix.ts b/src/auth/getTokenRequestErrorPrefix.ts new file mode 100644 index 0000000..bf2b313 --- /dev/null +++ b/src/auth/getTokenRequestErrorPrefix.ts @@ -0,0 +1,88 @@ +import { ServerType } from '@sasjs/utils/types' + +type Server = ServerType.SasViya | ServerType.Sasjs +type Operation = 'fetching access token' | 'refreshing tokens' + +const getServerName = (server: Server) => + server === ServerType.SasViya ? 'Viya' : 'Sasjs' + +const getResponseTitle = (server: Server) => + `Response from ${getServerName(server)} is below.` + +/** + * Forms error prefix for requests related to token operations. + * @param operation - string describing operation ('fetching access token' or 'refreshing tokens'). + * @param funcName - name of the function sent the request. + * @param server - server type (SASVIYA or SASJS). + * @param url - endpoint used to send the request. + * @param data - request payload. + * @param headers - request headers. + * @param clientId - client ID to authenticate with. + * @param clientSecret - client secret to authenticate with. + * @returns - string containing request information. Example: + * Error while fetching access token from /SASLogon/oauth/token + * Thrown by the @sasjs/adapter getAccessTokenForViya function. + * Payload: + * { + * "grant_type": "authorization_code", + * "code": "example_code" + * } + * Headers: + * { + * "Authorization": "Basic NEdMQXBwOjRHTEFwcDE=", + * "Accept": "application/json" + * } + * ClientId: exampleClientId + * ClientSecret: exampleClientSecret + * + * Response from Viya is below. + * Auth error: { + * "error": "invalid_token", + * "error_description": "No scopes were granted" + * } + */ +export const getTokenRequestErrorPrefix = ( + operation: Operation, + funcName: string, + server: Server, + url: string, + data?: {}, + headers?: {}, + clientId?: string, + clientSecret?: string +) => { + const stringify = (obj: {}) => JSON.stringify(obj, null, 2) + + const lines = [ + `Error while ${operation} from ${url}`, + `Thrown by the @sasjs/adapter ${funcName} function.` + ] + + if (data) { + lines.push('Payload:') + lines.push(stringify(data)) + } + if (headers) { + lines.push('Headers:') + lines.push(stringify(headers)) + } + if (clientId) lines.push(`ClientId: ${clientId}`) + if (clientSecret) lines.push(`ClientSecret: ${clientSecret}`) + + lines.push('') + lines.push(`${getResponseTitle(server)}`) + lines.push('') + + return lines.join(`\n`) +} + +/** + * Parse error prefix to get response payload. + * @param prefix - error prefix generated by getTokenRequestErrorPrefix function. + * @param server - server type (SASVIYA or SASJS). + * @returns - response payload. + */ +export const getTokenRequestErrorPrefixResponse = ( + prefix: string, + server: ServerType.SasViya | ServerType.Sasjs +) => prefix.split(`${getResponseTitle(server)}\n`).pop() as string diff --git a/src/auth/getTokens.ts b/src/auth/getTokens.ts index b4557e2..a655817 100644 --- a/src/auth/getTokens.ts +++ b/src/auth/getTokens.ts @@ -22,6 +22,7 @@ export async function getTokens( ): Promise { const logger = process.logger || console let { access_token, refresh_token, client, secret } = authConfig + if ( isAccessTokenExpiring(access_token) || isRefreshTokenExpiring(refresh_token) @@ -29,6 +30,7 @@ export async function getTokens( if (hasTokenExpired(refresh_token)) { const error = 'Unable to obtain new access token. Your refresh token has expired.' + logger.error(error) throw new Error(error) @@ -47,5 +49,6 @@ export async function getTokens( : await refreshTokensForSasjs(requestClient, refresh_token) ;({ access_token, refresh_token } = tokens) } + return { access_token, refresh_token, client, secret } } diff --git a/src/auth/refreshTokensForSasjs.ts b/src/auth/refreshTokensForSasjs.ts index bf4818c..ec7e1b0 100644 --- a/src/auth/refreshTokensForSasjs.ts +++ b/src/auth/refreshTokensForSasjs.ts @@ -1,5 +1,7 @@ import { prefixMessage } from '@sasjs/utils/error' import { RequestClient } from '../request/RequestClient' +import { getTokenRequestErrorPrefix } from './getTokenRequestErrorPrefix' +import { ServerType } from '@sasjs/utils' /** * Exchanges the refresh token for an access token for the given client. @@ -28,7 +30,15 @@ export async function refreshTokensForSasjs( } }) .catch((err) => { - throw prefixMessage(err, 'Error while refreshing tokens: ') + throw prefixMessage( + err, + getTokenRequestErrorPrefix( + 'refreshing tokens', + 'refreshTokensForSasjs', + ServerType.Sasjs, + url + ) + ) }) return authResponse diff --git a/src/auth/refreshTokensForViya.ts b/src/auth/refreshTokensForViya.ts index 4fb8f80..2ed0c3a 100644 --- a/src/auth/refreshTokensForViya.ts +++ b/src/auth/refreshTokensForViya.ts @@ -1,8 +1,9 @@ -import { SasAuthResponse } from '@sasjs/utils/types' +import { SasAuthResponse, ServerType } from '@sasjs/utils/types' import { prefixMessage } from '@sasjs/utils/error' import * as NodeFormData from 'form-data' import { RequestClient } from '../request/RequestClient' import { isNode } from '../utils' +import { getTokenRequestErrorPrefix } from './getTokenRequestErrorPrefix' /** * Exchanges the refresh token for an access token for the given client. @@ -46,7 +47,19 @@ export async function refreshTokensForViya( ) .then((res) => res.result) .catch((err) => { - throw prefixMessage(err, 'Error while refreshing tokens: ') + throw prefixMessage( + err, + getTokenRequestErrorPrefix( + 'refreshing tokens', + 'refreshTokensForViya', + ServerType.SasViya, + url, + formData, + headers, + clientId, + clientSecret + ) + ) }) return authResponse