From f0a355d8d5e17acafb5938df3cf64e3448de6e8d Mon Sep 17 00:00:00 2001 From: Luka Bulatovic Date: Fri, 16 Dec 2022 17:07:31 +0100 Subject: [PATCH] Added Nylas Session to nylas-js repo --- package.json | 10 + src/identity/README.md | 348 ++++++++++++++++++++ src/identity/app.ts | 488 ++++++++++++++++++++++++++++ src/identity/client/localStorage.ts | 23 ++ src/identity/client/store.ts | 88 +++++ src/identity/helpers/index.ts | 3 + src/identity/index.ts | 1 + src/identity/types/index.ts | 71 ++++ src/index.ts | 1 - 9 files changed, 1032 insertions(+), 1 deletion(-) create mode 100644 src/identity/README.md create mode 100644 src/identity/app.ts create mode 100644 src/identity/client/localStorage.ts create mode 100644 src/identity/client/store.ts create mode 100644 src/identity/helpers/index.ts create mode 100644 src/identity/index.ts create mode 100644 src/identity/types/index.ts diff --git a/package.json b/package.json index ea5c03f..320f8a2 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,24 @@ "lint:prettier": "prettier --write '**/*.{ts,js}'", "lint:prettier:check": "prettier --check '**/*.{ts,js}'" }, + "moduleResolution": "node", "devDependencies": { + "@types/jwt-decode": "^3.1.0", + "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.30.7", + "@typescript-eslint/parser": "^5.30.0", "eslint": "^8.20.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", "prettier": "^2.7.1", "typescript": "^4.7.4" }, + "dependencies": { + "buffer": "^6.0.3", + "js-sha256": "^0.9.0", + "jwt-decode": "^3.1.2", + "uuid": "^9.0.0" + }, "repository": { "type": "git", "url": "git+https://github.com/nylas/nylas-js.git" diff --git a/src/identity/README.md b/src/identity/README.md new file mode 100644 index 0000000..362e539 --- /dev/null +++ b/src/identity/README.md @@ -0,0 +1,348 @@ +# Nylas Identity + +SDK to handle uas sessions & auth + +## Introduction + +Nylas Identity is used to handle OAuth flow requests & sessions from UAS to the JS client + +## Table of content + +- [Install ](#install) +- [Initialization](#initialization) + - [Session config](#session-config) +- [Code challenge generation](#code-challenge) +- [Code exchange flow](#code-excahnge) +- [API](#api) + - [auth](#auth) + - [Config](#auth-config) + - [authIMAP](#auth-imap) + - [Payload](#auth-imap-payload) + - [Responses](#auth-imap-response) + - [isLoggedIn](#is-logged-in) + - [detectEmail](#detect-email) + - [Responses ](#detect-emai-response) + - [applicationInfo ](#application-info) + - [Responses](#application-info-response) + - [getAvailableProviders](#get-available-providers) + - [Responses](#get-available-providers-response) + - [getProfile](#get-profile) + - [getScopes](#get-scopes) + - [validateToken](#validate-token) + - [logout](#logout) + - [Events](#events) + +## Install + +TODO + +## Initialization + +When initialized the **NylasSessions** + +```js +const session = new NylasSessions({ + ClientID: "example_id", + RedirectURI: "http://localhost:3000/", +}); +``` + +### Session config + +Session config is used to init the identity library +Prop name | Type | Required | Description +--- | --- | --- | --- +ClientID | `string` | `true` | Nylas Client ID +RedirectURI | `string` | `true` | RedirectURI of your app +AccessType | `string` | `false` | Type of access you request from token (defaults to offline) +Domain | `string` | `false` | Your Nylas Auth domain +Store | `Store` | `false` | Set a store for handling sessions (defaults to localStorage) +Hosted | `boolean` | `false` | Set if you want to use hosted page instead of your own implementation + +## Code challenge generation + +Nylas Identity generates a PKCE code on the fyl upon initialization inside `localStorage`. If the user has no ongoing session & code is not present inside storge it generates a `uuid` that will represent the `code_challenge`. When the `auth` method is called we get the `base64` encoded challenge and also encrypt it with `SHA256`. If the login fails the code challege stays the same. When the user authenticates & afterwords logges out a new code challenge will be generated. + +## Code exchange flow + +Code exchange with Nylas Identity works by detecting that the redirect url is present & also that eather the flow returned an error or the code it extracts the code challenge from storage and attempts the code exchange if successful it will set the JWT token and if it fails it will fire the `onLoginFail()` event. + +## API + +All methods that are needed to interact with UAS authentication & sessions for client side apps. + +### auth + +`auth` method is used to generate a link for an OAuth provider or in the case of hosted oauth enabled generate a link to UAS hosted login screen or is `popup` prop is set also open that link inside a popup window instead of returning a link + +```js +const link = await session.auth({ + Proivder: "google", +}); +``` + +#### Auth config + +Auth config is used to configure the URL of the OAuth provider or Hosted url if hosted is enabled +Prop name | Type | Required | Description +--- | --- | --- | --- +Proivder | `string` | `true` | Nylas Client ID +Scope | `Array` | `false` | Scope overrides the default scope set in the Integration creation process +LoginHint | `string` | `false` | Set the email that will be used to scope provider suggestions +Metadata | `object` | `false` | Set additional metadata to be passed +Settings | `object` | `false` | Set additional settings to be passed +Hosted | `boolean` | `false` | Set if you want to use hosted page instead of your own implementation +Prompt | `string` | `false` | Only applies if you are using Hosted auth +Popup | `boolean` | `false` | Set if you want to open a popup instead of getting the link to the provider + +### authIMAP + +Used to authenticate IMAP emails. On success returnes a redirect url when the user redirects to it the authentication is finished (code excahnge is done). + +```js +const link = await session.authIMAP({ + Proivder: "google", +}); +``` + +#### IMAP payload + +Used to authenticate the user on the IMAP server specified in the payload. Note: Can only be used if you have an IMAP integration set, also the `getAvailableProviders` method returns IMAP providers with server configuration +Prop name | Type | Required | Description +--- | --- | --- | --- +imap_username | `string` | `true` | Email of IMAP account +imap_password | `string` | `true` | Password of the account +host | `string` | `true` | Host of IMAP server +port | `int` | `true` | Port of IMAP server +type | `string` | `true` | Type of IMAP provider (if the user provides IMAP server information set to `generic`) +smtp_host | `string` | `true` | Host of SMTP server +smtp_port | `int` | `true` | Host of SMTP server + +#### IMAP responses + +Successful response + +```json +{ + "success": true, + "data": { + "BaseURL": "http://localhost:3000?code=example_code" + } +} +``` + +Failed response + +```json +{ + "success": false, + "error": { + "type": "invalid_authentication", + "http_code": 400, + "event_code": 25022, + "message": "Authentication failed due to wrong input or credentials", + "request_id": "dummy_request_id" + } +} +``` + +### isLoggedIn + +Checks if the user is logged in (`true`/`false`). + +```js +const email = await session.isLoggedIn(); +``` + +### detectEmail + +Used to detect a provider from the provided email address. + +```js +const email = await session.detectEmail("test@nylas.com"); +``` + +#### Response + +Oauth detected + +```json +{ + "success": true, + "data": { + "provider": "google", + "email_address": "john@nylas.com", + "detected": true + } +} +``` + +IMAP detected + +```json +{ + "success": true, + "data": { + "provider": "imap", + "type": "yahoo", + "email_address": "john@yahoo.com", + "detected": true + } +} +``` + +No provider detected + +```json +{ + "success": true, + "data": { + "email_address": "john@asdad.com", + "detected": false + } +} +``` + +| Prop name | Type | Description | +| ------------- | --------- | ----------------------------------------------------------- | +| email_address | `string` | Email Address that was provided | +| detected | `boolean` | If the email has been paired with a provider | +| provider | `string` | Returns top level provider type (IMAP or an OAuth provider) | +| type | `string` | Returns IMAP type (provider) | + +### applicationInfo + +Returns information about application from the specified `ClientID`. + +```js +const email = await session.applicationInfo(); +``` + +#### Response + +```json +{ + "data": { + "application_id": "example_id", + "name": "UAS App", + "icon_url": "https://inbox-developer-resources.s3.amazonaws.com/icons/example" + } +} +``` + +### getAvailableProviders + +Used to get OAuth & IMAP providers for the specified `ClientID`. + +```js +const providers = await session.getAvailableProviders(); +``` + +#### Response + +```json +[ + { + "name": "Google", + "provider": "google", + "type": "oauth", + "settings": {} + }, + { + "name": "Yahoo", + "provider": "yahoo", + "type": "imap", + "settings": { + "name": "Yahoo", + "imap_host": "imap.mail.yahoo.com", + "imap_port": 993, + "smtp_host": "smtp.mail.yahoo.com", + "smtp_port": 587, + "password_link": "https://help.yahoo.com/kb/learn-generate-password-sln15241.html", + "primary": true + } + } +] +``` + +### getProfile + +If JWT present parses it and returns a profile object + +```js +const profile = await session.getProfile(); +``` + +### getScopes + +If user is logged in returnes authenticated provider scopes + +```js +const scopes = await session.getScopes(); +``` + +### validateToken + +Checks if the users token is valid when logged in (`true`/`false`) + +```js +const isValid = await session.validateToken(); +``` + +### logout + +Destory the current session and loggout the user + +```js +await session.logout(); +``` + +### Events + +Subscribe to events to get information about API interactions. + +#### onLoginSuccess +Returns the response of code exchange +```js +onLoginSuccess((event) => { + console.log(event); +}); +``` + +#### onLoginFail +Returns an error that happened during code exchange +```js +onLoginFail((event) => { + console.log(event); +}); +``` + +#### onLogoutSuccess +Returns the logged out user if needed for re-auth +```js +onLogoutSuccess((event) => { + console.log(event); +}); +``` +#### onTokenRefreshSuccess +Returns the response of token exchange +```js +onTokenRefreshSuccess((event) => { + console.log(event); +}); +``` +#### onTokenRefreshFail +Returns an error that happened during token exchange +```js +onTokenRefreshFail((event) => { + console.log(event); +}); +``` + +#### onSessionExpired +Returns the expired id token +```js +onSessionExpired((event) => { + console.log(event); +}); +``` diff --git a/src/identity/app.ts b/src/identity/app.ts new file mode 100644 index 0000000..d3f3959 --- /dev/null +++ b/src/identity/app.ts @@ -0,0 +1,488 @@ +import { Config, AuthConfig, IDToken, ProviderList } from "./types/index"; +import { Storage } from "./client/store"; +import { sha256 } from "js-sha256"; +import { Buffer } from "buffer"; +import { v4 as uuid } from "uuid"; +import { base64EncodeUrl } from "./helpers/index"; + + +export { Config, AuthConfig }; +export class NylasSessions { + private clientId: string; + private clientSecret?: string; + private redirectUri: string; + private accessType = "offline"; + private domain = "http://api.nylas.com"; + private versioned = false + private Storage: Storage; + + private hosted = false; + + public constructor(config: Config) { + window.addEventListener; + this.clientId = config.clientId; + this.redirectUri = config.redirectUri; + if (config.domain) { + this.domain = config.domain; + const versionedPart = this.domain.substring(this.domain.length - 3) + if (versionedPart.includes("/v")){ + this.versioned = true + } + } + this.Storage = new Storage(); + if (config.hosted) { + this.hosted = config.hosted; + } + this.codeExchange(null) + setInterval(async () => { + const tok = await this.Storage.getIDToken(); + if (tok) { + const timestamp = Math.floor(Date.now() / 1000); + if (tok.exp > timestamp) { + const timeLeft = tok.exp - timestamp + if (timeLeft < 600 && timeLeft % 60 === 0) { // If 10 minutes until token expires we try a re-auth every minute + await this.tokenExchange() + } + } else { + await this.tokenExchange() + const payload: CustomEventInit = { detail: tok }; + window.dispatchEvent(new CustomEvent("onSessionExpired", payload)); + } + } + }, 1000); + } + // Validates ID token expiration + public async validateToken(tok: IDToken | null): Promise { + let token = tok + if (tok){ + token = await this.Storage.getIDToken(); + } + if (!token) { + return false; + } + try { + const response: any = await fetch( + `${this.domain}/connect/tokeninfo?id_token=${token}`, + { + method: "GET", + } + ); + const responseData = await response.json(); + if (!responseData.data){ + + return false + } + return true + } catch (error) { + return false + } + } + + // Gets domain of UAS + public getDomain() { + return this.domain; + } + + // Gets auth link + public async auth(config: AuthConfig) { + if (this.hosted && (this.domain === window.location.origin || (this.versioned && this.domain.includes(window.location.origin)))) { + await this.hostedSetCodeChallenge(); + } + const url = await this.generateAuthURL(config); + if (config.popup) { + this.popUp(url); + return; + } + return url; + } + + // Generates auth URL + private async generateAuthURL(config: AuthConfig): Promise { + const codeChallenge = await this.getCodeChallege(); + let url = `${this.domain}/connect/auth?client_id=${this.clientId}&redirect_uri=${this.redirectUri}&access_type=${this.accessType}&response_type=code`; + if (codeChallenge) { + url += `&code_challenge=${codeChallenge}&code_challenge_method=S256&options=rotate_refresh_token`; + } + if (config.proivder) { + url += `&provider=${config.proivder}`; + } + if (config.loginHint) { + url += `&login_hint=${config.loginHint}`; + } + if (config.scope) { + url += `&scope=${config.scope.join(" ")}`; + } + if (config.prompt) { + url += `&prompt=${config.prompt}`; + } + if (config.metadata) { + url += `&metadata=${config.metadata}`; + } + if (config.state) { + url += `&state=${config.state}`; + } + return url; + } + + // Generates UUID code challenge + private async generateCodeChallenge() { + const codeVerifier = await this.Storage.getPKCE(); + if (codeVerifier) { + return; + } + const codeChallenge = uuid(); + this.Storage.setPKCE(codeChallenge); + return; + } + + // Gets code challenge from URL query params + private async hostedSetCodeChallenge() { + if (!this.hosted) { + throw console.error("Method only used with hosted flag enabled"); + } + const params = new URLSearchParams(window.location.search); + const codeChallenge = params.get("code_challenge"); + if (!codeChallenge) { + const codeVerifier = await this.Storage.getPKCE(); + if (codeVerifier) { + return; + } + console.warn( + "Code challenge is recomended" + ); + return + } + this.Storage.setPKCE(codeChallenge); + } +// Gets code challenge from store + private async getCodeChallege(): Promise { + if (this.hosted && (this.domain === window.location.origin || (this.versioned && this.domain.includes(window.location.origin)))) { + const params = new URLSearchParams(window.location.search); + const codeChallenge = params.get("code_challenge"); + if (!codeChallenge) { + console.warn( + "Code challenge is recomended" + ); + return ""; + } + return codeChallenge; + } + const codeVerifier = await this.Storage.getPKCE(); + if (codeVerifier) { + const codeChallengeHashed = sha256(codeVerifier); + let codeChallengeEncrypted = + Buffer.from(codeChallengeHashed).toString("base64"); + codeChallengeEncrypted = base64EncodeUrl(codeChallengeEncrypted); + return codeChallengeEncrypted; + } + return ""; + } + // checks if user is logged in + public async isLoggedIn(): Promise { + // if hosted identity isLoggedIn always returns false + if (this.hosted && (this.domain === window.location.origin || (this.versioned && this.domain.includes(window.location.origin)))) { + return false; + } + const tok = await this.Storage.getIDToken(); + if (tok) { + const valid = await this.validateToken(tok) + return valid; + } + await this.generateCodeChallenge(); + return false; + } + + // Logges out user removes all instances of user + public async logout() { + const profile = await this.getProfile() + this.Storage.removeIDToken(); + this.Storage.removeRefreshToken(); + this.Storage.removeAccessToken(); + this.Storage.removeScopes(); + const payload: CustomEventInit = { detail: profile }; + window.dispatchEvent(new CustomEvent("onLogoutSuccess", payload)); + } + + // Gets profile info from ID token + public async getProfile(): Promise { + const tok = await this.Storage.getIDToken(); + if (tok) { + return tok; + } + return null; + } + // Gets scopes info from storage + public async getScopes(): Promise { + const tok = await this.Storage.getIDToken(); + if (tok) { + return tok; + } + return null; + } + + // IMAP authentication + public async authIMAP(data: any): Promise { + const codeChallenge = await this.getCodeChallege(); + const payload = { + imap_username: data.username, + imap_password: data.password, + host: data.hostIMAP, + port: data.portIMAP, + type: data.type, + smtp_host: data.hostSMTP, + smtp_port: data.portSMTP, + redirect_uri: this.redirectUri, + code_challenge: codeChallenge, + code_challenge_method: "S256", + public_application_id: this.clientId, + }; + const response: any = await fetch(`${this.domain}/connect/login/imap`, { + method: "POST", // or 'PUT' + headers: new Headers({ "content-type": "application/json" }), + body: JSON.stringify(payload), + }); + const responseData = await response.json(); + return responseData; + } + + // Detects email + public async detectEmail(email: string): Promise { + const response: any = await fetch( + `${this.versioned?this.domain:this.domain+"/connect"}/providers/detect?client_id=${this.clientId}&email=${email}`, + { + method: "POST", // or 'PUT' + headers: new Headers({ "content-type": "application/json" }), + } + ); + const responseData = await response.json(); + return responseData.data; + } + // Gets app info from UAS + public async applicationInfo(): Promise { + const response: any = await fetch( + `${this.versioned?this.domain:this.domain+"/connect"}/applications?client_id=${this.clientId}`, + { + method: "GET", // or 'PUT' + headers: new Headers({ "content-type": "application/json" }), + } + ); + const responseData = await response.json(); + return responseData.data; + } + + // Gets providers form UAS + public async getAvailableProviders(): Promise { + const response: any | undefined = await fetch( + `${this.domain}/connect/providers/find?client_id=${this.clientId}`, + { + method: "GET", // or 'PUT' + headers: new Headers({ "content-type": "application/json" }), + } + ); + if (response) { + const responseData = await response.json(); + const providers = responseData.data; + return providers; + } + return null; + } + // EVENT HOOKS + public onLoginSuccess(callback: any): void { + window.addEventListener("onLoginSuccess", (e) => callback(e)); + } + public onLogoutSuccess(callback: any): void { + window.addEventListener("onLogoutSuccess", (e) => callback(e)); + } + public onLoginFail(callback: any): void { + window.addEventListener("onLoginFail", (e) => callback(e)); + } + public onTokenRefreshSuccess(callback: any): void { + window.addEventListener("onTokenRefreshSuccess", (e) => callback(e)); + } + public onTokenRefreshFail(callback: any): void { + window.addEventListener("onTokenRefreshFail", (e) => callback(e)); + } + public onSessionExpired(callback: any): void { + window.addEventListener("onSessionExpired", (e) => callback(e)); + } + // Exchanges code for ID token and refresh and access tokens + public async codeExchange(search: string | null) { + let params = new URLSearchParams(window.location.search); + if (search) { + params = new URLSearchParams(search); + } + const code = params.get("code"); + const state = params.get("state"); + const error = params.get("error"); + const errorDescription = params.get("error_description"); + const errorCode = params.get("error_code"); + + if (error && errorDescription && errorCode) { + const payload: CustomEventInit = { + detail: { error, errorDescription, errorCode }, + }; + window.dispatchEvent(new CustomEvent("onLoginFail", payload)); + window.history.pushState({}, document.title, window.location.pathname); + return false; + } + if (!code) { + return false; + } + // If popup window stop internal code exchange + if (window.opener && window.name === "uas-popup") { + return false; + } + // Get PKCE code_challenge from local storage + const codeVerifier = await this.Storage.getPKCE(); + if (!codeVerifier) { + return false; + } + // Prepare request + try { + const payload = { + client_id: this.clientId, + redirect_uri: this.redirectUri, + code: code, + grant_type: "authorization_code", + code_verifier: codeVerifier, + }; + const response: any = await fetch(`${this.domain}/connect/token`, { + method: "POST", // or 'PUT' + headers: new Headers({ "content-type": "application/json" }), + body: JSON.stringify(payload), + }); + const responseData = await response.json(); + + // Parse response + // Store ID token + if (responseData) { + if (responseData.error){ + const payload: CustomEventInit = { detail: responseData }; + window.dispatchEvent(new CustomEvent("onLoginFail", payload)); + return true; + } + if (responseData.id_token) { + this.Storage.setIDToken(responseData.id_token); + } + if (responseData.refresh_token) { + this.Storage.setRefreshToken(responseData.refresh_token); + } + if (responseData.access_token) { + this.Storage.setAccessToken(responseData.access_token); + } + if (responseData.scope) { + this.Storage.setScopes(responseData.scope); + } + if (state){ + responseData.state = state + } + const payload: CustomEventInit = { detail: responseData }; + window.dispatchEvent(new CustomEvent("onLoginSuccess", payload)); + window.history.pushState({}, document.title, window.location.pathname); + } + this.Storage.removePKCE(); + return true; + // Remove PKCE code_challenge from local storage + } catch (error: any) { + const payload: CustomEventInit = { detail: error }; + window.dispatchEvent(new CustomEvent("onLoginFail", payload)); + window.history.pushState({}, document.title, window.location.pathname); + return false; + } + } + + // Token Exchange for session maintenece + public async tokenExchange() { + const refresh = await this.Storage.getRefreshToken(); + try { + const payload = { + client_id: this.clientId, + redirect_uri: this.redirectUri, + refresh_token: refresh, + grant_type: "refresh_token", + }; + const response: any = await fetch(`${this.domain}/connect/token`, { + method: "POST", + headers: new Headers({ "content-type": "application/json" }), + body: JSON.stringify(payload), + }); + const responseData = await response.json(); + + // Parse response + // Store ID token + if (responseData) { + if (responseData.error){ + const payload: CustomEventInit = { detail: responseData }; + window.dispatchEvent(new CustomEvent("onTokenRefreshFail", payload)); + return true; + } + if (responseData.id_token) { + this.Storage.setIDToken(responseData.id_token); + } + if (responseData.refresh_token) { + this.Storage.setRefreshToken(responseData.refresh_token); + } + if (responseData.access_token) { + this.Storage.setAccessToken(responseData.access_token); + } + const payload: CustomEventInit = { detail: responseData }; + window.dispatchEvent(new CustomEvent("onTokenRefreshSuccess", payload)); + return true; + } + // Remove PKCE code_challenge from local storage + this.Storage.removePKCE(); + } catch (error: any) { + const payload: CustomEventInit = { detail: error }; + window.dispatchEvent(new CustomEvent("onTokenRefreshFail", payload)); + return false; + } + } + + // Regulates POPUP behaivior + public async popUp(url: string) { + const width = 500; + const height = 600; + const left = window.screenX + (window.outerWidth - width) / 2; + const top = window.screenY + (window.outerHeight - height) / 2.5; + const title = `uas-popup`; + const popupURL = url; + const externalPopup = window.open( + popupURL, + title, + `width=${width},height=${height},left=${left},top=${top}` + ); + if (!externalPopup) { + return; + } + + const timer = setInterval(async () => { + if (externalPopup.closed) { + const payload: CustomEventInit = { + detail: { error_description: "OAuth provider window closed" }, + }; + window.dispatchEvent(new CustomEvent("onLoginFail", payload)); + timer && clearInterval(timer); + return; + } + try { + const currentUrl = externalPopup.location.href.split("?"); + if (!currentUrl[0]) { + return; + } + if (currentUrl[0] === this.redirectUri && currentUrl.length > 1) { + const success = await this.codeExchange( + externalPopup.location.search + ); + externalPopup.close(); + if (success) { + location.reload(); + } + timer && clearInterval(timer); + return; + } + } catch (error) { + return; + } + }, 500); + } +} diff --git a/src/identity/client/localStorage.ts b/src/identity/client/localStorage.ts new file mode 100644 index 0000000..25bd18a --- /dev/null +++ b/src/identity/client/localStorage.ts @@ -0,0 +1,23 @@ +import { Store } from "../types/index"; + +class LocalStorge implements Store { + public get(key: string): Promise { + return new Promise((res) => { + const record = window.localStorage.getItem(key); + res(record); + }); + } + public async remove(key: string): Promise { + window.localStorage.removeItem(key); + return new Promise((res) => { + res(null); + }); + } + public set(key: string, value: string): Promise { + window.localStorage.setItem(key, value); + return new Promise((res) => { + res(null); + }); + } +} +export default LocalStorge; diff --git a/src/identity/client/store.ts b/src/identity/client/store.ts new file mode 100644 index 0000000..5614e9d --- /dev/null +++ b/src/identity/client/store.ts @@ -0,0 +1,88 @@ +import { IDToken, Store } from "../types/index"; +import jwtDecode from "jwt-decode"; +import { Buffer } from "buffer"; + +import local from "./localStorage"; + +const PKCE_KEY = "pkce"; // eslint-disable-line no-use-before-define +const IDTOKEN_KEY = "id_token"; // eslint-disable-line no-use-before-define +const SCOPES_KEY = "scopes"; // eslint-disable-line no-use-before-define +const REFRESH_TOKEN_KEY = "ref_token"; // eslint-disable-line no-use-before-define +const ACCESS_TOKEN_KEY = "acc_token"; // eslint-disable-line no-use-before-define + +export class Storage { + private Storage: Store = new local(); + public constructor(store?: Store) { + // if window object not present set up + if (!window) { + if (store) { + this.Storage = store; + return; + } + throw new Error("No storage set for session handling"); + } + } + public setPKCE(value: string) { + const encrypt = Buffer.from(value); + this.Storage.set(PKCE_KEY, encrypt.toString("base64")); + } + public async getPKCE(): Promise { + try { + const pkce = await this.Storage.get(PKCE_KEY); + if (pkce) { + const b = Buffer.from(pkce, "base64"); + return b.toString("utf8"); + } + } catch (error) { + return null; + } + return null; + } + public removePKCE() { + this.Storage.remove(PKCE_KEY); + } + public setRefreshToken(token: string) { + this.Storage.set(REFRESH_TOKEN_KEY, token); + } + public async getRefreshToken(): Promise { + const tokString = await this.Storage.get(REFRESH_TOKEN_KEY); + return tokString; + } + public removeRefreshToken() { + this.Storage.remove(REFRESH_TOKEN_KEY); + } + public setAccessToken(token: string) { + this.Storage.set(ACCESS_TOKEN_KEY, token); + } + public async getAccessToken(): Promise { + const tokString = await this.Storage.get(ACCESS_TOKEN_KEY); + return tokString; + } + public removeAccessToken() { + this.Storage.remove(ACCESS_TOKEN_KEY); + } + public setIDToken(token: string) { + this.Storage.set(IDTOKEN_KEY, token); + } + public async getIDToken(): Promise { + const tokString = await this.Storage.get(IDTOKEN_KEY); + if (tokString) { + const token: IDToken = jwtDecode(tokString); + return token; + } + return null; + } + public removeIDToken() { + this.Storage.remove(IDTOKEN_KEY); + } + public setScopes(token: string) { + this.Storage.set(SCOPES_KEY, token); + } + public async getScopes(): Promise { + const scopes = await this.Storage.get(SCOPES_KEY); + return scopes; + } + public removeScopes() { + this.Storage.remove(SCOPES_KEY); + } +} diff --git a/src/identity/helpers/index.ts b/src/identity/helpers/index.ts new file mode 100644 index 0000000..61b89de --- /dev/null +++ b/src/identity/helpers/index.ts @@ -0,0 +1,3 @@ +export function base64EncodeUrl(str: string): string { + return str.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} diff --git a/src/identity/index.ts b/src/identity/index.ts new file mode 100644 index 0000000..ac5307d --- /dev/null +++ b/src/identity/index.ts @@ -0,0 +1 @@ +export * from "./app"; diff --git a/src/identity/types/index.ts b/src/identity/types/index.ts new file mode 100644 index 0000000..049f9f3 --- /dev/null +++ b/src/identity/types/index.ts @@ -0,0 +1,71 @@ +/** Config - configuration required to init a nylas session */ +export interface Config { + /** ClientID - Nylas Client ID */ + clientId: string; + /** RedirectURI - RedirectURI of your app */ + redirectUri: string; + /** AccessType - Type of access you request from token (defaults to offline) */ + accessType?: string; + /** Domain - Your Nylas Auth domain */ + domain?: string; + /** Store - Set a store for handling sessions for node env only */ + store?: Store; + hosted?: boolean; +} +/** AuthConfig - configuration required to generate an oAuth link */ +export interface AuthConfig { + proivder?: string; + scope?: Array; + loginHint?: string; + prompt?: string; + metadata?: string; + state?: string; + settings?: any; + hosted?: boolean; + popup?: boolean; +} +/** IDToken - format of openID token */ +export interface IDToken { + iss: string; + aud: string; + sub: string; + email: string; + email_verified: boolean; + at_hash?: string; + iat: number; + exp: number; + name?: string; + given_name?: string; + family_name?: string; + nick_name?: string; + picture?: string; + gender?: string; + locale?: string; +} +/** CodeExcangeResponse - code exchange payload */ +export interface CodeExcangeResponse { + access_token: string; + grant_id: string; + id_token: string; + token_type: string; + scope: string; +} +/** ProviderListResponse - format of the provider response */ +export interface ProviderListResponse { + success: boolean; + data: ProviderList[]; +} +/** ProviderList- format of data of the ProviderListResponse */ +export interface ProviderList { + provider: string; + type: string; + settings: any; + name: string; +} + +/** Store - Interface for implementing your custom store for handling sessions */ +export interface Store { + set(key: string, value: string): Promise; + get(key: string): Promise; + remove(key: string): Promise; +} diff --git a/src/index.ts b/src/index.ts index 1d64149..ef905d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ import Request from './request'; - enum DefaultEndpoints { buildAuthUrl = '/nylas/generate-auth-url', exchangeCodeForToken = '/nylas/exchange-mailbox-token',