Skip to content

Commit

Permalink
request full profile; store profile and refresh token
Browse files Browse the repository at this point in the history
  • Loading branch information
groovecoder committed Dec 31, 2018
1 parent 335fd33 commit 86ad731
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 13 deletions.
10 changes: 10 additions & 0 deletions .travis.yml
@@ -1,14 +1,24 @@
language: node_js

dist: trusty

node_js:
- "node"
- "lts/*"

services:
- postgresql

addons:
postgresql: 9.6

env:
- NODE_ENV=tests

install:
- sudo pip install compare-locales
- npm install

before_script:
- compare-locales --validate l10n.toml .
- compare-locales l10n.toml . `ls locales`
Expand Down
18 changes: 13 additions & 5 deletions controllers/oauth.js
@@ -1,4 +1,5 @@
"use strict";
const { URL } = require("url");

const ClientOAuth2 = require("client-oauth2");
const crypto = require("crypto");
Expand All @@ -9,8 +10,12 @@ const DB = require("../db/DB");
const EmailUtils = require("../email-utils");
const { FluentError } = require("../locale-utils");
const HBSHelpers = require("../hbs-helpers");
const sha1 = require("../sha1-utils");
const HIBP = require("../hibp");
const mozlog = require("../log");
const sha1 = require("../sha1-utils");


const log = mozlog("controllers.oauth");

// This object exists instead of inlining the env vars to make it easy
// to abstract fetching API endpoints from the OAuth server (instead
Expand All @@ -27,16 +32,17 @@ const FxAOAuthClient = new ClientOAuth2({
accessTokenUri: FxAOAuthUtils.tokenUri,
authorizationUri: FxAOAuthUtils.authorizationUri,
redirectUri: AppConstants.SERVER_URL + "/oauth/confirmed",
scopes: ["profile:email"],
scopes: ["profile"],
});

function init(req, res, next, client = FxAOAuthClient) {
// Set a random state string in a cookie so that we can verify
// the user when they're redirected back to us after auth.
const state = crypto.randomBytes(40).toString("hex");
const uri = client.code.getUri({state});
req.session.state = state;
res.redirect(uri);
const url = new URL(client.code.getUri({state}));
url.searchParams.append("access_type", "offline");
res.redirect(url);
}


Expand All @@ -46,14 +52,16 @@ async function confirmed(req, res, next, client = FxAOAuthClient) {
}

const fxaUser = await client.code.getToken(req.originalUrl, { state: req.session.state });
log.debug("fxa-confirmed-fxaUser", fxaUser);
const data = await got(FxAOAuthUtils.profileUri,
{
headers: {
Authorization: `Bearer ${fxaUser.accessToken}`,
},
});
log.debug("fxa-confirmed-profile-data", data.body);
const email = JSON.parse(data.body).email;
await DB.addSubscriber(email);
await DB.addSubscriber(email, fxaUser.refreshToken, data.body);

const unsubscribeUrl = ""; // not totally sure yet how this gets handled long-term

Expand Down
52 changes: 46 additions & 6 deletions db/DB.js
Expand Up @@ -101,18 +101,40 @@ const DB = {
});
},

async addSubscriber(email) {
/**
* Add a subscriber:
* 1. Add a record to subscribers
* 2. Immediately call _verifySubscriber
* 3. For FxA subscriber, add refresh token and profile data
*
* @param {string} email to add
* @param {string} fxaRefreshToken from Firefox Account Oauth
* @param {string} fxaProfileData from Firefox Account
* @returns {object} subscriber knex object added to DB
*/
async addSubscriber(email, fxaRefreshToken=null, fxaProfileData=null) {
const emailHash = await this._addEmailHash(getSha1(email), email, true);
const verifiedSubscriber = await this._verifySubscriber(emailHash);
return verifiedSubscriber[0];
const verified = await this._verifySubscriber(emailHash);
const verifiedSubscriber = verified[0];
if (fxaRefreshToken || fxaProfileData) {
return this._updateFxAData(verifiedSubscriber, fxaRefreshToken, fxaProfileData);
}
return verifiedSubscriber;
},

/**
* When an email is verified, convert it into a subscriber:
* 1. Subscribe the hash to HIBP
* 2. Update our subscribers record to verified
* 3. (if opted in) Subscribe the email to Fx newsletter
*
* @param {object} emailHash knex object in DB
* @returns {object} verified subscriber knex object in DB
*/
async _verifySubscriber(emailHash) {
// Subscribe user to HIBP
// TODO: move this "up" into controllers/users ?
await HIBP.subscribeHash(emailHash.sha1);

// Update our subscriber record to verified
const verifiedSubscriber = await knex("subscribers")
.where("email", "=", emailHash.email)
.update({
Expand All @@ -121,7 +143,6 @@ const DB = {
})
.returning("*");

// If the user opted in, send newsletter subscription request to basket
// TODO: move this "up" into controllers/users ?
if (emailHash.fx_newsletter) {
Basket.subscribe(emailHash.email);
Expand All @@ -130,6 +151,25 @@ const DB = {
return verifiedSubscriber;
},

/**
* Update fxa_refresh_token and fxa_profile_json for subscriber
*
* @param {object} subscriber knex object in DB
* @param {string} fxaRefreshToken from Firefox Account Oauth
* @param {string} fxaProfileData from Firefox Account
* @returns {object} updated subscriber knex object in DB
*/
async _updateFxAData(subscriber, fxaRefreshToken, fxaProfileData) {
const updatedSubscriber = await knex("subscribers")
.where("id", "=", subscriber.id)
.update({
fxa_refresh_token: fxaRefreshToken,
fxa_profile_json: fxaProfileData,
})
.returning("*");
return updatedSubscriber;
},

async removeSubscriberByEmail(email) {
const sha1 = getSha1(email);
return await this._getSha1EntryAndDo(sha1, async aEntry => {
Expand Down
15 changes: 15 additions & 0 deletions db/migrations/20181227100332_add_fxa_columns.js
@@ -0,0 +1,15 @@
"use strict";

exports.up = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.string("fxa_refresh_token");
table.jsonb("fxa_profile_json");
});
};

exports.down = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.dropColumn("fxa_refresh_token");
table.dropColumn("fxa_profile_json");
});
};
5 changes: 3 additions & 2 deletions tests/controllers/oauth.test.js
Expand Up @@ -18,14 +18,15 @@ jest.mock("got");
const mockRequest = { fluentFormat: jest.fn() };


test("init request sets session cookie and redirects", () => {
test("init request sets session cookie and redirects with access_type=offline", () => {
mockRequest.session = { };
const mockResponse = { redirect: jest.fn() };

init(mockRequest, mockResponse);

const mockRedirectCallArgs = mockResponse.redirect.mock.calls[0];
expect(mockRedirectCallArgs[0]).toMatch(AppConstants.OAUTH_AUTHORIZATION_URI);
expect(mockRedirectCallArgs[0].href).toMatch(AppConstants.OAUTH_AUTHORIZATION_URI);
expect(mockRedirectCallArgs[0].href).toMatch("access_type=offline");
});


Expand Down

0 comments on commit 86ad731

Please sign in to comment.