Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
37eacfc1
Commit
37eacfc1
authored
Feb 09, 2017
by
Sean Hammond
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refresh access tokens before they expire
parent
4331bfd5
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
128 additions
and
7 deletions
+128
-7
oauth-auth.js
src/sidebar/oauth-auth.js
+25
-0
oauth-auth-test.js
src/sidebar/test/oauth-auth-test.js
+103
-7
No files found.
src/sidebar/oauth-auth.js
View file @
37eacfc1
...
...
@@ -37,6 +37,7 @@ function auth($http, settings) {
return
{
accessToken
:
data
.
access_token
,
expiresIn
:
data
.
expires_in
,
refreshToken
:
data
.
refresh_token
,
};
}
...
...
@@ -65,6 +66,29 @@ function auth($http, settings) {
});
}
// Exchange the refresh token for a new access token and refresh token pair.
// See https://tools.ietf.org/html/rfc6749#section-6
function
refreshAccessToken
(
refreshToken
)
{
var
data
=
{
grant_type
:
'refresh_token'
,
refresh_token
:
refreshToken
};
postToTokenUrl
(
data
).
then
(
function
(
response
)
{
var
tokenInfo
=
tokenInfoFrom
(
response
);
refreshAccessTokenBeforeItExpires
(
tokenInfo
);
accessTokenPromise
=
Promise
.
resolve
(
tokenInfo
.
accessToken
);
});
}
// Set a timeout to refresh the access token a few minutes before it expires.
function
refreshAccessTokenBeforeItExpires
(
tokenInfo
)
{
var
delay
=
tokenInfo
.
expiresIn
*
1000
;
// We actually have to refresh the access token _before_ it expires.
// If the access token expires in one hour, this should refresh it in
// about 55 mins.
delay
=
Math
.
floor
(
delay
*
0.91
);
window
.
setTimeout
(
refreshAccessToken
,
delay
,
tokenInfo
.
refreshToken
);
}
function
tokenGetter
()
{
if
(
!
accessTokenPromise
)
{
var
grantToken
;
...
...
@@ -75,6 +99,7 @@ function auth($http, settings) {
if
(
grantToken
)
{
accessTokenPromise
=
exchangeToken
(
grantToken
).
then
(
function
(
tokenInfo
)
{
refreshAccessTokenBeforeItExpires
(
tokenInfo
);
return
tokenInfo
.
accessToken
;
});
}
else
{
...
...
src/sidebar/test/oauth-auth-test.js
View file @
37eacfc1
...
...
@@ -10,6 +10,7 @@ describe('oauth auth', function () {
var
nowStub
;
var
fakeHttp
;
var
fakeSettings
;
var
clock
;
beforeEach
(
function
()
{
nowStub
=
sinon
.
stub
(
window
.
performance
,
'now'
);
...
...
@@ -19,8 +20,9 @@ describe('oauth auth', function () {
post
:
sinon
.
stub
().
returns
(
Promise
.
resolve
({
status
:
200
,
data
:
{
access_token
:
'
an-access-t
oken'
,
access_token
:
'
firstAccessT
oken'
,
expires_in
:
DEFAULT_TOKEN_EXPIRES_IN_SECS
,
refresh_token
:
'firstRefreshToken'
,
},
})),
};
...
...
@@ -34,10 +36,13 @@ describe('oauth auth', function () {
};
auth
=
authService
(
fakeHttp
,
fakeSettings
);
clock
=
sinon
.
useFakeTimers
();
});
afterEach
(
function
()
{
performance
.
now
.
restore
();
clock
.
restore
();
});
describe
(
'#tokenGetter'
,
function
()
{
...
...
@@ -49,7 +54,7 @@ describe('oauth auth', function () {
assert
.
calledWith
(
fakeHttp
.
post
,
'https://hypothes.is/api/token'
,
expectedBody
,
{
headers
:
{
'Content-Type'
:
'application/x-www-form-urlencoded'
},
});
assert
.
equal
(
token
,
'
an-access-t
oken'
);
assert
.
equal
(
token
,
'
firstAccessT
oken'
);
});
});
...
...
@@ -67,10 +72,10 @@ describe('oauth auth', function () {
it
(
'should cache tokens for future use'
,
function
()
{
return
auth
.
tokenGetter
().
then
(
function
()
{
fakeHttp
.
post
.
reset
();
resetHttpSpy
();
return
auth
.
tokenGetter
();
}).
then
(
function
(
token
)
{
assert
.
equal
(
token
,
'
an-access-t
oken'
);
assert
.
equal
(
token
,
'
firstAccessT
oken'
);
assert
.
notCalled
(
fakeHttp
.
post
);
});
});
...
...
@@ -80,9 +85,7 @@ describe('oauth auth', function () {
// the pending Promise for the first request again (and not send a second
// concurrent HTTP request).
it
(
'should not make two concurrent access token requests'
,
function
()
{
// Make $http.post() return a pending Promise (simulates an in-flight
// HTTP request).
fakeHttp
.
post
.
returns
(
new
Promise
(
function
()
{}));
makeServerUnresponsive
();
// The first time tokenGetter() is called it sends the access token HTTP
// request and returns a Promise for the access token.
...
...
@@ -109,5 +112,98 @@ describe('oauth auth', function () {
assert
.
equal
(
token
,
null
);
});
});
it
(
'should refresh the access token before it expires'
,
function
()
{
function
callTokenGetter
()
{
var
tokenPromise
=
auth
.
tokenGetter
();
fakeHttp
.
post
.
returns
(
Promise
.
resolve
({
status
:
200
,
data
:
{
access_token
:
'secondAccessToken'
,
expires_in
:
DEFAULT_TOKEN_EXPIRES_IN_SECS
,
refresh_token
:
'secondRefreshToken'
,
},
}));
return
tokenPromise
;
}
function
assertRefreshTokenWasUsed
(
refreshToken
)
{
return
function
()
{
var
expectedBody
=
'grant_type=refresh_token&refresh_token='
+
refreshToken
;
assert
.
calledWith
(
fakeHttp
.
post
,
'https://hypothes.is/api/token'
,
expectedBody
,
{
headers
:
{
'Content-Type'
:
'application/x-www-form-urlencoded'
},
});
};
}
function
assertThatTokenGetterNowReturnsNewAccessToken
()
{
return
auth
.
tokenGetter
().
then
(
function
(
token
)
{
assert
.
equal
(
token
,
'secondAccessToken'
);
});
}
return
callTokenGetter
()
.
then
(
resetHttpSpy
)
.
then
(
expireAccessToken
)
.
then
(
assertRefreshTokenWasUsed
(
'firstRefreshToken'
))
.
then
(
resetHttpSpy
)
.
then
(
assertThatTokenGetterNowReturnsNewAccessToken
)
.
then
(
expireAccessToken
)
.
then
(
assertRefreshTokenWasUsed
(
'secondRefreshToken'
));
});
// While a refresh token HTTP request is in-flight, calls to tokenGetter()
// should just return the old access token immediately.
it
(
'returns the access token while a refresh is in-flight'
,
function
()
{
return
auth
.
tokenGetter
().
then
(
function
(
firstAccessToken
)
{
makeServerUnresponsive
();
expireAccessToken
();
// The refresh token request will still be in-flight, but tokenGetter()
// should still return a Promise for the old access token.
return
auth
.
tokenGetter
().
then
(
function
(
secondAccessToken
)
{
assert
.
equal
(
firstAccessToken
,
secondAccessToken
);
});
});
});
// It only sends one refresh request, even if tokenGetter() is called
// multiple times and the refresh response hasn't come back yet.
it
(
'does not send more than one refresh request'
,
function
()
{
return
auth
.
tokenGetter
()
.
then
(
resetHttpSpy
)
// Reset fakeHttp.post.callCount to 0 so that the
// initial access token request isn't counted.
.
then
(
auth
.
tokenGetter
)
.
then
(
makeServerUnresponsive
)
.
then
(
auth
.
tokenGetter
)
.
then
(
expireAccessToken
)
.
then
(
function
()
{
assert
.
equal
(
fakeHttp
.
post
.
callCount
,
1
);
});
});
});
// Advance time forward so that any current access tokens will have expired.
function
expireAccessToken
()
{
clock
.
tick
(
DEFAULT_TOKEN_EXPIRES_IN_SECS
*
1000
);
}
// Make $http.post() return a pending Promise (simulates a still in-flight
// HTTP request).
function
makeServerUnresponsive
()
{
fakeHttp
.
post
.
returns
(
new
Promise
(
function
()
{}));
}
// Reset fakeHttp.post.callCount and other fields.
function
resetHttpSpy
()
{
fakeHttp
.
post
.
reset
();
}
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment