Skip to content

Commit

Permalink
Check if the user token is valid, revoke and re-request if necessary
Browse files Browse the repository at this point in the history
  • Loading branch information
joelpurra committed Jan 27, 2018
1 parent 9d172cb commit d585365
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 32 deletions.
72 changes: 54 additions & 18 deletions index.js
Expand Up @@ -71,8 +71,10 @@ assert(twitchAppOAuthRedirectUrl.length > 0, "TWITCH_APP_OAUTH_REDIRECT_URL");
assert.strictEqual(typeof twitchUserName, "string", "TWITCH_USER_NAME");
assert(twitchUserName.length > 0, "TWITCH_USER_NAME");

const twitchUserOAuthTokenUri = "https://api.twitch.tv/kraken/oauth2/token";
const twitchAppOAuthAuthorizationUri = "https://api.twitch.tv/kraken/oauth2/authorize";
const twitchOAuthTokenUri = "https://api.twitch.tv/kraken/oauth2/token";
const twitchOAuthTokenRevocationUri = "https://api.twitch.tv/kraken/oauth2/revoke";
const twitchOAuthAuthorizationUri = "https://api.twitch.tv/kraken/oauth2/authorize";
const twitchOAuthTokenVerificationUri = "https://api.twitch.tv/kraken";
const twitchUsersDataUri = "https://api.twitch.tv/helix/users";
const twitchPubSubWebSocketUri = "wss://pubsub-edge.twitch.tv/";
const twitchIrcWebSocketUri = "wss://irc-ws.chat.twitch.tv:443/";
Expand All @@ -81,8 +83,6 @@ const twitchAppScopes = [
"channel_feed_read",
];
const twitchAppTokenRefreshInterval = 45 * 60 * 1000;
const twitchAppTokenUri = "https://api.twitch.tv/kraken/oauth2/token";
const twitchAppTokenRevocationUri = "https://api.twitch.tv/kraken/oauth2/revoke";

// NOTE: assuming that the user only joins their own channel, with a "#" prefix.
const twitchChannelName = `#${twitchUserName}`;
Expand All @@ -106,8 +106,8 @@ const rootLogger = new PinoLogger(rootPinoLogger);
const indexLogger = rootLogger.child("index");
const shutdownManager = new ShutdownManager(rootLogger);
const databaseConnection = new DatabaseConnection(rootLogger, databaseUri);
const twitchPollingApplicationTokenConnection = new TwitchPollingApplicationTokenConnection(rootLogger, twitchAppClientId, twitchAppClientSecret, twitchAppScopes, twitchAppTokenRefreshInterval, false, twitchAppTokenUri, "post");
const twitchApplicationTokenManager = new TwitchApplicationTokenManager(rootLogger, twitchPollingApplicationTokenConnection, twitchAppClientId, twitchAppTokenRevocationUri);
const twitchPollingApplicationTokenConnection = new TwitchPollingApplicationTokenConnection(rootLogger, twitchAppClientId, twitchAppClientSecret, twitchAppScopes, twitchAppTokenRefreshInterval, false, twitchOAuthTokenUri, "post");
const twitchApplicationTokenManager = new TwitchApplicationTokenManager(rootLogger, twitchPollingApplicationTokenConnection, twitchAppClientId, twitchOAuthTokenRevocationUri);
const twitchCSRFHelper = new TwitchCSRFHelper(rootLogger);

Promise.resolve()
Expand Down Expand Up @@ -154,25 +154,61 @@ Promise.resolve()

const twitchApplicationAccessTokenProvider = () => twitchApplicationTokenManager.getOrWait();

const twitchUserHelper = new TwitchUserHelper(rootLogger, twitchCSRFHelper, UserRepository, twitchAppOAuthAuthorizationUri, twitchAppOAuthRedirectUrl, twitchUserOAuthTokenUri, twitchUsersDataUri, twitchAppClientId, twitchAppClientSecret, twitchApplicationAccessTokenProvider);

return Promise.all([
const twitchUserHelper = new TwitchUserHelper(
rootLogger,
twitchCSRFHelper,
UserRepository,
twitchOAuthTokenRevocationUri,
twitchOAuthAuthorizationUri,
twitchAppOAuthRedirectUrl,
twitchOAuthTokenUri,
twitchOAuthTokenVerificationUri,
twitchUsersDataUri,
twitchAppClientId,
twitchAppClientSecret,
twitchApplicationAccessTokenProvider
);

const userTokenManager = new TwitchUserTokenManager(rootLogger, twitchOAuthTokenUri, twitchOAuthTokenRevocationUri, twitchAppClientId, twitchAppClientSecret);

const twitchUserAccessTokenProvider = () => {
// TODO: replace with an https server.
// TODO: revoke user token?
twitchUserHelper.getUserToken(twitchUserName),
return twitchUserHelper.getUserToken(twitchUserName)
.then((twitchUserToken) => {
return twitchUserHelper.isTokenValid(twitchUserToken)
.then((isValid) => {
if (isValid) {
return twitchUserToken;
}

return twitchUserHelper.forgetUserToken(twitchUserName)
// TODO: user-wrappers with username for the generic token functions?
.then(() => twitchUserHelper.revokeToken(twitchUserToken))
.then(() => twitchUserHelper.getUserToken(twitchUserName));
});
})
// TODO: improve getting/refreshing the token to have a creation time, not just expiry time.
.then((twitchUserToken) => userTokenManager.get(twitchUserToken))
// TODO: don't store the token here, but in the userTokenManager, or in the twitchUserHelper?
.tap((refreshedToken) => twitchUserHelper.storeUserToken(twitchUserName, refreshedToken))
.then((refreshedToken) => refreshedToken.access_token);
};

return Promise.all([
twitchUserHelper.getUserIdByUserName(twitchUserName),
// TODO: move out of Promise.all?
twitchUserAccessTokenProvider(),
])
.then(([twitchUserToken, twitchUserId]) => {
.then((
[
twitchUserId,
/* eslint-disable no-unused-vars */twitchUserToken, /* eslint-enable no-unused-vars */
]
) => {
// TODO: use twitchUserIdProvider instead of twitchUserId.
// const twitchUserIdProvider = () => Promise.resolve(twitchUserId);

const userTokenManager = new TwitchUserTokenManager(rootLogger, twitchUserOAuthTokenUri, twitchAppTokenRevocationUri, twitchAppClientId, twitchAppClientSecret);

const twitchUserAccessTokenProvider = () => userTokenManager.get(twitchUserToken)
// TODO: don't store the token here, but in the userTokenManager, or in the twitchUserHelper?
.tap((refreshedToken) => twitchUserHelper.storeUserToken(twitchUserName, refreshedToken))
.then((refreshedToken) => refreshedToken.access_token);

const followingPollingUri = `https://api.twitch.tv/kraken/channels/${twitchUserId}/follows?limit=${followingPollingLimit}`;

const twitchPubSubConnection = new TwitchPubSubConnection(rootLogger, twitchPubSubWebSocketUri);
Expand Down
1 change: 1 addition & 0 deletions src/twitch/authentication/user-token-manager.js
Expand Up @@ -52,6 +52,7 @@ export default class UserTokenManager {
assert.strictEqual(typeof tokenToRefresh, "object");

return Promise.try(() => {
// TODO: improve getting/refreshing the token to have a creation time, not just expiry time.
const refreshTokenWithClientIdAndSecret = {
access_token: tokenToRefresh.access_token,
refresh_token: tokenToRefresh.refresh_token,
Expand Down
173 changes: 159 additions & 14 deletions src/twitch/helper/user-helper.js
Expand Up @@ -27,20 +27,39 @@ const axios = require("axios");
const qs = require("qs");

export default class UserHelper {
constructor(logger, csrfHelper, UserRepository, appOAuthAuthorizationUri, appOAuthRedirectUrl, userOAuthTokenUri, usersDataUri, appClientId, appClientSecret, applicationAccessTokenProvider) {
assert.strictEqual(arguments.length, 10);
constructor(
logger,
csrfHelper,
UserRepository,
oauthTokenRevocationUri,
oauthAuthorizationUri,
appOAuthRedirectUrl,
oauthTokenUri,
oauthTokenVerificationUri,
usersDataUri,
appClientId,
appClientSecret,
applicationAccessTokenProvider
) {
assert.strictEqual(arguments.length, 12);
assert.strictEqual(typeof logger, "object");
assert.strictEqual(typeof csrfHelper, "object");
assert.strictEqual(typeof UserRepository, "function");
assert.strictEqual(typeof appOAuthAuthorizationUri, "string");
assert(appOAuthAuthorizationUri.length > 0);
assert(appOAuthAuthorizationUri.startsWith("https://"));
assert.strictEqual(typeof oauthTokenRevocationUri, "string");
assert(oauthTokenRevocationUri.length > 0);
assert(oauthTokenRevocationUri.startsWith("https://"));
assert.strictEqual(typeof oauthAuthorizationUri, "string");
assert(oauthAuthorizationUri.length > 0);
assert(oauthAuthorizationUri.startsWith("https://"));
assert.strictEqual(typeof appOAuthRedirectUrl, "string");
assert(appOAuthRedirectUrl.length > 0);
assert(appOAuthRedirectUrl.startsWith("https://"));
assert.strictEqual(typeof userOAuthTokenUri, "string");
assert(userOAuthTokenUri.length > 0);
assert(userOAuthTokenUri.startsWith("https://"));
assert.strictEqual(typeof oauthTokenUri, "string");
assert(oauthTokenUri.length > 0);
assert(oauthTokenUri.startsWith("https://"));
assert.strictEqual(typeof oauthTokenVerificationUri, "string");
assert(oauthTokenVerificationUri.length > 0);
assert(oauthTokenVerificationUri.startsWith("https://"));
assert.strictEqual(typeof usersDataUri, "string");
assert(usersDataUri.length > 0);
assert(usersDataUri.startsWith("https://"));
Expand All @@ -53,10 +72,12 @@ export default class UserHelper {
this._logger = logger.child("UserHelper");
this._csrfHelper = csrfHelper;
this._UserRepository = UserRepository;
this._appOAuthAuthorizationUri = appOAuthAuthorizationUri;
this._usersDataUri = usersDataUri;
this._oauthTokenRevocationUri = oauthTokenRevocationUri;
this._oauthAuthorizationUri = oauthAuthorizationUri;
this._appOAuthRedirectUrl = appOAuthRedirectUrl;
this._userOAuthTokenUri = userOAuthTokenUri;
this._oauthTokenUri = oauthTokenUri;
this._oauthTokenVerificationUri = oauthTokenVerificationUri;
this._usersDataUri = usersDataUri;
this._appClientId = appClientId;
this._appClientSecret = appClientSecret;
this._applicationAccessTokenProvider = applicationAccessTokenProvider;
Expand Down Expand Up @@ -164,7 +185,7 @@ export default class UserHelper {

const serializedParams = this._twitchQuerystringSerializer(params);

const url = `${this._appOAuthAuthorizationUri}?${serializedParams}`;
const url = `${this._oauthAuthorizationUri}?${serializedParams}`;

return url;
});
Expand Down Expand Up @@ -296,7 +317,7 @@ export default class UserHelper {
const serializedData = this._twitchQuerystringSerializer(data);

// TODO: use an https class.
return Promise.resolve(axios.post(this._userOAuthTokenUri, serializedData))
return Promise.resolve(axios.post(this._oauthTokenUri, serializedData))
// NOTE: axios response data.
.get("data");
})
Expand Down Expand Up @@ -336,10 +357,11 @@ export default class UserHelper {
});
}

storeUserToken(username, token) {
_writeUserToken(username, token) {
assert.strictEqual(arguments.length, 2);
assert.strictEqual(typeof username, "string");
assert(username.length > 0);
// NOTE: token can be null, to "forget" it.
assert.strictEqual(typeof token, "object");

return Promise.resolve()
Expand All @@ -360,11 +382,37 @@ export default class UserHelper {
}
));
})
.tap((userAfterStoring) => {
this._logger.trace(userAfterStoring, "_writeUserToken");
});
}

storeUserToken(username, token) {
assert.strictEqual(arguments.length, 2);
assert.strictEqual(typeof username, "string");
assert(username.length > 0);
assert(token !== null);
assert.strictEqual(typeof token, "object");

return Promise.resolve()
.then(() => this._writeUserToken(username, token))
.tap((userAfterStoring) => {
this._logger.trace(userAfterStoring, "storeUserToken");
});
}

forgetUserToken(username) {
assert.strictEqual(arguments.length, 1);
assert.strictEqual(typeof username, "string");
assert(username.length > 0);

return Promise.resolve()
.then(() => this._writeUserToken(username, null))
.tap((userAfterStoring) => {
this._logger.trace(userAfterStoring, "forgetUserToken");
});
}

getUserToken(username) {
assert.strictEqual(arguments.length, 1);
assert.strictEqual(typeof username, "string");
Expand All @@ -384,4 +432,101 @@ export default class UserHelper {
this._logger.trace(token, "getUserToken");
});
}

_getTokenValidation(token) {
assert.strictEqual(arguments.length, 1);
assert.strictEqual(typeof token, "object");
assert.strictEqual(typeof token.access_token, "string");
assert(token.access_token.length > 0);

// TODO: move/refactor/reuse function for application access tokens?
// TODO: use as a way to get username/userid/scopes from a user access token.
// https://dev.twitch.tv/docs/v5#root-url
// const sampleResponse = {
// "token": {
// "authorization": {
// "created_at": "2016-12-14T15:51:16Z",
// "scopes": [
// "user_read",
// ],
// "updated_at": "2016-12-14T15:51:16Z",
// },
// "client_id": "uo6dggojyb8d6soh92zknwmi5ej1q2",
// "user_id": "44322889",
// "user_name": "dallas",
// "valid": true,
// },
// };

return Promise.try(() => {
const userAccessToken = token.access_token;

// TODO: use an https class.
return Promise.resolve(axios.get(
this._oauthTokenVerificationUri,
{
headers: {
Accept: "application/vnd.twitchtv.v5+json",
"Client-ID": this._appClientId,
Authorization: `OAuth ${userAccessToken}`,
},
}
))
// NOTE: axios response data.
.get("data")
// NOTE: twitch response data.
.get("token")
.tap((validatedToken) => {
this._logger.trace(token, validatedToken, "_getTokenValidation");
});
});
}

revokeToken(token) {
assert.strictEqual(arguments.length, 1);
assert.strictEqual(typeof token, "object");

// TODO: move/refactor/reuse function for application access tokens?
// TODO: use as a way to get username/userid/scopes from a user access token.
// https://dev.twitch.tv/docs/authentication#revoking-access-tokens

return Promise.try(() => {
const userAccessToken = token.access_token;

const params = {
client_id: this._appClientId,
token: userAccessToken,
};

// TODO: use an https class.
return Promise.resolve(axios.post(
this._oauthTokenRevocationUri,
{
paramsSerializer: this._twitchQuerystringSerializer,
params: params,
}
))
// NOTE: axios response data.
.get("data")
// NOTE: twitch response data.
.get("token")
.tap((validatedToken) => {
this._logger.trace(token, validatedToken, "revokeToken");
});
});
}

isTokenValid(token) {
assert.strictEqual(arguments.length, 1);
assert.strictEqual(typeof token, "object");

return Promise.try(() => {
return this._getTokenValidation(token)
// NOTE: twitch response data.
.get("valid")
.tap((valid) => {
this._logger.trace(token, valid, "isTokenValid");
});
});
}
}

0 comments on commit d585365

Please sign in to comment.