Browse Source

Cleanup and refactoring of authentication and server api

update-deps
Alex Mikhalev 7 years ago
parent
commit
4dd28098bf
  1. 13
      common/TokenClaims.ts
  2. 52
      server/express/api.ts
  3. 67
      server/express/authentication.ts
  4. 41
      server/express/index.ts
  5. 3
      server/sprinklersRpc/websocketServer.ts

13
common/TokenClaims.ts

@ -1,7 +1,16 @@ @@ -1,7 +1,16 @@
export default interface TokenClaims {
export interface BaseClaims {
iss: string;
exp?: number;
}
export interface AccessOrRefreshToken extends BaseClaims {
type: "access" | "refresh";
aud: number;
name: string;
exp: number;
}
export interface DeviceRegistrationToken extends BaseClaims {
type: "device_reg";
}
export type TokenClaims = AccessOrRefreshToken | DeviceRegistrationToken;

52
server/express/api.ts

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
import PromiseRouter from "express-promise-router";
import { serialize} from "serializr";
import ApiError from "@common/ApiError";
import { ErrorCode } from "@common/ErrorCode";
import * as schema from "@common/sprinklersRpc/schema";
import { ServerState } from "../state";
import { authentication, verifyAuthorization } from "./authentication";
export default function createApi(state: ServerState) {
const router = PromiseRouter();
router.get("/devices/:deviceId", verifyAuthorization(), (req, res) => {
// TODO: authorize device
const device = state.mqttClient.getDevice(req.params.deviceId);
const j = serialize(schema.sprinklersDevice, device);
res.send(j);
});
// router.post("/devices/register", verifyAuthorization({
// type: "device_reg",
// }), (req, res) => {
// res.json({ data: "device registered" });
// });
router.get("/users", verifyAuthorization(), (req, res) => {
state.database.users.find()
.then((users) => {
res.json({
data: users,
});
});
});
router.get("/api/users/:username", (req, res, next) => {
const { username } = req.params;
state.database.users.findByUsername(username)
.then((user) => {
if (!user) {
throw new ApiError(`user ${username} does not exist`, ErrorCode.NotFound);
}
res.json({
data: user,
});
})
.catch(next);
});
router.use("/", authentication(state));
return router;
}

67
server/express/authentication.ts

@ -10,7 +10,7 @@ import { @@ -10,7 +10,7 @@ import {
TokenGrantRequest,
TokenGrantResponse,
} from "@common/httpApi";
import TokenClaims from "@common/TokenClaims";
import { TokenClaims } from "@common/TokenClaims";
import { User } from "../entities";
import { ServerState } from "../state";
@ -29,6 +29,8 @@ if (!JWT_SECRET) { @@ -29,6 +29,8 @@ if (!JWT_SECRET) {
throw new Error("Must specify JWT_SECRET environment variable");
}
const ISSUER = "sprinklers3";
const ACCESS_TOKEN_LIFETIME = (30 * 60); // 30 minutes
const REFRESH_TOKEN_LIFETIME = (24 * 60 * 60); // 24 hours
@ -53,7 +55,9 @@ function signToken(claims: TokenClaims): Promise<string> { @@ -53,7 +55,9 @@ function signToken(claims: TokenClaims): Promise<string> {
export function verifyToken(token: string): Promise<TokenClaims> {
return new Promise((resolve, reject) => {
jwt.verify(token, JWT_SECRET, (err, decoded) => {
jwt.verify(token, JWT_SECRET, {
issuer: ISSUER,
}, (err, decoded) => {
if (err) {
if (err.name === "TokenExpiredError") {
reject(new ApiError("The specified token is expired", ErrorCode.BadToken, err));
@ -71,7 +75,7 @@ export function verifyToken(token: string): Promise<TokenClaims> { @@ -71,7 +75,7 @@ export function verifyToken(token: string): Promise<TokenClaims> {
function generateAccessToken(user: User, secret: string): Promise<string> {
const access_token_claims: TokenClaims = {
iss: "sprinklers3",
iss: ISSUER,
aud: user.id,
name: user.name,
type: "access",
@ -83,7 +87,7 @@ function generateAccessToken(user: User, secret: string): Promise<string> { @@ -83,7 +87,7 @@ function generateAccessToken(user: User, secret: string): Promise<string> {
function generateRefreshToken(user: User, secret: string): Promise<string> {
const refresh_token_claims: TokenClaims = {
iss: "sprinklers3",
iss: ISSUER,
aud: user.id,
name: user.name,
type: "refresh",
@ -93,6 +97,14 @@ function generateRefreshToken(user: User, secret: string): Promise<string> { @@ -93,6 +97,14 @@ function generateRefreshToken(user: User, secret: string): Promise<string> {
return signToken(refresh_token_claims);
}
function generateDeviceRegistrationToken(secret: string): Promise<string> {
const device_reg_token_claims: TokenClaims = {
iss: ISSUER,
type: "device_reg",
};
return signToken(device_reg_token_claims);
}
export function authentication(state: ServerState) {
const router = Router();
@ -149,7 +161,12 @@ export function authentication(state: ServerState) { @@ -149,7 +161,12 @@ export function authentication(state: ServerState) {
res.json(response);
});
router.post("/token/verify", authorizeAccess, async (req, res) => {
router.post("/token/grant_device_reg", verifyAuthorization(), async (req, res) => {
const token = await generateDeviceRegistrationToken(JWT_SECRET);
res.json({ token });
});
router.post("/token/verify", verifyAuthorization(), async (req, res) => {
res.json({
ok: true,
token: req.token,
@ -159,16 +176,34 @@ export function authentication(state: ServerState) { @@ -159,16 +176,34 @@ export function authentication(state: ServerState) {
return router;
}
export async function authorizeAccess(req: Express.Request, res: Express.Response) {
const bearer = req.headers.authorization;
if (!bearer) {
throw new ApiError("No Authorization header specified", ErrorCode.BadToken);
}
const matches = /^Bearer (.*)$/.exec(bearer);
if (!matches || !matches[1]) {
throw new ApiError("Invalid Authorization header, must be Bearer", ErrorCode.BadToken);
}
const token = matches[1];
export interface VerifyAuthorizationOpts {
type: TokenClaims["type"];
}
req.token = await verifyToken(token);
export function verifyAuthorization(options?: Partial<VerifyAuthorizationOpts>): Express.RequestHandler {
const opts: VerifyAuthorizationOpts = {
type: "access",
...options,
};
return (req, res, next) => {
const fun = async () => {
const bearer = req.headers.authorization;
if (!bearer) {
throw new ApiError("No Authorization header specified", ErrorCode.BadToken);
}
const matches = /^Bearer (.*)$/.exec(bearer);
if (!matches || !matches[1]) {
throw new ApiError("Invalid Authorization header, must be Bearer", ErrorCode.BadToken);
}
const token = matches[1];
req.token = await verifyToken(token);
if (req.token.type !== opts.type) {
throw new ApiError(`Invalid token type "${req.token.type}", must be "${opts.type}"`,
ErrorCode.BadToken);
}
};
fun().then(() => next(null), (err) => next(err));
};
}

41
server/express/index.ts

@ -1,54 +1,19 @@ @@ -1,54 +1,19 @@
import * as bodyParser from "body-parser";
import * as express from "express";
import { serialize} from "serializr";
import * as schema from "@common/sprinklersRpc/schema";
import { ServerState } from "../state";
import createApi from "./api";
import errorHandler from "./errorHandler";
import requestLogger from "./requestLogger";
import serveApp from "./serveApp";
import ApiError from "@common/ApiError";
import { ErrorCode } from "@common/ErrorCode";
import { authentication } from "./authentication";
import errorHandler from "./errorHandler";
export function createApp(state: ServerState) {
const app = express();
app.use(requestLogger);
app.use(bodyParser.json());
app.get("/api/devices/:deviceId", (req, res) => {
// TODO: authorize device
const device = state.mqttClient.getDevice(req.params.deviceId);
const j = serialize(schema.sprinklersDevice, device);
res.send(j);
});
app.get("/api/users", (req, res) => {
state.database.users.find()
.then((users) => {
res.json({
data: users,
});
});
});
app.get("/api/users/:username", (req, res, next) => {
const { username } = req.params;
state.database.users.findByUsername(username)
.then((user) => {
if (!user) {
throw new ApiError(`user ${username} does not exist`, ErrorCode.NotFound);
}
res.json({
data: user,
});
})
.catch(next);
});
app.use("/api", authentication(state));
app.use("/api", createApi(state));
serveApp(app);

3
server/sprinklersRpc/websocketServer.ts

@ -69,6 +69,9 @@ export class WebSocketClient { @@ -69,6 +69,9 @@ export class WebSocketClient {
} catch (e) {
throw new ws.RpcError("invalid token", ErrorCode.BadToken, e);
}
if (decoded.type !== "access") {
throw new ws.RpcError("not an access token", ErrorCode.BadToken);
}
this.userId = decoded.aud;
log.info({ userId: decoded.aud, name: decoded.name }, "authenticated websocket client");
this.subscribeBrokerConnection();

Loading…
Cancel
Save