scribe-node
Version:
Scribe java OAuth library port to node.js
658 lines (546 loc) • 19.3 kB
text/coffeescript
# Scribe Java OAuth library port to node.js
#
# This library abstracts different OAuth schemes and modularizes web services so
# that both authorization routines and data retrieval after authorization is easy to do.
# Library design is borrowed from the corresponding java library:
#
# https://github.com/fernandezpablo85/scribe-java
#
# See README for usage and examples.
#
# Author: Marko Manninen <mmstud .com> (http://about.me/markomanninen)
# Copyright (c) 2011
root = exports ? this
http = require 'http'
https = require 'https'
url = require 'url'
crypto = require 'crypto'
root.load = (apis) ->
for api in apis
root[api] = require('./widgets/' + api)[api]
return this
# Verifier class
class root.Verifier
constructor: ( ) ->
if not
console.log "Must provide a valid string for verifier"
getValue: ->
# Token class
class root.Token
# expires, type and refresh are OAuth 2.0 specific arguments
constructor: ( , , = null, = null, = null, = null) ->
updateToken: (refresh_token) ->
= refresh_token.getToken()
getToken: ->
getSecret: ->
getExpires: ->
getType: ->
getRefresh: ->
getRawResponse: ->
if not
console.log "This token object was not constructed by scribe and does not have a rawResponse"
return ""
# OAuth Consonants
OAuthConstants =
# OAuth 1.0a
TIMESTAMP: "oauth_timestamp"
SIGN_METHOD: "oauth_signature_method"
SIGNATURE: "oauth_signature"
CONSUMER_SECRET: "oauth_consumer_secret"
CONSUMER_KEY: "oauth_consumer_key"
CALLBACK: "oauth_callback"
VERSION: "oauth_version"
NONCE: "oauth_nonce"
PARAM_PREFIX: "oauth_"
TOKEN: "oauth_token"
TOKEN_SECRET: "oauth_token_secret"
OUT_OF_BAND: "oob"
VERIFIER: "oauth_verifier"
HEADER: "Authorization"
# TODO: I'm not sure if there is really rationale for token object. Empty string could work as well...
EMPTY_TOKEN: new root.Token("", "")
SCOPE: "scope"
# OAuth 2.0
ACCESS_TOKEN: "access_token"
CLIENT_ID: "client_id"
CLIENT_SECRET: "client_secret"
REDIRECT_URI: "redirect_uri"
GRANT_TYPE: "grant_type"
AUTHORIZATION_CODE: "authorization_code"
EXPIRES_IN: "expires_in"
TOKEN_TYPE: "token_type"
REFRESH_TOKEN: "refresh_token"
CODE: "code"
BEARER: "Bearer "
# Verbs
Verb =
GET: "GET"
POST: "POST"
PUT: "PUT"
DELETE: "DELETE"
# Signature types
SignatureType =
Header: "Header"
QueryString: "QueryString"
encode_data = (data) ->
if not data
return ""
data = encodeURIComponent data
# Fix the mismatch between OAuth's RFC3986 and Javascript
# Note: tokens can have % characters too why BaseStringExtractorImpl
# and getSortedAndEncodedParams has a custom fix too. TODO: are there other
# similar exceptions?
data.replace(/\!/g, "%21")
.replace(/\'/g, "%27")
.replace(/\(/g, "%28")
.replace(/\)/g, "%29")
.replace(/\*/g, "%2A")
decode_data = (data) ->
if not data
return ""
data = data.replace /\+/g, " "
return decodeURIComponent data
extract_token = (data, regex) ->
if data
result = regex.exec data
if result && result[1]
return result[1]
return ""
params_to_query = (params, cb = null) ->
query = ""
for key, value of params
if cb
value = cb value
query += key+"="+value+"&"
return query.substr 0, query.length-1
sort_by_keys = (obj) ->
keys = [];
for key in obj
keys.push(key)
return keys
object_merge = ->
out = {}
return out unless arguments.length
i = 0
while i < arguments.length
for key of arguments[i]
out[key] = arguments[i][key]
i++
return out
sort_obj = (obj, idx) ->
sortable = []
for k, v of obj
sortable.push [k, v]
sortable.sort (a, b) ->
a[idx] + b[idx]
sortable.sort()
root.get_nonce = () ->
tsi = new TimestampServiceImpl
return tsi.getNonce()
#
class JsonTokenExtractorImpl
extract: (response_data) ->
if not response_data
console.log "Response body is incorrect. Can't extract a token from an empty string"
return OAuthConstants.EMPTY_TOKEN
new root.Token(extract_token(response_data, /"access_token"\s*:\s*"(\S*?)"/g), "", response_data, extract_token(response_data, /"expires_in"\s*:\s*([0-9]*)/g), extract_token(response_data, /"token_type"\s*:\s*"(\S*?)"/g), extract_token(response_data, /"refresh_token"\s*:\s*"(\S*?)"/g))
#
class TokenExtractor20Impl
extract: (response_data) ->
if not response_data
console.log "Response body is incorrect. Can't extract a token from an empty string"
return OAuthConstants.EMPTY_TOKEN
new root.Token(extract_token(response_data, /access_token=([^&]+)/g), "", response_data, extract_token(response_data, /expires_in=([^&]+)/g), extract_token(response_data, /token_type=([^&]+)/g), extract_token(response_data, /refresh_token=([^&]+)/g))
#
class TokenExtractorImpl
extract: (response_data) ->
if not response_data
console.log "Response body is incorrect. Can't extract a token from an empty string"
return OAuthConstants.EMPTY_TOKEN
new root.Token(extract_token(response_data, /oauth_token=([^&]+)/g), extract_token(response_data, /oauth_token_secret=([^&]+)/g), response_data)
#
class BaseStringExtractorImpl
extract: (request) ->
if not request
console.log "Cannot extract base string from null object"
return ""
params = request
request.getVerb()+"&"+encode_data(request.getUrl())+"&"+encode_data(params)
getSortedAndEncodedParams: (request) ->
params = object_merge(request.queryStringParams,
request.bodyParams,
request.oauthParameters)
params = sort_obj params
query = ''
for pair in params
query += pair[0]+"="+encode_data(pair[1]).replace('%25', "%")+"&"
query.substr 0, query.length-1
#
class HeaderExtractorImpl
extract: (request) ->
if not request
console.log "Cannot extract a header from a null object"
return ""
header = "OAuth "
for key, value of request.oauthParameters
header += key+'="'+encode_data(value).replace('%25', "%")+'", '
header.substr 0, header.length-2
#
class HMACSha1SignatureService
constructor: ->
= "sha1"
= "HMAC-SHA1"
getSignature: (base_string, api_secret, token_secret) ->
if not base_string
console.log "Base string cant be null or empty string"
return ""
if not api_secret
console.log "Api secret cant be null or empty string"
return ""
base_string, api_secret + '&' + encode_data token_secret
doSign: (data, key) ->
crypto.createHmac( , key).update(data).digest "base64"
getSignatureMethod: ->
#
class PlaintextSignatureService
constructor: ->
= "plaintext"
getSignature: (base_string, api_secret, token_secret) ->
if not api_secret
console.log "Api secret cant be null or empty string"
return ""
api_secret + '&' + token_secret
getSignatureMethod: ->
#
class Timer
getMillis: ->
new Date().getTime()
getRandomInteger: ->
Math.floor Math.random()*100000000000000000
#
class TimestampServiceImpl
constructor: ->
= new Timer
getNonce: ->
() + .getRandomInteger()
getTimestampInSeconds: ->
Math.floor ( .getMillis() / 1000)
setTimer: ( ) ->
# Request class
class Request
constructor: ( , ) ->
= {}
= {}
= {}
= 'utf8'
# parse query string
query = .split('?')
if query[1]
vals = query[1].split("&")
for val in vals
pair = val.split("=")
pair[0], pair[1]
# set up plain url without query string
= query[0]
getBodyParams: ->
getUrl: ->
getVerb: ->
getHeaders: ->
request: (protocol, options, callback) ->
encoding =
protocol.request options, (res) ->
#console.log 'STATUS: ' + res.statusCode
#console.log 'HEADERS: ' + JSON.stringify res.headers
#console.log 'ENCODING: ' + encoding
res.setEncoding(encoding)
res.data = ''
res.on 'data', (chunk) ->
#console.log 'DATA: ...'
this.data += chunk
res.on 'end', () ->
#console.log 'END: ' + data
callback this
res.on 'close', () ->
#console.log 'CLOSE: ' + data
callback this
send: (callback) ->
parsed_options = url.parse( )
options = {}
options['host'] = parsed_options['hostname']
# TODO: handle ports other than 80, 443
#options['port'] = 80
params = params_to_query , encode_data
options['path'] = parsed_options['pathname'] + (if params then '?'+params else '')
options['method'] =
post_data = null
if == Verb.PUT || == Verb.POST
post_data = params_to_query , encode_data
#some services might need content length header, but question is, if req.write handles it already?
# ['Content-Length'] = Buffer.byteLength(post_data, )
options['headers'] =
#console.log 'OPTIONS: ' + JSON.stringify options
protocol = if parsed_options['protocol'] == 'https:' then https else http
req = protocol, options, callback
req.on 'error', (e) ->
console.log 'Problem with sent request: ' + e.message
if post_data
req.write post_data
req.end()
addHeader: (key, value) ->
[key] = value
addBodyParameter: (key, value) ->
[key] = value
addQueryStringParameter: (key, value) ->
[key] = value
setEncoding: ( ) ->
# OAuth request class
class OAuthRequest extends Request
constructor: (verb, url) ->
super verb, url
= {}
addOAuthParameter: (key, value) ->
[key] = value
# OAuth configuration class
class OAuthConfig
constructor: ( , , cb = null, type = null, = null) ->
if cb != null
= cb
else
= OAuthConstants.OUT_OF_BAND
if type != null
= type
else
= SignatureType.Header
getApiKey: ->
getApiSecret: ->
getCallback: ->
getSignatureType: ->
getScope: ->
hasScope: ->
if ()
return true
return false
# shared methods for 1.0a and 2.0 implementation
class OAuthServiceImpl
signedImagePostRequest: (token, cb, endpoint, params) ->
request = new OAuthRequest Verb.POST, endpoint
request.setEncoding('binary')
for key, value of params
request.addBodyParameter key, value
token, request
request.send cb
signedPostRequest: (token, cb, endpoint, params) ->
request = new OAuthRequest Verb.POST, endpoint
for key, value of params
request.addBodyParameter key, value
token, request
request.send cb
signedRequest: (token, cb, endpoint) ->
request = new OAuthRequest .getRequestVerb(), endpoint
token, request
request.send cb
getVersion: ->
addBodyParam: (key, value) ->
.addBodyParameter key, value
addBodyParams: (params) ->
for key, value in params
key, value
# OAuth 1.0a implementation
class OAuth10aServiceImpl extends OAuthServiceImpl
constructor: ( , ) ->
= "1.0"
= new OAuthRequest .getRequestTokenVerb(), .getRequestTokenEndpoint()
getRequestToken: (cb) ->
req =
if scope = .getScope()
# NOTE: google has a scope on signature for example
req.addQueryStringParameter 'scope', scope
req.addOAuthParameter OAuthConstants.CALLBACK, .getCallback()
req, OAuthConstants.EMPTY_TOKEN
req
req.send cb
addOAuthParams: (request, token) ->
request.addOAuthParameter OAuthConstants.TIMESTAMP, .getTimestampService().getTimestampInSeconds()
request.addOAuthParameter OAuthConstants.NONCE, .getTimestampService().getNonce()
request.addOAuthParameter OAuthConstants.CONSUMER_KEY, .getApiKey()
request.addOAuthParameter OAuthConstants.SIGN_METHOD, .getSignatureService().getSignatureMethod()
request.addOAuthParameter OAuthConstants.VERSION, ()
#if scope = .getScope()
# google doesnt have scope on oauth headers but on query string onl. how about others?
#request.addOAuthParameter OAuthConstants.SCOPE, scope
request.addOAuthParameter OAuthConstants.SIGNATURE, request, token
getAccessToken: (request_token, verifier, cb) ->
request = new OAuthRequest .getAccessTokenVerb(), .getAccessTokenEndpoint()
request.addOAuthParameter OAuthConstants.TOKEN, request_token.getToken()
request.addOAuthParameter OAuthConstants.VERIFIER, verifier.getValue()
request, request_token
request
request.send cb
signRequest: (token, request) ->
for key, value of .getHeaders()
request.addHeader key, value
request.addOAuthParameter OAuthConstants.TOKEN, token.getToken()
request, token
request
getAuthorizationUrl: (request_token) ->
.getAuthorizationUrl request_token
getSignature: (request, token) ->
base_string = .getBaseStringExtractor().extract request
.getSignatureService().getSignature base_string, .getApiSecret(), token.getSecret()
addSignature: (request) ->
if .getSignatureType() == SignatureType.Header
oauthHeader = .getHeaderExtractor().extract request
request.addHeader OAuthConstants.HEADER, oauthHeader
else if .getSignatureType() == SignatureType.QueryString
for key, value of request.oauthParameters
request.addQueryStringParameter key, value
# shared api methods for 1.0a and 2.0 implementation
class DefaultApi
GET: Verb.GET
POST: Verb.POST
PUT: Verb.PUT
DELETE: Verb.DELETE
getHeaders: () ->
headers = {}
headers['User-Agent'] = 'Scribe OAuth Client (node.js)'
return headers
getJsonTokenExtractor: ->
new JsonTokenExtractorImpl().extract
getAccessTokenVerb: ->
getRequestTokenVerb: ->
getRequestVerb: ->
# OAuth version 1a default API. To be included on widgets
class root.DefaultApi10a extends DefaultApi
getAccessTokenExtractor: ->
new TokenExtractorImpl().extract
getBaseStringExtractor: ->
new BaseStringExtractorImpl
getHeaderExtractor: ->
new HeaderExtractorImpl
getRequestTokenExtractor: ->
new TokenExtractorImpl().extract
getSignatureService: ->
new HMACSha1SignatureService
getTimestampService: ->
new TimestampServiceImpl
createService: (config) ->
new OAuth10aServiceImpl this, config
# OAuth 2.0 implementation
class OAuth20ServiceImpl extends OAuthServiceImpl
constructor: ( , ) ->
= "2.0"
getToken: (cb, params, endpoint) ->
verb = .getAccessTokenVerb()
request = new OAuthRequest verb, endpoint
# TODO: is this universal behavior or just Google requires to add params on body on post?
if verb == Verb.POST || verb == Verb.PUT
for key, value of params
request.addBodyParameter key, value
else
for key, value of params
request.addQueryStringParameter key, value
#NOTE: at least Google requires special content type (application/x-www-form-urlencoded) on headers
for key, value of .getHeaders()
request.addHeader key, value
return request.send cb
getAccessToken: (verifier, cb) ->
params = {}
params[OAuthConstants.CLIENT_ID] = .getApiKey()
params[OAuthConstants.CLIENT_SECRET] = .getApiSecret()
params[OAuthConstants.CODE] = verifier.getValue()
params[OAuthConstants.REDIRECT_URI] = .getCallback()
params[OAuthConstants.GRANT_TYPE] = OAuthConstants.AUTHORIZATION_CODE
# TODO: im not sure if scope is really needed to get access token?
if .hasScope()
params[OAuthConstants.SCOPE] = .getScope()
cb, params, .getAccessTokenEndpoint()
getRefreshToken: (access_token, cb) ->
params = {}
params[OAuthConstants.CLIENT_ID] = .getApiKey()
params[OAuthConstants.CLIENT_SECRET] = .getApiSecret()
params[OAuthConstants.REFRESH_TOKEN] = access_token.getRefresh()
params[OAuthConstants.GRANT_TYPE] = OAuthConstants.REFRESH_TOKEN
if .hasScope()
params[OAuthConstants.SCOPE] = .getScope()
cb, params, .getRefreshTokenEndpoint()
getRequestToken: ->
console.log "Unsupported operation, please use 'getAuthorizationUrl' and redirect your users there"
signRequest: (access_token, request) ->
type = access_token.getType()
if type and type.toLowerCase() == 'bearer'
request.addHeader OAuthConstants.HEADER, OAuthConstants.BEARER + access_token.getToken()
else
request.addQueryStringParameter OAuthConstants.ACCESS_TOKEN, access_token.getToken()
getAuthorizationUrl: ->
.getAuthorizationUrl
# OAuth version 2 default API. To be included on widgets
class root.DefaultApi20 extends DefaultApi
isFresh: (response_data) ->
if extract_token(extract_token(response_data, /"error"\s*:\s*\{(.*?)\}\}/g), /"message"\s*:\s*"(.*?)"/g).toLowerCase() == "invalid credentials" then false else true
getAccessTokenExtractor: ->
new TokenExtractor20Impl().extract
getAccessTokenVerb: ->
createService: (config) ->
new OAuth20ServiceImpl this, config
# Main API service builder
class root.ServiceBuilder
constructor: ( = null, = null) ->
= OAuthConstants.OUT_OF_BAND
provider: (apiClass) ->
if not apiClass
console.log "Error: API class not given!"
else
= new apiClass
this
_callback: ( ) ->
if not
console.log "Notice: Callback not given"
this
apiKey: ( ) ->
if not
console.log "Error: API key not given!"
this
apiSecret: ( ) ->
if not
console.log "Warning: API secret not given"
this
_scope: ( ) ->
if not
console.log "Warning: OAuth scope not given"
this
signatureType: ( ) ->
if not
# see OAuthConfig
console.log "Notice: Signature type not given. Header type will be used."
this
build: ->
if not
console.log "Error: You must specify a valid API through the provider() method"
if not
console.log "Error: You must provide an API key"
if not
console.log "Warning: You didnt provide an API secret"
.createService(new OAuthConfig , , , , )