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 { 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 { 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 { 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 { 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 { 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 { 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); }