PR 4: Merge feature/refresh to master
- Partial refresh token support - Automatically refresh stale account tokens
This commit is contained in:
Родитель
077a9c587a
Коммит
f178ffbcce
|
@ -2,6 +2,10 @@ import * as accounts from './db/accounts';
|
|||
import * as users from './db/users';
|
||||
import { Promise } from 'es6-promise';
|
||||
import * as _ from 'lodash';
|
||||
import * as moment from 'moment';
|
||||
import * as request from 'request';
|
||||
import * as nconf from 'nconf';
|
||||
var google = require('googleapis');
|
||||
|
||||
// re-export the database user details
|
||||
export import IUserDetails = users.IUserDetails;
|
||||
|
@ -14,6 +18,15 @@ export interface IUser {
|
|||
accounts: accounts.IAccount[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper structure to store access tokens
|
||||
*/
|
||||
export interface ITokens {
|
||||
access: string;
|
||||
expiration: string;
|
||||
refresh: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates a new user
|
||||
*/
|
||||
|
@ -21,6 +34,7 @@ export function createOrGetUser(
|
|||
provider: string,
|
||||
providerId: string,
|
||||
accessToken: string,
|
||||
expiration: string,
|
||||
refreshToken: string,
|
||||
details: IUserDetails): Promise<any> {
|
||||
|
||||
|
@ -31,13 +45,14 @@ export function createOrGetUser(
|
|||
// Create a user first and then link this account to it
|
||||
let newUserP = users.putUser(details);
|
||||
userIdP = newUserP.then((newUser) => {
|
||||
return accounts.linkAccount(provider, providerId, accessToken, refreshToken, newUser.id).then((account) => newUser.id);
|
||||
return accounts.linkAccount(provider, providerId, accessToken, expiration, refreshToken, newUser.id).then((account) => newUser.id);
|
||||
})
|
||||
}
|
||||
else {
|
||||
// Get the user but also go and update the refresh and access token at this point
|
||||
account.accessToken = accessToken;
|
||||
account.refreshToken = refreshToken;
|
||||
account.expiration = expiration;
|
||||
|
||||
var updateAccountP = accounts.updateAccount(account);
|
||||
|
||||
|
@ -58,6 +73,7 @@ export function linkAccount(
|
|||
provider: string,
|
||||
providerId: string,
|
||||
accessToken: string,
|
||||
expiration: string,
|
||||
refreshToken: string,
|
||||
userId: string): Promise<any> {
|
||||
|
||||
|
@ -68,7 +84,7 @@ export function linkAccount(
|
|||
throw { msg: "Account already linked" };
|
||||
}
|
||||
else {
|
||||
var linkP = accounts.linkAccount(provider, providerId, accessToken, refreshToken, userId);
|
||||
var linkP = accounts.linkAccount(provider, providerId, accessToken, expiration, refreshToken, userId);
|
||||
return linkP.then(() => getUser(userId));
|
||||
}
|
||||
})
|
||||
|
@ -101,3 +117,77 @@ export function getUser(userId: string): Promise<IUser> {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getTokenExpiration(expires: number): string {
|
||||
var expiration = moment().add(expires, "seconds");
|
||||
return expiration.utc().toISOString();
|
||||
}
|
||||
|
||||
function refreshTokens(account: accounts.IAccount): Promise<ITokens> {
|
||||
var udpatedAccountP = new Promise((resolve, reject) => {
|
||||
// TODO should consolidate the account specific behavior behind an interface
|
||||
if (account.provider === "microsoft") {
|
||||
var microsoftConfiguration = nconf.get("login:microsoft");
|
||||
request.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', { json: true, form: {
|
||||
grant_type: 'refresh_token',
|
||||
client_id: microsoftConfiguration.clientId,
|
||||
client_secret: microsoftConfiguration.secret,
|
||||
refresh_token: account.refreshToken
|
||||
}}, (error, response, body) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
else {
|
||||
console.log(body);
|
||||
account.accessToken = body.access_token;
|
||||
account.expiration = getTokenExpiration(body.expires_in);
|
||||
resolve(account);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (account.provider === 'google') {
|
||||
var googleConfig = nconf.get("login:google");
|
||||
var oauth2Client = new google.auth.OAuth2(googleConfig.clientId, googleConfig.secret, '/auth/google');;
|
||||
|
||||
// Retrieve tokens via token exchange explained above or set them:
|
||||
oauth2Client.setCredentials({ access_token: account.accessToken, refresh_token: account.refreshToken });
|
||||
oauth2Client.refreshAccessToken((error, tokens) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
else {
|
||||
account.accessToken = tokens.access_token;
|
||||
account.expiration = moment(tokens.expiry_date).utc().toISOString();
|
||||
account.refreshToken = tokens.refresh_token;
|
||||
resolve(account);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
throw { error: "Unknown Provider" };
|
||||
}
|
||||
});
|
||||
|
||||
// Get the updated account information and then use it to update the DB and then return the tokens back
|
||||
return udpatedAccountP.then((updatedAccount: accounts.IAccount) => {
|
||||
return accounts.updateAccount(updatedAccount).then(() => {
|
||||
return { access: updatedAccount.accessToken, expiration: updatedAccount.expiration, refresh: updatedAccount.refreshToken }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the access tokens for the given account
|
||||
*/
|
||||
export function getTokens(account: accounts.IAccount): Promise<ITokens> {
|
||||
var now = moment();
|
||||
var expiration = moment(account.expiration);
|
||||
|
||||
var diff = expiration.diff(now);
|
||||
if (now.isAfter(expiration)) {
|
||||
return refreshTokens(account);
|
||||
}
|
||||
else {
|
||||
return Promise.resolve({ access: account.accessToken, expiration: account.expiration, refresh: account.refreshToken });
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ import * as calendarRoute from './routes/calendar';
|
|||
|
||||
import * as passport from 'passport';
|
||||
import * as connectRedis from 'connect-redis';
|
||||
import * as moment from 'moment';
|
||||
import * as nconf from 'nconf';
|
||||
import * as redis from 'redis';
|
||||
import * as request from 'request';
|
||||
|
@ -81,11 +82,13 @@ function completeAuthentication(
|
|||
provider: string,
|
||||
providerId: string,
|
||||
accessToken: string,
|
||||
expires: number,
|
||||
refreshToken: string,
|
||||
details: accounts.IUserDetails,
|
||||
done: (error: any, user?: any) => void) {
|
||||
|
||||
var userP = accounts.createOrGetUser(provider, providerId, accessToken, refreshToken, details);
|
||||
let expiration = accounts.getTokenExpiration(expires);
|
||||
var userP = accounts.createOrGetUser(provider, providerId, accessToken, expiration, refreshToken, details);
|
||||
userP.then(
|
||||
(user) => {
|
||||
done(null, user);
|
||||
|
@ -99,11 +102,13 @@ function connectAccount(
|
|||
provider: string,
|
||||
providerId: string,
|
||||
accessToken: string,
|
||||
expires: number,
|
||||
refreshToken: string,
|
||||
userId: string,
|
||||
done: (error: any, user?: any) => void) {
|
||||
|
||||
let linkP = accounts.linkAccount(provider, providerId, accessToken, refreshToken, userId);
|
||||
let expiration = accounts.getTokenExpiration(expires);
|
||||
let linkP = accounts.linkAccount(provider, providerId, accessToken, expiration, refreshToken, userId);
|
||||
linkP.then(
|
||||
(user) => {
|
||||
done(null, user);
|
||||
|
@ -124,11 +129,11 @@ passport.use(
|
|||
},
|
||||
(req, accessToken, refreshToken, params, profile, done) => {
|
||||
if (!req.user) {
|
||||
console.log("Google / Not logged in");
|
||||
completeAuthentication(
|
||||
'google',
|
||||
profile.id,
|
||||
accessToken,
|
||||
params.expires_in,
|
||||
refreshToken,
|
||||
{
|
||||
displayName: profile.displayName,
|
||||
|
@ -137,8 +142,7 @@ passport.use(
|
|||
done);
|
||||
}
|
||||
else {
|
||||
console.log("Google / Logged in");
|
||||
connectAccount('google', profile.id, accessToken, refreshToken, req.user.user.id, done);
|
||||
connectAccount('google', profile.id, accessToken, params.expires_in,refreshToken, req.user.user.id, done);
|
||||
}
|
||||
}));
|
||||
|
||||
|
@ -154,9 +158,8 @@ passport.use(
|
|||
passReqToCallback: true
|
||||
},
|
||||
(req, iss, sub, profile, jwtClaims, accessToken, refreshToken, params, done) => {
|
||||
console.log(params);
|
||||
if (!req.user) {
|
||||
console.log("Microsoft / Not Logged In")
|
||||
|
||||
// use request to load in the user profile
|
||||
request.get('https://graph.microsoft.com/v1.0/me', { auth: { 'bearer': accessToken }, json: true }, (error, response, body) => {
|
||||
console.log('User profile information');
|
||||
|
@ -166,6 +169,7 @@ passport.use(
|
|||
'microsoft',
|
||||
sub,
|
||||
accessToken,
|
||||
params.expires_in,
|
||||
refreshToken,
|
||||
{
|
||||
displayName: body.displayName,
|
||||
|
@ -178,8 +182,7 @@ passport.use(
|
|||
});
|
||||
}
|
||||
else {
|
||||
console.log("Microsoft / Logged In");
|
||||
connectAccount('microsoft', sub, accessToken, refreshToken, req.user.user.id, done);
|
||||
connectAccount('microsoft', sub, accessToken, params.expires_in, refreshToken, req.user.user.id, done);
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
|
@ -18,6 +18,9 @@ export interface IAccount {
|
|||
// Access information for the account
|
||||
accessToken: string,
|
||||
|
||||
// Access token expiration time
|
||||
expiration: string,
|
||||
|
||||
// Used to refresh access to the account
|
||||
refreshToken: string,
|
||||
|
||||
|
@ -42,6 +45,7 @@ export function linkAccount(
|
|||
provider: string,
|
||||
providerId: string,
|
||||
accessToken: string,
|
||||
expiration: string,
|
||||
refreshToken: string,
|
||||
userId: string) {
|
||||
|
||||
|
@ -50,6 +54,7 @@ export function linkAccount(
|
|||
provider: provider,
|
||||
providerId: providerId,
|
||||
accessToken: accessToken,
|
||||
expiration: expiration,
|
||||
refreshToken: refreshToken,
|
||||
userId: userId
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ export let microsoft = {
|
|||
'offline_access',
|
||||
'profile',
|
||||
'email',
|
||||
'User.Read',
|
||||
'User.ReadBasic.All',
|
||||
'Calendars.ReadWrite',
|
||||
'Contacts.Read'
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as express from 'express';
|
||||
import * as request from 'request';
|
||||
import * as moment from 'moment';
|
||||
import * as accounts from '../accounts';
|
||||
import { Promise } from 'es6-promise';
|
||||
import { IUser } from '../accounts';
|
||||
import * as nconf from 'nconf';
|
||||
|
@ -24,10 +25,11 @@ router.get('/', (req: express.Request, response: express.Response) => {
|
|||
for (let account of user.accounts) {
|
||||
if (account.provider === 'microsoft') {
|
||||
var microsoftCalendarP = new Promise((resolve, reject) => {
|
||||
return accounts.getTokens(account).then((tokens) => {
|
||||
let url = `https://graph.microsoft.com/v1.0/me/calendar/calendarView?StartDateTime=${now.toISOString()}&endDateTime=${nextWeek.toISOString()}`;
|
||||
request.get(
|
||||
url,
|
||||
{ auth: { 'bearer': account.accessToken }, json: true }, (error, response, body) => {
|
||||
{ auth: { 'bearer': tokens.access }, json: true }, (error, response, body) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
|
@ -41,10 +43,12 @@ router.get('/', (req: express.Request, response: express.Response) => {
|
|||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
resultPromises.push(microsoftCalendarP);
|
||||
}
|
||||
else if (account.provider === 'google') {
|
||||
var googleCalendarP = new Promise((resolve, reject) => {
|
||||
return accounts.getTokens(account).then((tokens) => {
|
||||
let calendar = google.calendar('v3');
|
||||
var OAuth2 = google.auth.OAuth2;
|
||||
var googleConfig = nconf.get("login:google");
|
||||
|
@ -52,8 +56,8 @@ router.get('/', (req: express.Request, response: express.Response) => {
|
|||
|
||||
// Retrieve tokens via token exchange explained above or set them:
|
||||
oauth2Client.setCredentials({
|
||||
access_token: account.accessToken,
|
||||
refresh_token: account.refreshToken
|
||||
access_token: tokens.access,
|
||||
refresh_token: tokens.refresh
|
||||
});
|
||||
|
||||
calendar.events.list({
|
||||
|
@ -73,17 +77,17 @@ router.get('/', (req: express.Request, response: express.Response) => {
|
|||
start: item.start.dateTime,
|
||||
end: item.end.dateTime
|
||||
}));
|
||||
console.log(JSON.stringify(googleCalendarItems, null, 2));
|
||||
resolve({ provider: 'Google', items: googleCalendarItems });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
resultPromises.push(googleCalendarP);
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all(resultPromises).then((results) => {
|
||||
console.log(results.length);
|
||||
response.render(
|
||||
'calendar',
|
||||
{
|
||||
|
@ -91,6 +95,8 @@ router.get('/', (req: express.Request, response: express.Response) => {
|
|||
partials: defaultPartials,
|
||||
viewModel: results
|
||||
});
|
||||
}, (error) => {
|
||||
response.status(400).json(error);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
<h1>{{ title }}</h1>
|
||||
<p>Welcome to {{ title }}</p>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
{{#user}}
|
||||
<h2>Data Sources</h2>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="/calendar">Calendar</a></li>
|
||||
</ul>
|
||||
|
||||
{{/user}}
|
||||
{{^user}}
|
||||
{{<login_control}}{{/login_control}}
|
||||
{{/user}}
|
||||
|
|
Загрузка…
Ссылка в новой задаче