Skip to content

Commit b74cf3b

Browse files
committed
fix #4 and add token expiration check
1 parent 6644cec commit b74cf3b

File tree

8 files changed

+195
-38
lines changed

8 files changed

+195
-38
lines changed

docs/assets/app.bundle.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/assets/app.vendor.bundle.js

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-redux-graphql-apollo-bootstrap-webpack-starter",
3-
"version": "1.0.0",
3+
"version": "1.0.1",
44
"description": "react js + redux + graphQL + Apollo + react router4 + hot reload + devTools + bootstrap + webpack 3 starter",
55
"main": "src/index.js",
66
"scripts": {
@@ -120,6 +120,7 @@
120120
"history": "^4.6.3",
121121
"jquery": "^2.2.1",
122122
"js-base64": "^2.1.9",
123+
"jwt-decode": "^2.2.0",
123124
"moment": "^2.18.1",
124125
"pretty-error": "^2.1.1",
125126
"prop-types": "^15.5.10",

src/app/components/privateRoute/PrivateRoute.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,14 @@ class PrivateRoute extends Component {
3030
const { location } = this.props;
3131

3232
const isUserAuthenticated = this.isAuthenticated();
33+
const isTokenExpired = this.isExpired();
3334

3435
return (
3536
<Route
3637
{...rest}
3738
render={
3839
props => (
39-
isUserAuthenticated
40+
!isTokenExpired && isUserAuthenticated
4041
? <Component {...props} />
4142
: <Redirect to={{ pathname: "/login", state: { from: location } }} />
4243
)
@@ -53,6 +54,10 @@ class PrivateRoute extends Component {
5354

5455
return isAuthenticated;
5556
}
57+
58+
isExpired() {
59+
return auth.isExpiredToken(auth.getToken());
60+
}
5661
}
5762

5863
export default withRouter(PrivateRoute);

src/app/services/auth/index.js

Lines changed: 164 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,48 @@
1-
// @flow weak
1+
// @flow
2+
3+
import type {
4+
Storage,
5+
TokenKey,
6+
UserInfoKey,
7+
STORES_TYPES
8+
} from './type';
9+
import decode from 'jwt-decode';
10+
import moment from 'moment';
11+
212

313
const TOKEN_KEY = 'token';
414
const USER_INFO = 'userInfo';
515

6-
const APP_PERSIST_STORES_TYPES = ['localStorage', 'sessionStorage'];
16+
const APP_PERSIST_STORES_TYPES: Array<STORES_TYPES> = [
17+
'localStorage',
18+
'sessionStorage'
19+
];
720

8-
const parse = JSON.parse;
21+
const parse = JSON.parse;
922
const stringify = JSON.stringify;
23+
1024
/*
1125
auth object
1226
-> store "TOKEN_KEY"
1327
- default storage is "localStorage"
1428
- default token key is 'token'
1529
*/
1630
export const auth = {
17-
// -------------------------
18-
// token
19-
// -------------------------
20-
getToken(fromStorage = APP_PERSIST_STORES_TYPES[0], tokenKey = TOKEN_KEY) {
31+
// /////////////////////////////////////////////////////////////
32+
// TOKEN
33+
// /////////////////////////////////////////////////////////////
34+
35+
/**
36+
* get token from localstorage
37+
*
38+
* @param {'localStorage' | 'sessionStorage'} [fromStorage='localStorage'] specify storage
39+
* @param {any} [tokenKey=TOKEN_KEY] optionnal parameter to specify a token key
40+
* @returns {string} token value
41+
*/
42+
getToken(
43+
fromStorage: Storage = APP_PERSIST_STORES_TYPES[0],
44+
tokenKey: TokenKey = TOKEN_KEY
45+
): ?string {
2146
// localStorage:
2247
if (fromStorage === APP_PERSIST_STORES_TYPES[0]) {
2348
return (localStorage && localStorage.getItem(tokenKey)) || null;
@@ -30,7 +55,19 @@ export const auth = {
3055
return null;
3156
},
3257

33-
setToken(value = '', toStorage = APP_PERSIST_STORES_TYPES[0], tokenKey = TOKEN_KEY) {
58+
/**
59+
* set the token value into localstorage (managed by localforage)
60+
*
61+
* @param {string} [value=''] token value
62+
* @param {'localStorage' | 'sessionStorage'} [toStorage='localStorage'] specify storage
63+
* @param {any} [tokenKey='token'] token key
64+
* @returns {boolean} success/failure flag
65+
*/
66+
setToken(
67+
value: string = '',
68+
toStorage: Storage = APP_PERSIST_STORES_TYPES[0],
69+
tokenKey: TokenKey = TOKEN_KEY
70+
): ?string {
3471
if (!value || value.length <= 0) {
3572
return;
3673
}
@@ -47,19 +84,33 @@ export const auth = {
4784
}
4885
}
4986
},
50-
/*
51-
Note: 'isAuthenticated' just checks 'tokenKey' on store (localStorage by default or sessionStorage)
5287

53-
You may think: 'ok I just put an empty token key and I have access to protected routes?''
54-
-> answer is: YES^^
55-
BUT
56-
-> : your backend will not recognize a wrong token so private data or safe and you protected view could be a bit ugly without any data.
5788

58-
=> ON CONCLUSION: this aim of 'isAuthenticated'
59-
-> is to help for a better "user experience" (= better than displaying a view with no data since server did not accept the user).
60-
-> it is not a security purpose (security comes from backend, since frontend is easily hackable => user has access to all your frontend)
89+
/**
90+
* check
91+
* - if token key contains a valid token value (defined and not an empty value)
92+
* - if the token expiration date is passed
93+
*
94+
*
95+
* Note: 'isAuthenticated' just checks 'tokenKey' on store (localStorage by default or sessionStorage)
96+
*
97+
* You may think: 'ok I just put an empty token key and I have access to protected routes?''
98+
* -> answer is: YES^^
99+
* BUT
100+
* -> : your backend will not recognize a wrong token so private data or safe and you protected view could be a bit ugly without any data.
101+
*
102+
* => ON CONCLUSION: this aim of 'isAuthenticated'
103+
* -> is to help for a better "user experience" (= better than displaying a view with no data since server did not accept the user).
104+
* -> it is not a security purpose (security comes from backend, since frontend is easily hackable => user has access to all your frontend)
105+
*
106+
* @param {'localStorage' | 'sessionStorage'} [fromStorage='localStorage'] specify storage
107+
* @param {any} [tokenKey=TOKEN_KEY] token key
108+
* @returns {bool} is authenticed response
61109
*/
62-
isAuthenticated(fromStorage = APP_PERSIST_STORES_TYPES[0], tokenKey = TOKEN_KEY) {
110+
isAuthenticated(
111+
fromStorage: Storage = APP_PERSIST_STORES_TYPES[0],
112+
tokenKey: TokenKey = TOKEN_KEY
113+
): boolean {
63114
// localStorage:
64115
if (fromStorage === APP_PERSIST_STORES_TYPES[0]) {
65116
if ((localStorage && localStorage.getItem(tokenKey))) {
@@ -80,22 +131,82 @@ export const auth = {
80131
return false;
81132
},
82133

83-
clearToken(tokenKey = TOKEN_KEY) {
134+
/**
135+
* delete token
136+
*
137+
* @param {any} [tokenKey='token'] token key
138+
* @returns {bool} success/failure flag
139+
*/
140+
clearToken(
141+
storage: Storage = APP_PERSIST_STORES_TYPES[0],
142+
tokenKey: TokenKey = TOKEN_KEY
143+
): boolean {
84144
// localStorage:
85145
if (localStorage && localStorage[tokenKey]) {
86146
localStorage.removeItem(tokenKey);
147+
return true;
87148
}
88149
// sessionStorage:
89150
if (sessionStorage && sessionStorage[tokenKey]) {
90151
sessionStorage.removeItem(tokenKey);
152+
return true;
91153
}
154+
155+
return false;
92156
},
93157

158+
/**
159+
* return expiration date from token
160+
*
161+
* @param {string} encodedToken - base 64 token received from server and stored in local storage
162+
* @returns {date | null} returns expiration date or null id expired props not found in decoded token
163+
*/
164+
getTokenExpirationDate(
165+
encodedToken: any
166+
): Date {
167+
if (!encodedToken) {
168+
return new Date(0); // is expired
169+
}
94170

95-
// -------------------------
171+
const token = decode(encodedToken);
172+
if (!token.exp) {
173+
return new Date(0); // is expired
174+
}
175+
const expirationDate = new Date(token.exp*1000);
176+
return expirationDate;
177+
},
178+
179+
/**
180+
* tell is token is expired (compared to now)
181+
*
182+
* @param {string} encodedToken - base 64 token received from server and stored in local storage
183+
* @returns {bool} returns true if expired else false
184+
*/
185+
isExpiredToken(
186+
encodedToken: any
187+
): boolean {
188+
const expirationDate = this.getTokenExpirationDate(encodedToken);
189+
const rightNow = moment();
190+
const isExpiredToken = moment(rightNow).isAfter(moment(expirationDate));
191+
192+
return isExpiredToken;
193+
},
194+
195+
196+
// /////////////////////////////////////////////////////////////
96197
// USER_INFO
97-
// -------------------------
98-
getUserInfo(fromStorage = APP_PERSIST_STORES_TYPES[0], userInfoKey = USER_INFO) {
198+
// /////////////////////////////////////////////////////////////
199+
/**
200+
* get user info from localstorage
201+
*
202+
* @param {'localStorage' | 'sessionStorage'} [fromStorage='localStorage'] specify storage
203+
* @param {any} [userInfoKey='userInfo'] optionnal parameter to specify a token key
204+
* @returns {string} token value
205+
*/
206+
getUserInfo(
207+
fromStorage: Storage = APP_PERSIST_STORES_TYPES[0],
208+
userInfoKey: UserInfoKey = USER_INFO
209+
): ?string {
99210
// localStorage:
100211
if (fromStorage === APP_PERSIST_STORES_TYPES[0]) {
101212
return (localStorage && parse(localStorage.getItem(userInfoKey))) || null;
@@ -108,7 +219,19 @@ export const auth = {
108219
return null;
109220
},
110221

111-
setUserInfo(value = '', toStorage = APP_PERSIST_STORES_TYPES[0], userInfoKey = USER_INFO) {
222+
/**
223+
* set the userInfo value into localstorage
224+
*
225+
* @param {object} [value=''] token value
226+
* @param {'localStorage' | 'sessionStorage'} [toStorage='localStorage'] specify storage
227+
* @param {any} [userInfoKey='userInfo'] token key
228+
* @returns {boolean} success/failure flag
229+
*/
230+
setUserInfo(
231+
value: string = '',
232+
toStorage:Storage = APP_PERSIST_STORES_TYPES[0],
233+
userInfoKey:UserInfoKey = USER_INFO
234+
): any {
112235
if (!value || value.length <= 0) {
113236
return;
114237
}
@@ -126,7 +249,15 @@ export const auth = {
126249
}
127250
},
128251

129-
clearUserInfo(userInfoKey = USER_INFO) {
252+
/**
253+
* delete userInfo
254+
*
255+
* @param {string} [userInfoKey='userInfo'] token key
256+
* @returns {bool} success/failure flag
257+
*/
258+
clearUserInfo(
259+
userInfoKey: UserInfoKey = USER_INFO
260+
): any {
130261
// localStorage:
131262
if (localStorage && localStorage[userInfoKey]) {
132263
localStorage.removeItem(userInfoKey);
@@ -138,10 +269,15 @@ export const auth = {
138269
},
139270

140271

141-
// ---------------------------
142-
// common
143-
// ---------------------------
144-
clearAllAppStorage() {
272+
// /////////////////////////////////////////////////////////////
273+
// COMMON
274+
// /////////////////////////////////////////////////////////////
275+
276+
/**
277+
* forget me method: clear all
278+
* @returns {bool} success/failure flag
279+
*/
280+
clearAllAppStorage(): any {
145281
if (localStorage) {
146282
localStorage.clear();
147283
}

src/app/services/auth/type.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
// @flow
3+
4+
export type STORES_TYPES =
5+
| 'localStorage'
6+
| 'sessionStorage';
7+
8+
export type Storage = STORES_TYPES
9+
export type TokenKey = string;
10+
export type UserInfoKey = string;

src/app/views/pageNotfound/PageNotfound.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import React, {
55
} from 'react';
66
import PropTypes from 'prop-types';
77
import Jumbotron from '../../components/jumbotron/Jumbotron';
8+
import cx from 'classnames';
89

910
class PageNotFound extends PureComponent {
1011
static propTypes = {

yarn.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4556,6 +4556,10 @@ jsx-ast-utils@^1.4.1:
45564556
version "1.4.1"
45574557
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1"
45584558

4559+
jwt-decode@^2.2.0:
4560+
version "2.2.0"
4561+
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
4562+
45594563
keycode@^2.1.2:
45604564
version "2.1.9"
45614565
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.9.tgz#964a23c54e4889405b4861a5c9f0480d45141dfa"

0 commit comments

Comments
 (0)