From 187172e9e796942af0e2233a7027e4fe82cd317b Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Sat, 11 Aug 2018 20:35:34 +0300 Subject: [PATCH] Token type improvement --- client/state/Token.ts | 4 ++-- client/state/TokenStore.ts | 5 +++-- client/styles/app.scss | 1 + common/TokenClaims.ts | 12 +++++++++--- server/express/api/devices.ts | 4 ++-- server/express/authentication.ts | 24 ++++++++++++++---------- server/sprinklersRpc/websocketServer.ts | 14 ++++++-------- 7 files changed, 37 insertions(+), 27 deletions(-) diff --git a/client/state/Token.ts b/client/state/Token.ts index 67fdc48..f980ee8 100644 --- a/client/state/Token.ts +++ b/client/state/Token.ts @@ -2,10 +2,10 @@ import { TokenClaims } from "@common/TokenClaims"; import * as jwt from "jsonwebtoken"; import { computed, createAtom, IAtom, observable } from "mobx"; -export class Token { +export class Token { @observable token: string | null; - @computed get claims(): TokenClaims | null { + @computed get claims(): TClaims | null { if (this.token == null) { return null; } diff --git a/client/state/TokenStore.ts b/client/state/TokenStore.ts index 6c89a4f..b50f03e 100644 --- a/client/state/TokenStore.ts +++ b/client/state/TokenStore.ts @@ -1,12 +1,13 @@ import { observable } from "mobx"; import { Token } from "@client/state/Token"; +import { AccessToken, RefreshToken } from "@common/TokenClaims"; const LOCAL_STORAGE_KEY = "TokenStore"; export class TokenStore { - @observable accessToken: Token = new Token(); - @observable refreshToken: Token = new Token(); + @observable accessToken: Token = new Token(); + @observable refreshToken: Token = new Token(); clear() { this.accessToken.token = null; diff --git a/client/styles/app.scss b/client/styles/app.scss index 03744b6..2b4e233 100644 --- a/client/styles/app.scss +++ b/client/styles/app.scss @@ -1,5 +1,6 @@ .app { padding-top: 1em; + padding-bottom: 1em; } .flex-horizontal-space-between { diff --git a/common/TokenClaims.ts b/common/TokenClaims.ts index 2fe76ec..eaf7479 100644 --- a/common/TokenClaims.ts +++ b/common/TokenClaims.ts @@ -3,8 +3,14 @@ export interface BaseClaims { exp?: number; } -export interface AccessOrRefreshToken extends BaseClaims { - type: "access" | "refresh"; +export interface AccessToken extends BaseClaims { + type: "access"; + aud: number; + name: string; +} + +export interface RefreshToken extends BaseClaims { + type: "refresh"; aud: number; name: string; } @@ -13,4 +19,4 @@ export interface DeviceRegistrationToken extends BaseClaims { type: "device_reg"; } -export type TokenClaims = AccessOrRefreshToken | DeviceRegistrationToken; +export type TokenClaims = AccessToken | RefreshToken | DeviceRegistrationToken; diff --git a/server/express/api/devices.ts b/server/express/api/devices.ts index 9d038e5..56554b1 100644 --- a/server/express/api/devices.ts +++ b/server/express/api/devices.ts @@ -4,7 +4,7 @@ import { serialize} from "serializr"; import ApiError from "@common/ApiError"; import { ErrorCode } from "@common/ErrorCode"; import * as schema from "@common/sprinklersRpc/schema"; -import { AccessOrRefreshToken } from "@common/TokenClaims"; +import { AccessToken } from "@common/TokenClaims"; import { verifyAuthorization } from "@server/express/authentication"; import { ServerState } from "@server/state"; @@ -12,7 +12,7 @@ export function devices(state: ServerState) { const router = PromiseRouter(); router.get("/:deviceId", verifyAuthorization(), async (req, res) => { - const token = req.token! as AccessOrRefreshToken; + const token = req.token!; const userId = token.aud; const deviceId = req.params.deviceId; const userDevice = await state.database.sprinklersDevices diff --git a/server/express/authentication.ts b/server/express/authentication.ts index 4860971..b7869a9 100644 --- a/server/express/authentication.ts +++ b/server/express/authentication.ts @@ -10,16 +10,14 @@ import { TokenGrantRequest, TokenGrantResponse, } from "@common/httpApi"; -import { TokenClaims } from "@common/TokenClaims"; +import { AccessToken, DeviceRegistrationToken, RefreshToken, TokenClaims } from "@common/TokenClaims"; import { User } from "../entities"; import { ServerState } from "../state"; -export { TokenClaims }; - declare global { namespace Express { interface Request { - token?: TokenClaims; + token?: AccessToken; } } } @@ -53,7 +51,9 @@ function signToken(claims: TokenClaims): Promise { }); } -export function verifyToken(token: string): Promise { +export function verifyToken( + token: string, type?: TClaims["type"], +): Promise { return new Promise((resolve, reject) => { jwt.verify(token, JWT_SECRET, { issuer: ISSUER, @@ -67,14 +67,18 @@ export function verifyToken(token: string): Promise { reject(err); } } else { - resolve(decoded as any); + const claims: TokenClaims = decoded as any; + if (type != null && claims.type !== type) { + reject(new ApiError(`Expected a "${type} token, received a "${claims.type}" token`)); + } + resolve(claims as TClaims); } }); }); } function generateAccessToken(user: User, secret: string): Promise { - const access_token_claims: TokenClaims = { + const access_token_claims: AccessToken = { iss: ISSUER, aud: user.id, name: user.name, @@ -86,7 +90,7 @@ function generateAccessToken(user: User, secret: string): Promise { } function generateRefreshToken(user: User, secret: string): Promise { - const refresh_token_claims: TokenClaims = { + const refresh_token_claims: RefreshToken = { iss: ISSUER, aud: user.id, name: user.name, @@ -98,7 +102,7 @@ function generateRefreshToken(user: User, secret: string): Promise { } function generateDeviceRegistrationToken(secret: string): Promise { - const device_reg_token_claims: TokenClaims = { + const device_reg_token_claims: DeviceRegistrationToken = { iss: ISSUER, type: "device_reg", }; @@ -197,7 +201,7 @@ export function verifyAuthorization(options?: Partial): } const token = matches[1]; - req.token = await verifyToken(token); + req.token = await verifyToken(token, "access"); if (req.token.type !== opts.type) { throw new ApiError(`Invalid token type "${req.token.type}", must be "${opts.type}"`, diff --git a/server/sprinklersRpc/websocketServer.ts b/server/sprinklersRpc/websocketServer.ts index b1a4849..f229428 100644 --- a/server/sprinklersRpc/websocketServer.ts +++ b/server/sprinklersRpc/websocketServer.ts @@ -9,8 +9,9 @@ import * as deviceRequests from "@common/sprinklersRpc/deviceRequests"; import * as schema from "@common/sprinklersRpc/schema"; import * as ws from "@common/sprinklersRpc/websocketData"; import { User } from "@server/entities"; -import { TokenClaims, verifyToken } from "@server/express/authentication"; +import { verifyToken } from "@server/express/authentication"; import { ServerState } from "@server/state"; +import { AccessToken } from "@common/TokenClaims"; // tslint:disable:member-ordering @@ -78,19 +79,16 @@ export class WebSocketClient { if (!data.accessToken) { throw new ws.RpcError("no token specified", ErrorCode.BadRequest); } - let decoded: TokenClaims; + let claims: AccessToken; try { - decoded = await verifyToken(data.accessToken); + claims = await verifyToken(data.accessToken, "access"); } 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; + this.userId = claims.aud; this.user = await this.state.database.users. findById(this.userId, { devices: true }) || null; - log.info({ userId: decoded.aud, name: decoded.name }, "authenticated websocket client"); + log.info({ userId: claims.aud, name: claims.name }, "authenticated websocket client"); this.subscribeBrokerConnection(); return { result: "success",