Browse Source

Some great token routes and stuff

update-deps
Alex Mikhalev 7 years ago
parent
commit
dbb5c37efe
  1. 3
      package.json
  2. 170
      server/express/authentication.ts
  3. 11
      server/express/errors.ts
  4. 22
      server/express/index.ts
  5. 10
      server/models/User.ts
  6. 87
      yarn.lock

3
package.json

@ -40,7 +40,9 @@
"chalk": "^2.4.1", "chalk": "^2.4.1",
"express": "^4.16.3", "express": "^4.16.3",
"express-pino-logger": "^3.0.2", "express-pino-logger": "^3.0.2",
"express-promise-router": "^3.0.2",
"fork-ts-checker-webpack-plugin": "^0.4.2", "fork-ts-checker-webpack-plugin": "^0.4.2",
"jsonwebtoken": "^8.3.0",
"mobx": "^5.0.3", "mobx": "^5.0.3",
"module-alias": "^2.1.0", "module-alias": "^2.1.0",
"moment": "^2.22.2", "moment": "^2.22.2",
@ -57,6 +59,7 @@
"@types/classnames": "^2.2.4", "@types/classnames": "^2.2.4",
"@types/core-js": "^2.5.0", "@types/core-js": "^2.5.0",
"@types/express": "^4.16.0", "@types/express": "^4.16.0",
"@types/jsonwebtoken": "^7.2.7",
"@types/lodash": "^4.14.110", "@types/lodash": "^4.14.110",
"@types/lodash-es": "^4.17.0", "@types/lodash-es": "^4.17.0",
"@types/node": "^10.3.5", "@types/node": "^10.3.5",

170
server/express/authentication.ts

@ -0,0 +1,170 @@
import log from "@common/logger";
import * as Express from "express";
import Router from "express-promise-router";
import * as jwt from "jsonwebtoken";
import { User } from "../models/User";
import { ServerState } from "../state";
import { ApiError } from "./errors";
const ACCESS_TOKEN_LIFETIME = (30 * 60); // 30 minutes
const REFRESH_TOKEN_LIFETIME = (24 * 60 * 60); // 24 hours
/**
* @param {number} lifetime in seconds
*/
function getExpTime(lifetime: number) {
return Math.floor(Date.now() / 1000) + lifetime;
}
interface TokenClaims {
iss: string;
type: "access" | "refresh";
aud: string;
name: string;
exp: number;
}
function signToken(claims: TokenClaims, secret: string): Promise<string> {
return new Promise((resolve, reject) => {
jwt.sign(claims, secret, (err: Error, encoded: string) => {
if (err) {
reject(err);
} else {
resolve(encoded);
}
});
});
}
function verifyToken(token: string, secret: string): Promise<TokenClaims> {
return new Promise((resolve, reject) => {
jwt.verify(token, secret, (err, decoded) => {
if (err) {
if (err.name === "TokenExpiredError") {
reject(new ApiError(401, "The specified token is expired", err));
} else if (err.name === "JsonWebTokenError") {
reject(new ApiError(400, "Invalid token", err));
} else {
reject(err);
}
} else {
resolve(decoded as any);
}
});
});
}
function generateAccessToken(user: User, secret: string): Promise<string> {
const access_token_claims: TokenClaims = {
iss: "sprinklers3",
aud: user.id || "",
name: user.name,
type: "access",
exp: getExpTime(ACCESS_TOKEN_LIFETIME),
};
return signToken(access_token_claims, secret);
}
function generateRefreshToken(user: User, secret: string): Promise<string> {
const refresh_token_claims: TokenClaims = {
iss: "sprinklers3",
aud: user.id || "",
name: user.name,
type: "refresh",
exp: getExpTime(REFRESH_TOKEN_LIFETIME),
};
return signToken(refresh_token_claims, secret);
}
export function authentication(state: ServerState) {
const JWT_SECRET = process.env.JWT_SECRET!;
if (!JWT_SECRET) {
throw new Error("Must specify JWT_SECRET environment variable");
}
const router = Router();
async function passwordGrant(req: Express.Request, res: Express.Response) {
const { body } = req;
const { username, password } = body;
if (!body || !username || !password) {
throw new ApiError(400, "Must specify username and password");
}
const user = await User.loadByUsername(state.database, username);
if (!user) {
throw new ApiError(401, "User does not exist");
}
const passwordMatches = user.comparePassword(password);
if (passwordMatches) {
const [access_token, refresh_token] = await Promise.all(
[await generateAccessToken(user, JWT_SECRET),
await generateRefreshToken(user, JWT_SECRET)]);
res.json({
access_token, refresh_token,
});
} else {
res.status(400)
.json({
message: "incorrect login",
});
}
}
async function refreshGrant(req: Express.Request, res: Express.Response) {
const { body } = req;
const { refresh_token } = body;
if (!body || !refresh_token) {
throw new ApiError(400, "Must specify a refresh_token");
}
const claims = await verifyToken(refresh_token, JWT_SECRET);
if (claims.type !== "refresh") {
throw new ApiError(400, "Not a refresh token");
}
const user = await User.load(state.database, claims.aud);
if (!user) {
throw new ApiError(400, "User does not exist");
}
const [access_token, new_refresh_token] = await Promise.all(
[await generateAccessToken(user, JWT_SECRET),
await generateRefreshToken(user, JWT_SECRET)]);
res.json({
access_token, refresh_token: new_refresh_token,
});
}
router.post("/token/grant", async (req, res) => {
const { body } = req;
const { grant_type } = body;
if (grant_type === "password") {
await passwordGrant(req, res);
} else if (grant_type === "refresh") {
await refreshGrant(req, res);
} else {
throw new ApiError(400, "Invalid grant_type");
}
});
router.post("/token/verify", async (req, res) => {
const bearer = req.headers.authorization;
if (!bearer) {
throw new ApiError(401, "No bearer token specified");
}
const matches = /^Bearer (.*)$/.exec(bearer);
if (!matches || !matches[1]) {
throw new ApiError(400, "Invalid bearer token specified");
}
const token = matches[1];
log.info({ token });
const decoded = await verifyToken(token, JWT_SECRET);
res.json({
ok: true,
decoded,
});
});
return router;
}

11
server/express/errors.ts

@ -0,0 +1,11 @@
export class ApiError extends Error {
name = "ApiError";
statusCode: number;
cause?: Error;
constructor(statusCode: number, message: string, cause?: Error) {
super(message);
this.statusCode = statusCode;
this.cause = cause;
}
}

22
server/express/index.ts

@ -7,9 +7,8 @@ import { ServerState } from "../state";
import logger from "./logger"; import logger from "./logger";
import serveApp from "./serveApp"; import serveApp from "./serveApp";
import log from "@common/logger";
import { User } from "../models/User"; import { User } from "../models/User";
import { authentication } from "./authentication";
export function createApp(state: ServerState) { export function createApp(state: ServerState) {
const app = express(); const app = express();
@ -41,24 +40,7 @@ export function createApp(state: ServerState) {
.catch(next); .catch(next);
}); });
app.post("/api/authenticate", (req, res, next) => { app.use("/api", authentication(state));
const body = req.body;
log.info({body}, "/api/authenticate: " + req.body);
if (!body || !body.username || !body.password) {
return next(new Error("must specify username and password"));
}
User.loadByUsername(state.database, body.username)
.then((user) => {
if (!user) {
throw new Error("user does not exist");
}
return user.comparePassword(body.password);
}).then((passwordMatches) => {
res.json({
passwordMatches,
});
}).catch(next);
});
serveApp(app); serveApp(app);

10
server/models/User.ts

@ -51,6 +51,16 @@ export class User implements IUser {
}); });
} }
static async load(db: Database, id: string): Promise<User | null> {
const data = await db.db.table(User.tableName)
.get(id)
.run(db.conn);
if (data == null) {
return null;
}
return new User(db, data);
}
static async loadByUsername(db: Database, username: string): Promise<User | null> { static async loadByUsername(db: Database, username: string): Promise<User | null> {
const seq = await db.db.table(User.tableName) const seq = await db.db.table(User.tableName)
.filter(r.row("username").eq(username)) .filter(r.row("username").eq(username))

87
yarn.lock

@ -76,6 +76,12 @@
version "4.6.2" version "4.6.2"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0"
"@types/jsonwebtoken@^7.2.7":
version "7.2.7"
resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-7.2.7.tgz#5dd62e0c0a0c6f211c3c1d13d322360894625b47"
dependencies:
"@types/node" "*"
"@types/lodash-es@^4.17.0": "@types/lodash-es@^4.17.0":
version "4.17.0" version "4.17.0"
resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.0.tgz#ed9044d62ee36a93e0650b112701986b1c74c766" resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.0.tgz#ed9044d62ee36a93e0650b112701986b1c74c766"
@ -884,6 +890,10 @@ browserslist@^3.2.8:
caniuse-lite "^1.0.30000844" caniuse-lite "^1.0.30000844"
electron-to-chromium "^1.3.47" electron-to-chromium "^1.3.47"
buffer-equal-constant-time@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
buffer-from@^1.0.0: buffer-from@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz#87fcaa3a298358e0ade6e442cfce840740d1ad04" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz#87fcaa3a298358e0ade6e442cfce840740d1ad04"
@ -1853,6 +1863,12 @@ ecc-jsbn@~0.1.1:
dependencies: dependencies:
jsbn "~0.1.0" jsbn "~0.1.0"
ecdsa-sig-formatter@1.0.10:
version "1.0.10"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3"
dependencies:
safe-buffer "^5.0.1"
ee-first@1.1.1: ee-first@1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@ -2121,6 +2137,14 @@ express-pino-logger@^3.0.2:
dependencies: dependencies:
pino-http "^3.0.1" pino-http "^3.0.1"
express-promise-router@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/express-promise-router/-/express-promise-router-3.0.2.tgz#2cf0dde8d903605071b52278a6dd1ae0ff0093e2"
dependencies:
is-promise "^2.1.0"
lodash.flattendeep "^4.0.0"
methods "^1.0.0"
express@^4.16.2, express@^4.16.3: express@^4.16.2, express@^4.16.3:
version "4.16.3" version "4.16.3"
resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53" resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53"
@ -3563,6 +3587,20 @@ jsonpointer@^4.0.0:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
jsonwebtoken@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz#056c90eee9a65ed6e6c72ddb0a1d325109aaf643"
dependencies:
jws "^3.1.5"
lodash.includes "^4.3.0"
lodash.isboolean "^3.0.3"
lodash.isinteger "^4.0.4"
lodash.isnumber "^3.0.3"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.once "^4.0.0"
ms "^2.1.1"
jsprim@^1.2.2: jsprim@^1.2.2:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@ -3572,6 +3610,21 @@ jsprim@^1.2.2:
json-schema "0.2.3" json-schema "0.2.3"
verror "1.10.0" verror "1.10.0"
jwa@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6"
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.10"
safe-buffer "^5.0.1"
jws@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f"
dependencies:
jwa "^1.1.5"
safe-buffer "^5.0.1"
keyboard-key@^1.0.1: keyboard-key@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/keyboard-key/-/keyboard-key-1.0.1.tgz#a946294fe59ad5431c63a3ea269f023e51fac6aa" resolved "https://registry.yarnpkg.com/keyboard-key/-/keyboard-key-1.0.1.tgz#a946294fe59ad5431c63a3ea269f023e51fac6aa"
@ -3699,10 +3752,34 @@ lodash.endswith@^4.2.1:
version "4.2.1" version "4.2.1"
resolved "https://registry.yarnpkg.com/lodash.endswith/-/lodash.endswith-4.2.1.tgz#fed59ac1738ed3e236edd7064ec456448b37bc09" resolved "https://registry.yarnpkg.com/lodash.endswith/-/lodash.endswith-4.2.1.tgz#fed59ac1738ed3e236edd7064ec456448b37bc09"
lodash.flattendeep@^4.0.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
lodash.isfunction@^3.0.8: lodash.isfunction@^3.0.8:
version "3.0.9" version "3.0.9"
resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
lodash.isnumber@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
lodash.isstring@^4.0.1: lodash.isstring@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
@ -3715,6 +3792,10 @@ lodash.mergewith@^4.6.0:
version "4.6.1" version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
lodash.startswith@^4.2.1: lodash.startswith@^4.2.1:
version "4.2.1" version "4.2.1"
resolved "https://registry.yarnpkg.com/lodash.startswith/-/lodash.startswith-4.2.1.tgz#c598c4adce188a27e53145731cdc6c0e7177600c" resolved "https://registry.yarnpkg.com/lodash.startswith/-/lodash.startswith-4.2.1.tgz#c598c4adce188a27e53145731cdc6c0e7177600c"
@ -3880,7 +3961,7 @@ merge-descriptors@1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
methods@~1.1.2: methods@^1.0.0, methods@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
@ -4119,6 +4200,10 @@ ms@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
ms@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
multicast-dns-service-types@^1.1.0: multicast-dns-service-types@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901"

Loading…
Cancel
Save