diff --git a/README.md b/README.md index 88f8b177..020aecde 100644 --- a/README.md +++ b/README.md @@ -71,13 +71,16 @@ Follow the instructions below to use the library : 1. Install the NPM package: ```sh - npm install intuit-oauth --save + npm install intuit-oauth-ts --save ``` - + or + ```sh + yarn install intuit-oauth-ts + ``` 2. Require the Library: ```js - const OAuthClient = require('intuit-oauth'); + import OAuthClient from 'intuit-oauth'; const oauthClient = new OAuthClient({ clientId: '', diff --git a/package.json b/package.json index 729373d0..4a072b46 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,11 @@ { - "name": "intuit-oauth", - "version": "4.0.0", + "name": "intuit-oauth-ts", + "version": "0.0.4", "description": "Intuit Node.js client for OAuth2.0 and OpenIDConnect", "main": "./src/OAuthClient.js", + "typings": "./src/OAuthClient.d.ts", + "author": "bigbizze", + "license": "MIT", "scripts": { "start": "node index.js", "karma": "karma start karma.conf.js", @@ -14,9 +17,7 @@ "test-watch": "mocha --watch --reporter=spec", "test-debug": "mocha --inspect-brk --watch test", "show-coverage": "npm test; open -a 'Google Chrome' coverage/index.html", - "clean-install": "rm -rf node_modules && npm install", - "snyk-protect": "snyk protect", - "prepublish": "npm run snyk-protect" + "clean-install": "rm -rf node_modules && npm install" }, "keywords": [ "intuit-oauth", @@ -55,27 +56,23 @@ }, "repository": { "type": "git", - "url": "https://github.com/intuit/oauth-jsclient.git" + "url": "https://github.com/bigbizze/oauth-jsclient.git" }, - "author": { - "name": "Anil Kumar", - "email": "anil_kumar3@intuit.com" - }, - "license": "Apache-2.0", "bugs": { - "url": "https://github.com/intuit/oauth-jsclient/issues" + "url": "https://github.com/bigbizze/oauth-jsclient/issues" }, - "homepage": "https://github.com/intuit/oauth-jsclient", + "homepage": "https://github.com/bigbizze/oauth-jsclient", "dependencies": { "atob": "2.1.2", "csrf": "^3.0.4", - "jsonwebtoken": "^8.3.0", + "jsonwebtoken": "^9.0.0", "popsicle": "10.0.1", "query-string": "^6.12.1", "rsa-pem-from-mod-exp": "^0.8.4", "winston": "^3.1.0" }, "devDependencies": { + "@types/jsonwebtoken": "^9.0.1", "btoa": "^1.2.1", "chai": "^4.1.2", "chai-as-promised": "^7.1.1", @@ -87,8 +84,6 @@ "nock": "^9.2.3", "nyc": "^15.0.1", "prettier": "^2.0.5", - "sinon": "^9.0.2", - "snyk": "^1.316.1" - }, - "snyk": true + "sinon": "^9.0.2" + } } diff --git a/src/OAuthClient.d.ts b/src/OAuthClient.d.ts new file mode 100644 index 00000000..4d49f2c8 --- /dev/null +++ b/src/OAuthClient.d.ts @@ -0,0 +1,95 @@ +import * as winston from "winston"; +import * as popsicle from 'popsicle'; +import * as jwt from 'jsonwebtoken'; +import { AuthResponse } from "./response/AuthResponse"; +import { Token } from "./access-token/Token"; +import * as Csrf from 'csrf'; + +interface Scopes { + Accounting: 'com.intuit.quickbooks.accounting', + Payment: 'com.intuit.quickbooks.payment', + Payroll: 'com.intuit.quickbooks.payroll', + TimeTracking: 'com.intuit.quickbooks.payroll.timetracking', + Benefits: 'com.intuit.quickbooks.payroll.benefits', + Profile: 'profile', + Email: 'email', + Phone: 'phone', + Address: 'address', + OpenId: 'openid', + Intuit_name: 'intuit_name', +} +interface Environment { + sandbox: 'https://sandbox-quickbooks.api.intuit.com/', + production: 'https://quickbooks.api.intuit.com/', +} + +export default class OAuthClient { + static cacheId: 'cacheID'; + static authorizeEndpoint: 'https://appcenter.intuit.com/connect/oauth2'; + static tokenEndpoint: 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer'; + static revokeEndpoint: 'https://developer.api.intuit.com/v2/oauth2/tokens/revoke'; + static userinfo_endpoint_production: + 'https://accounts.platform.intuit.com/v1/openid_connect/userinfo'; + static userinfo_endpoint_sandbox: + 'https://sandbox-accounts.platform.intuit.com/v1/openid_connect/userinfo'; + static migrate_sandbox: 'https://developer-sandbox.api.intuit.com/v2/oauth2/tokens/migrate'; + static migrate_production: 'https://developer.api.intuit.com/v2/oauth2/tokens/migrate'; + static environment: Environment; + static jwks_uri: 'https://oauth.platform.intuit.com/op/v1/jwks'; + static user_agent: string; + static scopes: Scopes; + environment: keyof Environment; + clientId: string; + clientSecret: string; + redirectUri: string; + token: Token; + logging: boolean; + logger: winston.Logger | null; + state: Csrf; + + constructor(params: { + clientId: string, + clientSecret: string, + environment: keyof Environment, + redirectUri: string + }); + getJson(): Record; + setToken(opts: { + access_token: string; + refresh_token: string; + token_type: string; + expires_in: number; + x_refresh_token_expires_in: number; + id_token?: string; + createdAt?: number + }): Token; + setAuthorizeURLs(params: { + authorizeEndpoint: string; + tokenEndpoint: string; + revokeEndpoint: string; + userInfoEndpoint: string; + }): this; + authorizeUri(opts: { + scope: Scopes[keyof Scopes][], + state: string + }): string; + createToken(uri: string): Promise; + refresh(): Promise; + refreshUsingToken(refreshToken: string): Promise; + revoke(params?: { access_token: string, refresh_token: string }): Promise; + getUserInfo(): Promise; + makeApiCall(params: { transport: popsicle.TransportOptions, url: string, method: string, headers: Record, body: Record }): Promise; + validateIdToken(params?: { id_token: string }): Promise; + getValidatedIdToken(params?: { id_token: string }): Promise; + getKeyFromJWKsURI(id_token: string, kid: string, request: popsicle.Request): Promise; + getPublicKey(): string; + getTokenRequest(request: popsicle.Request): Promise; + validateToken(): boolean; + loadResponse(request: popsicle.Request): Promise; + loadResponseFromJWKsURI(request: popsicle.Request): Promise; + createError(e: Error, authResponse: AuthResponse): Error; + isAccessTokenValid(): boolean; + getToken(): Token; + authHeader(): string; + log(level: keyof winston.config.NpmConfigSetLevels, message: string, messageData: string): void; +} diff --git a/src/OAuthClient.js b/src/OAuthClient.js index 9662c002..dd4bc5a7 100644 --- a/src/OAuthClient.js +++ b/src/OAuthClient.js @@ -463,6 +463,77 @@ OAuthClient.prototype.validateIdToken = function validateIdToken(params = {}) { }); }; +/** + * Validate id_token + * * + * @param {Object} params(optional) + * @returns {Promise} + */ +OAuthClient.prototype.getValidatedIdToken = function getValidatedIdToken(params = {}) { + return new Promise((resolve) => { + if (!this.getToken().id_token) throw new Error('The bearer token does not have id_token'); + + const id_token = this.getToken().id_token || params.id_token; + + // Decode ID Token + const token_parts = id_token.split('.'); + const id_token_header = JSON.parse(atob(token_parts[0])); + const id_token_payload = JSON.parse(atob(token_parts[1])); + + // Step 1 : First check if the issuer is as mentioned in "issuer" + if (id_token_payload.iss !== 'https://oauth.platform.intuit.com/op/v1') return false; + + // Step 2 : check if the aud field in idToken contains application's clientId + if (!id_token_payload.aud.find((audience) => audience === this.clientId)) return false; + + // Step 3 : ensure the timestamp has not elapsed + if (id_token_payload.exp < Date.now() / 1000) return false; + + const request = { + url: OAuthClient.jwks_uri, + method: 'GET', + headers: { + Accept: AuthResponse._jsonContentType, + 'User-Agent': OAuthClient.user_agent, + }, + }; + + return resolve(this.getKeyFromJWKsURI(id_token, id_token_header.kid, request)); + }) + .then((res) => { + this.log('info', 'The validateIdToken () response is : ', JSON.stringify(res, null, 2)); + return res; + }) + .catch((e) => { + this.log('error', 'The validateIdToken () threw an exception : ', JSON.stringify(e, null, 2)); + throw e; + }); +}; + +OAuthClient.prototype.getIdTokenComponents = async function(params = {}) { + if (!this.getToken().id_token) { + throw new Error('The bearer token does not have id_token'); + } + const id_token = this.getToken().id_token || params.id_token; + const token_parts = id_token.split('.'); + const id_token_header = JSON.parse(atob(token_parts[0])); + const id_token_payload = JSON.parse(atob(token_parts[1])); + if (id_token_payload.iss !== 'https://oauth.platform.intuit.com/op/v1') { + throw new Error('The issuer is not as mentioned in "issuer"'); + } + + // Step 2 : check if the aud field in idToken contains application's clientId + if (!id_token_payload.aud.find((audience) => audience === this.clientId)) { + throw new Error('The aud field in idToken does not contain application\'s clientId') + } + + // Step 3 : ensure the timestamp has not elapsed + if (id_token_payload.exp < Date.now() / 1000) { + throw new Error('The timestamp has elapsed'); + } + return { id_token_header, id_token_payload }; +} + /** * Get Key from JWKURI * * diff --git a/src/access-token/Token.d.ts b/src/access-token/Token.d.ts new file mode 100644 index 00000000..9557db49 --- /dev/null +++ b/src/access-token/Token.d.ts @@ -0,0 +1,39 @@ + +export class Token { + realmId: string; + token_type: string; + access_token: string; + refresh_token: string; + expires_in: number; + x_refresh_token_expires_in: number; + id_token: string; + latency: number; + createdAt: number; + constructor(opts?: { + realmId: string, + token_type: string, + access_token: string, + refresh_token: string, + expires_in: number, + x_refresh_token_expires_in: number, + id_token: string, + latency: number, + createdAt: number + }); + isAccessTokenValid(): boolean; + isRefreshTokenValid(): boolean; + accessToken(): string; + refreshToken(): string; + tokenType(): string; + getToken(): Token; + setToken(opts: { + access_token: string; + refresh_token: string; + token_type: string; + expires_in: number; + x_refresh_token_expires_in: number; + id_token?: string; + createdAt?: number + }): this; + clearToken(): this; +} diff --git a/src/response/AuthResponse.d.ts b/src/response/AuthResponse.d.ts new file mode 100644 index 00000000..c91ca3fe --- /dev/null +++ b/src/response/AuthResponse.d.ts @@ -0,0 +1,27 @@ +import * as popsicle from "popsicle"; +import { Token } from "../access-token/Token"; + +export class AuthResponse { + response: popsicle.Response; + body: string; + json: Record; + intuit_tid: string; + token: Token; + constructor(params: { + token?: Token, + response?: popsicle.Response, + body?: string, + intuit_id?: string + }); + processResponse(response: popsicle.Response): void; + getToken(): Token; + text(): string; + status(): number; + headers(): Record; + valid(): boolean; + getJson(): Record; + get_intuit_tid(): string; + isContentType(): boolean; + getContentType(): string; + isJson(): boolean; +}