Skip to content

Commit

Permalink
Wrapper for OAuth 2 errors.
Browse files Browse the repository at this point in the history
  • Loading branch information
pwnall committed Jun 25, 2013
1 parent e84eb35 commit 262b9fd
Show file tree
Hide file tree
Showing 11 changed files with 298 additions and 48 deletions.
6 changes: 4 additions & 2 deletions Cakefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ task 'fasttest', ->
clean ->
build ->
ssl_cert ->
fasttest()
fasttest (code) ->
process.exit code

task 'webtest', ->
vendor ->
Expand Down Expand Up @@ -221,7 +222,8 @@ fasttest = (callback) ->
test_cases.sort() # Consistent test case order.
run 'node node_modules/mocha/bin/mocha --colors --slow 200 --timeout 1000 ' +
'--require test/js/helpers/fast_setup.js --reporter min ' +
test_cases.join(' '), callback
test_cases.join(' '), noExit: true, (code) ->
callback(code) if callback

webtest = (callback) ->
webFileServer = require './test/js/helpers/web_file_server.js'
Expand Down
15 changes: 7 additions & 8 deletions src/api_error.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@
class Dropbox.ApiError
# @property {Number} the HTTP error code (e.g., 403); compare against the
# constants defined on Dropbox.ApiError
status: undefined
status: null

# @property {String} the HTTP method of the failed request (e.g., 'GET')
method: undefined
method: null

# @property {String} the URL of the failed request
url: undefined
url: null

# @property {?String} the body of the HTTP error response; can be null if
# the error was caused by a network failure or by a security issue
responseText: undefined
responseText: null

# @property {?Object} the result of parsing the JSON in the HTTP error
# response; can be null if the API server didn't return JSON, or if the
# HTTP response body is unavailable
response: undefined
response: null

# Status value indicating an error at the XMLHttpRequest layer.
#
Expand All @@ -42,8 +42,7 @@ class Dropbox.ApiError
# Status value indicating a malformed OAuth request.
#
# This indicates a bug in dropbox.js and should never occur under normal
# circumstances. However, a Safari bug causes this status to be reported
# instead of INVALID_TOKEN.
# circumstances.
@OAUTH_ERROR: 403

# Status value indicating that a file or path was not found in Dropbox.
Expand Down Expand Up @@ -101,7 +100,7 @@ class Dropbox.ApiError
@responseText = '(no response)'
@response = null

# Used when the error is printed out by developers.
# Developer-friendly summary of the error.
toString: ->
"Dropbox API error #{@status} from #{@method} #{@url} :: #{@responseText}"

Expand Down
93 changes: 93 additions & 0 deletions src/auth_error.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Information about an OAuth 2.0 error returned from the Dropbox API.
class Dropbox.AuthError
# @property {String} one of the {Dropbox.AuthError} constants.
#
# @see http://tools.ietf.org/html/rfc6749
code: null

# @property {?String} developer-friendly explanation of the error
description: null

# @property {?String} URL to a developer-friendly page
uri: null

# Error code indicating the user did not authorize the application.
#
# This error is reported when a user clicks the 'Deny' button on the
# Authorization dialog.
@ACCESS_DENIED: 'access_denied'

# Error code indicating a malformed OAuth request.
#
# This indicates a bug in dropbx.js and should never occur under normal
# circumstanes.
@INVALID_REQUEST: 'invalid_request'

# Error code indicating that the client is not allowed to use a OAuth method.
#
# This is most likely due to an error in the application's configuration.
# @see https://www.dropbox.com/developers/apps
@UNAUTHORIZED_CLIENT: 'unauthorized_client'

# Error code indicating an invalid or already-used authorization code.
#
# This indicates a bug in dropbox.js and should never occur under normal
# circumstances. A faulty OAuth driver might cause this error.
@INVALID_GRANT: 'invalid_grant'

# Error code indicating an invalid scope parameter.
#
# This version of dropbox.js does not use OAuth 2.0 scopes, so this error
# indicates a bug in the library, or that the library must be updated.
@INVALID_SCOPE: 'invalid_scope'

# Error code indicating an un-implemented or invalid authorization method.
#
# This indicates a bug in dropbox.js and should never occur under normal
# circumstances.
@UNSUPPORTED_GRANT_TYPE: 'unsupported_grant_type'

# Error code indicating an un-implemented or invalid authorization method.
#
# This indicates a bug in dropbox.js and should never occur under normal
# circumstances.
@UNSUPPORTED_RESPONSE_TYPE: 'unsupported_response_type'

# The OAuth 2.0 equivalent of a HTTP 500 error code.
#
# This indicates a bug in the Dropbox API server. The application and/or
# dropbox.js will have to be modified to work around the bug.
@SERVER_ERROR: 'server_error'

# The OAuth 2.0 equivalent of a HTTP 503 error code.
#
# This occurrs when the application is rate-limited.
@TEMPORARILY_UNAVAILABLE: 'temporarily_unavailable'

# Wraps an XHR error.
#
# @param {Object} queryString a parsed response from the /authorize or /token
# OAuth 2.0 endpoints
# @throw {Error} if queryString does not represent an OAuth 2.0 error
# response
constructor: (queryString) ->
unless queryString.error
throw new Error("Not an OAuth 2.0 error: #{JSON.stringify(queryString)}")

if typeof queryString.error is 'object' and queryString.error.error
# The API server sometimes returns the OAuth 2.0 error information
# wrapped in an 'error' object.
root = queryString.error
else
root = queryString
@code = root.error
@description = root.error_description or null
@uri = root.error_uri or null

# Developer-friendly summary of the error.
toString: ->
"Dropbox OAuth error #{@code} :: #{@description}"

# Used by some testing frameworks.
inspect: ->
@toString()
50 changes: 26 additions & 24 deletions src/client.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ class Dropbox.Client
# about to be sent; if the event is canceled by returning a falsey value
# from a listener, the network request is silently discarded; whenever
# possible, listeners should restrict themselves to using the xhr property
# of the Dropbox.Util.Xhr instance passed to them; everything else in the
# Dropbox.Util.Xhr API is in flux
# of the {Dropbox.Util.Xhr} instance passed to them; everything else in the
# {Dropbox.Util.Xhr} API is in flux
onXhr: null

# @property {Dropbox.Util.EventSource<Dropbox.ApiError>} fires non-cancelable
Expand Down Expand Up @@ -84,14 +84,20 @@ class Dropbox.Client

# Authenticates the app's user to Dropbox' API server.
#
# In most cases, the process will involve sending the user to an
# authorization server on the Dropbox servers. If the user clicks "Allow",
# the application will be authorized. If the user clicks "Deny", the method
# will pass a {Dropbox.AuthError} to its callback, and the error's code will
# be {Dropbox.AuthError.ACCESS_DENIED}.
#
# @param {?Object} options one or more of the advanced settings below
# @option options {Boolean} interactive if false, the authentication process
# will stop and call the callback whenever it would have to wait for an
# authorization; true by default; this is useful for determining if the
# authDriver has cached credentials available
# @param {?function(?Dropbox.ApiError, Dropbox.Client)} callback called when
# the authentication completes; if successful, the second parameter is this
# client and the first parameter is null
# @param {?function(?Dropbox.ApiError|Dropbox.AuthError, Dropbox.Client)}
# callback called when the authentication completes; if successful, the
# second parameter is this client and the first parameter is null
# @return {Dropbox.Client} this, for easy call chaining
authenticate: (options, callback) ->
if !callback and typeof options is 'function'
Expand All @@ -117,6 +123,7 @@ class Dropbox.Client
# This is repetitive stuff done at the end of each step.
_fsmNextStep = =>
@authStep = @oauth.step()
@authError = @oauth.error() if @authStep is DropboxClient.ERROR
@_credentials = null
@onAuthStepChange.dispatch @
_fsmStep()
Expand Down Expand Up @@ -160,14 +167,9 @@ class Dropbox.Client
authUrl = @authorizeUrl()
@driver.doAuthorize authUrl, @oauth.authStateParam(), @,
(queryParams) =>
if queryParams.error
# TODO(pwnall): wrap the error around a Dropbox.ApiError
# or create a Dropbox.AuthError
_fsmErrorStep()
else
@oauth.processRedirectParams queryParams
@uid = queryParams.uid
_fsmNextStep()
@oauth.processRedirectParams queryParams
@uid = queryParams.uid if queryParams.uid
_fsmNextStep()

when DropboxClient.PARAM_LOADED
# Check a previous state parameter.
Expand All @@ -177,14 +179,9 @@ class Dropbox.Client
_fsmNextStep()
return
@driver.resumeAuthorize @oauth.authStateParam(), @, (queryParams) =>
if queryParams.error
# TODO(pwnall): wrap the error around a Dropbox.ApiError
# or create a Dropbox.AuthError
_fsmErrorStep()
else
@oauth.processRedirectParams queryParams
@uid = queryParams.uid
_fsmNextStep()
@oauth.processRedirectParams queryParams
@uid = queryParams.uid if queryParams.uid
_fsmNextStep()

when DropboxClient.AUTHORIZED
# Request token authorized, switch it for an access token.
Expand Down Expand Up @@ -1308,13 +1305,18 @@ class Dropbox.Client
# @private
# This a low-level method called by authorize. Users should call authorize.
#
# @param {function(error, data)} callback called with the result of the
# /oauth/access_token HTTP request
# @param {function(?Dropbox.ApiError|Dropbox.AuthError, Object)} callback
# called with the result of the /oauth/access_token HTTP request
getAccessToken: (callback) ->
params = @oauth.accessTokenParams @driver.url()
xhr = new Dropbox.Util.Xhr('POST', @urls.token).setParams(params).
addOauthParams(@oauth)
@dispatchXhr xhr, callback
@dispatchXhr xhr, (error, data) ->
if error and error.status is Dropbox.ApiError.INVALID_PARAM and
error.response and error.response.error
# Return AuthError instances for OAuth errors.
error = new Dropbox.AuthError error.response
callback error, data

# Prepares and sends an XHR to the Dropbox API server.
#
Expand Down
23 changes: 22 additions & 1 deletion src/util/oauth.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Dropbox.Util.Oauth
@_token = null
@_tokenKey = null
@_tokenKid = null
@_error = null
@_appHash = null
@_loaded = null
@setCredentials options
Expand All @@ -32,6 +33,7 @@ class Dropbox.Util.Oauth
@_id = null
@_secret = options.secret or null
@_appHash = null
@_error = null
@_loaded = true

@reset()
Expand Down Expand Up @@ -79,6 +81,8 @@ class Dropbox.Util.Oauth
Dropbox.Client.PARAM_LOADED
else
Dropbox.Client.PARAM_SET
else if @_error isnt null
Dropbox.Client.ERROR
else
Dropbox.Client.RESET

Expand Down Expand Up @@ -113,6 +117,12 @@ class Dropbox.Util.Oauth
authStateParam: ->
@_stateParam

# @private
# This should only be called by Dropbox.Client#authenticate. All other code
# should use Dropbox.Client#error.
error: ->
@_error

# Assimilates the information in an /authorize redirect's query parameters.
#
# The parameters may contain an access code, which will bring the Oauth
Expand All @@ -130,6 +140,13 @@ class Dropbox.Util.Oauth
# @see RFC 6750 for OAuth 2.0 Bearer Tokens
# @see draft-ietf-oauth-v2-http-mac for OAuth 2.0 MAC Tokens
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')
Expand All @@ -140,6 +157,10 @@ class Dropbox.Util.Oauth

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}")

Expand Down Expand Up @@ -302,7 +323,7 @@ class Dropbox.Util.Oauth
# @return {String} a string that uniquely identifies the OAuth application
appHash: ->
return @_appHash if @_appHash
@_appHash = Dropbox.Util.sha1(@_id).replace(/\=/g, '')
@_appHash = Dropbox.Util.sha1('oauth2-' + @_id).replace(/[\/+=]/g, '')

# Drops all user-specific OAuth information.
#
Expand Down
2 changes: 2 additions & 0 deletions test/html/redirect_driver_test.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
client.reset();
client.authDriver(new Dropbox.AuthDriver.Redirect(
{scope: "redirect-integration"}));
console.log(client);
client.authenticate(function(error, _client) {
var message = [error || null, _client && _client.credentials()];
var json = JSON.stringify(message);
console.log(json);
try {
// NOTE: postMessage to '*' is acceptable for a test case, but
// not for production code, as it can leak data; see
Expand Down
9 changes: 5 additions & 4 deletions test/src/client_test.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -1504,10 +1504,11 @@ buildClientTests = (clientKeys) ->
@client.authenticate interactive: false, (error, client) ->
expect(error).to.be.ok
unless Dropbox.Util.Xhr.ieXdr
expect(error.status).to.equal Dropbox.ApiError.INVALID_PARAM
expect(error.response).to.have.property 'error'
expect(error.response.error).to.have.property 'error_description'
expect(error.response.error.error_description).to.
expect(error).to.be.instanceOf Dropbox.AuthError
expect(error).to.have.property 'code'
expect(error.code).to.equal Dropbox.AuthError.INVALID_GRANT
expect(error).to.have.property 'description'
expect(error.description).to.
match(/code.*not valid/i)
done()

Expand Down
Loading

0 comments on commit 262b9fd

Please sign in to comment.