Cleanup and refactoring of authentication and server api
This commit is contained in:
parent
ded45e7d44
commit
4dd28098bf
@ -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
Normal file
52
server/express/api.ts
Normal file
@ -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;
|
||||
}
|
@ -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) {
|
||||
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> {
|
||||
|
||||
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> {
|
||||
|
||||
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> {
|
||||
|
||||
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> {
|
||||
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) {
|
||||
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) {
|
||||
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];
|
||||
|
||||
req.token = await verifyToken(token);
|
||||
export interface VerifyAuthorizationOpts {
|
||||
type: TokenClaims["type"];
|
||||
}
|
||||
|
||||
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));
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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…
x
Reference in New Issue
Block a user