nebulab-dropbox
Version:
Client library for the Dropbox API
423 lines (396 loc) • 16.1 kB
text/coffeescript
# Stripped-down OAuth 2 implementation that works with the Dropbox API server.
class Dropbox.Util.Oauth
# Creates an Oauth instance that manages an application's key and token data.
#
# @private
# This constructor is called by {Dropbox.Client}. This class should not be
# instantiated directly.
#
# @param {Object} options the application type and API key; alternatively,
# the result of a previous {Dropbox.Util.Oauth#credentials} call can be
# passed in to create an identical {Dropbox.Util.Oauth} instance
# @option options {String} key the Dropbox application's key (client
# identifier, in OAuth2 vocabulary)
# @option options {String} secret the Dropbox application's secret (client
# secret, in OAuth vocabulary); browser-side applications should not use
# this option
# @option options {String} token (optional) the user's OAuth 2.0 access token
constructor: (options) ->
@_id = null
@_secret = null
@_stateParam = null
@_authCode = null
@_token = null
@_tokenKey = null
@_tokenKid = null
@_error = null
@_appHash = null
@_loaded = null
@setCredentials options
# Resets the credentials used by this Oauth instance.
#
# @private
# Use {Dropbox.Client#setCredentials} instead.
#
# @param {Object} options same as for #{Dropbox.Util.Oauth#constructor}
# @return {Dropbox.Util.Oauth} this, for easy call chaining
#
# @see Dropbox.Util.Oauth#constructor
setCredentials: (options) ->
if options.key
@_id = options.key
else
unless options.token
throw new Error 'No API key supplied'
@_id = null
@_secret = options.secret or null
@_appHash = null
@_error = null
@_loaded = true
@reset()
if options.token
@_token = options.token
if options.tokenKey
@_tokenKey = options.tokenKey
@_tokenKid = options.tokenKid
else if options.oauthCode
@_authCode = options.oauthCode
else if options.oauthStateParam
@_stateParam = options.oauthStateParam
@
# The credentials used by this Oauth instance.
#
# @private
# Use {Dropbox.Client#credentials} instead.
#
# @return {Object<String, String>} an object that can be passed into
# {Dropbox.Util.Oauth#constructor} or into {Dropbox.Util.Oauth#reset} to
# obtain a new instance that uses the same credentials; the object can be
# serialized using JSON
credentials: ->
returnValue = {}
returnValue.key = @_id if @_id
returnValue.secret = @_secret if @_secret
if @_token isnt null
returnValue.token = @_token
if @_tokenKey
returnValue.tokenKey = @_tokenKey
returnValue.tokenKid = @_tokenKid
else if @_authCode isnt null
returnValue.oauthCode = @_authCode
else if @_stateParam isnt null
returnValue.oauthStateParam = @_stateParam
returnValue
# The authentication process step that this instance's credentials are for
#
# @private
# Use {Dropbox.Client#authStep} instead.
#
# @return {Number} one of the constants defined on {Dropbox.Client}, such as
# {Dropbox.Client.DONE}
step: ->
if @_token isnt null
Dropbox.Client.DONE
else if @_authCode isnt null
Dropbox.Client.AUTHORIZED
else if @_stateParam isnt null
if @_loaded
Dropbox.Client.PARAM_LOADED
else
Dropbox.Client.PARAM_SET
else if @_error isnt null
Dropbox.Client.ERROR
else
Dropbox.Client.RESET
# Sets the "state" parameter value for the following /authorize request.
#
# @private
# Use {Dropbox.Client#authorize} instead.
#
# @param {String} stateParam the value of the "state" parameter
# @return {Dropbox.Util.OAuth} this, for easy call chaining
setAuthStateParam: (stateParam) ->
if @_id is null
throw new Error('No API key supplied, cannot do authorization')
@reset()
@_loaded = false
@_stateParam = stateParam
@
# Verifies the "state" query parameter in an /authorize redirect.
#
# If this method returns false, the /authorize redirect should be ignored.
#
# @param {String} stateParam the value of the "state" query parameter in the
# request caused by an /authorize HTTP redirect
# @return {Boolean} true if the given value matches the "state" parameter
# sent to /authorize; false if no /authorize redirect was expected, or if
# the value doesn't match
checkAuthStateParam: (stateParam) ->
(@_stateParam is stateParam) and (@_stateParam isnt null)
# @private
# This should only be called by {Dropbox.Client#authenticate}. Use
# {Dropbox.Util.Oauth#checkAuthStateParam} instead.
#
# @return {String} the "state" query parameter set by setAuthStateParam
authStateParam: ->
@_stateParam
# @private
# This should only be called by {Dropbox.Client#authenticate}. Use
# {Dropbox.Client#error} instead.
error: ->
@_error
# Assimilates the information in an /authorize redirect's query parameters.
#
# The parameters may contain an access code, which will bring the Oauth
# instance in the {Dropbox.Client.AUTHORIZED} state, or may contain an access
# token, which will bring the Oauth instance in the {Dropbox.Client.DONE}
# state.
#
# @private
# This should only be called by {Dropbox.Client#authenticate}. OAuth drivers
# should pass the redirect parameters to the callback of
# {Dropbox.AuthDriver#doAuthorize} or {Dropbox.AuthDriver#resumeAuthorize}.
#
# @param {Object<String, String>} queryParams the query parameters that
# contain an authorization code or access token; these should be query
# parameters received from an /authorize redirect
# @return {Boolean} true if the query parameters contained information about
# an OAuth 2 authorization code or access token; false if no useful
# information was found and this instance's state was not changed
#
# @see http://tools.ietf.org/html/rfc6749#section-4.1.2 RFC 6749 Section 4.1.2
# @see http://tools.ietf.org/html/rfc6750#section-4 RFC 6750 Section 4
# @see http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-03
processRedirectParams: (queryParams) ->
if queryParams.error
if @_id is null
throw new Error('No API key supplied, cannot process errors')
@reset()
@_error = new Dropbox.AuthError(queryParams)
return true
if queryParams.code
if @_id is null
throw new Error('No API key supplied, cannot do Authorization Codes')
@reset()
@_loaded = false
@_authCode = queryParams.code
return true
tokenType = queryParams.token_type
if tokenType
# NOTE: the API server used to use 'bearer' instead of 'Bearer' as the
# token_type; the OAuth spec is hard to follow, so it's better to
# be permissive
tokenType = tokenType.toLowerCase()
if tokenType isnt 'bearer' and tokenType isnt 'mac'
throw new Error("Unimplemented token type #{tokenType}")
@reset()
@_loaded = false
if tokenType is 'mac'
if queryParams.mac_algorithm isnt 'hmac-sha-1'
throw new Error(
"Unimplemented MAC algorithms #{queryParams.mac_algorithm}")
@_tokenKey = queryParams.mac_key
@_tokenKid = queryParams.kid
@_token = queryParams.access_token
return true
false
# Computes the value of the OAuth 2-specified Authorization HTTP header.
#
# OAuth 2 supports two methods of passing authorization information. The
# Authorization header (implemented by this method) is the recommended
# method, and form parameters (implemented by addAuthParams) is the fallback
# method. The fallback method is useful for avoiding CORS preflight requests.
#
# @private
# This should only be called by {Dropbox.Client#authenticate}.
#
# @param {String} method the HTTP method used to make the request ('GET',
# 'POST', etc)
# @param {String} url the HTTP URL (e.g. "http://www.example.com/photos")
# that receives the request
# @param {Object} params an associative array (hash) containing the HTTP
# request parameters
# @return {String} the value to be used for the Authorization HTTP header
authHeader: (method, url, params) ->
if @_token is null
# RFC 6749: OAuth 2.0 (Client Authentication, Protocol Endpoints)
userPassword = if @_secret is null
Dropbox.Util.btoa("#{@_id}:")
else
Dropbox.Util.btoa("#{@_id}:#{@_secret}")
"Basic #{userPassword}"
else
if @_tokenKey is null
# RFC 6750: Bearer Tokens.
"Bearer #{@_token}"
else
# IETF draft-ietf-oauth-v2-http-mac
macParams = @macParams method, url, params
"MAC kid=#{macParams.kid} ts=#{macParams.ts} " +
"access_token=#{@_token} mac=#{macParams.mac}"
# Generates OAuth-required form parameters.
#
# OAuth 2 supports two methods of passing authorization information. The
# Authorization header (implemented by authHeader) is the recommended method,
# and form parameters (implemented by this method) is the fallback method.
# The fallback method is useful for avoiding CORS preflight requests.
#
# @private
# This should only be called by {Dropbox.Client#authenticate}.
#
# @param {String} method the HTTP method used to make the request ('GET',
# 'POST', etc)
# @param {String} url the HTTP URL (e.g. "http://www.example.com/photos")
# that receives the request
# @param {Object} params an associative array (hash) containing the HTTP
# request parameters; this parameter will be mutated
# @return {Object} the value of the params argument
addAuthParams: (method, url, params) ->
if @_token is null
# RFC 6749: OAuth 2.0 (Client Authentication, Protocol Endpoints)
params.client_id = @_id
if @_secret isnt null
params.client_secret = @_secret
else
if @_tokenKey isnt null
# IETF draft-ietf-oauth-v2-http-mac
macParams = @macParams method, url, params
params.kid = macParams.kid
params.ts = macParams.ts
params.mac = macParams.mac
# RFC 6750: Bearer Tokens and IETF draft-ietf-oauth-v2-http-mac
params.access_token = @_token
params
# The query parameters to be used in an OAuth 2 /authorize URL.
#
# @private
# This should only be called by {Dropbox.Client#authenticate}.
#
# @param {String} responseType one of the /authorize response types
# implemented by dropbox.js
# @param {?String} redirectUrl the URL that the user's browser should be
# redirected to in order to perform an /authorize request
# @return {Object<String, String>} the query parameters for the
# /authorize URL
#
# @see Dropbox.AuthDriver#authType
# @see RFC 6749 for the authorization process in OAuth 2.0
authorizeUrlParams: (responseType, redirectUrl) ->
if responseType isnt 'token' and responseType isnt 'code'
throw new Error("Unimplemented /authorize response type #{responseType}")
# NOTE: these parameters will never contain the client secret
params =
client_id: @_id, state: @_stateParam, response_type: responseType
params.redirect_uri = redirectUrl if redirectUrl
params
# The query parameters to be used in an /oauth2/token URL.
#
# @private
# This should only be called by {Dropbox.Client#authenticate}.
#
# @param {?String} redirectUrl the URL that the user's browser was redirected
# to after performing the /oauth2/authorize request; this must be the same
# as the redirectUrl parameter passed to authorizeUrlParams
# @return {Object<String, String>} the query parameters for the /oauth2/token
# URL
accessTokenParams: (redirectUrl) ->
params = { grant_type: 'authorization_code', code: @_authCode }
params.redirect_uri = redirectUrl if redirectUrl
params
# Extracts the query parameters in an /authorize redirect URL.
#
# This is provided as a helper for dropbox.js OAuth drivers. It is not a
# general-purpose URL parser, but it will handle the URLs generated by the
# Dropbox API server.
#
# @param {String} url a redirect URL generated by Dropbox's OAuth
# authorization page
# @return {Object<String, String>} the OAuth 2 parameters passed in the URL;
# this should be passed to the callback of {Dropbox.AuthDriver#doAuthorize}
# or {Dropbox.AuthDriver#resumeAuthorize}
@queryParamsFromUrl: (url) ->
match = /^[^?#]+(\?([^\#]*))?(\#(.*))?$/.exec url
return {} unless match
query = match[2] or ''
# angular.js routing prepends / to the fragment URL.
query = query.substring 1 if query.substring(0, 1) is '/'
fragment = match[4] or ''
fragmentOffset = fragment.indexOf '?'
if fragmentOffset isnt -1
fragment = fragment.substring fragmentOffset + 1
# angular.js routing prepends / to the fragment URL.
fragment = fragment.substring 1 if fragment.substring(0, 1) is '/'
params = {}
for kvp in query.split('&').concat fragment.split('&')
offset = kvp.indexOf '='
continue if offset is -1
params[decodeURIComponent(kvp.substring(0, offset))] =
decodeURIComponent kvp.substring(offset + 1)
params
# The parameters of an OAuth 2 MAC authenticator.
#
# @private
# This is called internally by addHeader and addAuthParams when OAuth 2 MAC
# tokens are in use.
#
# @param {String} method the HTTP method used to make the request ('GET',
# 'POST', etc)
# @param {String} url the HTTP URL (e.g. "http://www.example.com/photos")
# that receives the request
# @param {Object} queryParams an associative array (hash) containing the
# query parameters in the HTTP request URL
# @return {Object<String, String>} the MAC authenticator attributes
macParams: (method, url, params) ->
macParams = { kid: @_tokenKid, ts: Dropbox.Util.Oauth.timestamp() }
# TODO(pwnall): upgrade to the OAuth 2 MAC tokens algorithm
string = method.toUpperCase() + '&' +
Dropbox.Util.Xhr.urlEncodeValue(url) + '&' +
Dropbox.Util.Xhr.urlEncodeValue(Dropbox.Util.Xhr.urlEncode(params))
macParams.mac = Dropbox.Util.hmac string, @_tokenKey
macParams
# @private
# Use {Dropbox.Client#appHash} instead.
#
# @return {String} a string that uniquely identifies the application, based
# on its OAuth client id
appHash: ->
return @_appHash if @_appHash
@_appHash = Dropbox.Util.sha1('oauth2-' + @_id).replace(/[\/+=]/g, '')
# Drops all user-specific OAuth information.
#
# @private
# Use {Dropbox.Client#reset} instead.
#
# After this method is called, the Oauth instance will be in the
# {Dropbox.Client.RESET} step.
#
# @return {Dropbox.Util.Oauth} this, for easy call chaining
reset: ->
@_stateParam = null
@_authCode = null
@_token = null
@_tokenKey = null
@_tokenKid = null
@_error = null
@
# The timestamp used for an OAuth 2 request.
#
# @private
# This method is separated out for testing purposes.
#
# @return {Number} a timestamp suitable for use in computing OAuth 2 MAC
# authenticators
@timestamp: ->
Math.floor(Date.now() / 1000)
# Generates a random OAuth 2 authentication state parameter value.
#
# This is used for authentication drivers that do not implement
# Dropbox.AuthDriver#getStateParam.
#
# @return {String} a randomly generated parameter
@randomAuthStateParam: ->
['oas', Date.now().toString(36), Math.random().toString(36)].join '_'
# Date.now() workaround for Internet Explorer 8.
unless Date.now?
Dropbox.Util.Oauth.timestamp = ->
Math.floor((new Date()).getTime() / 1000)