UNPKG

jsforce

Version:

Salesforce API Library for JavaScript

307 lines (287 loc) 8.34 kB
/** * */ import {createHash, randomBytes} from 'crypto'; import querystring from 'querystring'; import Transport, {HttpProxyTransport, XdProxyTransport} from './transport'; import {Optional} from './types'; const defaultOAuth2Config = { loginUrl: 'https://login.salesforce.com', }; // Makes a nodejs base64 encoded string compatible with rfc4648 alternative encoding for urls. // @param base64Encoded a nodejs base64 encoded string function base64UrlEscape(base64Encoded: string): string { // builtin node js base 64 encoding is not 64 url compatible. // See https://toolsn.ietf.org/html/rfc4648#section-5 return base64Encoded .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } /** * type defs */ export type OAuth2Config = { clientId?: string; clientSecret?: string; redirectUri?: string; loginUrl?: string; authzServiceUrl?: string; tokenServiceUrl?: string; revokeServiceUrl?: string; proxyUrl?: string; httpProxy?: string; useVerifier?: boolean; }; export type AuthzRequestParams = { scope?: string; state?: string; code_challenge?: string; } & { [attr: string]: string; }; export type TokenResponse = { token_type: 'Bearer'; /** * Identity URL * * The format of the URL is https://login.salesforce.com/id/orgID/userID. */ id: string; access_token: string; refresh_token?: string; signature: string; issued_at: string; instance_url: string; sfdc_community_url?: string; sfdc_community_id?: string; }; /** * OAuth2 class */ export class OAuth2 { loginUrl: string; authzServiceUrl: string; tokenServiceUrl: string; revokeServiceUrl: string; clientId: Optional<string>; clientSecret: Optional<string>; redirectUri: Optional<string>; codeVerifier: Optional<string>; _transport: Transport; /** * */ constructor(config: OAuth2Config) { const { loginUrl, authzServiceUrl, tokenServiceUrl, revokeServiceUrl, clientId, clientSecret, redirectUri, proxyUrl, httpProxy, useVerifier, } = config; if (authzServiceUrl && tokenServiceUrl) { this.loginUrl = authzServiceUrl.split('/').slice(0, 3).join('/'); this.authzServiceUrl = authzServiceUrl; this.tokenServiceUrl = tokenServiceUrl; this.revokeServiceUrl = revokeServiceUrl || `${this.loginUrl}/services/oauth2/revoke`; } else { this.loginUrl = loginUrl ?? defaultOAuth2Config.loginUrl const maybeSlash = this.loginUrl.endsWith('/') ? '' : '/' this.authzServiceUrl = `${this.loginUrl}${maybeSlash}services/oauth2/authorize` this.tokenServiceUrl = `${this.loginUrl}${maybeSlash}services/oauth2/token` this.revokeServiceUrl = `${this.loginUrl}${maybeSlash}services/oauth2/revoke` } this.clientId = clientId; this.clientSecret = clientSecret; this.redirectUri = redirectUri; if (proxyUrl) { this._transport = new XdProxyTransport(proxyUrl); } else if (httpProxy) { this._transport = new HttpProxyTransport(httpProxy); } else { this._transport = new Transport(); } if (useVerifier) { // Set a code verifier string for OAuth authorization this.codeVerifier = base64UrlEscape( randomBytes(Math.ceil(128)).toString('base64'), ); } } /** * Get Salesforce OAuth2 authorization page URL to redirect user agent. */ getAuthorizationUrl(params: AuthzRequestParams = {}) { if (this.codeVerifier) { // code verifier must be a base 64 url encoded hash of 128 bytes of random data. Our random data is also // base 64 url encoded. See Connection.create(); params.code_challenge = base64UrlEscape( createHash('sha256').update(this.codeVerifier).digest('base64'), ); } const _params = { ...params, response_type: 'code', client_id: this.clientId, redirect_uri: this.redirectUri, }; return ( this.authzServiceUrl + (this.authzServiceUrl.includes('?') ? '&' : '?') + querystring.stringify(_params as { [name: string]: any }) ); } /** * OAuth2 Refresh Token Flow */ async refreshToken(refreshToken: string): Promise<TokenResponse> { if (!this.clientId) { throw new Error('No OAuth2 client id information is specified'); } const params: { [prop: string]: string } = { grant_type: 'refresh_token', refresh_token: refreshToken, client_id: this.clientId, }; if (this.clientSecret) { params.client_secret = this.clientSecret; } const ret = await this._postParams(params); return ret as TokenResponse; } /** * Send access token request to the token endpoint. * When a code (string) is passed in first argument, it will use Web Server Authentication Flow (Authorization Code Grant). * Otherwise, it will use the specified `grant_type` and pass parameters to the endpoint. */ async requestToken( codeOrParams: string | { grant_type: string; [name: string]: string }, params: { [prop: string]: string } = {}, ): Promise<TokenResponse> { if ( typeof codeOrParams === 'string' && (!this.clientId || !this.redirectUri) ) { throw new Error( 'No OAuth2 client id or redirect uri configuration is specified', ); } const _params: { [prop: string]: string } = { ...params, ...(typeof codeOrParams === 'string' ? { grant_type: 'authorization_code', code: codeOrParams } : codeOrParams), }; if (this.clientId) { _params.client_id = this.clientId; } if (this.clientSecret) { _params.client_secret = this.clientSecret; } if (this.redirectUri) { _params.redirect_uri = this.redirectUri; } const ret = await this._postParams(_params); return ret as TokenResponse; } /** * OAuth2 Username-Password Flow (Resource Owner Password Credentials) */ async authenticate( username: string, password: string, ): Promise<TokenResponse> { if (!this.clientId || !this.clientSecret) { throw new Error('No valid OAuth2 client configuration set'); } const ret = await this._postParams({ grant_type: 'password', username, password, client_id: this.clientId, client_secret: this.clientSecret, }); return ret as TokenResponse; } /** * OAuth2 Revoke Session Token */ async revokeToken(token: string): Promise<void> { const response = await this._transport.httpRequest({ method: 'POST', url: this.revokeServiceUrl, body: querystring.stringify({ token }), headers: { 'content-type': 'application/x-www-form-urlencoded', }, }); if (response.statusCode >= 400) { let res: any = querystring.parse(response.body); if (!res || !res.error) { res = { error: `ERROR_HTTP_${response.statusCode}`, error_description: response.body, }; } throw new (class extends Error { constructor({ error, error_description, }: { error: string; error_description: string; }) { super(error_description); this.name = error; } })(res); } } /** * @private */ async _postParams(params: { [name: string]: string }): Promise<any> { if (this.codeVerifier) params.code_verifier = this.codeVerifier; const response = await this._transport.httpRequest({ method: 'POST', url: this.tokenServiceUrl, body: querystring.stringify(params), headers: { 'content-type': 'application/x-www-form-urlencoded', }, }); let res; try { res = JSON.parse(response.body); } catch (e) { /* eslint-disable no-empty */ } if (response.statusCode >= 400) { res = res || { error: `ERROR_HTTP_${response.statusCode}`, error_description: response.body, }; throw new (class extends Error { constructor({ error, error_description, }: { error: string; error_description: string; }) { super(error_description); this.name = error; } })(res); } return res; } } export default OAuth2;