Skip to content
This repository has been archived by the owner on Mar 5, 2021. It is now read-only.

Commit

Permalink
Implement session-based authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
elisee committed Jun 27, 2016
1 parent 0ceeab2 commit 3ccc755
Show file tree
Hide file tree
Showing 27 changed files with 2,394 additions and 1,538 deletions.
3 changes: 1 addition & 2 deletions SupClient/src/index.ts
Expand Up @@ -63,9 +63,8 @@ export function connect(projectId: string, options?: { reconnection?: boolean; }

const namespace = (projectId != null) ? `project:${projectId}` : "hub";

const supServerAuth = cookies.get("supServerAuth");
const socket = io.connect(`${window.location.protocol}//${window.location.host}/${namespace}`,
{ transports: [ "websocket" ], reconnection: options.reconnection, query: { supServerAuth } }
{ transports: [ "websocket" ], reconnection: options.reconnection }
);

socket.on("welcome", (clientId: number, config: { systemId: string; }) => {
Expand Down
4 changes: 2 additions & 2 deletions SupCore/Data/Room.ts
Expand Up @@ -52,7 +52,7 @@ export default class Room extends SupData.Base.Hash {
}

join(client: SupCore.RemoteClient, callback: (err: string, item?: any, index?: number) => any) {
const username = (client.socket as any).username;
const username = client.socket.request.user.username;
let item = this.users.byId[username];
if (item != null) {
item.connectionCount++;
Expand All @@ -74,7 +74,7 @@ export default class Room extends SupData.Base.Hash {
}

leave(client: SupCore.RemoteClient, callback: (err: string, username?: any) => any) {
const username = (client.socket as any).username;
const username = client.socket.request.user.username;
const item = this.users.byId[username];
if (item.connectionCount > 1) {
item.connectionCount--;
Expand Down
12 changes: 6 additions & 6 deletions SupCore/SupCore.d.ts
Expand Up @@ -338,12 +338,12 @@ declare namespace SupCore {
export let system: System;

class EventEmitter implements NodeJS.EventEmitter {
addListener(event: string, listener: Function): EventEmitter;
on(event: string, listener: Function): EventEmitter;
once(event: string, listener: Function): EventEmitter;
removeListener(event: string, listener: Function): EventEmitter;
removeAllListeners(event?: string): EventEmitter;
setMaxListeners(n: number): EventEmitter;
addListener(event: string, listener: Function): this;
on(event: string, listener: Function): this;
once(event: string, listener: Function): this;
removeListener(event: string, listener: Function): this;
removeAllListeners(event?: string): this;
setMaxListeners(n: number): this;
getMaxListeners(): number;
listeners(event: string): Function[];
emit(event: string, ...args: any[]): boolean;
Expand Down
12 changes: 6 additions & 6 deletions client/src/login/index.jade
Expand Up @@ -13,19 +13,19 @@ html
.server-name
.connecting
p= t("common:states.connecting")
form.login(hidden)
form.login(method="post",hidden)
.welcome= t("login:welcome")

table.properties
tr(hidden)
th= t("login:password")
td
input.server-password(type="password",placeholder="Password",autofocus)
tr
th= t("login:username")
td
// NOTE: The pattern and min/max lengths must match the regex in server/authenticate.ts
input.username(type="text",placeholder="Your username",spellcheck="false",minlength="3",maxlength="20",pattern="[A-Za-z0-9_-]+",title=t("login:usernamePatternDescription"))
input.username(type="text",name="username",placeholder="Your username",spellcheck="false",minlength="3",maxlength="20",pattern="[A-Za-z0-9_-]+",title=t("login:usernamePatternDescription"),autofocus)
tr(hidden)
th= t("login:password")
td
input.server-password(name="password",type="password",placeholder="Password")

.buttons
button.log-in= t("login:logIn")
Expand Down
24 changes: 0 additions & 24 deletions client/src/login/index.ts
Expand Up @@ -3,25 +3,9 @@ const port = (window.location.port.length === 0) ? "80" : window.location.port;
const connectingElt = document.querySelector(".connecting") as HTMLDivElement;
const formElt = document.querySelector(".login") as HTMLDivElement;
const serverPasswordElt = document.querySelector(".server-password") as HTMLInputElement;
const usernameElt = document.querySelector(".username") as HTMLInputElement;

formElt.hidden = true;

let supServerAuth = SupClient.cookies.getJSON("supServerAuth");

// NOTE: Superpowers used to store auth info in local storage
if (supServerAuth == null) {
const supServerAuthJSON = localStorage.getItem("supServerAuth");
if (supServerAuthJSON != null) supServerAuth = JSON.parse(supServerAuthJSON);
}

if (supServerAuth != null) {
serverPasswordElt.value = supServerAuth.serverPassword;
usernameElt.value = supServerAuth.username;
}

const redirect: string = (SupClient.query as any).redirect != null ? (SupClient.query as any).redirect : "/";

SupClient.fetch("superpowers.json", "json", (err, data) => {
serverPasswordElt.parentElement.parentElement.hidden = data.hasPassword === false;
SupClient.i18n.load([{ root: "/", name: "hub" }, { root: "/", name: "login" }], start);
Expand All @@ -31,12 +15,4 @@ function start() {
formElt.hidden = false;
connectingElt.hidden = true;
document.querySelector(".server-name").textContent = SupClient.i18n.t("hub:serverAddress", { hostname: window.location.hostname, port });
document.querySelector("form.login").addEventListener("submit", onFormSubmit);
}

function onFormSubmit(event: Event) {
event.preventDefault();

SupClient.cookies.set("supServerAuth", { serverPassword: serverPasswordElt.value, username: usernameElt.value }, { expires: 7 });
window.location.replace(redirect);
}
5 changes: 5 additions & 0 deletions package.json
Expand Up @@ -37,14 +37,19 @@
"dependencies": {
"async": "^1.5.2",
"async-lock": "^0.3.8",
"body-parser": "^1.15.2",
"cookie-parser": "^1.4.1",
"dnd-tree-view": "^3.1.2",
"express": "^4.13.3",
"express-session": "^1.13.0",
"follow-redirects": "0.0.7",
"fuzzy": "^0.1.1",
"js-cookie": "^2.1.0",
"lodash": "^4.8.1",
"mkdirp": "^0.5.1",
"passport": "^0.3.2",
"passport-local": "^1.0.0",
"passport.socketio": "^3.6.1",
"recursive-readdir": "^1.3.0",
"resize-handle": "^4.0.0",
"rimraf": "^2.5.0",
Expand Down
2 changes: 0 additions & 2 deletions server/ProjectHub.ts
Expand Up @@ -2,7 +2,6 @@ import * as fs from "fs";
import * as path from "path";
import * as async from "async";

import authMiddleware from "./authenticate";
import ProjectServer from "./ProjectServer";
import RemoteHubClient from "./RemoteHubClient";

Expand Down Expand Up @@ -48,7 +47,6 @@ export default class ProjectHub {

const serve = (callback: Function) => {
this.io = this.globalIO.of("/hub");
this.io.use(authMiddleware);

this.io.on("connection", this.onAddSocket);
callback();
Expand Down
2 changes: 0 additions & 2 deletions server/ProjectServer.ts
Expand Up @@ -2,7 +2,6 @@ import * as fs from "fs";
import * as path from "path";
import * as async from "async";

import authMiddleware from "./authenticate";
import RemoteProjectClient from "./RemoteProjectClient";
import * as schemas from "./schemas";
import migrateProject from "./migrateProject";
Expand Down Expand Up @@ -128,7 +127,6 @@ export default class ProjectServer {
const serve = (callback: (err: Error) => any) => {
// Setup the project's namespace
this.io = globalIO.of(`/project:${this.data.manifest.pub.id}`);
this.io.use(authMiddleware);
this.io.on("connection", this.onAddSocket);
callback(null);
};
Expand Down
22 changes: 0 additions & 22 deletions server/authenticate.ts

This file was deleted.

49 changes: 45 additions & 4 deletions server/commands/start.ts
@@ -1,10 +1,16 @@
import * as path from "path";
import * as fs from "fs";
import * as crypto from "crypto";
import * as http from "http";
import * as express from "express";
import * as cookieParser from "cookie-parser";
import * as socketio from "socket.io";

import passportMiddleware from "../passportMiddleware";
import * as bodyParser from "body-parser";
import * as cookieParser from "cookie-parser";
import * as expressSession from "express-session";
import * as passportSocketIo from "passport.socketio";

import * as config from "../config";
import * as schemas from "../schemas";
import getLocalizedFilename from "../getLocalizedFilename";
Expand Down Expand Up @@ -59,11 +65,28 @@ export default function start(serverDataPath: string) {
// Main HTTP server
mainApp = express();

if (typeof config.server.sessionSecret !== "string") throw new Error("serverConfig.sessionSecret is null");
const sessionSettings = {
name: "supSession",
secret: config.server.sessionSecret,
store: new expressSession.MemoryStore(),
resave: false,
saveUninitialized: false
};

mainApp.use(cookieParser());
mainApp.use(bodyParser.urlencoded({ extended: false }));
mainApp.use(handleLanguage);
mainApp.use(expressSession(sessionSettings));
mainApp.use(passportMiddleware.initialize());
mainApp.use(passportMiddleware.session());

mainApp.get("/", (req, res) => { res.redirect("/hub"); });

mainApp.post("/login", passportMiddleware.authenticate("local", { successReturnToOrRedirect: "/", failureRedirect: "/login" }));
mainApp.get("/login", serveLoginIndex);
mainApp.get("/logout", (req, res) => { req.logout(); res.redirect("/"); });

mainApp.get("/hub", enforceAuth, serveHubIndex);
mainApp.get("/project", enforceAuth, serveProjectIndex);

Expand All @@ -74,6 +97,12 @@ export default function start(serverDataPath: string) {
mainHttpServer.on("error", onHttpServerError.bind(null, config.server.mainPort));

io = socketio(mainHttpServer, { transports: [ "websocket" ] });
io.use(passportSocketIo.authorize({
cookieParser: cookieParser,
key: sessionSettings.name,
secret: sessionSettings.secret,
store: sessionSettings.store
}));

// Build HTTP server
buildApp = express();
Expand Down Expand Up @@ -103,6 +132,8 @@ export default function start(serverDataPath: string) {
}

function loadConfig() {
let mustWriteConfig = false;

const serverConfigPath = `${dataPath}/config.json`;
if (fs.existsSync(serverConfigPath)) {
config.server = JSON.parse(fs.readFileSync(serverConfigPath, { encoding: "utf8" }));
Expand All @@ -112,10 +143,19 @@ function loadConfig() {
if (config.server[key] == null) config.server[key] = config.defaults[key];
}
} else {
fs.writeFileSync(serverConfigPath, JSON.stringify(config.defaults, null, 2) + "\n", { encoding: "utf8" });
mustWriteConfig = true;
config.server = {} as any;
for (const key in config.defaults) config.server[key] = config.defaults[key];
}

if (config.server.sessionSecret == null) {
config.server.sessionSecret = crypto.randomBytes(48).toString("hex");
mustWriteConfig = true;
}

if (mustWriteConfig) {
fs.writeFileSync(serverConfigPath, JSON.stringify(config.server, null, 2) + "\n", { encoding: "utf8" });
}
}

function handleLanguage(req: express.Request, res: express.Response, next: Function) {
Expand All @@ -137,8 +177,9 @@ function handleLanguage(req: express.Request, res: express.Response, next: Funct
}

function enforceAuth(req: express.Request, res: express.Response, next: Function) {
if (req.cookies["supServerAuth"] == null) {
res.redirect(`/login?redirect=${encodeURIComponent(req.originalUrl)}`);
if (!req.isAuthenticated()) {
req.session["returnTo"] = req.originalUrl;
res.redirect(`/login`);
return;
}

Expand Down
2 changes: 2 additions & 0 deletions server/config.ts
Expand Up @@ -2,6 +2,7 @@ export interface Config {
mainPort: number;
buildPort: number;
password: string;
sessionSecret: string;
maxRecentBuilds: number;
[key: string]: any;
}
Expand All @@ -10,6 +11,7 @@ export const defaults: Config = {
mainPort: 4237,
buildPort: 4238,
password: "",
sessionSecret: null,
maxRecentBuilds: 10
};

Expand Down
14 changes: 14 additions & 0 deletions server/index.d.ts
Expand Up @@ -7,3 +7,17 @@ interface BaseServer {

removeRemoteClient(socketId: string): void;
}

declare module "passport.socketio" {
interface AuthorizeOptions {
passport?: any;
key?: string;
secret?: string;
store?: any;
cookieParser?: any;
success?: (data: any, accept: boolean) => void;
fail?: (data: any, message: string, critical: boolean, accept: boolean) => void;
}

export function authorize(options: AuthorizeOptions): (socket: any, fn: (err?: any) => void) => void;
}
20 changes: 20 additions & 0 deletions server/passportMiddleware.ts
@@ -0,0 +1,20 @@
import * as passport from "passport";
import { Strategy as LocalStrategy } from "passport-local";
import { server as serverConfig } from "./config";

// NOTE: The regex must match the pattern and min/max lengths in client/src/login/index.jade
const usernameRegex = /^[A-Za-z0-9_-]{3,20}$/;

passport.serializeUser((user, done) => { done(null, user.username); });
passport.deserializeUser((username, done) => { done(null, { username }); });

const strategy = new LocalStrategy((username, password, done) => {
if (!usernameRegex.test(username)) return done(null, false, { message: "invalidUsername" });
if (password !== serverConfig.password) return done(null, false, { message: "invalidCredentials" });

done(null, { username });
});

passport.use(strategy);

export default passport;
1 change: 1 addition & 0 deletions server/schemas.ts
Expand Up @@ -9,6 +9,7 @@ const config = {
mainPort: { type: "number" },
buildPort: { type: "number" },
password: { type: "string" },
sessionSecret: { type: "string" },
maxRecentBuilds: { type: "number", min: 1 }
}
};
Expand Down

0 comments on commit 3ccc755

Please sign in to comment.