Skip to content

Commit

Permalink
Refactor Alexa with real express routes
Browse files Browse the repository at this point in the history
  • Loading branch information
krwenholz committed Mar 26, 2020
1 parent 70c2ce3 commit 40a7839
Show file tree
Hide file tree
Showing 22 changed files with 137 additions and 64 deletions.
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -47,6 +47,7 @@
"morgan": "1.9.1",
"oauth2orize": "1.11.0",
"passport": "0.4.0",
"passport-anonymous": "1.0.1",
"passport-http": "0.3.0",
"passport-http-bearer": "1.0.1",
"passport-local": "1.0.0",
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 3 additions & 1 deletion src/routes/api/alexa/index.js → src/alexa/index.js
Expand Up @@ -87,7 +87,7 @@ const skill = Alexa.SkillBuilders.custom()
.addErrorHandlers(ErrorHandler)
.create();

export const post = (req, res, next) => {
const post = (req, res, next) => {
return new SkillRequestSignatureVerifier()
.verify(req.rawBody, req.headers)
.then(() => {
Expand All @@ -104,3 +104,5 @@ export const post = (req, res, next) => {
res.status(500);
});
};

export default post;
Expand Up @@ -7,7 +7,7 @@ import {
storyDecisionChoices,
updateStoryDecisionChoicesDirective,
findConfirmedSlotValue
} from "src/routes/api/alexa/_utilities";
} from "src/alexa/_utilities";
import * as Stories from "src/routes/story/_stories";
import * as History from "src/components/Adventure/history";

Expand All @@ -30,6 +30,7 @@ const DecisionGivenChooseStoryDecisionIntentHandler = {

const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
const storyId = sessionAttributes.storyId;
// TODO(kyle): record visitation
sessionAttributes.store = History.goToDecision(
decision,
sessionAttributes.store
Expand Down
Expand Up @@ -5,7 +5,7 @@ import {
asSpeakableStoryText,
storyDecisionChoices,
updateStoryDecisionChoicesDirective
} from "src/routes/api/alexa/_utilities";
} from "src/alexa/_utilities";

const GoBackIntentHandler = {
canHandle(handlerInput) {
Expand Down
@@ -1,4 +1,4 @@
import { speechWithSubscriptionPrompt } from "src/routes/api/alexa/_utilities";
import { speechWithSubscriptionPrompt } from "src/alexa/_utilities";

const HelpIntentHandler = {
canHandle(handlerInput) {
Expand Down
@@ -1,7 +1,5 @@
import * as Stories from "src/routes/story/_stories";
import {
startFreshStory
} from "src/routes/api/alexa/_utilities";
import { startFreshStory } from "src/alexa/_utilities";

const RestartStoryIntentHandler = {
name: "RestartStoryIntentHandler",
Expand Down
@@ -1,4 +1,4 @@
import { listStoriesForAlexa } from "src/routes/api/alexa/_utilities";
import { listStoriesForAlexa } from "src/alexa/_utilities";
import { logger } from "src/logging";

const StartedInProgressChooseStoryIntentHandler = {
Expand Down
@@ -1,7 +1,4 @@
import {
startFreshStory,
findConfirmedSlotValue
} from "src/routes/api/alexa/_utilities";
import { startFreshStory, findConfirmedSlotValue } from "src/alexa/_utilities";
import * as Stories from "src/routes/story/_stories.js";

const StoryTitleChoiceGivenChooseStoryIntentHandler = {
Expand Down
File renamed without changes.
51 changes: 33 additions & 18 deletions src/authentication/index.js
Expand Up @@ -2,6 +2,7 @@ import config from "config";
import passport from "passport";
import passportLocal from "passport-local";
import securePassword from "secure-password";
import { Strategy as AnonymousStrategy } from "passport-anonymous";
import { BasicStrategy } from "passport-http";
import { ensureLoggedIn } from "connect-ensure-login";
import { findUser, findUserSafeDetails } from "src/lib/server/users";
Expand Down Expand Up @@ -45,6 +46,9 @@ const compare = (userPassword, hash) => {
});
};

// TODO(kyle): Should be able to configure actual routes that don't need auth or
// such using a real express setup in server.js, now that we know how Sapper works.
// Kind of. Probably need one "optional" auth middleware wrapping usual passport.authenticate
const noAuthRoutes = [
"/",
"/about",
Expand Down Expand Up @@ -73,7 +77,6 @@ const noAuthPrefixes = [
"/?",
"/api/user/login",
"/api/user/new",
"/api/alexa",
"/client",
"/error",
"/experimental",
Expand All @@ -82,31 +85,41 @@ const noAuthPrefixes = [
"/story"
];

const requiresAuth = req => {
const routeRequiresAuth = req => {
return (
noAuthRoutes.includes(req.path) ||
noAuthPrefixes.some(prefix => req.path.startsWith(prefix))
!noAuthRoutes.includes(req.path) &&
!noAuthPrefixes.some(prefix => req.path.startsWith(prefix))
);
};

/**
* Protects our routes based on some rules.
* You get a redirect for bad HTML requests and a 401 for API-esque JS or JSON requests.
*/
const protectNonDefaultRoutes = (req, res, next) => {
if (req.isAuthenticated()) next();
else if (requiresAuth(req)) next();
else {
logger.info({ url: req.url }, "Unauthenticated access found on url");

if (req.path.endsWith(".js") || req.path.endsWith(".json")) {
res.writeHead(401);
} else {
return ensureLoggedIn("/user/login")(req, res, next);
const authenticateNonDefaultRoutes = (req, res, next) => {
passport.authenticate("local", (err, user, info) => {
if (user) {
return req.logIn(user, function(err) {
if (err) {
return next(err);
}
next();
});
}
res.end();
return;
}

if (routeRequiresAuth(req)) {
if (req.path.endsWith(".js") || req.path.endsWith(".json")) {
res.writeHead(401);
} else {
return ensureLoggedIn("/user/login")(req, res, next);
}
res.end();
return;
}

// No user and no auth required
next();
})(req, res, next);
};

/**
Expand Down Expand Up @@ -162,6 +175,8 @@ export const initPassport = () => {

passport.use(tokenStrategy);

passport.use(new AnonymousStrategy());

// We only do this for Alexa OAUTH.
passport.use(
new BasicStrategy(function(userId, secret, done) {
Expand All @@ -175,7 +190,7 @@ export const initPassport = () => {
})
);

passport.authenticationMiddleware = () => protectNonDefaultRoutes;
passport.authenticateNonDefaultRoutes = () => authenticateNonDefaultRoutes;
};

export { passport };
32 changes: 19 additions & 13 deletions src/authentication/oauth.js
Expand Up @@ -5,10 +5,11 @@ import securePassword from "secure-password";
import uuidv4 from "uuid/v4";
import { passport } from "src/authentication";
import { findUser, findUserSafeDetails } from "src/lib/server/users";
import { logger } from "src/logging";
import { logger, logResponse } from "src/logging";
import { pool } from "src/lib/server/database.js";
import { join, times } from "lodash";
import { join, times, includes } from "lodash";

// TODO(kyle): create a route for users to delete tokens
const tokenHasher = securePassword();

export const server = oauth2orize.createServer();
Expand All @@ -17,14 +18,18 @@ const findAuthorizationCode = async (code, done) => {
try {
const results = await pool.query(
`
DELETE *
FROM oauth_authorization_codes
WHERE created < CURRENT_DATE - INTERVAL '30 minutes';
SELECT
client_id AS clientId,
redirect_uri AS redirectUri,
user_id AS userId
FROM
oauth_authorization_codes
WHERE
code = $1
code = $1;
`,
[code]
);
Expand Down Expand Up @@ -67,6 +72,10 @@ const findAccessToken = async (token, done) => {
try {
const results = await pool.query(
`
DELETE *
FROM oauth_access_tokens
WHERE created < CURRENT_DATE - INTERVAL '1 year';
SELECT
client_id AS clientId,
user_id AS userId
Expand Down Expand Up @@ -123,19 +132,15 @@ server.grant(
);

server.serializeClient((client, done) => {
logger.info(client, "Serializing oauth client");
done(null, client.clientId);
});

server.deserializeClient(async (id, done) => {
try {
const details = await findUserSafeDetails(id);

if (!details) return done(null, false);

return done(null, details);
} catch (error) {
return done(error);
}
server.deserializeClient((clientId, done) => {
logger.info({ clientId }, "Deserializing oauth client");
// We only have one client right now, so this is pretty dumb
if (clientId !== config.get("alexa.clientId")) return done(null, false);
return done(null, { clientId, isTrusted: true });
});

server.exchange(
Expand Down Expand Up @@ -221,6 +226,7 @@ const findAccessTokenByUserIdAndClientId = async (userId, clientId, done) => {
};

export const authorize = [
logResponse,
server.authorize(
(clientId, redirectUri, done) => {
if (
Expand Down
14 changes: 7 additions & 7 deletions src/lib/utilities.js
@@ -1,11 +1,11 @@
import { take, takeRight, set, clone, omit } from 'lodash';
import { take, takeRight, set, clone, omit } from "lodash";

export const dropIdx = (coll, idx) => ([
export const dropIdx = (coll, idx) => [
...take(coll, idx),
...takeRight(coll, (coll.length - idx - 1)),
]);
...takeRight(coll, coll.length - idx - 1)
];

export const renameKey = (object, key, newKey) => ({
...omit(object, [key]),
[newKey]: object[key]
})
...omit(object, [key]),
[newKey]: object[key]
});
36 changes: 36 additions & 0 deletions src/logging.js
Expand Up @@ -5,3 +5,39 @@ import morgan from "morgan";
export const logger = bunyan.createLogger({ name: "browser" });

export const requestLoggingMiddleware = morgan(config.get("logging.format"));

export const logResponse = (req, res, next) => {
const oldWrite = res.write;
const oldEnd = res.end;

const chunks = [];

res.write = (...restArgs) => {
chunks.push(Buffer.from(restArgs[0]));
oldWrite.apply(res, restArgs);
};

res.end = (...restArgs) => {
if (restArgs[0]) {
chunks.push(Buffer.from(restArgs[0]));
}
const body = Buffer.concat(chunks).toString("utf8");

logger.info({
time: new Date().toUTCString(),
fromIP: req.headers["x-forwarded-for"] || req.connection.remoteAddress,
method: req.method,
originalUri: req.originalUrl,
uri: req.url,
headers: res.getHeaders(),
requestData: req.body,
responseData: body,
referer: req.headers.referer || ""
});

// console.log(body);
oldEnd.apply(res, restArgs);
};

next();
};
5 changes: 4 additions & 1 deletion src/routes/api/user/login.js
Expand Up @@ -15,8 +15,11 @@ const post = (req, res, next) => {
if (err) {
return next(err);
}
logger.info(
{ returnTo: req.session.returnTo, userId: user.id },
"User logged in succesfully"
);
const redirect = req.session.returnTo || "/";
logger.info({ redirect, userId: user.id }, "User logged in succesfully");
return findUserSafeDetails(user.id).then(user => res.redirect(redirect));
});
})(req, res, next);
Expand Down
2 changes: 1 addition & 1 deletion src/routes/oauth/authorize/dialog.svelte
Expand Up @@ -49,7 +49,7 @@
type="hidden"
value="{$page.query.transactionId}"
/>
<input name="deny" id="deny" type="hidden" />
<input name="cancel" id="cancel" type="hidden" />
<Button type="submit">Deny</Button>
</form>
</nav>
Expand Down

0 comments on commit 40a7839

Please sign in to comment.