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
1118937d
Commit
1118937d
authored
Feb 09, 2017
by
Sean Hammond
Committed by
GitHub
Feb 09, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #221 from hypothesis/add-oauth-refresh-support-2
Add OAuth refresh support
parents
29af928e
37eacfc1
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
170 additions
and
19 deletions
+170
-19
oauth-auth.js
src/sidebar/oauth-auth.js
+67
-12
oauth-auth-test.js
src/sidebar/test/oauth-auth-test.js
+103
-7
No files found.
src/sidebar/oauth-auth.js
View file @
1118937d
...
@@ -16,25 +16,79 @@ function auth($http, settings) {
...
@@ -16,25 +16,79 @@ function auth($http, settings) {
var
accessTokenPromise
;
var
accessTokenPromise
;
var
tokenUrl
=
resolve
(
'token'
,
settings
.
apiUrl
);
var
tokenUrl
=
resolve
(
'token'
,
settings
.
apiUrl
);
/**
* An object holding the details of an access token from the tokenUrl endpoint.
* @typedef {Object} TokenInfo
* @property {string} accessToken - The access token itself.
* @property {number} expiresIn - The lifetime of the access token,
* in seconds.
* @property {string} refreshToken - The refresh token that can be used to
* get a new access token.
*/
/**
* Return a new TokenInfo object from the given tokenUrl endpoint response.
* @param {Object} response - The HTTP response from a POST to the tokenUrl
* endpoint (an Angular $http response object).
* @returns {TokenInfo}
*/
function
tokenInfoFrom
(
response
)
{
var
data
=
response
.
data
;
return
{
accessToken
:
data
.
access_token
,
expiresIn
:
data
.
expires_in
,
refreshToken
:
data
.
refresh_token
,
};
}
// Post the given data to the tokenUrl endpoint as a form submission.
// Return a Promise for the access token response.
function
postToTokenUrl
(
data
)
{
data
=
queryString
.
stringify
(
data
);
var
requestConfig
=
{
headers
:
{
'Content-Type'
:
'application/x-www-form-urlencoded'
},
};
return
$http
.
post
(
tokenUrl
,
data
,
requestConfig
);
}
// Exchange the JWT grant token for an access token.
// Exchange the JWT grant token for an access token.
// See https://tools.ietf.org/html/rfc7523#section-4
// See https://tools.ietf.org/html/rfc7523#section-4
function
exchangeToken
(
grantToken
)
{
function
exchangeToken
(
grantToken
)
{
var
data
=
queryString
.
stringify
(
{
var
data
=
{
grant_type
:
'urn:ietf:params:oauth:grant-type:jwt-bearer'
,
grant_type
:
'urn:ietf:params:oauth:grant-type:jwt-bearer'
,
assertion
:
grantToken
,
assertion
:
grantToken
,
});
var
requestConfig
=
{
headers
:
{
'Content-Type'
:
'application/x-www-form-urlencoded'
},
};
};
return
$http
.
post
(
tokenUrl
,
data
,
requestConfig
)
return
postToTokenUrl
(
data
).
then
(
function
(
response
)
{
.
then
(
function
(
response
)
{
if
(
response
.
status
!==
200
)
{
if
(
response
.
status
!==
200
)
{
throw
new
Error
(
'Failed to retrieve access token'
);
throw
new
Error
(
'Failed to retrieve access token'
);
}
}
return
response
.
data
;
return
tokenInfoFrom
(
response
)
;
});
});
}
}
// 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
()
{
function
tokenGetter
()
{
if
(
!
accessTokenPromise
)
{
if
(
!
accessTokenPromise
)
{
var
grantToken
;
var
grantToken
;
...
@@ -45,7 +99,8 @@ function auth($http, settings) {
...
@@ -45,7 +99,8 @@ function auth($http, settings) {
if
(
grantToken
)
{
if
(
grantToken
)
{
accessTokenPromise
=
exchangeToken
(
grantToken
).
then
(
function
(
tokenInfo
)
{
accessTokenPromise
=
exchangeToken
(
grantToken
).
then
(
function
(
tokenInfo
)
{
return
tokenInfo
.
access_token
;
refreshAccessTokenBeforeItExpires
(
tokenInfo
);
return
tokenInfo
.
accessToken
;
});
});
}
else
{
}
else
{
accessTokenPromise
=
Promise
.
resolve
(
null
);
accessTokenPromise
=
Promise
.
resolve
(
null
);
...
...
src/sidebar/test/oauth-auth-test.js
View file @
1118937d
...
@@ -10,6 +10,7 @@ describe('oauth auth', function () {
...
@@ -10,6 +10,7 @@ describe('oauth auth', function () {
var
nowStub
;
var
nowStub
;
var
fakeHttp
;
var
fakeHttp
;
var
fakeSettings
;
var
fakeSettings
;
var
clock
;
beforeEach
(
function
()
{
beforeEach
(
function
()
{
nowStub
=
sinon
.
stub
(
window
.
performance
,
'now'
);
nowStub
=
sinon
.
stub
(
window
.
performance
,
'now'
);
...
@@ -19,8 +20,9 @@ describe('oauth auth', function () {
...
@@ -19,8 +20,9 @@ describe('oauth auth', function () {
post
:
sinon
.
stub
().
returns
(
Promise
.
resolve
({
post
:
sinon
.
stub
().
returns
(
Promise
.
resolve
({
status
:
200
,
status
:
200
,
data
:
{
data
:
{
access_token
:
'
an-access-t
oken'
,
access_token
:
'
firstAccessT
oken'
,
expires_in
:
DEFAULT_TOKEN_EXPIRES_IN_SECS
,
expires_in
:
DEFAULT_TOKEN_EXPIRES_IN_SECS
,
refresh_token
:
'firstRefreshToken'
,
},
},
})),
})),
};
};
...
@@ -34,10 +36,13 @@ describe('oauth auth', function () {
...
@@ -34,10 +36,13 @@ describe('oauth auth', function () {
};
};
auth
=
authService
(
fakeHttp
,
fakeSettings
);
auth
=
authService
(
fakeHttp
,
fakeSettings
);
clock
=
sinon
.
useFakeTimers
();
});
});
afterEach
(
function
()
{
afterEach
(
function
()
{
performance
.
now
.
restore
();
performance
.
now
.
restore
();
clock
.
restore
();
});
});
describe
(
'#tokenGetter'
,
function
()
{
describe
(
'#tokenGetter'
,
function
()
{
...
@@ -49,7 +54,7 @@ describe('oauth auth', function () {
...
@@ -49,7 +54,7 @@ describe('oauth auth', function () {
assert
.
calledWith
(
fakeHttp
.
post
,
'https://hypothes.is/api/token'
,
expectedBody
,
{
assert
.
calledWith
(
fakeHttp
.
post
,
'https://hypothes.is/api/token'
,
expectedBody
,
{
headers
:
{
'Content-Type'
:
'application/x-www-form-urlencoded'
},
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 () {
...
@@ -67,10 +72,10 @@ describe('oauth auth', function () {
it
(
'should cache tokens for future use'
,
function
()
{
it
(
'should cache tokens for future use'
,
function
()
{
return
auth
.
tokenGetter
().
then
(
function
()
{
return
auth
.
tokenGetter
().
then
(
function
()
{
fakeHttp
.
post
.
reset
();
resetHttpSpy
();
return
auth
.
tokenGetter
();
return
auth
.
tokenGetter
();
}).
then
(
function
(
token
)
{
}).
then
(
function
(
token
)
{
assert
.
equal
(
token
,
'
an-access-t
oken'
);
assert
.
equal
(
token
,
'
firstAccessT
oken'
);
assert
.
notCalled
(
fakeHttp
.
post
);
assert
.
notCalled
(
fakeHttp
.
post
);
});
});
});
});
...
@@ -80,9 +85,7 @@ describe('oauth auth', function () {
...
@@ -80,9 +85,7 @@ describe('oauth auth', function () {
// the pending Promise for the first request again (and not send a second
// the pending Promise for the first request again (and not send a second
// concurrent HTTP request).
// concurrent HTTP request).
it
(
'should not make two concurrent access token requests'
,
function
()
{
it
(
'should not make two concurrent access token requests'
,
function
()
{
// Make $http.post() return a pending Promise (simulates an in-flight
makeServerUnresponsive
();
// HTTP request).
fakeHttp
.
post
.
returns
(
new
Promise
(
function
()
{}));
// The first time tokenGetter() is called it sends the access token HTTP
// The first time tokenGetter() is called it sends the access token HTTP
// request and returns a Promise for the access token.
// request and returns a Promise for the access token.
...
@@ -109,5 +112,98 @@ describe('oauth auth', function () {
...
@@ -109,5 +112,98 @@ describe('oauth auth', function () {
assert
.
equal
(
token
,
null
);
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