PR 4: Merge feature/refresh to master

- Partial refresh token support
 - Automatically refresh stale account tokens
This commit is contained in:
Kurt Berglund 2016-09-28 17:18:10 +00:00
Родитель 077a9c587a
Коммит f178ffbcce
7 изменённых файлов: 155 добавлений и 50 удалений

Просмотреть файл

@ -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));
}
})
@ -100,4 +116,78 @@ 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);
@ -122,13 +127,13 @@ passport.use(
callbackURL: '/auth/google/callback',
passReqToCallback: true
},
(req, accessToken, refreshToken, params, profile, done) => {
(req, accessToken, refreshToken, params, profile, done) => {
if (!req.user) {
console.log("Google / Not logged in");
completeAuthentication(
'google',
profile.id,
accessToken,
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) => {
if (!req.user) {
console.log("Microsoft / Not Logged In")
console.log(params);
if (!req.user) {
// 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,7 +169,8 @@ passport.use(
'microsoft',
sub,
accessToken,
refreshToken,
params.expires_in,
refreshToken,
{
displayName: body.displayName,
name: {
@ -177,9 +181,8 @@ passport.use(
done);
});
}
else {
console.log("Microsoft / Logged In");
connectAccount('microsoft', sub, accessToken, refreshToken, req.user.user.id, done);
else {
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,27 +25,30 @@ router.get('/', (req: express.Request, response: express.Response) => {
for (let account of user.accounts) {
if (account.provider === 'microsoft') {
var microsoftCalendarP = new Promise((resolve, reject) => {
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) => {
if (error) {
reject(error);
}
else {
var microsoftResults = body.value.map((item) => ({
summary: item.subject,
start: item.start.dateTime,
end: item.end.dateTime
}));
resolve({ provider: 'Microsoft', items: microsoftResults });
}
});
})
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': tokens.access }, json: true }, (error, response, body) => {
if (error) {
reject(error);
}
else {
var microsoftResults = body.value.map((item) => ({
summary: item.subject,
start: item.start.dateTime,
end: item.end.dateTime
}));
resolve({ provider: 'Microsoft', items: microsoftResults });
}
});
})
})
resultPromises.push(microsoftCalendarP);
}
else if (account.provider === 'google') {
var googleCalendarP = new Promise((resolve, reject) => {
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({
@ -72,18 +76,18 @@ router.get('/', (req: express.Request, response: express.Response) => {
summary: item.summary,
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);
Promise.all(resultPromises).then((results) => {
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/>
<h2>Data Sources</h2>
<ul class="list-unstyled">
<li><a href="/calendar">Calendar</a></li>
</ul>
{{#user}}
<h2>Data Sources</h2>
<ul class="list-unstyled">
<li><a href="/calendar">Calendar</a></li>
</ul>
{{/user}}
{{^user}}
{{<login_control}}{{/login_control}}
{{/user}}

Просмотреть файл

@ -10,7 +10,7 @@
<a href="{{details.disconnect}}" class="btn btn-danger">Disconnect</a>
{{/details.connected}}
{{^details.connected}}
<a href="{{details.connect}}" class="btn btn-primary">Connect</a>
<a href="{{details.connect}}" class="btn btn-primary">Connect</a>
{{/details.connected}}
</h3>
</li>