diff --git a/README.md b/README.md
index d880486..40ead81 100644
--- a/README.md
+++ b/README.md
@@ -5,28 +5,20 @@ React JWT user store
## Usage
```javascript
-let React = require('react'),
- userStore = require('react-jwt-store')();
-
-class someComponent extends React.Component {
- constructor() {
- super(props);
-
- this.state = {
- user: userStore.getUser(),
- token: userStore.getToken()
- };
- }
- render() {
- let user = this.state.user,
- token = this.state.token;
-
- return (
-
-
{user}
- {token}
- }
-}
+const userStore = require('react-jwt-store')()
+
+userStore.on('Token received', (token, user) => {
+ console.log(token, user)
+})
+
+userStore.init()
+```
+
+### Initialize
+In order to trigger the store's refresh mechanism and send data to any event
+handlers, you must call the `init` method.
+```javascript
+userStore.init()
```
### Set the token
@@ -35,6 +27,12 @@ You can set the token without interacting with cookies via the following.
userStore.setToken('jwt')
```
+### Refresh the token
+You can force a refresh of the token via the following.
+```javascript
+userStore.refreshToken()
+```
+
### Override Cookie Key
By default, the JWT assumes the cookie key is `XSRF-TOKEN`. This can be overridden
@@ -43,3 +41,20 @@ by passing `cookie` on the `options` hash:
```javascript
let userStore = require('react-jwt-store')({ cookie: 'NOT-XSRF-TOKEN'});
```
+
+### Set a logger
+
+By default, the store does not log anything, but if you pass in a `console`
+compatible logger, the store will log the state of the token as it changes.
+
+### Terminate
+
+By default, if you set token to null:
+```javascript
+userStore.setToken(null)
+```
+...an interval responsible for refreshing tokens (the one set up on init) will not be cleared.
+If you want to clear it explicitly, then you can do it with:
+```javascript
+userStore.terminate()
+```
diff --git a/package.json b/package.json
index e9f1e38..869a9ba 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-jwt-store",
- "version": "1.2.1",
+ "version": "2.0.0",
"description": "React JWT store",
"license": "MIT",
"author": "Seth Carney",
@@ -42,9 +42,6 @@
"local-storage": "^1.4.2",
"xtend": "^4.0.0"
},
- "babel": {
- "stage": 0
- },
"standard": {
"parser": "babel-eslint",
"globals": [
diff --git a/src/index.js b/src/index.js
index d20dce7..7f54a0b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -7,70 +7,86 @@ import events from 'events'
import ls from 'local-storage'
const EventEmitter = events.EventEmitter
-
-const canWarn = () => console && console.warn && typeof console.warn === 'function'
-
-const decodeToken = token => {
- if (token) {
- try {
- return decode(token)
- } catch (e) {
- canWarn() && console.warn(`Invalid JWT: ${token}`)
- return void 0
- }
- }
-}
+const noop = function () { }
module.exports = (options) => {
- options = extend({ cookie: 'XSRF-TOKEN' }, options)
-
- let token = options.localStorageKey
- ? ls.get(options.localStorageKey)
- : cookie.get && cookie.get(options.cookie)
-
let user
+ let token
+ let refreshTimer
- const tokenStore = extend({
- getToken () {
- return token
- },
-
- getUser () {
- return user
- },
+ options = extend({ cookie: 'XSRF-TOKEN' }, options)
- getUserId () {
- return user ? user.id : void 0
- },
+ const logger = options.logger || {
+ info: noop,
+ warn: noop
+ }
- setToken (newToken) {
- token = newToken
- user = decodeToken(token)
- this.emit('Token received')
+ const decodeToken = token => {
+ if (token) {
+ try {
+ return decode(token)
+ } catch (e) {
+ logger.warn(`[JWT store] Invalid JWT: ${token}`)
+ return void 0
+ }
}
- }, EventEmitter.prototype)
+ }
+
+ const refreshInterval = options.refreshInterval
+ ? options.refreshInterval
+ : (60000)
const refreshToken = () => {
if (!token && options.refresh) {
- options.refresh()
- .then(tokenStore.setToken.bind(tokenStore))
+ tokenStore.refreshToken()
} else {
user = decodeToken(token)
}
let expDate = user ? new Date(user.exp * 1000 - 2 * refreshInterval) : null
if (expDate && expDate < new Date() && options.refresh) {
- options.refresh()
- .then(tokenStore.setToken.bind(tokenStore))
+ tokenStore.refreshToken()
}
}
- const refreshInterval = options.refreshInterval
- ? options.refreshInterval
- : (60000)
- refreshToken()
+ const tokenStore = extend({
+ init () {
+ let token
+ if (options.localStorageKey) {
+ try {
+ token = ls.get(options.localStorageKey)
+ } catch (e) {
+ logger.warn('[JWT store] Unable to get token', e)
+ }
+ } else {
+ token = cookie.get && cookie.get(options.cookie)
+ }
+
+ if (token) { this.setToken(token) }
+ refreshToken()
+
+ refreshTimer = setInterval(refreshToken, refreshInterval)
+ },
- setInterval(refreshToken, refreshInterval)
+ setToken (newToken) {
+ logger.info('[JWT store] setting new token', newToken)
+ token = newToken
+ user = decodeToken(token)
+ this.emit('Token received', token, user)
+ },
+
+ refreshToken () {
+ logger.info('[JWT store] refreshing token', token)
+ options.refresh(token)
+ .then(tokenStore.setToken.bind(tokenStore))
+ },
+
+ terminate () {
+ user = undefined
+ token = undefined
+ clearInterval(refreshTimer)
+ }
+ }, EventEmitter.prototype)
return tokenStore
}
diff --git a/test/data/token-timezone.json b/test/data/token-timezone.json
new file mode 100644
index 0000000..42e1d7f
--- /dev/null
+++ b/test/data/token-timezone.json
@@ -0,0 +1 @@
+"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6Mjc1MTA1NSwidXNlcl9pZCI6Mjc1MTA1NSwibG9naW5faWQiOjIzMSwib3JnYW5pemF0aW9uX2lkIjoyNzMxMTExLCJvcmdhbml6YXRpb25fbmFtZSI6IkRFVjogRGV2ZWxvcG1lbnQgRW52aXJvbm1lbnQiLCJlbWFpbCI6Im1pa2UuYXRraW5zQGxhbmV0aXguY29tIiwiZmlyc3RfbmFtZSI6Ik1pa2UiLCJsYXN0X25hbWUiOiJBdGtpbnMiLCJhdmF0YXJfdXJsIjoiaHR0cHM6Ly9zZWN1cmUubGFuZXRpeC5jb20vY3NzL2ltYWdlcy9kZWZhdWx0LWF2YXRhci5wbmciLCJwZXJtaXNzaW9ucyI6WyJsYW5ldGl4LXN0YWZmIiwiY3VzdG9tZXItYWRtaW5pc3RyYXRvciIsImFuYWx5dGljcyJdLCJ0aW1lem9uZSI6IlVUQyIsImZlYXR1cmVzIjp7fSwiaWF0IjoxNDI5NjUzNDI3LCJleHAiOjE0NjEwMTk0MTguNjQ5LCJhdWQiOiJ1cm46bGFuZXRpeC9hcGkiLCJpc3MiOiJ1cm46bGFuZXRpeC9hdXRoIiwic3ViIjoibWlrZS5hdGtpbnNAbGFuZXRpeC5jb20ifQ.kOAGDWy2lYSKTQSn2_jKSXJYRlPMJl8wWAJQSbgFQBM"
diff --git a/test/index.js b/test/index.js
index 6d883eb..d58940a 100644
--- a/test/index.js
+++ b/test/index.js
@@ -4,6 +4,7 @@ import assert from 'assert'
import { btoa } from 'Base64'
import decode from 'jwt-decode'
import token from './data/token'
+import tokenTimezone from './data/token-timezone'
import ls from 'local-storage'
import bluebird from 'bluebird'
@@ -21,10 +22,10 @@ describe('Token Store', () => {
beforeEach(() => {
// HACK around https://github.com/auth0/jwt-decode/issues/5
- GLOBAL.window = GLOBAL
+ global.window = global
// HACK around cookie monster returning undefined when document isn't there
- GLOBAL.document = {}
+ global.document = {}
})
beforeEach(() => {
@@ -32,41 +33,62 @@ describe('Token Store', () => {
})
afterEach(() => {
- delete GLOBAL.window
- delete GLOBAL.document
+ delete global.window
+ delete global.document
ls.remove(localStorageKey)
})
it('should set user after no token is present', () => {
const tokenStore = require('../src')()
- tokenStore.setToken(token)
- let user = tokenStore.getUser()
+ tokenStore.on('Token received', (_, user) => {
+ assert.equal(user.first_name, 'Mike')
+ assert.equal(user.last_name, 'Atkins')
+ })
- assert.equal(user.first_name, 'Mike')
- assert.equal(user.last_name, 'Atkins')
+ tokenStore.init()
+ tokenStore.setToken(token)
})
it('should get the token out of local storage', () => {
ls.set(localStorageKey, token)
const tokenStore = require('../src')({localStorageKey})
- const user = tokenStore.getUser()
+ tokenStore.on('Token received', (_, user) => {
+ assert.equal(user.first_name, 'Mike')
+ assert.equal(user.last_name, 'Atkins')
+ })
+ tokenStore.init()
+ })
- assert.equal(user.first_name, 'Mike')
- assert.equal(user.last_name, 'Atkins')
+ it('should catch an exception token is not present in local storage', () => {
+ ls.set(localStorageKey, undefined)
+ const tokenStore = require('../src')({localStorageKey})
+ tokenStore.on('Token received', assert.fail)
+ tokenStore.init()
})
it('if no token call refresh & set token', done => {
const tokenStore = require('../src')({refresh: () =>
bluebird.resolve(updatedToken)
})
- tokenStore.on('Token received', () => {
- const user = tokenStore.getUser()
-
+ tokenStore.on('Token received', (_, user) => {
assert.equal(user.first_name, 'Mike')
assert.equal(user.last_name, 'Atkins')
done()
})
+ tokenStore.init()
+ })
+
+ it('if token is expired, call refresh with expired token', done => {
+ ls.set(localStorageKey, token)
+ require('../src')({
+ localStorageKey,
+ refresh: (t) => {
+ assert.equal(t, token)
+ done()
+ return bluebird.resolve(updatedToken)
+ }
+ }).init()
})
it('if token is expired, call refresh & set token', done => {
@@ -76,13 +98,13 @@ describe('Token Store', () => {
refresh: () =>
bluebird.resolve(updatedToken)
})
- tokenStore.on('Token received', () => {
- const user = tokenStore.getUser()
-
+ let callCount = 0
+ tokenStore.on('Token received', (_, user) => {
assert.equal(user.first_name, 'Mike')
assert.equal(user.last_name, 'Atkins')
- done()
+ if (++callCount === 2) { done() }
})
+ tokenStore.init()
})
it('if token valid, leave as is', () => {
@@ -91,36 +113,58 @@ describe('Token Store', () => {
localStorageKey,
refresh: () => assert.fail('should not be called')
})
-
- const user = tokenStore.getUser()
-
- assert.equal(user.first_name, 'Mike')
- assert.equal(user.last_name, 'Atkins')
+ tokenStore.on('Token received', (_, user) => {
+ assert.equal(user.first_name, 'Mike')
+ assert.equal(user.last_name, 'Atkins')
+ })
+ tokenStore.init()
})
- it('it token to expire soon, refresh after interval', done => {
+ it('if token to expire soon, refresh after interval', done => {
ls.set(localStorageKey, updatedToken)
const tokenStore = require('../src')({
localStorageKey,
refresh: () => bluebird.resolve(updatedToken),
refreshInterval: 1000
})
- tokenStore.on('Token received', () => {
- const user = tokenStore.getUser()
-
+ let callCount = 0
+ tokenStore.on('Token received', (_, user) => {
assert.equal(user.first_name, 'Mike')
assert.equal(user.last_name, 'Atkins')
- done()
+ if (++callCount === 2) { done() }
})
+ tokenStore.init()
+ })
+
+ it('refreshes the token and sets it', done => {
+ ls.set(localStorageKey, setTokenExp(Date.now() + 100 * 60 * 1000))
+ const tokenStore = require('../src')({
+ localStorageKey,
+ refresh: () => bluebird.resolve(tokenTimezone)
+ })
+
+ let callCount = 0
+ tokenStore.on('Token received', (_, user) => {
+ callCount++
+ if (callCount === 1) {
+ assert(!user.timezone)
+ } else if (callCount === 2) {
+ assert.equal(user.timezone, 'UTC')
+ done()
+ } else {
+ assert.fail('shouldn\'t be called more than twice')
+ }
+ })
+
+ tokenStore.init()
+ tokenStore.refreshToken()
})
describe('sad path', () => {
it('should not blow up when cookie is not present', () => {
let tokenStore
assert.doesNotThrow(() => tokenStore = require('../src')())
- assert.ok(tokenStore.getToken() === void 0)
- assert.ok(tokenStore.getUser() === void 0)
- assert.ok(tokenStore.getUserId() === void 0)
+ assert.doesNotThrow(() => tokenStore.init())
})
it('should not blow up when cookie is invalid', () => {
@@ -129,54 +173,103 @@ describe('Token Store', () => {
let tokenStore
assert.doesNotThrow(() => tokenStore = require('../src')())
- assert.ok(tokenStore.getToken() === token)
- assert.ok(tokenStore.getUser() === void 0)
- assert.ok(tokenStore.getUserId() === void 0)
+ assert.doesNotThrow(() => tokenStore.init())
})
})
describe('default cookie key', () => {
- let tokenStore
+ let tokenFromStore
+ let user
beforeEach(() => {
require('cookie-monster').set('XSRF-TOKEN', token)
- tokenStore = require('../src')()
+ const tokenStore = require('../src')()
+ tokenStore.on('Token received', (t, u) => {
+ tokenFromStore = t
+ user = u
+ })
+ tokenStore.init()
})
it('should get the XSRF-TOKEN and return it', () => {
- assert.equal(tokenStore.getToken(), token)
+ assert.equal(tokenFromStore, token)
})
it('should return user', () => {
- let user = tokenStore.getUser()
-
assert.equal(user.first_name, 'Mike')
assert.equal(user.last_name, 'Atkins')
})
-
- it('should return user id', () => {
- let id = tokenStore.getUserId()
- assert.equal(id, 2751055)
- })
-
- it('should return token', () => {
- let t = tokenStore.getToken()
- assert.equal(token, t)
- })
})
describe('override cookie key', () => {
- let tokenStore
+ let tokenFromStore
beforeEach(() => {
require('cookie-monster').set('NOT-XSRF-TOKEN', token)
- tokenStore = require('../src')({ cookie: 'NOT-XSRF-TOKEN' })
+ const tokenStore = require('../src')({ cookie: 'NOT-XSRF-TOKEN' })
+ tokenStore.on('Token received', (t) => {
+ tokenFromStore = t
+ })
+ tokenStore.init()
})
it('should get the NOT-XSRF-TOKEN and return it', () => {
- assert.equal(tokenStore.getToken(), token)
+ assert.equal(tokenFromStore, token)
+ })
+ })
+
+ describe('terminate', () => {
+ let cookieMonster
+ beforeEach(() => {
+ cookieMonster = require('cookie-monster')
+ cookieMonster.set('XSRF-TOKEN', token)
+ })
+ it('should set token to undefined on explicit termination', done => {
+ let callCount = 0
+ const tokenStore = require('../src')({
+ refresh: (t) => {
+ if (callCount === 0) {
+ cookieMonster.set('XSRF-TOKEN', token, {expires: 'Thu, 01 Jan 1970 00:00:01 GMT'})
+ assert.equal(t, token)
+ }
+ if (callCount === 1) {
+ assert.equal(t, undefined)
+ }
+ callCount++
+
+ return bluebird.resolve(t)
+ }
+ })
+ tokenStore.init()
+ tokenStore.terminate()
+ tokenStore.refreshToken()
+ done()
+ })
+
+ it('should not set token to undefined when no explicit termination', done => {
+ let callCount = 0
+ const tokenStore = require('../src')({
+ refresh: (t) => {
+ if (!t) {
+ return bluebird.resolve()
+ }
+ if (callCount === 0) {
+ cookieMonster.set('XSRF-TOKEN', token, {expires: 'Thu, 01 Jan 1970 00:00:01 GMT'})
+ assert.equal(t, token)
+ }
+ if (callCount === 1) {
+ assert.equal(t, token)
+ }
+ callCount++
+
+ return bluebird.resolve(t)
+ }
+ })
+ tokenStore.init()
+ tokenStore.refreshToken()
+ done()
})
})
})