examples/gae/showcase/static/js/authomatic.coffee
###
# CoffeeDoc example documentation #
This is a module-level docstring, and will be displayed at the top of the module documentation.
Documentation generated by [CoffeeDoc](http://github.com/omarkhan/coffeedoc)
npm install -g coffeedoc
###
$ = jQuery
jsonPCallbackCounter = 0
globalOptions =
# Popup options
logging: on
popupWidth: 800
popupHeight: 600
popupLinkSelector: 'a.authomatic'
popupFormSelector: 'form.authomatic'
popupFormValidator: ($form) -> true
# Access options
backend: null
forceBackend: off
substitute: {}
params: {}
headers: {}
body: ''
jsonpCallbackPrefix: 'authomaticJsonpCallback'
# Callbacks
onPopupInvalid: null # ($form) ->
onPopupOpen: null # (url) ->
onLoginComplete: null # (result) ->
onBackendStart: null # (data) ->
onBackendComplete: null # (data, textStatus, jqXHR) ->
onAccessSuccess: null # (data, textStatus, jqXHR) ->
onAccessComplete: null # (jqXHR, textStatus) ->
###########################################################
# Internal functions
###########################################################
# Logs to the console if available and if globalOptions.logging is true.
log = (args...) ->
console?.log('Authomatic:', args...) if (globalOptions.logging and console?.log?.apply?)
# Opens a new centered window with the URL specified.
openWindow = (url) ->
width = globalOptions.popupWidth
height = globalOptions.popupHeight
top = (screen.height / 2) - (height / 2)
left = (screen.width / 2) - (width / 2)
settings = "width=#{width},height=#{height},top=#{top},left=#{left}"
log('Opening popup:', url)
globalOptions.onPopupOpen?(url)
window.open(url, '', settings)
# Parses a querystring and returns object.
parseQueryString = (queryString) ->
result = {}
for item in queryString.split('&')
[k, v] = item.split('=')
v = decodeURIComponent(v)
if result.hasOwnProperty k
if Array.isArray result[k]
result[k].push(v)
else
result[k] = [result[k], v]
else
result[k] = v
result
# Parses URL and returns an object with url, query and params properties.
parseUrl = (url) ->
log('parseUrl', url)
questionmarkIndex = url.indexOf('?')
if questionmarkIndex >= 0
u = url.substring(0, questionmarkIndex)
qs = url.substring(questionmarkIndex + 1)
else
u = url
url: u
query: qs
params: parseQueryString(qs) if qs
# Deserializes credentials and returns an object with id, typeId, type, subtype and rest properties.
deserializeCredentials = (credentials) ->
sc = decodeURIComponent(credentials).split('\n')
typeId = sc[1]
[type, subtype] = typeId.split('-')
id: parseInt(sc[0])
typeId: typeId
type: parseInt(type)
subtype: parseInt(subtype)
rest: sc[2..]
# Resolves provider class based on credentials.
getProviderClass = (credentials) ->
{type, subtype} = deserializeCredentials(credentials)
if type is 1
if subtype is 2
# Flickr needs special treatment.
Flickr
else
# OAuth 1.0a providers.
Oauth1Provider
else if type is 2
# OAuth 2 providers.
if subtype is 6
# Foursquare needs special treatment.
Foursquare
else if subtype is 9
# So does LinkedIn.
LinkedIn
else if subtype is 14
# So does WindowsLive.
WindowsLive
else if subtype in [12, 15]
# Viadeo and Yammer support neither CORS nor JSONP.
BaseProvider
else
Oauth2Provider
else
# Base provider allways works.
BaseProvider
# Substitutes {dotted.path.to.object.property} tags in a template string with the value
# found in the path target in the substitute object.
format = (template, substitute) ->
# Replace all {object.value} tags
template.replace /{([^}]*)}/g, (match, tag)->
# Traverse through dotted path in substitute
target = substitute
for level in tag.split('.')
target = target[level]
# Return value of the last object in the path
# TODO: URL encode
target
###########################################################
# Global objects
###########################################################
window.authomatic = new class Authomatic
setup: (options) ->
$.extend(globalOptions, options)
log 'Setting up authomatic to:', globalOptions
popupInit: ->
$(globalOptions.popupLinkSelector).click (e) ->
e.preventDefault()
openWindow($(this).attr('href'))
$(globalOptions.popupFormSelector).submit (e) ->
e.preventDefault()
$form = $(this);
url = $form.attr('action') + '?' + $form.serialize()
if globalOptions.popupFormValidator($form)
openWindow(url)
else
globalOptions.onPopupInvalid?($form)
loginComplete: (result, closer) ->
# We need to deepcopy the result before closing the popup.
# Otherwise IE would loose the reference to the result object.
result_copy = $.extend(true, {}, result)
log('Login procedure complete', result_copy)
# Now we can close the popup.
closer()
globalOptions.onLoginComplete(result_copy)
access: (credentials, url, options = {}) ->
localEvents =
onBackendStart: null
onBackendComplete: null
onAccessSuccess: null
onAccessComplete: null
# Merge options with global options and local events
updatedOptions = {}
$.extend(updatedOptions, globalOptions, localEvents, options)
# Format url.
url = format(url, updatedOptions.substitute)
log 'access options', updatedOptions, globalOptions
if updatedOptions.forceBackend
Provider = BaseProvider
else
Provider = getProviderClass(credentials)
provider = new Provider(options.backend, credentials, url, updatedOptions)
log 'Instantiating provider:', provider
provider.access()
###########################################################
# Providers
###########################################################
# Makes a request to backend. If request elements return, tries cross-domain request first, then JSONP.
class BaseProvider
_x_jsonpCallbackParamName: 'callback'
constructor: (@backend, @credentials, url, @options) ->
@backendRequestType = 'auto'
@jsonpRequest = no
@jsonpCallbackName = "#{globalOptions.jsonpCallbackPrefix}#{jsonPCallbackCounter}"
# Credentials
@deserializedCredentials = deserializeCredentials(@credentials)
@providerID = @deserializedCredentials.id
@providerType = @deserializedCredentials.type
@credentialsRest = @deserializedCredentials.rest
# Request elements
parsedUrl = parseUrl(url)
@url = parsedUrl.url
@params = {}
$.extend(@params, parsedUrl.params, @options.params)
contactBackend: (callback) =>
if @jsonpRequest and @options.method is not 'GET'
@backendRequestType = 'fetch'
data =
type: @backendRequestType
credentials: @credentials
url: @url
method: @options.method
body: @options.body
params: JSON.stringify(@params)
headers: JSON.stringify(@options.headers)
globalOptions.onBackendStart?(data)
@options.onBackendStart?(data)
log "Contacting backend at #{@options.backend}.", data
$.get(@options.backend, data, callback)
contactProvider: (requestElements) =>
{url, method, params, headers, body} = requestElements
# jQuery.ajax options for cross domain request.
options =
type: method
data: params
headers: headers
complete: [
((jqXHR, textStatus) -> log 'Request complete.', textStatus, jqXHR)
globalOptions.onAccessComplete
@options.onAccessComplete
]
success: [
((data) -> log 'Request successfull.', data)
globalOptions.onAccessSuccess
@options.onAccessSuccess
]
error: (jqXHR, textStatus, errorThrown) =>
# If cross domain fails,
if jqXHR.state() is 'rejected'
if @options.method is 'GET'
log 'Cross domain request failed! trying JSONP request.'
# access again but with JSONP request type.
@jsonpRequest = yes
else
@backendRequestType = 'fetch'
@access()
# Additional JSONP options for jQuery.ajax
if @jsonpRequest
jsonpOptions =
jsonpCallback: @jsonpCallbackName
jsonp: @_x_jsonpCallbackParamName
cache: true # If false, jQuery would add a nonce to query string which would break signature.
dataType: 'jsonp'
error: (jqXHR, textStatus, errorThrown) ->
# If JSONP fails, there is not much to do.
log 'JSONP failed! State:', jqXHR.state()
# Add JSONP arguments to options
$.extend(options, jsonpOptions)
log "Contacting provider with JSONP request.", url, options
else
log "Contacting provider with cross domain request", url, options
$.ajax(url, options)
access: =>
callback = (data, textStatus, jqXHR) =>
# Call backend complete callbacks.
globalOptions.onBackendComplete?(data, textStatus, jqXHR)
@options.onBackendComplete?(data, textStatus, jqXHR)
# Find out whether backend returned fetch result or request elements.
responseTo = jqXHR?.getResponseHeader('Authomatic-Response-To')
if responseTo is 'fetch'
log "Fetch data returned from backend.", jqXHR.getResponseHeader('content-type'), data
# Call onAccessSuccess and onAccessComplete callbacks manually.
globalOptions.onAccessSuccess?(data)
@options.onAccessSuccess?(data)
globalOptions.onAccessComplete?(jqXHR, textStatus)
@options.onAccessComplete?(jqXHR, textStatus)
else if responseTo is 'elements'
log "Request elements data returned from backend.", data
@contactProvider(data)
# Increase JSONP callback counter for next JSONP request.
jsonPCallbackCounter += 1 if @jsonpRequest
@contactBackend(callback)
###########################################################
# OAuth 1.0a
###########################################################
# Allways makes a JSONP request.
class Oauth1Provider extends BaseProvider
access: =>
@jsonpRequest = yes
# Add JSONP callback name to params to be included in the OAuth 1.0a signature
@params[@_x_jsonpCallbackParamName] = @jsonpCallbackName
super()
contactProvider: (requestElements) =>
# We must remove the JSONP callback from params, because jQuery will add it too and
# the OAuth 1.0a signature would not be valid if there are two callback params in the querystring.
delete requestElements.params.callback
super(requestElements)
class Flickr extends Oauth1Provider
_x_jsonpCallbackParamName: 'jsoncallback'
###########################################################
# OAuth 2.0
###########################################################
# Skips the backend. Tries cross-domain, then JSONP if method is GET or fetches through backend.
class Oauth2Provider extends BaseProvider
_x_accessToken: 'access_token'
_x_bearer: 'Bearer'
constructor: (args...) ->
super(args...)
# Unpack credentials elements.
[@accessToken, @refreshToken, @expirationTime, @tokenType] = @credentialsRest
@handleTokenType()
handleTokenType: =>
# Handle token type.
if @tokenType is '1'
# If token type is "1" which means "Bearer", pass access token in authorisation header
@options.headers['Authorization'] = "#{@_x_bearer} #{@accessToken}"
else
# Else pass it as querystring parameter
@params[@_x_accessToken] = @accessToken
access: () =>
if @backendRequestType is 'fetch'
super()
else
# Skip backend and go directly to provider.
requestElements =
url: @url
method: @options.method
params: @params
headers: @options.headers
body: @options.body
@contactProvider(requestElements)
class Foursquare extends Oauth2Provider
_x_accessToken: 'oauth_token'
class Google extends Oauth2Provider
_x_bearer: 'OAuth'
class LinkedIn extends Oauth2Provider
_x_accessToken: 'oauth2_access_token'
class WindowsLive extends Oauth2Provider
handleTokenType: =>
# Handle token type.
if @tokenType is '1'
# If token type is "1" which means "Bearer", pass access token in authorisation header
@options.headers['Authorization'] = "#{@_x_bearer} #{@accessToken}"
# Windows Live allways needs access token in querystring.
@params[@_x_accessToken] = @accessToken
# @ sourceMappingURL=authomatic.map