-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial implementation #1
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I commented this file a lot, but please use it only as suggestion. I think you did a great job :) I didn't review tests, I will look it in second iteration. 🥇
} | ||
|
||
return token; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should maybe be default way of handling authorization, but we should also allow to override it in config from Builder.
src/helpers/tokenFormatter.js
Outdated
@@ -0,0 +1,25 @@ | |||
export function formatBearer(token) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only for tests? If so move it to /test/helpers
src/const.js
Outdated
export const ENVIRONMENT_IS_REACT_NATIVE = typeof navigator === 'object' && navigator.product === 'ReactNative'; | ||
export const ENVIRONMENT_IS_NODE = typeof process === 'object' && typeof require === 'function'; | ||
export const ENVIRONMENT_IS_WEB = typeof window === 'object'; | ||
export const ENVIRONMENT_IS_WORKER = typeof importScripts === 'function'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be changed into functions with camelCase and moved maybe to service/environment.js
src/index.js
Outdated
return fetchInterceptor(fetch, ...args); | ||
}; | ||
})(env.fetch); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function with above check of environment should be encapsulated into environment
that would be called in index.js like environment.init(); only once upon index.js load. Names are just suggestions.
src/index.js
Outdated
refreshToken: null, | ||
}; | ||
|
||
let refreshAccessTokenPromise = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of above let
variables should be changed into const
and moved into encapsulated parts of lib that use them.
src/index.js
Outdated
/** | ||
* Configures fetch token intercept | ||
*/ | ||
export function configure(initConfig) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interceptor class and Interceptor class should pass part of config to AccessToken provider?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm currently passing the whole config to AccessTokenProvider until we settle on the design. It's easy to extract necessary methods in the end.
src/index.js
Outdated
STATUS_OK, | ||
} from './const'; | ||
|
||
let config = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Document each config key
src/index.js
Outdated
|
||
return response; | ||
}) | ||
.then(response => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check usage of .then
sometimes it's not necessary, I think once you encapsulate logic into methods that need for .then
will diminish. You explained me concept of config methods that returns promise, and for that cases you don't have other options.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This whole part was redesigned into requestUnit idea as part of fetchInterceptor class and fetchWithRetry method.
src/index.js
Outdated
if (!tokens.accessToken) { | ||
// check if we are alredy fetching it | ||
if (!refreshAccessTokenPromise) { | ||
// as a side-effect refreshTokenPromise is set |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't like side-effect :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sideeffect is now part of accessTokenProvider :)
src/index.js
Outdated
outerReject(error); | ||
} else { | ||
// if we fail to resolve refresh token, and it's not internal error | ||
// just pass the request normally |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part is weird, I wouldn't expect it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's how normal fetch behaves:
fetch(url).then(result).catch(error)
- catch is called only on network errors (e.g. DNS unresolved)
- then is called on all other responses, including 401
Initially I've solved this flow by throwing this exception, then capturing it in main fetch promise and resolving accordingly. In new version I did it without throwing exceptions.
These are all great suggestions, I've already fixed some od these suggestions on the following commit, like encapsulating refresh token handling into separate class, and your ideja od interceptor correlates with my idea od interceptor provider. Also to enable promisifying any method od config I've converted a request into a smarter object which I've called 'requestUnit' which passes through interceptor promise pipeline and holds resolved values of all config functions. This effectively enables us to cashe values for resolved config methods and whole process acts as a middleware chain (got the ideja from node). :) |
* fetchInterceptor - main class for intercepting * accessTokenProvider - access token resolver multiple services to increase readability as per PR comments
Fixed scope for fetch method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should discuss some aspects of code.
expect(result).to.be.equal('token'); | ||
}); | ||
}) | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
new line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
package.json
Outdated
"access" | ||
], | ||
"author": "Shoutem", | ||
"license": "MIT", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BSD? custom?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note, this should probably be updated on redux io repo too.
package.json
Outdated
"author": "Shoutem", | ||
"license": "MIT", | ||
"bugs": { | ||
"url": "" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
link to github issues page?
src/accessTokenProvider.js
Outdated
isResponseUnauthorized, | ||
} from './services/http'; | ||
|
||
export class AccessTokenProvider { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add nice jsdoc comment /** about this class, explaining it to others
src/accessTokenProvider.js
Outdated
refresh() { | ||
// if token resolver is not authorized it should just resolve | ||
if (!this.isAuthorized()) { | ||
return Promise.resolve(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should it reject?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it rejects it will cause a chain reaction which will cause fetchIntercept chain to reject also, which in turn will reject an initial fetch request which is undesirable as we don't want to break the fetch spec which currently states that fetch will only reject on network errors and this is not a network error.
But indeed it could be potentially problematic if you call renew
without calling authorize
first as it will silently ignore your renew
requests. Two solutions here: silently fail with console error, or raise an exception which will be caught by fetchInterceptor which won't propagate it out of fetch call, but catch it, perhaps again log it, and then resolve as normal request would. I feel that the second solution only raises the handling to upper level without any flow control benefits as this can't be handled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking from outside, I would want for my requests to fail with 40x error with equal behavior as in case that I don't use fetch-token-intercept and didn't add proper Auth header. Also it would be useful to see additional error or warning from fetch-token-intercept about need to call authorize
with instruction to pass refreshToken
.
src/fetchInterceptor.js
Outdated
.then(this.fetchRequest) | ||
.then(this.shouldInvalidateAccessToken) | ||
.then(this.invalidateAccessToken) | ||
.then(this.handleUnauthorizedRequest) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handleUnauthorizedRequest hides that you are renewing token. Maybe this could be more explicit, by rejecting and then in rejectHandler try to resolve problem?
src/fetchInterceptor.js
Outdated
this.config.onResponse(response); | ||
} | ||
|
||
outerResolve(response); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code in then
block could be in handleResponse()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only reason that it isn't is because it needs access to methods for resolving fetch promise (outerResolve and outerReject are basically mapped like this fetch.then(outerResolve).then(outerReject)), but when I though about it, it totally makes sense to put references of outer fetch reject and resolve in request unit as any stage of pipeline could do
src/fetchInterceptor.js
Outdated
shouldFetch: !!accessToken, | ||
})) | ||
.then(this.authorizeRequest) | ||
.then(this.fetchRequest) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block in if (isResponseUnauthorized(response)) {
is problematic, what happens if it fails twice? You only have one retry right? Should you reuse fetchWithRetry and move request through flow once again?
src/helpers/tokenFormatter.js
Outdated
} | ||
|
||
const bearerRegex = /^Bearer (.+)$/; | ||
const matches = bearerRegex.exec(authorizationHeaderValue); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
string.match(regex)
test/fetchInterceptor.spec.js
Outdated
}); | ||
}); | ||
|
||
it('should propagate 401 for multiple requests', function (done) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens here? Can we test what happens if one request gets multiple 401s?
- Request is emitted with expired token
- Response is 401
- token is refreshed, request is reemitted
- Response is 401
- token is refreshed, request is reemitted
- Response is 401
- token is refreshed, request is reemitted
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrote a test, it will retry as many times as it is defined on config with fetchRetryCount property. (should stop after retry count is exceeded and resolve unauthorized)
Bump versoin
Refactored tests Minor renames and improvements
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a couple of minor changes.
@@ -0,0 +1,59 @@ | |||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't ahve README file, add it. I know you don't have time to write everything. But maybe you could just add short description about what this lib solves and add link to somethnig like this:
https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/
src/AccessTokenProvider.js
Outdated
renew() { | ||
// if token resolver is not authorized it should just resolve | ||
if (!this.isAuthorized()) { | ||
console.log('Please authorize provider before renewing or check shouldIntercept config.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
warning
src/AccessTokenProvider.js
Outdated
* @param refreshToken | ||
* @param accessToken | ||
*/ | ||
authorize(refreshToken, accessToken) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need to pass accessToken here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to allow passing both refresh token and access token for two reasons:
- perhaps you already have both stored externally and both are valid (why forcing a renew?)
- RFC spec allows authorization response to return both https://tools.ietf.org/html/rfc6749#page-10
src/AccessTokenProvider.js
Outdated
} | ||
|
||
handleAccessToken(token, resolve) { | ||
this.tokens.accessToken = token; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.tokens = {...this.tokens, accessToken}
src/AccessTokenProvider.js
Outdated
return this.config.parseAccessToken(response); | ||
} | ||
|
||
handleAccessToken(token, resolve) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
accessToken
src/FetchInterceptor.js
Outdated
invalidateAccessToken(requestUnit) { | ||
const { shouldIntercept, shouldInvalidateAccessToken } = requestUnit; | ||
|
||
if (shouldIntercept && shouldInvalidateAccessToken) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here
} | ||
|
||
if (this.config.onResponse) { | ||
this.config.onResponse(response); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if someone in onResponse function doesn't know how to handle response
with clone
is it going to destroy it before fetchResolve
, should you pass response.clone here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As this is the last callback it won't matter, but generally speaking, when accessing body you should clone first, because if anyone also has to read it after you it will fail I don't clone it first to avoid clone overhead when it's not necessary. Also, I've designed onResponse
as fire and forget as we don't wait for events to finish during fetch intercept.
src/FetchInterceptor.js
Outdated
throw new Error(error); | ||
} | ||
|
||
isConfigValid() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe put it higher in class to be closer to call
this.message = 'Retry count has been exceeded'; | ||
this.requestUnit = requestUnit; | ||
|
||
// Use V8's native method if available, otherwise fallback |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What we gain with this? It only for Chrome?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
http://stackoverflow.com/a/27724419/1008529
...with the enhancement that stack traces work on Firefox and other browsers.
|
||
RetryCountExceededException.prototype = Object.create(Error.prototype); | ||
RetryCountExceededException.prototype.name = "RetryCountExceededException"; | ||
RetryCountExceededException.prototype.constructor = RetryCountExceededException; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
new line.
Is there a ES6 way to do this? using extends
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Babel 6.x does not correctly support extending core classes yet:
http://stackoverflow.com/questions/31089801/extending-error-in-javascript-with-es6-syntax
babel/babel#3083
there's a babel plugin https://www.npmjs.com/package/babel-plugin-transform-builtin-extend that enables extending from Error and Array. Don't know how we feel about this..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would add this babel plugin, It's only overhead during transpiling but simplifies code.
Added readme Minor renames in AccessTokenProvider and FetchInterceptor Version bumped
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks awesome 👍
//Event invoked when access token has changed | ||
onAccessTokenChange: null, | ||
|
||
//Event invoked when response is resolved |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You usually have a single space after // except in the examples above :)
} | ||
``` | ||
|
||
All required methods support returning a promise to enable reading of body. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe ... return a promise to allow reading of the request body.
} | ||
|
||
/** | ||
* Returns current authorization for fetch fetchInterceptor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fetch fetchInterceptor
is a bit confusing here...
} | ||
|
||
/** | ||
* Authorizes fetch interceptor with given renew token |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
refresh?
resolveIntercept(resolve, reject, ...args) { | ||
const request = new Request(...args); | ||
const { accessToken } = this.accessTokenProvider.getAuthorization(); | ||
const requestUnit = this.createRequestUnit(request, resolve, reject); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is a request unit? Maybe add a short description to explain it here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed to RequestContext, also added comments on createRequestContext method describing what it represents.
.catch(this.handleUnauthorizedRequest); | ||
} | ||
|
||
createRequestUnit(request, fetchResolve, fetchReject) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It wasn't clear to me what a request unit should be from its name. Maybe we should consider renaming it to something like RequestContext
or possibly RequestTask
.
No description provided.