ng-token-auth.coffee 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903
  1. if typeof module != 'undefined' and typeof exports != 'undefined' and module.exports == exports
  2. module.exports = 'ng-token-auth'
  3. angular.module('ng-token-auth', ['ipCookie'])
  4. .provider('$auth', ->
  5. configs =
  6. default:
  7. apiUrl: '/api'
  8. signOutUrl: '/auth/sign_out'
  9. emailSignInPath: '/auth/sign_in'
  10. emailRegistrationPath: '/auth'
  11. accountUpdatePath: '/auth'
  12. accountDeletePath: '/auth'
  13. confirmationSuccessUrl: -> window.location.href
  14. passwordResetPath: '/auth/password'
  15. passwordUpdatePath: '/auth/password'
  16. passwordResetSuccessUrl: -> window.location.href
  17. tokenValidationPath: '/auth/validate_token'
  18. proxyIf: -> false
  19. proxyUrl: '/proxy'
  20. validateOnPageLoad: true
  21. omniauthWindowType: 'sameWindow'
  22. storage: 'cookies'
  23. tokenFormat:
  24. "access-token": "{{ token }}"
  25. "token-type": "Bearer"
  26. client: "{{ clientId }}"
  27. expiry: "{{ expiry }}"
  28. uid: "{{ uid }}"
  29. parseExpiry: (headers) ->
  30. # convert from ruby time (seconds) to js time (millis)
  31. (parseInt(headers['expiry'], 10) * 1000) || null
  32. handleLoginResponse: (resp) -> resp.data
  33. handleAccountUpdateResponse: (resp) -> resp.data
  34. handleTokenValidationResponse: (resp) -> resp.data
  35. authProviderPaths:
  36. github: '/auth/github'
  37. facebook: '/auth/facebook'
  38. google: '/auth/google_oauth2'
  39. defaultConfigName = "default"
  40. return {
  41. configure: (params) ->
  42. # user is using multiple concurrent configs (>1 user types).
  43. if params instanceof Array and params.length
  44. # extend each item in array from default settings
  45. for conf, i in params
  46. # get the name of the config
  47. label = null
  48. for k, v of conf
  49. label = k
  50. # set the first item in array as default config
  51. defaultConfigName = label if i == 0
  52. # use copy preserve the original default settings object while
  53. # extending each config object
  54. defaults = angular.copy(configs["default"])
  55. fullConfig = {}
  56. fullConfig[label] = angular.extend(defaults, conf[label])
  57. angular.extend(configs, fullConfig)
  58. # remove existng default config
  59. delete configs["default"] unless defaultConfigName == "default"
  60. # user is extending the single default config
  61. else if params instanceof Object
  62. angular.extend(configs["default"], params)
  63. # user is doing something wrong
  64. else
  65. throw "Invalid argument: ng-token-auth config should be an Array or Object."
  66. return configs
  67. $get: [
  68. '$http'
  69. '$q'
  70. '$location'
  71. 'ipCookie'
  72. '$window'
  73. '$timeout'
  74. '$rootScope'
  75. '$interpolate'
  76. ($http, $q, $location, ipCookie, $window, $timeout, $rootScope, $interpolate) =>
  77. header: null
  78. dfd: null
  79. user: {}
  80. mustResetPassword: false
  81. listener: null
  82. # called once at startup
  83. initialize: ->
  84. @initializeListeners()
  85. @cancelOmniauthInAppBrowserListeners = (->)
  86. @addScopeMethods()
  87. initializeListeners: ->
  88. #@listener = @handlePostMessage.bind(@)
  89. @listener = angular.bind(@, @handlePostMessage)
  90. if $window.addEventListener
  91. $window.addEventListener("message", @listener, false)
  92. cancel: (reason) ->
  93. # cancel any pending timers
  94. if @requestCredentialsPollingTimer?
  95. $timeout.cancel(@requestCredentialsPollingTimer)
  96. # cancel inAppBrowser listeners if set
  97. @cancelOmniauthInAppBrowserListeners()
  98. # reject any pending promises
  99. if @dfd?
  100. @rejectDfd(reason)
  101. # nullify timer after reflow
  102. return $timeout((=> @requestCredentialsPollingTimer = null), 0)
  103. # cancel any pending processes, clean up garbage
  104. destroy: ->
  105. @cancel()
  106. if $window.removeEventListener
  107. $window.removeEventListener("message", @listener, false)
  108. # handle the events broadcast from external auth tabs/popups
  109. handlePostMessage: (ev) ->
  110. if ev.data.message == 'deliverCredentials'
  111. delete ev.data.message
  112. # check if a new user was registered
  113. oauthRegistration = ev.data.oauth_registration
  114. delete ev.data.oauth_registration
  115. @handleValidAuth(ev.data, true)
  116. $rootScope.$broadcast('auth:login-success', ev.data)
  117. if oauthRegistration
  118. $rootScope.$broadcast('auth:oauth-registration', ev.data)
  119. if ev.data.message == 'authFailure'
  120. error = {
  121. reason: 'unauthorized'
  122. errors: [ev.data.error]
  123. }
  124. @cancel(error)
  125. $rootScope.$broadcast('auth:login-error', error)
  126. # make all public API methods available to directives
  127. addScopeMethods: ->
  128. # bind global user object to auth user
  129. $rootScope.user = @user
  130. # template access to authentication method
  131. $rootScope.authenticate = angular.bind(@, @authenticate)
  132. # template access to view actions
  133. $rootScope.signOut = angular.bind(@, @signOut)
  134. $rootScope.destroyAccount = angular.bind(@, @destroyAccount)
  135. $rootScope.submitRegistration = angular.bind(@, @submitRegistration)
  136. $rootScope.submitLogin = angular.bind(@, @submitLogin)
  137. $rootScope.requestPasswordReset = angular.bind(@, @requestPasswordReset)
  138. $rootScope.updatePassword = angular.bind(@, @updatePassword)
  139. $rootScope.updateAccount = angular.bind(@, @updateAccount)
  140. # check to see if user is returning user
  141. if @getConfig().validateOnPageLoad
  142. @validateUser({config: @getSavedConfig()})
  143. # register by email. server will send confirmation email
  144. # containing a link to activate the account. the link will
  145. # redirect to this site.
  146. submitRegistration: (params, opts={}) ->
  147. successUrl = @getResultOrValue(@getConfig(opts.config).confirmationSuccessUrl)
  148. angular.extend(params, {
  149. confirm_success_url: successUrl,
  150. config_name: @getCurrentConfigName(opts.config)
  151. })
  152. $http.post(@apiUrl(opts.config) + @getConfig(opts.config).emailRegistrationPath, params)
  153. .success((resp)->
  154. $rootScope.$broadcast('auth:registration-email-success', params)
  155. )
  156. .error((resp) ->
  157. $rootScope.$broadcast('auth:registration-email-error', resp)
  158. )
  159. # capture input from user, authenticate serverside
  160. submitLogin: (params, opts={}) ->
  161. @initDfd()
  162. $http.post(@apiUrl(opts.config) + @getConfig(opts.config).emailSignInPath, params)
  163. .success((resp) =>
  164. @setConfigName(opts.config)
  165. authData = @getConfig(opts.config).handleLoginResponse(resp, @)
  166. @handleValidAuth(authData)
  167. $rootScope.$broadcast('auth:login-success', @user)
  168. )
  169. .error((resp) =>
  170. @rejectDfd({
  171. reason: 'unauthorized'
  172. errors: ['Invalid credentials']
  173. })
  174. $rootScope.$broadcast('auth:login-error', resp)
  175. )
  176. @dfd.promise
  177. # check if user is authenticated
  178. userIsAuthenticated: ->
  179. @retrieveData('auth_headers') and @user.signedIn and not @tokenHasExpired()
  180. # request password reset from API
  181. requestPasswordReset: (params, opts={}) ->
  182. successUrl = @getResultOrValue(
  183. @getConfig(opts.config).passwordResetSuccessUrl
  184. )
  185. params.redirect_url = successUrl
  186. params.config_name = opts.config if opts.config?
  187. $http.post(@apiUrl(opts.config) + @getConfig(opts.config).passwordResetPath, params)
  188. .success((resp) ->
  189. $rootScope.$broadcast('auth:password-reset-request-success', params)
  190. )
  191. .error((resp) ->
  192. $rootScope.$broadcast('auth:password-reset-request-error', resp)
  193. )
  194. # update user password
  195. updatePassword: (params) ->
  196. $http.put(@apiUrl() + @getConfig().passwordUpdatePath, params)
  197. .success((resp) =>
  198. $rootScope.$broadcast('auth:password-change-success', resp)
  199. @mustResetPassword = false
  200. )
  201. .error((resp) ->
  202. $rootScope.$broadcast('auth:password-change-error', resp)
  203. )
  204. # update user account info
  205. updateAccount: (params) ->
  206. $http.put(@apiUrl() + @getConfig().accountUpdatePath, params)
  207. .success((resp) =>
  208. updateResponse = @getConfig().handleAccountUpdateResponse(resp)
  209. curHeaders = @retrieveData('auth_headers')
  210. angular.extend @user, updateResponse
  211. # ensure any critical headers (uid + ?) that are returned in
  212. # the update response are updated appropriately in storage
  213. if curHeaders
  214. newHeaders = {}
  215. for key, val of @getConfig().tokenFormat
  216. if curHeaders[key] && updateResponse[key]
  217. newHeaders[key] = updateResponse[key]
  218. @setAuthHeaders(newHeaders)
  219. $rootScope.$broadcast('auth:account-update-success', resp)
  220. )
  221. .error((resp) ->
  222. $rootScope.$broadcast('auth:account-update-error', resp)
  223. )
  224. # permanently destroy a user's account.
  225. destroyAccount: (params) ->
  226. $http.delete(@apiUrl() + @getConfig().accountUpdatePath, params)
  227. .success((resp) =>
  228. @invalidateTokens()
  229. $rootScope.$broadcast('auth:account-destroy-success', resp)
  230. )
  231. .error((resp) ->
  232. $rootScope.$broadcast('auth:account-destroy-error', resp)
  233. )
  234. # open external auth provider in separate window, send requests for
  235. # credentials until api auth callback page responds.
  236. authenticate: (provider, opts={}) ->
  237. unless @dfd?
  238. @setConfigName(opts.config)
  239. @initDfd()
  240. @openAuthWindow(provider, opts)
  241. @dfd.promise
  242. setConfigName: (configName) ->
  243. configName ?= defaultConfigName
  244. @persistData('currentConfigName', configName, configName)
  245. # open external window to authentication provider
  246. openAuthWindow: (provider, opts) ->
  247. omniauthWindowType = @getConfig(opts.config).omniauthWindowType
  248. authUrl = @buildAuthUrl(omniauthWindowType, provider, opts)
  249. if omniauthWindowType is 'newWindow'
  250. @requestCredentialsViaPostMessage(@createPopup(authUrl))
  251. else if omniauthWindowType is 'inAppBrowser'
  252. @requestCredentialsViaExecuteScript(@createPopup(authUrl))
  253. else if omniauthWindowType is 'sameWindow'
  254. @visitUrl(authUrl)
  255. else
  256. throw 'Unsupported omniauthWindowType "#{omniauthWindowType}"'
  257. # testing actual redirects is difficult. stub this for testing
  258. visitUrl: (url) ->
  259. $window.location.replace(url)
  260. buildAuthUrl: (omniauthWindowType, provider, opts={}) ->
  261. authUrl = @getConfig(opts.config).apiUrl
  262. authUrl += @getConfig(opts.config).authProviderPaths[provider]
  263. authUrl += '?auth_origin_url=' + encodeURIComponent($window.location.href)
  264. params = angular.extend({}, opts.params || {}, {
  265. omniauth_window_type: omniauthWindowType
  266. })
  267. for key, val of params
  268. authUrl += '&'
  269. authUrl += encodeURIComponent(key)
  270. authUrl += '='
  271. authUrl += encodeURIComponent(val)
  272. return authUrl
  273. # ping auth window to see if user has completed registration.
  274. # this method is recursively called until:
  275. # 1. user completes authentication
  276. # 2. user fails authentication
  277. # 3. auth window is closed
  278. requestCredentialsViaPostMessage: (authWindow) ->
  279. # user has closed the external provider's auth window without
  280. # completing login.
  281. if authWindow.closed
  282. @handleAuthWindowClose(authWindow)
  283. # still awaiting user input
  284. else
  285. authWindow.postMessage("requestCredentials", "*")
  286. @requestCredentialsPollingTimer = $timeout((=>@requestCredentialsViaPostMessage(authWindow)), 500)
  287. # handle inAppBrowser's executeScript flow
  288. # flow will complete if:
  289. # 1. user completes authentication
  290. # 2. user fails authentication
  291. # 3. inAppBrowser auth window is closed
  292. requestCredentialsViaExecuteScript: (authWindow) ->
  293. @cancelOmniauthInAppBrowserListeners()
  294. handleAuthWindowClose = @handleAuthWindowClose.bind(this, authWindow)
  295. handleLoadStop = @handleLoadStop.bind(this, authWindow)
  296. authWindow.addEventListener('loadstop', handleLoadStop)
  297. authWindow.addEventListener('exit', handleAuthWindowClose)
  298. this.cancelOmniauthInAppBrowserListeners = () ->
  299. authWindow.removeEventListener('loadstop', handleLoadStop)
  300. authWindow.removeEventListener('exit', handleAuthWindowClose)
  301. # responds to inAppBrowser window loads
  302. handleLoadStop: (authWindow) ->
  303. _this = this
  304. authWindow.executeScript({code: 'requestCredentials()'}, (response) ->
  305. data = response[0]
  306. if data
  307. ev = new Event('message')
  308. ev.data = data
  309. _this.cancelOmniauthInAppBrowserListeners()
  310. $window.dispatchEvent(ev)
  311. _this.initDfd();
  312. authWindow.close()
  313. )
  314. # responds to inAppBrowser window closes
  315. handleAuthWindowClose: (authWindow) ->
  316. @cancel({
  317. reason: 'unauthorized'
  318. errors: ['User canceled login']
  319. })
  320. @cancelOmniauthInAppBrowserListeners
  321. $rootScope.$broadcast('auth:window-closed')
  322. # popups are difficult to test. mock this method in testing.
  323. createPopup: (url) ->
  324. $window.open(url, '_blank')
  325. # this needs to happen after a reflow so that the promise
  326. # can be rejected properly before it is destroyed.
  327. resolveDfd: ->
  328. @dfd.resolve(@user)
  329. $timeout((=>
  330. @dfd = null
  331. $rootScope.$digest() unless $rootScope.$$phase
  332. ), 0)
  333. # generates query string based on simple or complex object graphs
  334. buildQueryString: (param, prefix) ->
  335. str = []
  336. for k,v of param
  337. k = if prefix then prefix + "[" + k + "]" else k
  338. encoded = if angular.isObject(v) then @buildQueryString(v, k) else (k) + "=" + encodeURIComponent(v)
  339. str.push encoded
  340. str.join "&"
  341. # parses raw URL for querystring parameters to account for issues
  342. # with querystring / fragment ordering in angular < 1.4.x
  343. parseLocation: (location) ->
  344. pairs = location.substring(1).split('&')
  345. obj = {}
  346. pair = undefined
  347. i = undefined
  348. for i of pairs
  349. `i = i`
  350. if pairs[i] == ''
  351. continue
  352. pair = pairs[i].split('=')
  353. obj[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1])
  354. obj
  355. # this is something that can be returned from 'resolve' methods
  356. # of pages that have restricted access
  357. validateUser: (opts={}) ->
  358. configName = opts.config
  359. unless @dfd?
  360. @initDfd()
  361. # save trip to API if possible. assume that user is still signed
  362. # in if auth headers are present and token has not expired.
  363. if @userIsAuthenticated()
  364. # user is still presumably logged in
  365. @resolveDfd()
  366. else
  367. # token querystring is present. user most likely just came from
  368. # registration email link.
  369. search = $location.search()
  370. # determine querystring params accounting for possible angular parsing issues
  371. location_parse = @parseLocation(window.location.search)
  372. params = if Object.keys(search).length==0 then location_parse else search
  373. # auth_token matches what is sent with postMessage, but supporting token for
  374. # backwards compatability
  375. token = params.auth_token || params.token
  376. if token != undefined
  377. clientId = params.client_id
  378. uid = params.uid
  379. expiry = params.expiry
  380. configName = params.config
  381. # use the configuration that was used in creating
  382. # the confirmation link
  383. @setConfigName(configName)
  384. # check if redirected from password reset link
  385. @mustResetPassword = params.reset_password
  386. # check if redirected from email confirmation link
  387. @firstTimeLogin = params.account_confirmation_success
  388. # check if redirected from auth registration
  389. @oauthRegistration = params.oauth_registration
  390. # persist these values
  391. @setAuthHeaders(@buildAuthHeaders({
  392. token: token
  393. clientId: clientId
  394. uid: uid
  395. expiry: expiry
  396. }))
  397. # build url base
  398. url = ($location.path() || '/')
  399. # strip token-related qs from url to prevent re-use of these params
  400. # on page refresh
  401. ['token', 'client_id', 'uid', 'expiry', 'config', 'reset_password', 'account_confirmation_success', 'oauth_registration'].forEach (prop) ->
  402. delete params[prop];
  403. # append any remaining params, if any
  404. if Object.keys(params).length > 0
  405. url += '?' + @buildQueryString(params);
  406. # redirect to target url
  407. $location.url(url)
  408. # token cookie is present. user is returning to the site, or
  409. # has refreshed the page.
  410. else if @retrieveData('currentConfigName')
  411. configName = @retrieveData('currentConfigName')
  412. unless isEmpty(@retrieveData('auth_headers'))
  413. # if token has expired, do not verify token with API
  414. if @tokenHasExpired()
  415. $rootScope.$broadcast('auth:session-expired')
  416. @rejectDfd({
  417. reason: 'unauthorized'
  418. errors: ['Session expired.']
  419. })
  420. else
  421. # token has been saved in session var, token has not
  422. # expired. must be verified with API.
  423. @validateToken({config: configName})
  424. # new user session. will redirect to login
  425. else
  426. @rejectDfd({
  427. reason: 'unauthorized'
  428. errors: ['No credentials']
  429. })
  430. $rootScope.$broadcast('auth:invalid')
  431. @dfd.promise
  432. # confirm that user's auth token is still valid.
  433. validateToken: (opts={}) ->
  434. unless @tokenHasExpired()
  435. $http.get(@apiUrl(opts.config) + @getConfig(opts.config).tokenValidationPath)
  436. .success((resp) =>
  437. authData = @getConfig(opts.config).handleTokenValidationResponse(resp)
  438. @handleValidAuth(authData)
  439. # broadcast event for first time login
  440. if @firstTimeLogin
  441. $rootScope.$broadcast('auth:email-confirmation-success', @user)
  442. if @oauthRegistration
  443. $rootScope.$broadcast('auth:oauth-registration', @user)
  444. if @mustResetPassword
  445. $rootScope.$broadcast('auth:password-reset-confirm-success', @user)
  446. $rootScope.$broadcast('auth:validation-success', @user)
  447. )
  448. .error((data) =>
  449. # broadcast event for first time login failure
  450. if @firstTimeLogin
  451. $rootScope.$broadcast('auth:email-confirmation-error', data)
  452. if @mustResetPassword
  453. $rootScope.$broadcast('auth:password-reset-confirm-error', data)
  454. $rootScope.$broadcast('auth:validation-error', data)
  455. @rejectDfd({
  456. reason: 'unauthorized'
  457. errors: data.errors
  458. })
  459. )
  460. else
  461. @rejectDfd({
  462. reason: 'unauthorized'
  463. errors: ['Expired credentials']
  464. })
  465. # ensure token has not expired
  466. tokenHasExpired: ->
  467. expiry = @getExpiry()
  468. now = new Date().getTime()
  469. return (expiry and expiry < now)
  470. # get expiry by method provided in config
  471. getExpiry: ->
  472. @getConfig().parseExpiry(@retrieveData('auth_headers') || {})
  473. # this service attempts to cache auth tokens, but sometimes we
  474. # will want to discard saved tokens. examples include:
  475. # 1. login failure
  476. # 2. token validation failure
  477. # 3. user logs out
  478. invalidateTokens: ->
  479. # cannot delete user object for scoping reasons. instead, delete
  480. # all keys on object.
  481. delete @user[key] for key, val of @user
  482. # remove any assumptions about current configuration
  483. @deleteData('currentConfigName')
  484. $timeout.cancel @timer if @timer?
  485. # kill cookies, otherwise session will resume on page reload
  486. # setting this value to null will force the validateToken method
  487. # to re-validate credentials with api server when validate is called
  488. @deleteData('auth_headers')
  489. # destroy auth token on server, destroy user auth credentials
  490. signOut: ->
  491. $http.delete(@apiUrl() + @getConfig().signOutUrl)
  492. .success((resp) =>
  493. @invalidateTokens()
  494. $rootScope.$broadcast('auth:logout-success')
  495. )
  496. .error((resp) =>
  497. @invalidateTokens()
  498. $rootScope.$broadcast('auth:logout-error', resp)
  499. )
  500. # handle successful authentication
  501. handleValidAuth: (user, setHeader=false) ->
  502. # cancel any pending postMessage checks
  503. $timeout.cancel(@requestCredentialsPollingTimer) if @requestCredentialsPollingTimer?
  504. # cancel any inAppBrowser listeners
  505. @cancelOmniauthInAppBrowserListeners()
  506. # must extend existing object for scoping reasons
  507. angular.extend @user, user
  508. # add shortcut to determine user auth status
  509. @user.signedIn = true
  510. @user.configName = @getCurrentConfigName()
  511. # postMessage will not contain header. must save headers manually.
  512. if setHeader
  513. @setAuthHeaders(@buildAuthHeaders({
  514. token: @user.auth_token
  515. clientId: @user.client_id
  516. uid: @user.uid
  517. expiry: @user.expiry
  518. }))
  519. # fulfill promise
  520. @resolveDfd()
  521. # configure auth token format.
  522. buildAuthHeaders: (ctx) ->
  523. headers = {}
  524. for key, val of @getConfig().tokenFormat
  525. headers[key] = $interpolate(val)(ctx)
  526. return headers
  527. # abstract persistent data store
  528. persistData: (key, val, configName) ->
  529. if @getConfig(configName).storage instanceof Object
  530. @getConfig(configName).storage.persistData(key, val, @getConfig(configName))
  531. else
  532. switch @getConfig(configName).storage
  533. when 'localStorage'
  534. $window.localStorage.setItem(key, JSON.stringify(val))
  535. else
  536. ipCookie(key, val, {path: '/', expires: 9999, expirationUnit: 'days'})
  537. # abstract persistent data retrieval
  538. retrieveData: (key) ->
  539. if @getConfig().storage instanceof Object
  540. @getConfig().storage.retrieveData(key)
  541. else
  542. switch @getConfig().storage
  543. when 'localStorage'
  544. JSON.parse($window.localStorage.getItem(key))
  545. else ipCookie(key)
  546. # abstract persistent data removal
  547. deleteData: (key) ->
  548. if @getConfig().storage instanceof Object
  549. @getConfig().storage.deleteData(key);
  550. switch @getConfig().storage
  551. when 'localStorage'
  552. $window.localStorage.removeItem(key)
  553. else
  554. ipCookie.remove(key, {path: '/'})
  555. # persist authentication token, client id, uid
  556. setAuthHeaders: (h) ->
  557. newHeaders = angular.extend((@retrieveData('auth_headers') || {}), h)
  558. result = @persistData('auth_headers', newHeaders)
  559. expiry = @getExpiry()
  560. now = new Date().getTime()
  561. if expiry > now
  562. $timeout.cancel @timer if @timer?
  563. @timer = $timeout (=>
  564. @validateUser {config: @getSavedConfig()}
  565. ), parseInt (expiry - now)
  566. result
  567. initDfd: ->
  568. @dfd = $q.defer()
  569. # failed login. invalidate auth header and reject promise.
  570. # defered object must be destroyed after reflow.
  571. rejectDfd: (reason) ->
  572. @invalidateTokens()
  573. if @dfd?
  574. @dfd.reject(reason)
  575. # must nullify after reflow so promises can be rejected
  576. $timeout((=> @dfd = null), 0)
  577. # use proxy for IE
  578. apiUrl: (configName) ->
  579. if @getConfig(configName).proxyIf()
  580. @getConfig(configName).proxyUrl
  581. else
  582. @getConfig(configName).apiUrl
  583. getConfig: (name) ->
  584. configs[@getCurrentConfigName(name)]
  585. # if value is a method, call the method. otherwise return the
  586. # argument itself
  587. getResultOrValue: (arg) ->
  588. if typeof(arg) == 'function'
  589. arg()
  590. else
  591. arg
  592. # a config name will be return in the following order of precedence:
  593. # 1. matches arg
  594. # 2. saved from past authentication
  595. # 3. first available config name
  596. getCurrentConfigName: (name) ->
  597. name || @getSavedConfig()
  598. # can't rely on retrieveData because it will cause a recursive loop
  599. # if config hasn't been initialized. instead find first available
  600. # value of 'defaultConfigName'. searches the following places in
  601. # this priority:
  602. # 1. localStorage
  603. # 2. cookies
  604. # 3. default (first available config)
  605. getSavedConfig: ->
  606. c = undefined
  607. key = 'currentConfigName'
  608. # accessing $window.localStorage will
  609. # throw an error if localStorage is disabled
  610. hasLocalStorage = false
  611. try
  612. hasLocalStorage = !!$window.localStorage
  613. catch error
  614. if hasLocalStorage
  615. c ?= JSON.parse($window.localStorage.getItem(key))
  616. c ?= ipCookie(key)
  617. return c || defaultConfigName
  618. ]
  619. }
  620. )
  621. # each response will contain auth headers that have been updated by
  622. # the server. copy those headers for use in the next request.
  623. .config(['$httpProvider', ($httpProvider) ->
  624. # responses are sometimes returned out of order. check that response is
  625. # current before saving the auth data.
  626. tokenIsCurrent = ($auth, headers) ->
  627. oldTokenExpiry = Number($auth.getExpiry())
  628. newTokenExpiry = Number($auth.getConfig().parseExpiry(headers || {}))
  629. return newTokenExpiry >= oldTokenExpiry
  630. # uniform handling of response headers for success or error conditions
  631. updateHeadersFromResponse = ($auth, resp) ->
  632. newHeaders = {}
  633. for key, val of $auth.getConfig().tokenFormat
  634. if resp.headers(key)
  635. newHeaders[key] = resp.headers(key)
  636. if tokenIsCurrent($auth, newHeaders)
  637. $auth.setAuthHeaders(newHeaders)
  638. # this is ugly...
  639. # we need to configure an interceptor (must be done in the configuration
  640. # phase), but we need access to the $http service, which is only available
  641. # during the run phase. the following technique was taken from this
  642. # stackoverflow post:
  643. # http://stackoverflow.com/questions/14681654/i-need-two-instances-of-angularjs-http-service-or-what
  644. $httpProvider.interceptors.push ['$injector', ($injector) ->
  645. request: (req) ->
  646. $injector.invoke ['$http', '$auth', ($http, $auth) ->
  647. if req.url.match($auth.apiUrl())
  648. for key, val of $auth.retrieveData('auth_headers')
  649. req.headers[key] = val
  650. ]
  651. return req
  652. response: (resp) ->
  653. $injector.invoke ['$http', '$auth', ($http, $auth) ->
  654. if resp.config.url.match($auth.apiUrl())
  655. return updateHeadersFromResponse($auth, resp)
  656. ]
  657. return resp
  658. responseError: (resp) ->
  659. $injector.invoke ['$http', '$auth', ($http, $auth) ->
  660. if resp.config.url.match($auth.apiUrl())
  661. return updateHeadersFromResponse($auth, resp)
  662. ]
  663. return $injector.get('$q').reject(resp)
  664. ]
  665. # define http methods that may need to carry auth headers
  666. httpMethods = ['get', 'post', 'put', 'patch', 'delete']
  667. # disable IE ajax request caching for each of the necessary http methods
  668. angular.forEach(httpMethods, (method) ->
  669. $httpProvider.defaults.headers[method] ?= {}
  670. $httpProvider.defaults.headers[method]['If-Modified-Since'] = 'Mon, 26 Jul 1997 05:00:00 GMT'
  671. )
  672. ])
  673. .run(['$auth', '$window', '$rootScope', ($auth, $window, $rootScope) ->
  674. $auth.initialize()
  675. ])
  676. # ie8 and ie9 require special handling
  677. window.isOldIE = ->
  678. out = false
  679. nav = navigator.userAgent.toLowerCase()
  680. if nav and nav.indexOf('msie') != -1
  681. version = parseInt(nav.split('msie')[1])
  682. if version < 10
  683. out = true
  684. out
  685. # ie <= 11 do not support postMessage
  686. window.isIE = ->
  687. nav = navigator.userAgent.toLowerCase()
  688. ((nav and nav.indexOf('msie') != -1) || !!navigator.userAgent.match(/Trident.*rv\:11\./))
  689. window.isEmpty = (obj) ->
  690. # null and undefined are "empty"
  691. return true unless obj
  692. # Assume if it has a length property with a non-zero value
  693. # that that property is correct.
  694. return false if (obj.length > 0)
  695. return true if (obj.length == 0)
  696. # Otherwise, does it have any properties of its own?
  697. # Note that this doesn't handle
  698. # toString and valueOf enumeration bugs in IE < 9
  699. for key, val of obj
  700. return false if (Object.prototype.hasOwnProperty.call(obj, key))
  701. return true