diff --git a/package-lock.json b/package-lock.json
index 02f7b925..12d7470c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8254,6 +8254,14 @@
"pause": "0.0.1"
}
},
+ "passport-anonymous": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/passport-anonymous/-/passport-anonymous-1.0.1.tgz",
+ "integrity": "sha1-JB43J07ETft/bK0jS0HEODhrwRc=",
+ "requires": {
+ "passport-strategy": "1.x.x"
+ }
+ },
"passport-http": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/passport-http/-/passport-http-0.3.0.tgz",
diff --git a/package.json b/package.json
index de7bbf7a..008b67c4 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/routes/api/alexa/_error_handler.js b/src/alexa/_error_handler.js
similarity index 100%
rename from src/routes/api/alexa/_error_handler.js
rename to src/alexa/_error_handler.js
diff --git a/src/routes/api/alexa/_skill_definition.json b/src/alexa/_skill_definition.json
similarity index 100%
rename from src/routes/api/alexa/_skill_definition.json
rename to src/alexa/_skill_definition.json
diff --git a/src/routes/api/alexa/_utilities.js b/src/alexa/_utilities.js
similarity index 100%
rename from src/routes/api/alexa/_utilities.js
rename to src/alexa/_utilities.js
diff --git a/src/routes/api/alexa/index.js b/src/alexa/index.js
similarity index 98%
rename from src/routes/api/alexa/index.js
rename to src/alexa/index.js
index eac0315e..32867087 100644
--- a/src/routes/api/alexa/index.js
+++ b/src/alexa/index.js
@@ -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(() => {
@@ -104,3 +104,5 @@ export const post = (req, res, next) => {
res.status(500);
});
};
+
+export default post;
diff --git a/src/routes/api/alexa/intent_handlers/_cancel_and_stop.js b/src/alexa/intent_handlers/_cancel_and_stop.js
similarity index 100%
rename from src/routes/api/alexa/intent_handlers/_cancel_and_stop.js
rename to src/alexa/intent_handlers/_cancel_and_stop.js
diff --git a/src/routes/api/alexa/intent_handlers/_decision_given_choose_story_decision.js b/src/alexa/intent_handlers/_decision_given_choose_story_decision.js
similarity index 96%
rename from src/routes/api/alexa/intent_handlers/_decision_given_choose_story_decision.js
rename to src/alexa/intent_handlers/_decision_given_choose_story_decision.js
index 3948eab4..7fcbafdb 100644
--- a/src/routes/api/alexa/intent_handlers/_decision_given_choose_story_decision.js
+++ b/src/alexa/intent_handlers/_decision_given_choose_story_decision.js
@@ -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";
@@ -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
diff --git a/src/routes/api/alexa/intent_handlers/_go_back.js b/src/alexa/intent_handlers/_go_back.js
similarity index 97%
rename from src/routes/api/alexa/intent_handlers/_go_back.js
rename to src/alexa/intent_handlers/_go_back.js
index 014d1c6c..91429605 100644
--- a/src/routes/api/alexa/intent_handlers/_go_back.js
+++ b/src/alexa/intent_handlers/_go_back.js
@@ -5,7 +5,7 @@ import {
asSpeakableStoryText,
storyDecisionChoices,
updateStoryDecisionChoicesDirective
-} from "src/routes/api/alexa/_utilities";
+} from "src/alexa/_utilities";
const GoBackIntentHandler = {
canHandle(handlerInput) {
diff --git a/src/routes/api/alexa/intent_handlers/_help.js b/src/alexa/intent_handlers/_help.js
similarity index 92%
rename from src/routes/api/alexa/intent_handlers/_help.js
rename to src/alexa/intent_handlers/_help.js
index a8b770ee..0330d165 100644
--- a/src/routes/api/alexa/intent_handlers/_help.js
+++ b/src/alexa/intent_handlers/_help.js
@@ -1,4 +1,4 @@
-import { speechWithSubscriptionPrompt } from "src/routes/api/alexa/_utilities";
+import { speechWithSubscriptionPrompt } from "src/alexa/_utilities";
const HelpIntentHandler = {
canHandle(handlerInput) {
diff --git a/src/routes/api/alexa/intent_handlers/_restart_story.js b/src/alexa/intent_handlers/_restart_story.js
similarity index 89%
rename from src/routes/api/alexa/intent_handlers/_restart_story.js
rename to src/alexa/intent_handlers/_restart_story.js
index 03e92b3f..bbf605a7 100644
--- a/src/routes/api/alexa/intent_handlers/_restart_story.js
+++ b/src/alexa/intent_handlers/_restart_story.js
@@ -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",
diff --git a/src/routes/api/alexa/intent_handlers/_started_in_progress_choose_story.js b/src/alexa/intent_handlers/_started_in_progress_choose_story.js
similarity index 87%
rename from src/routes/api/alexa/intent_handlers/_started_in_progress_choose_story.js
rename to src/alexa/intent_handlers/_started_in_progress_choose_story.js
index 63f8feea..b0c1803e 100644
--- a/src/routes/api/alexa/intent_handlers/_started_in_progress_choose_story.js
+++ b/src/alexa/intent_handlers/_started_in_progress_choose_story.js
@@ -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 = {
diff --git a/src/routes/api/alexa/intent_handlers/_story_title_choice_given_choose_story.js b/src/alexa/intent_handlers/_story_title_choice_given_choose_story.js
similarity index 88%
rename from src/routes/api/alexa/intent_handlers/_story_title_choice_given_choose_story.js
rename to src/alexa/intent_handlers/_story_title_choice_given_choose_story.js
index 1c27f328..f90b1591 100644
--- a/src/routes/api/alexa/intent_handlers/_story_title_choice_given_choose_story.js
+++ b/src/alexa/intent_handlers/_story_title_choice_given_choose_story.js
@@ -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 = {
diff --git a/src/routes/api/alexa/request_handlers/_launch.js b/src/alexa/request_handlers/_launch.js
similarity index 100%
rename from src/routes/api/alexa/request_handlers/_launch.js
rename to src/alexa/request_handlers/_launch.js
diff --git a/src/routes/api/alexa/request_handlers/_session_ended.js b/src/alexa/request_handlers/_session_ended.js
similarity index 100%
rename from src/routes/api/alexa/request_handlers/_session_ended.js
rename to src/alexa/request_handlers/_session_ended.js
diff --git a/src/authentication/index.js b/src/authentication/index.js
index 6259f76e..01e6e46b 100644
--- a/src/authentication/index.js
+++ b/src/authentication/index.js
@@ -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";
@@ -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",
@@ -73,7 +77,6 @@ const noAuthPrefixes = [
"/?",
"/api/user/login",
"/api/user/new",
- "/api/alexa",
"/client",
"/error",
"/experimental",
@@ -82,10 +85,10 @@ 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))
);
};
@@ -93,20 +96,30 @@ const requiresAuth = req => {
* 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);
};
/**
@@ -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) {
@@ -175,7 +190,7 @@ export const initPassport = () => {
})
);
- passport.authenticationMiddleware = () => protectNonDefaultRoutes;
+ passport.authenticateNonDefaultRoutes = () => authenticateNonDefaultRoutes;
};
export { passport };
diff --git a/src/authentication/oauth.js b/src/authentication/oauth.js
index 7a755f57..c1f59226 100644
--- a/src/authentication/oauth.js
+++ b/src/authentication/oauth.js
@@ -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();
@@ -17,6 +18,10 @@ 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,
@@ -24,7 +29,7 @@ const findAuthorizationCode = async (code, done) => {
FROM
oauth_authorization_codes
WHERE
- code = $1
+ code = $1;
`,
[code]
);
@@ -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
@@ -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(
@@ -221,6 +226,7 @@ const findAccessTokenByUserIdAndClientId = async (userId, clientId, done) => {
};
export const authorize = [
+ logResponse,
server.authorize(
(clientId, redirectUri, done) => {
if (
diff --git a/src/lib/utilities.js b/src/lib/utilities.js
index 7c2608ca..535b62c9 100644
--- a/src/lib/utilities.js
+++ b/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]
+});
diff --git a/src/logging.js b/src/logging.js
index 58eb5d99..5f1a8119 100644
--- a/src/logging.js
+++ b/src/logging.js
@@ -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();
+};
diff --git a/src/routes/api/user/login.js b/src/routes/api/user/login.js
index 4c265ee2..5c5c214c 100644
--- a/src/routes/api/user/login.js
+++ b/src/routes/api/user/login.js
@@ -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);
diff --git a/src/routes/oauth/authorize/dialog.svelte b/src/routes/oauth/authorize/dialog.svelte
index c65e4912..36a7016c 100644
--- a/src/routes/oauth/authorize/dialog.svelte
+++ b/src/routes/oauth/authorize/dialog.svelte
@@ -49,7 +49,7 @@
type="hidden"
value="{$page.query.transactionId}"
/>
-
+
diff --git a/src/server.js b/src/server.js
index 3fccb73c..49e9bab2 100644
--- a/src/server.js
+++ b/src/server.js
@@ -1,12 +1,14 @@
+import * as oauth from "src/authentication/oauth";
import * as sapper from "@sapper/server";
+import alexaPost from "src/alexa";
import bodyParser from "body-parser";
import compression from "compression";
import config from "config";
import cookieParser from "cookie-parser";
import express from "express";
+import fs from "fs";
import helmet from "helmet";
import https from "https";
-import fs from "fs";
import pgSession from "connect-pg-simple";
import securePassword from "secure-password";
import session from "express-session";
@@ -16,9 +18,8 @@ import { Store } from "svelte/store";
import { Strategy as LocalStrategy } from "passport-local";
import { csrfProtection, exposeCsrfMiddleware } from "src/lib/server/csrf";
import { exposeStripeKeyMiddleware } from "src/lib/server/stripe";
+import { logger, logResponse, requestLoggingMiddleware } from "./logging";
import { passport, initPassport } from "src/authentication";
-import * as oauth from "src/authentication/oauth";
-import { logger, requestLoggingMiddleware } from "./logging";
import { pool } from "src/lib/server/database.js";
import { requireHttps } from "src/lib/server/require_https";
import { requireRoot } from "src/lib/server/require_root";
@@ -52,8 +53,7 @@ const middleware = [
bodyParser.json({
verify: (req, res, buf) => {
// Sometimes (e.g. Stripe hook routes) we'll want the raw body. So we'll save it.
- // TODO(kyle): Properly done we would split our express routes up a bit.
- if (req.path.includes("api")) req.rawBody = buf;
+ req.rawBody = buf;
}
}),
requestLoggingMiddleware,
@@ -85,9 +85,6 @@ const middleware = [
secure: !config.get("dev") || config.get("server.enableHttps")
}
}),
- csrfProtection,
- exposeCsrfMiddleware,
- exposeStripeKeyMiddleware,
nonce,
helmet.contentSecurityPolicy({
reportOnly: false,
@@ -128,20 +125,29 @@ app.use(...middleware);
app.get("/oauth/authorize", oauth.authorize);
-app.post("/oauth/authorize/decision", oauth.server.decision());
+app.post("/oauth/authorize/decision", logResponse, oauth.server.decision());
-app.get(
+app.post(
"/oauth/token",
- passport.authenticate(["basic"], {
+ passport.authenticate(["basic", "oauth2-client-password"], {
session: false
}),
oauth.server.token(),
oauth.server.errorHandler()
);
+app.post(
+ "/alexa",
+ passport.authenticate(["bearer", "anonymous"], { session: false }),
+ alexaPost
+);
+
app.use(
"/",
- passport.authenticationMiddleware(),
+ csrfProtection,
+ exposeCsrfMiddleware,
+ exposeStripeKeyMiddleware,
+ passport.authenticateNonDefaultRoutes(),
sapper.middleware({
session: req => ({
user: req.user