From a344767d341641c10b2e6964a9193a812db714f4 Mon Sep 17 00:00:00 2001 From: Peter van Gulik Date: Tue, 1 Aug 2023 18:42:52 +0200 Subject: [PATCH] feat: re-implement OAuth2 authentication --- packages/salesforce/src/connection/oath2.ts | 119 +++++++++++++++++--- 1 file changed, 102 insertions(+), 17 deletions(-) diff --git a/packages/salesforce/src/connection/oath2.ts b/packages/salesforce/src/connection/oath2.ts index 26d7562c..b65fc21c 100644 --- a/packages/salesforce/src/connection/oath2.ts +++ b/packages/salesforce/src/connection/oath2.ts @@ -1,40 +1,126 @@ import { LogManager } from "@vlocode/core"; -import { CustomError, decorate } from "@vlocode/util"; -import { OAuth2 } from 'jsforce'; +import { CustomError } from "@vlocode/util"; import { HttpTransport } from './httpTransport'; -import { SalesforceConnection } from './salesforceConnection'; -interface OAuth2TokenResponse { +interface OAuth2TokenResponse { id: string; instance_url: string; access_token: string; refresh_token: string; } -export class SalesforceOAuth2 extends decorate(OAuth2) { +interface SalesforceOAuth2Options { + loginUrl?: string; + authzServiceUrl?: string; + tokenServiceUrl?: string; + revokeServiceUrl?: string; + clientId: string; + clientSecret?: string; + redirectUri: string; +} + +export class SalesforceOAuth2 { + + private readonly transport: HttpTransport; - private transport: HttpTransport; + public readonly loginUrl: string; + public readonly authzServiceUrl: string; + public readonly tokenServiceUrl: string; + public readonly revokeServiceUrl: string; - constructor(oauth: OAuth2, connection: SalesforceConnection) { - super(oauth); + public readonly clientId: string; + public readonly clientSecret: string; + public readonly redirectUri: string; + + constructor(options: SalesforceOAuth2Options) { + if (options.authzServiceUrl && options.tokenServiceUrl) { + this.loginUrl = options.authzServiceUrl.split('/').slice(0, 3).join('/'); + this.authzServiceUrl = options.authzServiceUrl; + this.tokenServiceUrl = options.tokenServiceUrl; + this.revokeServiceUrl = options.revokeServiceUrl ?? `${this.loginUrl}/services/oauth2/revoke`; + } else { + if (!options.loginUrl) { + throw new Error('Cannot create OAuth instance without setting the loginUrl'); + } + this.loginUrl = options.loginUrl; + this.authzServiceUrl = `${this.loginUrl}/services/oauth2/authorize`; + this.tokenServiceUrl = `${this.loginUrl}/services/oauth2/token`; + this.revokeServiceUrl = `${this.loginUrl}/services/oauth2/revoke`; + } this.transport = new HttpTransport({ handleCookies: false, // OAuth endpoints do not support gzip encoding useGzipEncoding: false, shouldKeepAlive: false, - instanceUrl: connection.instanceUrl, - baseUrl: connection._baseUrl() + instanceUrl: options.loginUrl, + baseUrl: options.loginUrl }, LogManager.get(SalesforceOAuth2)); + + this.clientId = options.clientId; + this.clientSecret = options.clientSecret!; + this.redirectUri = options.redirectUri; + } + + public getAuthorizationUrl(params?: { scope?: string | undefined; state?: string | undefined; }): string { + const authzParams: Record = { + response_type: 'code', + client_id: this.clientId, + redirect_uri: this.redirectUri, + ...params + } + const queryString = this.transport.toQueryString(authzParams); + return `${this.authzServiceUrl}${this.authzServiceUrl.includes('?') ? '&' : '?'}${queryString}`; + } + + public requestToken(code: string): Promise; + public requestToken(code: string, extraParams?: Record): Promise { + const params: Record = { + grant_type: 'authorization_code', + code, + client_id: this.clientId, + redirect_uri: this.redirectUri, + ...extraParams + } + if (this.clientSecret) { + params.client_secret = this.clientSecret; + } + return this.post(params, { url: this.revokeServiceUrl }); + } + + public authenticate(username: string, password: string): Promise { + const params: Record = { + grant_type: 'password', + username : username, + password : password, + client_id: this.clientId, + redirect_uri: this.redirectUri, + } + if (this.clientSecret) { + params.client_secret = this.clientSecret; + } + return this.post(params, { url: this.revokeServiceUrl }); + } + + revokeToken(token: string): Promise { + return this.post({ token }, { url: this.revokeServiceUrl }); } /** * Refreshes the oauth token and returns an OAuth2TokenResponse object. - * @param code session token + * @param refreshToken The refresh token used to get a new access token * @returns New access token */ - refreshToken(code: string): Promise { - return this.inner.refreshToken(code) as Promise; + public refreshToken(refreshToken: string): Promise { + const params: Record = { + grant_type : "refresh_token", + refresh_token : refreshToken, + client_id : this.clientId + }; + if (this.clientSecret) { + params.client_secret = this.clientSecret; + } + return this.post(params); } /** @@ -42,21 +128,20 @@ export class SalesforceOAuth2 extends decorate(OAuth2) { * @param params Params as object send as URL encoded data * @returns Response body as JSON object */ - private async _postParams(params: Record) { + private async post(params: Record, options?: { url?: string; }): Promise { const response = await this.transport.httpRequest({ method: 'POST', - url: this.tokenServiceUrl, + url: options?.url ?? this.tokenServiceUrl, headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: Object.entries(params).map(([v,k]) => `${v}=${encodeURIComponent(k)}`).join('&'), + body: this.transport.toQueryString(params), }); if (response.statusCode && response.statusCode >= 400) { if (typeof response.body === 'object') { throw new CustomError(response.body['error_description'], { name: response.body['error'] }); } - throw new CustomError(response.body ?? '(SalesforceOAuth2) No response from server', { name: `ERROR_HTTP_${response.statusCode}` });