Skip to content

Commit e1f7d7d

Browse files
author
Domagoj Rukavina
authored
Merge pull request #7 from shoutem/feature/added-is-response-unauthorized
Added is response unauthorized callback to config
2 parents f3b9340 + 4102a75 commit e1f7d7d

File tree

9 files changed

+87
-47
lines changed

9 files changed

+87
-47
lines changed

.babelrc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
2-
presets: ["es2015"],
3-
plugins: ["transform-object-rest-spread",
2+
"presets": ["es2015"],
3+
"plugins": ["transform-object-rest-spread",
44
["babel-plugin-transform-builtin-extend", {
5-
globals: ["Error", "Array"]
5+
"globals": ["Error", "Array"]
66
}]],
7-
sourceMaps: true
7+
"sourceMaps": true
88
}

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ by renewing the current access token and then retrying an initial fetch operatio
66
If you are not familiar with refresh token flow you should check some of the following resources:
77
- [RFC standards track regarding refresh token flow](https://tools.ietf.org/html/rfc6749#page-10)
88
- [Auth0 blog - Refresh Tokens: When to Use Them and How They Interact with JWTs](https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/)
9+
- [Shoutem blog - Keeping your tokens fresh](https://medium.com/shoutem/keeping-your-api-tokens-fresh-72059a7b0586)
910

1011
>Note:
1112
This library expects that fetch and promise api's are available at target environment. You should
@@ -43,6 +44,11 @@ config: {
4344
// When set, response which invalidates token will be resolved after the token has been renewed
4445
// in effect, token will be loaded in sync with response, otherwise renew will run async to response
4546
shouldWaitForTokenRenewal: boolean,
47+
48+
// Checks if response should be considered unauthorized (by default only 401 responses are
49+
// considered unauthorized). Override this method if you need to trigger token renewal for
50+
// other response statuses. Check API reference for helper method which defines default behaviour
51+
isResponseUnauthorized: (response) => boolean,
4652
4753
// (Required) Adds authorization for intercepted requests
4854
authorizeRequest: (request, accessToken) => authorizedRequest,
@@ -92,7 +98,7 @@ to stop fetch interception.
9298
...
9399
```
94100

95-
## API reference
101+
## API reference <a name="api-reference"></a>
96102

97103
### Exports
98104
`configure(configuration)`
@@ -107,6 +113,14 @@ to stop fetch interception.
107113

108114
Clears all tokens from interceptor.
109115

116+
`isResponseUnauthorized(response)`
117+
118+
Utility method which determines if given response should be considered unauthorized.
119+
By default, responses with status code `401` are considered unauthorized.
120+
You can use this method in `isResponseUnauthorized` of `config` object
121+
when you want to extend default behaviour.
122+
123+
110124
## Tests
111125

112126
```

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@shoutem/fetch-token-intercept",
3-
"version": "0.1.3",
3+
"version": "0.2.1",
44
"description": "Fetch interceptor for managing refresh token flow.",
55
"main": "lib/index.js",
66
"files": [
@@ -34,6 +34,7 @@
3434
"babel-core": "^6.9.1",
3535
"babel-eslint": "^6.0.0",
3636
"babel-plugin-transform-builtin-extend": "^1.1.2",
37+
"babel-plugin-transform-object-rest-spread": "^6.23.0",
3738
"babel-preset-es2015": "^6.9.0",
3839
"babel-preset-stage-0": "^6.3.13",
3940
"babel-register": "^6.9.0",

src/AccessTokenProvider.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
import {
2-
isResponseUnauthorized,
3-
} from './services/http';
4-
51
/**
62
* Provides a way for renewing access token with correct refresh token. It will automatically
73
* dispatch a call to server with request provided via config. It also ensures that
@@ -39,7 +35,6 @@ export default class AccessTokenProvider {
3935
renew() {
4036
// if token resolver is not authorized it should just resolve
4137
if (!this.isAuthorized()) {
42-
console.warn('Please authorize provider before renewing or check shouldIntercept config.');
4338
return Promise.resolve();
4439
}
4540

@@ -89,7 +84,7 @@ export default class AccessTokenProvider {
8984
handleFetchAccessTokenResponse(response) {
9085
this.renewAccessTokenPromise = null;
9186

92-
if (isResponseUnauthorized(response)) {
87+
if (this.config.isResponseUnauthorized(response)) {
9388
this.clear();
9489
return null;
9590
}

src/FetchInterceptor.js

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import {
22
ERROR_INVALID_CONFIG,
33
} from './const';
4-
import {
5-
isResponseUnauthorized,
6-
} from './services/http';
4+
import * as http from './services/http';
75
import TokenExpiredException from './services/TokenExpiredException';
86
import RetryCountExceededException from './services/RetryCountExceededException';
97
import AccessTokenProvider from './AccessTokenProvider';
@@ -22,6 +20,7 @@ export default class FetchInterceptor {
2220
createAccessTokenRequest: null,
2321
shouldIntercept: () => true,
2422
shouldInvalidateAccessToken: () => false,
23+
isResponseUnauthorized: http.isResponseUnauthorized,
2524
parseAccessToken: null,
2625
authorizeRequest: null,
2726
onAccessTokenChange: null,
@@ -66,6 +65,11 @@ export default class FetchInterceptor {
6665
* (Required) Adds authorization for intercepted requests
6766
* authorizeRequest: (request, accessToken) => authorizedRequest,
6867
*
68+
* Checks if response should be considered unauthorized (by default only 401 responses are
69+
* considered unauthorized. Override this method if you need to trigger token renewal for
70+
* other response statuses.
71+
* isResponseUnauthorized: (response) => boolean,
72+
*
6973
* Number of retries after initial request was unauthorized
7074
* fetchRetryCount: 1,
7175
*
@@ -191,7 +195,7 @@ export default class FetchInterceptor {
191195
fetchArgs,
192196
fetchResolve,
193197
fetchReject,
194-
}
198+
};
195199
}
196200

197201
createRequest(requestContext) {
@@ -201,14 +205,13 @@ export default class FetchInterceptor {
201205
return {
202206
...requestContext,
203207
request,
204-
}
208+
};
205209
}
206210

207211
shouldIntercept(requestContext) {
208212
const { request } = requestContext;
209-
const { shouldIntercept } = this.config;
210213

211-
return Promise.resolve(shouldIntercept(request))
214+
return Promise.resolve(this.config.shouldIntercept(request))
212215
.then(shouldIntercept =>
213216
({ ...requestContext, shouldIntercept })
214217
);
@@ -225,10 +228,10 @@ export default class FetchInterceptor {
225228
const { accessToken } = this.accessTokenProvider.getAuthorization();
226229
const { authorizeRequest } = this.config;
227230

228-
if (request && accessToken){
231+
if (request && accessToken) {
229232
return Promise.resolve(authorizeRequest(request, accessToken))
230-
.then(request =>
231-
({ ...requestContext, accessToken, request })
233+
.then(authorizedRequest =>
234+
({ ...requestContext, accessToken, request: authorizedRequest })
232235
);
233236
}
234237

@@ -237,14 +240,13 @@ export default class FetchInterceptor {
237240

238241
shouldFetch(requestContext) {
239242
const { request } = requestContext;
240-
const { shouldFetch } = this.config;
241243

242244
// verifies all outside conditions from config are met
243-
if (!shouldFetch) {
245+
if (!this.config.shouldFetch) {
244246
return requestContext;
245247
}
246248

247-
return Promise.resolve(shouldFetch(request))
249+
return Promise.resolve(this.config.shouldFetch(request))
248250
.then(shouldFetch =>
249251
({ ...requestContext, shouldFetch })
250252
);
@@ -278,15 +280,14 @@ export default class FetchInterceptor {
278280

279281
shouldInvalidateAccessToken(requestContext) {
280282
const { shouldIntercept } = requestContext;
281-
const { shouldInvalidateAccessToken } = this.config;
282283

283284
if (!shouldIntercept) {
284285
return requestContext;
285286
}
286287

287288
const { response } = requestContext;
288289
// check if response invalidates access token
289-
return Promise.resolve(shouldInvalidateAccessToken(response))
290+
return Promise.resolve(this.config.shouldInvalidateAccessToken(response))
290291
.then(shouldInvalidateAccessToken =>
291292
({ ...requestContext, shouldInvalidateAccessToken })
292293
);
@@ -311,15 +312,15 @@ export default class FetchInterceptor {
311312

312313
handleResponse(requestContext) {
313314
const { shouldIntercept, response, fetchResolve, fetchReject } = requestContext;
315+
const { isResponseUnauthorized } = this.config;
314316

315317
// can only be empty on network errors
316318
if (!response) {
317-
fetchReject();
318-
return;
319+
return fetchReject();
319320
}
320321

321322
if (shouldIntercept && isResponseUnauthorized(response)) {
322-
throw new TokenExpiredException({ ...requestContext })
323+
throw new TokenExpiredException({ ...requestContext });
323324
}
324325

325326
if (this.config.onResponse) {

src/index.js

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,11 @@ import {
44
isWeb,
55
isNode,
66
} from './services/environment';
7+
import { isResponseUnauthorized } from './services/http';
78
import FetchInterceptor from './FetchInterceptor';
89

910
let interceptor = null;
1011

11-
function init() {
12-
if (isReactNative()) {
13-
attach(global);
14-
} else if (isWorker()) {
15-
attach(self);
16-
} else if (isWeb()) {
17-
attach(window);
18-
} else if (isNode()) {
19-
attach(global);
20-
} else {
21-
throw new Error('Unsupported environment for fetch-token-intercept');
22-
}
23-
}
24-
2512
export function attach(env) {
2613
if (!env.fetch) {
2714
throw Error('No fetch available. Unable to register fetch-token-intercept');
@@ -35,10 +22,26 @@ export function attach(env) {
3522
interceptor = new FetchInterceptor(env.fetch);
3623

3724
// monkey patch fetch
25+
// eslint-disable-next-line no-unused-vars
3826
const fetchWrapper = fetch => (...args) => interceptor.intercept(...args);
27+
// eslint-disable-next-line no-param-reassign
3928
env.fetch = fetchWrapper(env.fetch);
4029
}
4130

31+
function init() {
32+
if (isReactNative()) {
33+
attach(global);
34+
} else if (isWorker()) {
35+
attach(self);
36+
} else if (isWeb()) {
37+
attach(window);
38+
} else if (isNode()) {
39+
attach(global);
40+
} else {
41+
throw new Error('Unsupported environment for fetch-token-intercept');
42+
}
43+
}
44+
4245
export function configure(config) {
4346
interceptor.configure(config);
4447
}
@@ -55,4 +58,8 @@ export function clear() {
5558
return interceptor.clear();
5659
}
5760

61+
export {
62+
isResponseUnauthorized,
63+
};
64+
5865
init();

src/services/http.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ function isResponseStatus(response, status) {
66
return false;
77
}
88

9-
return response['status'] === status;
9+
return response.status === status;
1010
}
1111

1212
export function isResponseOk(response) {

test/fetchInterceptor.spec.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const configuration = config => ({
2424
},
2525
onAccessTokenChange: null,
2626
onResponse: null,
27+
isResponseUnauthorized: response => response.status === 401,
2728
...config,
2829
});
2930

@@ -410,6 +411,26 @@ describe('fetch-intercept', function () {
410411
done(error);
411412
});
412413
});
414+
415+
it('should fetch successfully with access token expired and status 403', function (done) {
416+
fetchInterceptor.configure(configuration({
417+
isResponseUnauthorized: (response) => response.status === 403
418+
}));
419+
// set expired access token
420+
fetchInterceptor.authorize('refresh_token', 'token1');
421+
422+
fetch('http://localhost:5000/401/1?respondStatus=403').then(response => {
423+
expect(response.status).to.be.equal(200);
424+
return response.json();
425+
})
426+
.then(data => {
427+
expect(data.value).to.be.equal('1');
428+
done();
429+
})
430+
.catch(error => {
431+
done(error);
432+
});
433+
});
413434
});
414435

415436
describe('refresh token is invalid', () => {

test/helpers/server.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,18 @@ app.get('/200', function(req, res) {
1616
function handleUnauthorizedRequest(req, res ){
1717
const response = () => {
1818
const token = req.header('authorization') && req.header('authorization').split(' ')[1];
19+
const responseStatus = req.query.respondStatus || 401;
1920

2021
if (token === EXPIRED_TOKEN) {
21-
res.status(401).send();
22+
res.status(responseStatus).send();
2223
} else if (token === VALID_TOKEN) {
2324
if (req.query.invalidate){
2425
res.set('invalidates-token', true);
2526
}
2627

2728
res.json({ 'value': req.params.id });
2829
} else {
29-
res.status(401).send();
30+
res.status(responseStatus).send();
3031
}
3132
};
3233

0 commit comments

Comments
 (0)