Refactored out jwt token stuff
This commit is contained in:
parent
6c1ba0022f
commit
457a124874
client/state
common
server
@ -1,13 +1,13 @@
|
|||||||
import { action, observable } from "mobx";
|
import { action, observable } from "mobx";
|
||||||
|
|
||||||
import { Token } from "@client/state/Token";
|
import { Token } from "@client/state/Token";
|
||||||
import { AccessToken, RefreshToken } from "@common/TokenClaims";
|
import { AccessToken, BaseClaims, RefreshToken } from "@common/TokenClaims";
|
||||||
|
|
||||||
const LOCAL_STORAGE_KEY = "TokenStore";
|
const LOCAL_STORAGE_KEY = "TokenStore";
|
||||||
|
|
||||||
export class TokenStore {
|
export class TokenStore {
|
||||||
@observable accessToken: Token<AccessToken> = new Token();
|
@observable accessToken: Token<AccessToken & BaseClaims> = new Token();
|
||||||
@observable refreshToken: Token<RefreshToken> = new Token();
|
@observable refreshToken: Token<RefreshToken & BaseClaims> = new Token();
|
||||||
|
|
||||||
@action
|
@action
|
||||||
clearAccessToken() {
|
clearAccessToken() {
|
||||||
|
@ -3,30 +3,32 @@ export interface BaseClaims {
|
|||||||
exp?: number;
|
exp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AccessToken extends BaseClaims {
|
export interface AccessToken {
|
||||||
type: "access";
|
type: "access";
|
||||||
aud: number;
|
aud: number;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefreshToken extends BaseClaims {
|
export interface RefreshToken {
|
||||||
type: "refresh";
|
type: "refresh";
|
||||||
aud: number;
|
aud: number;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceRegistrationToken extends BaseClaims {
|
export interface DeviceRegistrationToken {
|
||||||
type: "device_reg";
|
type: "device_reg";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceToken extends BaseClaims {
|
export interface DeviceToken {
|
||||||
type: "device";
|
type: "device";
|
||||||
aud: string;
|
aud: string;
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SuperuserToken extends BaseClaims {
|
export interface SuperuserToken {
|
||||||
type: "superuser";
|
type: "superuser";
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TokenClaims = AccessToken | RefreshToken | DeviceRegistrationToken | DeviceToken | SuperuserToken;
|
export type TokenClaimTypes = AccessToken | RefreshToken | DeviceRegistrationToken | DeviceToken | SuperuserToken;
|
||||||
|
|
||||||
|
export type TokenClaims = TokenClaimTypes & BaseClaims;
|
||||||
|
111
server/authentication.ts
Normal file
111
server/authentication.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import * as Express from "express";
|
||||||
|
import Router from "express-promise-router";
|
||||||
|
import * as jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import ApiError from "@common/ApiError";
|
||||||
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
|
import {
|
||||||
|
TokenGrantPasswordRequest,
|
||||||
|
TokenGrantRefreshRequest,
|
||||||
|
TokenGrantRequest,
|
||||||
|
TokenGrantResponse,
|
||||||
|
} from "@common/httpApi";
|
||||||
|
import * as tok from "@common/TokenClaims";
|
||||||
|
import { User } from "@server/entities";
|
||||||
|
import { ServerState } from "@server/state";
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET!;
|
||||||
|
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
|
||||||
|
|
||||||
|
function signToken(claims: tok.TokenClaimTypes, opts?: jwt.SignOptions): Promise<string> {
|
||||||
|
const options: jwt.SignOptions = {
|
||||||
|
issuer: ISSUER,
|
||||||
|
...opts,
|
||||||
|
};
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
jwt.sign(claims, JWT_SECRET, options, (err: Error, encoded: string) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(encoded);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyToken<TClaims extends tok.TokenClaimTypes = tok.TokenClaimTypes>(
|
||||||
|
token: string, type?: TClaims["type"],
|
||||||
|
): Promise<TClaims & tok.BaseClaims> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
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));
|
||||||
|
} else if (err.name === "JsonWebTokenError") {
|
||||||
|
reject(new ApiError("Invalid token", ErrorCode.BadToken, err));
|
||||||
|
} else {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const claims: tok.TokenClaims = decoded as any;
|
||||||
|
if (type != null && claims.type !== type) {
|
||||||
|
reject(new ApiError(`Expected a "${type}" token, received a "${claims.type}" token`,
|
||||||
|
ErrorCode.BadToken));
|
||||||
|
}
|
||||||
|
resolve(claims as TClaims & tok.BaseClaims);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateAccessToken(user: User): Promise<string> {
|
||||||
|
const access_token_claims: tok.AccessToken = {
|
||||||
|
aud: user.id,
|
||||||
|
name: user.name,
|
||||||
|
type: "access",
|
||||||
|
};
|
||||||
|
|
||||||
|
return signToken(access_token_claims, { expiresIn: ACCESS_TOKEN_LIFETIME });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateRefreshToken(user: User): Promise<string> {
|
||||||
|
const refresh_token_claims: tok.RefreshToken = {
|
||||||
|
aud: user.id,
|
||||||
|
name: user.name,
|
||||||
|
type: "refresh",
|
||||||
|
};
|
||||||
|
|
||||||
|
return signToken(refresh_token_claims, { expiresIn: REFRESH_TOKEN_LIFETIME });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateDeviceRegistrationToken(): Promise<string> {
|
||||||
|
const device_reg_token_claims: tok.DeviceRegistrationToken = {
|
||||||
|
type: "device_reg",
|
||||||
|
};
|
||||||
|
return signToken(device_reg_token_claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateDeviceToken(id: number, deviceId: string): Promise<string> {
|
||||||
|
const device_token_claims: tok.DeviceToken = {
|
||||||
|
type: "device",
|
||||||
|
aud: deviceId,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
return signToken(device_token_claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateSuperuserToken(): Promise<string> {
|
||||||
|
const superuser_claims: tok.SuperuserToken = {
|
||||||
|
type: "superuser",
|
||||||
|
};
|
||||||
|
return signToken(superuser_claims);
|
||||||
|
}
|
@ -4,7 +4,8 @@ import { serialize} from "serializr";
|
|||||||
import ApiError from "@common/ApiError";
|
import ApiError from "@common/ApiError";
|
||||||
import { ErrorCode } from "@common/ErrorCode";
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
import * as schema from "@common/sprinklersRpc/schema";
|
import * as schema from "@common/sprinklersRpc/schema";
|
||||||
import { generateDeviceToken, verifyAuthorization } from "@server/express/authentication";
|
import { generateDeviceToken } from "@server/authentication";
|
||||||
|
import { verifyAuthorization } from "@server/express/verifyAuthorization";
|
||||||
import { ServerState } from "@server/state";
|
import { ServerState } from "@server/state";
|
||||||
|
|
||||||
const DEVICE_ID_LEN = 20;
|
const DEVICE_ID_LEN = 20;
|
||||||
|
@ -2,10 +2,11 @@ import PromiseRouter from "express-promise-router";
|
|||||||
|
|
||||||
import ApiError from "@common/ApiError";
|
import ApiError from "@common/ApiError";
|
||||||
import { ErrorCode } from "@common/ErrorCode";
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
import { authentication } from "@server/express/authentication";
|
|
||||||
import { ServerState } from "@server/state";
|
import { ServerState } from "@server/state";
|
||||||
|
|
||||||
import { devices } from "./devices";
|
import { devices } from "./devices";
|
||||||
import { mosquitto } from "./mosquitto";
|
import { mosquitto } from "./mosquitto";
|
||||||
|
import { token } from "./token";
|
||||||
import { users } from "./users";
|
import { users } from "./users";
|
||||||
|
|
||||||
export default function createApi(state: ServerState) {
|
export default function createApi(state: ServerState) {
|
||||||
@ -14,7 +15,7 @@ export default function createApi(state: ServerState) {
|
|||||||
router.use("/devices", devices(state));
|
router.use("/devices", devices(state));
|
||||||
router.use("/users", users(state));
|
router.use("/users", users(state));
|
||||||
router.use("/mosquitto", mosquitto(state));
|
router.use("/mosquitto", mosquitto(state));
|
||||||
router.use("/token", authentication(state));
|
router.use("/token", token(state));
|
||||||
|
|
||||||
router.use("*", (req, res) => {
|
router.use("*", (req, res) => {
|
||||||
throw new ApiError("API endpoint not found", ErrorCode.NotFound);
|
throw new ApiError("API endpoint not found", ErrorCode.NotFound);
|
||||||
|
@ -4,7 +4,7 @@ import ApiError from "@common/ApiError";
|
|||||||
import { ErrorCode } from "@common/ErrorCode";
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
import { DEVICE_PREFIX } from "@common/sprinklersRpc/mqtt";
|
import { DEVICE_PREFIX } from "@common/sprinklersRpc/mqtt";
|
||||||
import { DeviceToken, SuperuserToken } from "@common/TokenClaims";
|
import { DeviceToken, SuperuserToken } from "@common/TokenClaims";
|
||||||
import { verifyToken } from "@server/express/authentication";
|
import { verifyToken } from "@server/authentication";
|
||||||
import { ServerState } from "@server/state";
|
import { ServerState } from "@server/state";
|
||||||
|
|
||||||
export const SUPERUSER = "sprinklers3";
|
export const SUPERUSER = "sprinklers3";
|
||||||
|
81
server/express/api/token.ts
Normal file
81
server/express/api/token.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import PromiseRouter from "express-promise-router";
|
||||||
|
|
||||||
|
import ApiError from "@common/ApiError";
|
||||||
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
|
import * as httpApi from "@common/httpApi";
|
||||||
|
import * as authentication from "@server/authentication";
|
||||||
|
import { User } from "@server/entities";
|
||||||
|
import { verifyAuthorization } from "@server/express/verifyAuthorization";
|
||||||
|
import { ServerState } from "@server/state";
|
||||||
|
|
||||||
|
export function token(state: ServerState) {
|
||||||
|
const router = PromiseRouter();
|
||||||
|
|
||||||
|
async function passwordGrant(body: httpApi.TokenGrantPasswordRequest, res: Express.Response): Promise<User> {
|
||||||
|
const { username, password } = body;
|
||||||
|
if (!body || !username || !password) {
|
||||||
|
throw new ApiError("Must specify username and password");
|
||||||
|
}
|
||||||
|
const user = await state.database.users.findByUsername(username);
|
||||||
|
if (!user) {
|
||||||
|
throw new ApiError("User does not exist");
|
||||||
|
}
|
||||||
|
const passwordMatches = await user.comparePassword(password);
|
||||||
|
if (passwordMatches) {
|
||||||
|
return user;
|
||||||
|
} else {
|
||||||
|
throw new ApiError("Invalid user credentials");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshGrant(body: httpApi.TokenGrantRefreshRequest, res: Express.Response): Promise<User> {
|
||||||
|
const { refresh_token } = body;
|
||||||
|
if (!body || !refresh_token) {
|
||||||
|
throw new ApiError("Must specify a refresh_token", ErrorCode.BadToken);
|
||||||
|
}
|
||||||
|
const claims = await authentication.verifyToken(refresh_token);
|
||||||
|
if (claims.type !== "refresh") {
|
||||||
|
throw new ApiError("Not a refresh token", ErrorCode.BadToken);
|
||||||
|
}
|
||||||
|
const user = await state.database.users.findOne(claims.aud);
|
||||||
|
if (!user) {
|
||||||
|
throw new ApiError("User no longer exists", ErrorCode.BadToken);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post("/grant", async (req, res) => {
|
||||||
|
const body: httpApi.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("Invalid grant_type");
|
||||||
|
}
|
||||||
|
const [access_token, refresh_token] = await Promise.all([
|
||||||
|
await authentication.generateAccessToken(user),
|
||||||
|
await authentication.generateRefreshToken(user),
|
||||||
|
]);
|
||||||
|
const response: httpApi.TokenGrantResponse = {
|
||||||
|
access_token, refresh_token,
|
||||||
|
};
|
||||||
|
res.json(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/grant_device_reg", verifyAuthorization(), async (req, res) => {
|
||||||
|
// tslint:disable-next-line:no-shadowed-variable
|
||||||
|
const token = await authentication.generateDeviceRegistrationToken();
|
||||||
|
res.json({ token });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/verify", verifyAuthorization(), async (req, res) => {
|
||||||
|
res.json({
|
||||||
|
ok: true,
|
||||||
|
token: req.token,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
@ -3,7 +3,7 @@ import PromiseRouter from "express-promise-router";
|
|||||||
import ApiError from "@common/ApiError";
|
import ApiError from "@common/ApiError";
|
||||||
import { ErrorCode } from "@common/ErrorCode";
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
import { User } from "@server/entities";
|
import { User } from "@server/entities";
|
||||||
import { verifyAuthorization } from "@server/express/authentication";
|
import { verifyAuthorization } from "@server/express/verifyAuthorization";
|
||||||
import { ServerState } from "@server/state";
|
import { ServerState } from "@server/state";
|
||||||
|
|
||||||
export function users(state: ServerState) {
|
export function users(state: ServerState) {
|
||||||
|
@ -1,227 +0,0 @@
|
|||||||
import * as Express from "express";
|
|
||||||
import Router from "express-promise-router";
|
|
||||||
import * as jwt from "jsonwebtoken";
|
|
||||||
|
|
||||||
import ApiError from "@common/ApiError";
|
|
||||||
import { ErrorCode } from "@common/ErrorCode";
|
|
||||||
import {
|
|
||||||
TokenGrantPasswordRequest,
|
|
||||||
TokenGrantRefreshRequest,
|
|
||||||
TokenGrantRequest,
|
|
||||||
TokenGrantResponse,
|
|
||||||
} from "@common/httpApi";
|
|
||||||
import * as tok from "@common/TokenClaims";
|
|
||||||
import { User } from "../entities";
|
|
||||||
import { ServerState } from "../state";
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
namespace Express {
|
|
||||||
interface Request {
|
|
||||||
token?: tok.AccessToken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET!;
|
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} lifetime in seconds
|
|
||||||
*/
|
|
||||||
function getExpTime(lifetime: number) {
|
|
||||||
return Math.floor(Date.now() / 1000) + lifetime;
|
|
||||||
}
|
|
||||||
|
|
||||||
function signToken(claims: tok.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<TClaims extends tok.TokenClaims = tok.TokenClaims>(
|
|
||||||
token: string, type?: TClaims["type"],
|
|
||||||
): Promise<TClaims> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
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));
|
|
||||||
} else if (err.name === "JsonWebTokenError") {
|
|
||||||
reject(new ApiError("Invalid token", ErrorCode.BadToken, err));
|
|
||||||
} else {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const claims: tok.TokenClaims = decoded as any;
|
|
||||||
if (type != null && claims.type !== type) {
|
|
||||||
reject(new ApiError(`Expected a "${type}" token, received a "${claims.type}" token`,
|
|
||||||
ErrorCode.BadToken));
|
|
||||||
}
|
|
||||||
resolve(claims as TClaims);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateAccessToken(user: User, secret: string): Promise<string> {
|
|
||||||
const access_token_claims: tok.AccessToken = {
|
|
||||||
iss: ISSUER,
|
|
||||||
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: tok.RefreshToken = {
|
|
||||||
iss: ISSUER,
|
|
||||||
aud: user.id,
|
|
||||||
name: user.name,
|
|
||||||
type: "refresh",
|
|
||||||
exp: getExpTime(REFRESH_TOKEN_LIFETIME),
|
|
||||||
};
|
|
||||||
|
|
||||||
return signToken(refresh_token_claims);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateDeviceRegistrationToken(secret: string): Promise<string> {
|
|
||||||
const device_reg_token_claims: tok.DeviceRegistrationToken = {
|
|
||||||
iss: ISSUER,
|
|
||||||
type: "device_reg",
|
|
||||||
};
|
|
||||||
return signToken(device_reg_token_claims);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateDeviceToken(id: number, deviceId: string): Promise<string> {
|
|
||||||
const device_token_claims: tok.DeviceToken = {
|
|
||||||
iss: ISSUER,
|
|
||||||
type: "device",
|
|
||||||
aud: deviceId,
|
|
||||||
id,
|
|
||||||
};
|
|
||||||
return signToken(device_token_claims);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateSuperuserToken(): Promise<string> {
|
|
||||||
const superuser_claims: tok.SuperuserToken = {
|
|
||||||
iss: ISSUER,
|
|
||||||
type: "superuser",
|
|
||||||
};
|
|
||||||
return signToken(superuser_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("Must specify username and password");
|
|
||||||
}
|
|
||||||
const user = await state.database.users.findByUsername(username);
|
|
||||||
if (!user) {
|
|
||||||
throw new ApiError("User does not exist");
|
|
||||||
}
|
|
||||||
const passwordMatches = await user.comparePassword(password);
|
|
||||||
if (passwordMatches) {
|
|
||||||
return user;
|
|
||||||
} else {
|
|
||||||
throw new ApiError("Invalid user credentials");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshGrant(body: TokenGrantRefreshRequest, res: Express.Response): Promise<User> {
|
|
||||||
const { refresh_token } = body;
|
|
||||||
if (!body || !refresh_token) {
|
|
||||||
throw new ApiError("Must specify a refresh_token", ErrorCode.BadToken);
|
|
||||||
}
|
|
||||||
const claims = await verifyToken(refresh_token);
|
|
||||||
if (claims.type !== "refresh") {
|
|
||||||
throw new ApiError("Not a refresh token", ErrorCode.BadToken);
|
|
||||||
}
|
|
||||||
const user = await state.database.users.findOne(claims.aud);
|
|
||||||
if (!user) {
|
|
||||||
throw new ApiError("User no longer exists", ErrorCode.BadToken);
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.post("/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("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("/grant_device_reg", verifyAuthorization(), async (req, res) => {
|
|
||||||
const token = await generateDeviceRegistrationToken(JWT_SECRET);
|
|
||||||
res.json({ token });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post("/verify", verifyAuthorization(), async (req, res) => {
|
|
||||||
res.json({
|
|
||||||
ok: true,
|
|
||||||
token: req.token,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VerifyAuthorizationOpts {
|
|
||||||
type: tok.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, opts.type) as any;
|
|
||||||
};
|
|
||||||
fun().then(() => next(null), (err) => next(err));
|
|
||||||
};
|
|
||||||
}
|
|
41
server/express/verifyAuthorization.ts
Normal file
41
server/express/verifyAuthorization.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import * as Express from "express";
|
||||||
|
|
||||||
|
import ApiError from "@common/ApiError";
|
||||||
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
|
import * as tok from "@common/TokenClaims";
|
||||||
|
import { verifyToken } from "@server/authentication";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
token?: tok.AccessToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerifyAuthorizationOpts {
|
||||||
|
type: tok.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, opts.type) as any;
|
||||||
|
};
|
||||||
|
fun().then(() => next(null), (err) => next(err));
|
||||||
|
};
|
||||||
|
}
|
@ -10,8 +10,8 @@ import * as deviceRequests from "@common/sprinklersRpc/deviceRequests";
|
|||||||
import * as schema from "@common/sprinklersRpc/schema";
|
import * as schema from "@common/sprinklersRpc/schema";
|
||||||
import * as ws from "@common/sprinklersRpc/websocketData";
|
import * as ws from "@common/sprinklersRpc/websocketData";
|
||||||
import { AccessToken } from "@common/TokenClaims";
|
import { AccessToken } from "@common/TokenClaims";
|
||||||
|
import { verifyToken } from "@server/authentication";
|
||||||
import { User } from "@server/entities";
|
import { User } from "@server/entities";
|
||||||
import { verifyToken } from "@server/express/authentication";
|
|
||||||
|
|
||||||
import { WebSocketApi } from "./WebSocketApi";
|
import { WebSocketApi } from "./WebSocketApi";
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import logger from "@common/logger";
|
import logger from "@common/logger";
|
||||||
import * as mqtt from "@common/sprinklersRpc/mqtt";
|
import * as mqtt from "@common/sprinklersRpc/mqtt";
|
||||||
|
import { generateSuperuserToken } from "@server/authentication";
|
||||||
import { SUPERUSER } from "@server/express/api/mosquitto";
|
import { SUPERUSER } from "@server/express/api/mosquitto";
|
||||||
import { generateSuperuserToken } from "@server/express/authentication";
|
|
||||||
import { Database } from "./Database";
|
import { Database } from "./Database";
|
||||||
|
|
||||||
export class ServerState {
|
export class ServerState {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user