UNPKG

nebulab-dropbox

Version:
495 lines (448 loc) 19 kB
# Base class for OAuth drivers that run in the browser. # # Inheriting from this class makes a driver use HTML5 localStorage to preserve # OAuth tokens across page reloads. class Dropbox.AuthDriver.BrowserBase # Sets up the OAuth driver. # # Subclasses should pass the options object they receive to the superclass # constructor. # # @param {Object} options (optional) one or more of the options below # @option options {String} scope embedded in the localStorage key that holds # the authentication data; useful for having multiple OAuth tokens in a # single application # @option options {Boolean} rememberUser if false, the user's OAuth tokens # are not saved in localStorage; true by default constructor: (options) -> if options @rememberUser = if 'rememberUser' of options options.rememberUser else true @scope = options.scope or 'default' else @rememberUser = true @scope = 'default' @storageKey = null @storage = Dropbox.AuthDriver.BrowserBase.localStorage() @stateRe = /^[^#]+\#(.*&)?state=([^&]+)(&|$)/ # Browser-side authentication should always use OAuth 2 Implicit Grant. # # @see Dropbox.AuthDriver#authType authType: -> 'token' # Persists tokens. # # @see Dropbox.AuthDriver#onAuthStepChange onAuthStepChange: (client, callback) -> @setStorageKey client switch client.authStep when Dropbox.Client.RESET @loadCredentials (credentials) => return callback() unless credentials client.setCredentials credentials if client.authStep isnt Dropbox.Client.DONE return callback() # There is an old access token. Only use it if the app supports # logout. unless @rememberUser return @forgetCredentials(callback) client.setCredentials credentials callback() when Dropbox.Client.DONE if @rememberUser return @storeCredentials(client.credentials(), callback) @forgetCredentials callback when Dropbox.Client.SIGNED_OUT @forgetCredentials callback when Dropbox.Client.ERROR @forgetCredentials callback else callback() @ # Computes the @storageKey used by loadCredentials and forgetCredentials. # # @private # This is called by onAuthStepChange. # # @param {Dropbox.Client} client the client instance that is running the # authorization process # @return {Dropbox.AuthDriver} this, for easy call chaining setStorageKey: (client) -> # NOTE: the storage key is dependent on the app hash so that multiple apps # hosted off the same server don't step on eachother's toes @storageKey = "dropbox-auth:#{@scope}:#{client.appHash()}" @ # Stores a Dropbox.Client's credentials in localStorage. # # @private # onAuthStepChange calls this method during the authentication flow. # # @param {Object} credentials the result of a Drobpox.Client#credentials call # @param {function()} callback called when the storing operation is complete # @return {Dropbox.AuthDriver.BrowserBase} this, for easy call chaining storeCredentials: (credentials, callback) -> jsonString = JSON.stringify credentials try @storage.setItem @storageKey, jsonString catch storageError # Safari disables localStorage in Private Browsing mode. name = encodeURIComponent @storageKey value = encodeURIComponent jsonString document.cookie = "#{name}=#{value}; path=/" callback() @ # Retrieves a token and secret from localStorage. # # @private # onAuthStepChange calls this method during the authentication flow. # # @param {function(Object)} callback supplied with the credentials object # stored by a previous call to # {Dropbox.AuthDriver.BrowserBase#storeCredentials}; the argument is null # if no credentials were stored, or if the previously stored credentials # were deleted # @return {Dropbox.AuthDriver.BrowserBase} this, for easy call chaining loadCredentials: (callback) -> try jsonString = @storage.getItem @storageKey catch storageError # Safari disables localStorage in Private Browsing mode, but # localStorage.getItem exists and returns null. This is here to mimic # that behavior in environments that have localStorage disabled for some # reason. jsonString = null if jsonString is null # Safari disables localStorage in Private Browsing mode. We need to check # for cookies as well. name = encodeURIComponent @storageKey # Characters unescaped by encodeURIComponent: # https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent # Characters that must be escaped in regular expressions: # http://stackoverflow.com/a/3561711/537046 nameRegexp = name.replace(/[.*+()]/g, '\\$&') cookieRegexp = new RegExp "(^|(;\\s*))#{name}=([^;]*)(;|$)" if match = cookieRegexp.exec(document.cookie) jsonString = decodeURIComponent match[3] unless jsonString callback null return @ try callback JSON.parse(jsonString) catch jsonError # Parse errors. callback null @ # Deletes information previously stored by a call to storeCredentials. # # @private # onAuthStepChange calls this method during the authentication flow. # # @param {function()} callback called after the credentials are deleted # @return {Dropbox.AuthDriver.BrowserBase} this, for easy call chaining forgetCredentials: (callback) -> try @storage.removeItem @storageKey catch storageError # Safari disables localStorage in Private Browsing mode. name = encodeURIComponent @storageKey expires = (new Date(0)).toGMTString() document.cookie = "#{name}={}; expires=#{expires}; path=/" callback() @ # Figures out if a URL is an OAuth 2.0 /authorize redirect URL. # # @param {String} the URL to check; if null, the current location's URL is # checked # @return {String} the state parameter value received from the /authorize # redirect, or null if the URL is not the result of an /authorize redirect locationStateParam: (url) -> location = url or Dropbox.AuthDriver.BrowserBase.currentLocation() # Extract the state. match = @stateRe.exec location return decodeURIComponent(match[2]) if match null # Replaces the filename (basename) in an URL. # # @private # This is used by subclasses to compute the redirect_uri value passed to # /authorize. # # @param {String} url the URL whose basename will be replaced # @param {String} basename the file name in the returned URL # @return {String} an URL whose basename has been replaced; the URL will not # have a query or fragment replaceUrlBasename: (url, basename) -> hashIndex = url.indexOf '#' url = url.substring 0, hashIndex if hashIndex isnt -1 queryIndex = url.indexOf '?' url = url.substring 0, queryIndex if queryIndex isnt -1 fragments = url.split '/' fragments[fragments.length - 1] = basename fragments.join '/' # Wrapper for window.localStorage. # # Drivers should call this method instead of using localStorage directly, to # simplify stubbing. # # @return {Storage} the browser's implementation of the WindowLocalStorage # interface in the Web Storage specification @localStorage: -> if typeof window isnt 'undefined' try window.localStorage catch deprecationError # Simply accessing window.localStorage in Chrome apps is deprecated. null else null # Wrapper for window.location. # # Drivers should call this method instead of using browser APIs directly, to # simplify stubbing. # # @return {String} the current page's URL @currentLocation: -> window.location.href # Removes the OAuth 2 access token from the current page's URL. # # This hopefully also removes the access token from the browser history. # # @return {void} @cleanupLocation: -> if window.history and window.history.replaceState pageUrl = @currentLocation() hashIndex = pageUrl.indexOf '#' window.history.replaceState {}, document.title, pageUrl.substring(0, hashIndex) else window.location.hash = '' return # OAuth driver that uses a redirect and localStorage to complete the flow. class Dropbox.AuthDriver.Redirect extends Dropbox.AuthDriver.BrowserBase # Sets up the redirect-based OAuth driver. # # @param {Object} options (optional) one more of the options below # @option options {String} redirectUrl URL to the page that receives the # /authorize redirect # @option options {String} redirectFile the URL to the receiver page will be # computed by replacing the file name (everything after the last /) of # the current location with this parameter's value # @option options {String} scope embedded in the localStorage key that holds # the authentication data; useful for having multiple OAuth tokens in a # single application # @option options {Boolean} rememberUser if false, the user's OAuth tokens # are not saved in localStorage; true by default constructor: (options) -> super options @receiverUrl = @baseUrl options # The URL of the page that will receive the callback. # # @private # This should only be called by the constructor. # # @param {Object} options (optional) the options passed to the constructor # @option options {String} redirectUrl URL to the page that receives the # /authorize redirect # @option options {String} redirectFile the URL to the receiver page will be # computed by replacing the file name (everything after the last /) of # the current location with this parameter's value # @return {String} the current URL, minus any fragment it might have baseUrl: (options) -> url = Dropbox.AuthDriver.BrowserBase.currentLocation() if options if options.redirectUrl return options.redirectUrl if options.redirectFile return @replaceUrlBasename(url, options.redirectFile) hashIndex = url.indexOf '#' url = url.substring 0, hashIndex if hashIndex isnt -1 url # URL of the page that the user will be redirected to. # # @return {String} the URL of the app page that the user will be redirected # to after /authorize; if no constructor option is set, this will be the # current page # @see Dropbox.AuthDriver#url url: -> @receiverUrl # Saves the OAuth 2 credentials, and redirects to the authorize page. # # @see Dropbox.AuthDriver#doAuthorize doAuthorize: (authUrl, stateParam, client) -> @storeCredentials client.credentials(), -> window.location.assign authUrl # Finishes the OAuth 2 process after the user has been redirected. # # @see Dropbox.AuthDriver#resumeAuthorize resumeAuthorize: (stateParam, client, callback) -> if @locationStateParam() is stateParam pageUrl = Dropbox.AuthDriver.BrowserBase.currentLocation() Dropbox.AuthDriver.BrowserBase.cleanupLocation() callback Dropbox.Util.Oauth.queryParamsFromUrl pageUrl else @forgetCredentials -> callback error: 'Authorization error' # OAuth driver that uses a popup window and postMessage to complete the flow. class Dropbox.AuthDriver.Popup extends Dropbox.AuthDriver.BrowserBase # Sets up a popup-based OAuth driver. # # @param {Object} options (optional) one or more of the options below # @option options {String} receiverUrl URL to the page that receives the # /authorize redirect and performs the postMessage # @option options {String} receiverFile the URL to the receiver page will be # computed by replacing the file name (everything after the last /) of # the current location with this parameter's value # @option options {String} scope embedded in the localStorage key that holds # the authentication data; useful for having multiple OAuth tokens in a # single application # @option options {Boolean} rememberUser if false, the user's OAuth tokens # are not saved in localStorage; true by default constructor: (options) -> super options @receiverUrl = @baseUrl options # URL of the redirect receiver page, which posts a message back to this page. # # @return {String} receiver page URL # @see Dropbox.AuthDriver#url url: -> @receiverUrl # Shows the authorization URL in a pop-up, waits for it to send a message. # # @see Dropbox.AuthDriver#doAuthorize doAuthorize: (authUrl, stateParam, client, callback) -> @listenForMessage stateParam, callback @openWindow authUrl # The URL of the page that will receive the OAuth callback. # # @private # This should only be called by the constructor. # # @param {Object} options the options passed to the constructor # @option options {String} receiverUrl URL to the page that receives the # /authorize redirect and performs the postMessage # @option options {String} receiverFile the URL to the receiver page will be # computed by replacing the file name (everything after the last /) of # the current location with this parameter's value # @return {String} absolute URL of the receiver page baseUrl: (options) -> url = Dropbox.AuthDriver.BrowserBase.currentLocation() if options if options.receiverUrl return options.receiverUrl else if options.receiverFile return @replaceUrlBasename(url, options.receiverFile) url # Creates a popup window. # # @private # This should only be called by {Dropbox.AuthDriver.Popup#doAuthorize}. # # @param {String} url the URL that will be loaded in the popup window # @return {DOMRef} reference to the opened window, or null if the call failed openWindow: (url) -> window.open url, '_dropboxOauthSigninWindow', @popupWindowSpec(980, 700) # Spec string for window.open to create a nice popup. # # @private # This should only be called by {Dropbox.AuthDriver.Popup#openWindow}. # # @param {Number} popupWidth the desired width of the popup window # @param {Number} popupHeight the desired height of the popup window # @return {String} spec string for the popup window popupWindowSpec: (popupWidth, popupHeight) -> # Metrics for the current browser window. x0 = window.screenX ? window.screenLeft y0 = window.screenY ? window.screenTop width = window.outerWidth ? document.documentElement.clientWidth height = window.outerHeight ? document.documentElement.clientHeight # Computed popup window metrics. popupLeft = Math.round x0 + (width - popupWidth) / 2 popupTop = Math.round y0 + (height - popupHeight) / 2.5 popupLeft = x0 if popupLeft < x0 popupTop = y0 if popupTop < y0 # The specification string. "width=#{popupWidth},height=#{popupHeight}," + "left=#{popupLeft},top=#{popupTop}" + 'dialog=yes,dependent=yes,scrollbars=yes,location=yes' # Listens for a postMessage from a previously opened popup window. # # @private # This should only be called by {Dropbox.AuthDriver.Popup#doAuthorize}. # # @param {String} stateParam the state parameter passed to the OAuth 2 # /authorize endpoint # @param {function()} called when the received message matches stateParam listenForMessage: (stateParam, callback) -> listener = (event) => if event.data # Message coming from postMessage. data = event.data else # Message coming from Dropbox.Util.EventSource. data = event try oauthInfo = JSON.parse(data)._dropboxjs_oauth_info catch jsonError return return unless oauthInfo if @locationStateParam(oauthInfo) is stateParam stateParam = false # Avoid having this matched in the future. window.removeEventListener 'message', listener Dropbox.AuthDriver.Popup.onMessage.removeListener listener callback Dropbox.Util.Oauth.queryParamsFromUrl(data) window.addEventListener 'message', listener, false Dropbox.AuthDriver.Popup.onMessage.addListener listener # The origin of a location, in the context of the same-origin policy. # # @param {String} location the URL whose origin is computed # @return {String} the location's origin @locationOrigin: (location) -> # file:// URLs -- the origin is the whole path match = /^(file:\/\/[^\?\#]*)(\?|\#|$)/.exec location return match[1] if match # xxx:// URLs -- the origin is the scheme and the first path segment # e.g. http://, https:// match = /^([^\:]+\:\/\/[^\/\?\#]*)(\/|\?|\#|$)/.exec location return match[1] if match # e.g., data: URLs -- the origin is everything location # Communicates with the driver from the OAuth receiver page. # # The easiest way for an application to keep up to date with dropbox.js is to # set up a popup receiver page that loads dropbox.js and calls this method. # This guarantees that the code used to communicate between the popup # receiver page and {Dropbox.AuthDriver.Popup#doAuthorize} stays up to date # as dropbox.js is updated. # # @return {void} @oauthReceiver: -> window.addEventListener 'load', -> pageUrl = window.location.href message = JSON.stringify _dropboxjs_oauth_info: pageUrl Dropbox.AuthDriver.BrowserBase.cleanupLocation() opener = window.opener if window.parent isnt window.top opener or= window.parent if opener try pageOrigin = window.location.origin or locationOrigin(pageUrl) opener.postMessage message, pageOrigin window.close() catch ieError # IE <= 9 doesn't support opener.postMessage for popup windows. try # postMessage doesn't work in IE, but direct object access does. opener.Dropbox.AuthDriver.Popup.onMessage.dispatch message window.close() catch frameError # Got nothing left to do. # Leave the window opened so it can be debugged. return # Works around postMessage failures on Internet Explorer. # # @private # This should only be used by {Dropbox.AuthDriver.Popup#doAuthorize} and # {Dropbox.AuthDriver.Popup.oauthReceiver}. @onMessage = new Dropbox.Util.EventSource