You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

175 lines
5.2 KiB

import * as Express from "express";
import Router from "express-promise-router";
import * as jwt from "jsonwebtoken";
import TokenClaims from "@common/TokenClaims";
import { User } from "../models/User";
import { ServerState } from "../state";
import { ApiError } from "./errors";
import {
TokenGrantPasswordRequest,
TokenGrantRefreshRequest,
TokenGrantRequest,
TokenGrantResponse
} from "@common/http";
export { TokenClaims };
declare global {
namespace Express {
interface Request {
token?: TokenClaims;
}
}
}
const JWT_SECRET = process.env.JWT_SECRET!;
if (!JWT_SECRET) {
throw new Error("Must specify JWT_SECRET environment variable");
}
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;
}
function signToken(claims: TokenClaims): Promise<string> {
return new Promise((resolve, reject) => {
jwt.sign(claims, JWT_SECRET, (err: Error, encoded: string) => {
if (err) {
reject(err);
} else {
resolve(encoded);
}
});
});
}
export function verifyToken(token: string): Promise<TokenClaims> {
return new Promise((resolve, reject) => {
jwt.verify(token, JWT_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);
}
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);
}
export function authentication(state: ServerState) {
const router = Router();
async function passwordGrant(body: TokenGrantPasswordRequest, res: Express.Response): Promise<User> {
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(400, "User does not exist");
}
const passwordMatches = await user.comparePassword(password);
if (passwordMatches) {
return user;
} else {
throw new ApiError(401, "Invalid user credentials");
}
}
async function refreshGrant(body: TokenGrantRefreshRequest, res: Express.Response): Promise<User> {
const { refresh_token } = body;
if (!body || !refresh_token) {
throw new ApiError(400, "Must specify a refresh_token");
}
const claims = await verifyToken(refresh_token);
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");
}
return user;
}
router.post("/token/grant", async (req, res) => {
const body: TokenGrantRequest = req.body;
let user: User;
if (body.grant_type === "password") {
user = await passwordGrant(body, res);
} else if (body.grant_type === "refresh") {
user = await refreshGrant(body, res);
} else {
throw new ApiError(400, "Invalid grant_type");
}
const [access_token, refresh_token] = await Promise.all(
[await generateAccessToken(user, JWT_SECRET),
await generateRefreshToken(user, JWT_SECRET)]);
const response: TokenGrantResponse = {
access_token, refresh_token,
};
res.json(response);
});
router.post("/token/verify", authorizeAccess, async (req, res) => {
res.json({
ok: true,
token: req.token,
});
});
return router;
}
export async function authorizeAccess(req: Express.Request, res: Express.Response) {
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];
req.token = await verifyToken(token);
}