| | import { |
| | getTokenExpiresAtDate, |
| | isBrowserEnv, |
| | createBrowserSafeString, |
| | OAuth2AuthorizationUrl, |
| | OAuth2TokenUrl, |
| | isWorkerEnv, |
| | } from './utils.js'; |
| | import { parseResponse } from './response.js'; |
| |
|
| | let fetch; |
| | let crypto; |
| | let Encoder; |
| |
|
| | |
| | const TokenExpirationBuffer = 300 * 1000; |
| | const PKCELength = 128; |
| | const TokenAccessTypes = ['legacy', 'offline', 'online']; |
| | const GrantTypes = ['code', 'token']; |
| | const IncludeGrantedScopes = ['none', 'user', 'team']; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export default class DropboxAuth { |
| | constructor(options) { |
| | options = options || {}; |
| |
|
| | if (isBrowserEnv()) { |
| | fetch = window.fetch.bind(window); |
| | crypto = window.crypto || window.msCrypto; |
| | } else if (isWorkerEnv()) { |
| | |
| | fetch = self.fetch.bind(self); |
| | crypto = self.crypto; |
| | |
| | } else { |
| | fetch = require('node-fetch'); |
| | crypto = require('crypto'); |
| | } |
| |
|
| | if (typeof TextEncoder === 'undefined') { |
| | Encoder = require('util').TextEncoder; |
| | } else { |
| | Encoder = TextEncoder; |
| | } |
| |
|
| | this.fetch = options.fetch || fetch; |
| | this.accessToken = options.accessToken; |
| | this.accessTokenExpiresAt = options.accessTokenExpiresAt; |
| | this.refreshToken = options.refreshToken; |
| | this.clientId = options.clientId; |
| | this.clientSecret = options.clientSecret; |
| |
|
| | this.domain = options.domain; |
| | this.domainDelimiter = options.domainDelimiter; |
| | this.customHeaders = options.customHeaders; |
| | this.dataOnBody = options.dataOnBody; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | setAccessToken(accessToken) { |
| | this.accessToken = accessToken; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | getAccessToken() { |
| | return this.accessToken; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | setClientId(clientId) { |
| | this.clientId = clientId; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | getClientId() { |
| | return this.clientId; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | setClientSecret(clientSecret) { |
| | this.clientSecret = clientSecret; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | getClientSecret() { |
| | return this.clientSecret; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | getRefreshToken() { |
| | return this.refreshToken; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | setRefreshToken(refreshToken) { |
| | this.refreshToken = refreshToken; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | getAccessTokenExpiresAt() { |
| | return this.accessTokenExpiresAt; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | setAccessTokenExpiresAt(accessTokenExpiresAt) { |
| | this.accessTokenExpiresAt = accessTokenExpiresAt; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | setCodeVerifier(codeVerifier) { |
| | this.codeVerifier = codeVerifier; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | getCodeVerifier() { |
| | return this.codeVerifier; |
| | } |
| |
|
| | generateCodeChallenge() { |
| | const encoder = new Encoder(); |
| | const codeData = encoder.encode(this.codeVerifier); |
| | let codeChallenge; |
| | if (isBrowserEnv() || isWorkerEnv()) { |
| | return crypto.subtle.digest('SHA-256', codeData) |
| | .then((digestedHash) => { |
| | const base64String = btoa(String.fromCharCode.apply(null, new Uint8Array(digestedHash))); |
| | codeChallenge = createBrowserSafeString(base64String).substr(0, 128); |
| | this.codeChallenge = codeChallenge; |
| | }); |
| | } |
| | const digestedHash = crypto.createHash('sha256').update(codeData).digest(); |
| | codeChallenge = createBrowserSafeString(digestedHash); |
| | this.codeChallenge = codeChallenge; |
| | return Promise.resolve(); |
| | } |
| |
|
| | generatePKCECodes() { |
| | let codeVerifier; |
| | if (isBrowserEnv() || isWorkerEnv()) { |
| | const array = new Uint8Array(PKCELength); |
| | const randomValueArray = crypto.getRandomValues(array); |
| | const base64String = btoa(randomValueArray); |
| | codeVerifier = createBrowserSafeString(base64String).substr(0, 128); |
| | } else { |
| | const randomBytes = crypto.randomBytes(PKCELength); |
| | codeVerifier = createBrowserSafeString(randomBytes).substr(0, 128); |
| | } |
| | this.codeVerifier = codeVerifier; |
| |
|
| | return this.generateCodeChallenge(); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | getAuthenticationUrl(redirectUri, state, authType = 'token', tokenAccessType = null, scope = null, includeGrantedScopes = 'none', usePKCE = false) { |
| | const clientId = this.getClientId(); |
| | const baseUrl = OAuth2AuthorizationUrl(this.domain); |
| |
|
| | if (!clientId) { |
| | throw new Error('A client id is required. You can set the client id using .setClientId().'); |
| | } |
| | if (authType !== 'code' && !redirectUri) { |
| | throw new Error('A redirect uri is required.'); |
| | } |
| | if (!GrantTypes.includes(authType)) { |
| | throw new Error('Authorization type must be code or token'); |
| | } |
| | if (tokenAccessType && !TokenAccessTypes.includes(tokenAccessType)) { |
| | throw new Error('Token Access Type must be legacy, offline, or online'); |
| | } |
| | if (scope && !(scope instanceof Array)) { |
| | throw new Error('Scope must be an array of strings'); |
| | } |
| | if (!IncludeGrantedScopes.includes(includeGrantedScopes)) { |
| | throw new Error('includeGrantedScopes must be none, user, or team'); |
| | } |
| |
|
| | let authUrl; |
| | if (authType === 'code') { |
| | authUrl = `${baseUrl}?response_type=code&client_id=${clientId}`; |
| | } else { |
| | authUrl = `${baseUrl}?response_type=token&client_id=${clientId}`; |
| | } |
| |
|
| | if (redirectUri) { |
| | authUrl += `&redirect_uri=${redirectUri}`; |
| | } |
| | if (state) { |
| | authUrl += `&state=${state}`; |
| | } |
| | if (tokenAccessType) { |
| | authUrl += `&token_access_type=${tokenAccessType}`; |
| | } |
| | if (scope) { |
| | authUrl += `&scope=${scope.join(' ')}`; |
| | } |
| | if (includeGrantedScopes !== 'none') { |
| | authUrl += `&include_granted_scopes=${includeGrantedScopes}`; |
| | } |
| | if (usePKCE) { |
| | return this.generatePKCECodes() |
| | .then(() => { |
| | authUrl += '&code_challenge_method=S256'; |
| | authUrl += `&code_challenge=${this.codeChallenge}`; |
| | return authUrl; |
| | }); |
| | } |
| | return Promise.resolve(authUrl); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | getAccessTokenFromCode(redirectUri, code) { |
| | const clientId = this.getClientId(); |
| | const clientSecret = this.getClientSecret(); |
| |
|
| | if (!clientId) { |
| | throw new Error('A client id is required. You can set the client id using .setClientId().'); |
| | } |
| | let path = OAuth2TokenUrl(this.domain, this.domainDelimiter); |
| | path += '?grant_type=authorization_code'; |
| | path += `&code=${code}`; |
| | path += `&client_id=${clientId}`; |
| |
|
| | if (clientSecret) { |
| | path += `&client_secret=${clientSecret}`; |
| | } else { |
| | if (!this.codeVerifier) { |
| | throw new Error('You must use PKCE when generating the authorization URL to not include a client secret'); |
| | } |
| | path += `&code_verifier=${this.codeVerifier}`; |
| | } |
| | if (redirectUri) { |
| | path += `&redirect_uri=${redirectUri}`; |
| | } |
| |
|
| | const fetchOptions = { |
| | method: 'POST', |
| | headers: { |
| | 'Content-Type': 'application/x-www-form-urlencoded', |
| | }, |
| | }; |
| | return this.fetch(path, fetchOptions) |
| | .then((res) => parseResponse(res)); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | checkAndRefreshAccessToken() { |
| | const canRefresh = this.getRefreshToken() && this.getClientId(); |
| | const needsRefresh = !this.getAccessTokenExpiresAt() |
| | || (new Date(Date.now() + TokenExpirationBuffer)) >= this.getAccessTokenExpiresAt(); |
| | const needsToken = !this.getAccessToken(); |
| | if ((needsRefresh || needsToken) && canRefresh) { |
| | return this.refreshAccessToken(); |
| | } |
| | return Promise.resolve(); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | refreshAccessToken(scope = null) { |
| | const clientId = this.getClientId(); |
| | const clientSecret = this.getClientSecret(); |
| |
|
| | if (!clientId) { |
| | throw new Error('A client id is required. You can set the client id using .setClientId().'); |
| | } |
| | if (scope && !(scope instanceof Array)) { |
| | throw new Error('Scope must be an array of strings'); |
| | } |
| |
|
| | let refreshUrl = OAuth2TokenUrl(this.domain, this.domainDelimiter); |
| | const fetchOptions = { |
| | headers: { 'Content-Type': 'application/json' }, |
| | method: 'POST', |
| | }; |
| |
|
| | if (this.dataOnBody) { |
| | const body = { grant_type: 'refresh_token', client_id: clientId, refresh_token: this.getRefreshToken() }; |
| |
|
| | if (clientSecret) { |
| | body.client_secret = clientSecret; |
| | } |
| | if (scope) { |
| | body.scope = scope.join(' '); |
| | } |
| |
|
| | fetchOptions.body = body; |
| | } else { |
| | refreshUrl += `?grant_type=refresh_token&refresh_token=${this.getRefreshToken()}`; |
| | refreshUrl += `&client_id=${clientId}`; |
| | if (clientSecret) { |
| | refreshUrl += `&client_secret=${clientSecret}`; |
| | } |
| | if (scope) { |
| | refreshUrl += `&scope=${scope.join(' ')}`; |
| | } |
| | } |
| |
|
| | return this.fetch(refreshUrl, fetchOptions) |
| | .then((res) => parseResponse(res)) |
| | .then((res) => { |
| | this.setAccessToken(res.result.access_token); |
| | this.setAccessTokenExpiresAt(getTokenExpiresAtDate(res.result.expires_in)); |
| | }); |
| | } |
| | } |
| |
|