From b59fbb456b68cb1e32c4e48bf505bd68a0d5c13b Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Sat, 11 Aug 2018 19:28:19 +0300 Subject: [PATCH] Fixed authentication and checking device authorization --- client/components/DeviceView.tsx | 4 +- client/sprinklersRpc/WebSocketRpcClient.ts | 11 ++++-- client/state/AppState.ts | 4 +- client/state/UserStore.ts | 5 +++ common/sprinklersRpc/websocketData.ts | 2 +- server/repositories/UserRepository.ts | 22 ++++++----- server/sprinklersRpc/websocketServer.ts | 45 +++++++++++++--------- 7 files changed, 58 insertions(+), 35 deletions(-) create mode 100644 client/state/UserStore.ts diff --git a/client/components/DeviceView.tsx b/client/components/DeviceView.tsx index 1550409..fd5a5b5 100644 --- a/client/components/DeviceView.tsx +++ b/client/components/DeviceView.tsx @@ -22,11 +22,11 @@ const ConnectionState = observer(({ connectionState, className }: connectionText = "Connected"; iconName = "linkify"; clazzName = "connected"; - } else if (connected === false) { - connectionText = "Device Disconnected"; } else if (connectionState.noPermission) { connectionText = "No permission for this device"; iconName = "ban"; + } else if (connected === false) { + connectionText = "Device Disconnected"; } else if (connectionState.clientToServer === false) { connectionText = "Disconnected from server"; } else { diff --git a/client/sprinklersRpc/WebSocketRpcClient.ts b/client/sprinklersRpc/WebSocketRpcClient.ts index 7d2eb77..7f5ea96 100644 --- a/client/sprinklersRpc/WebSocketRpcClient.ts +++ b/client/sprinklersRpc/WebSocketRpcClient.ts @@ -2,6 +2,7 @@ import { action, autorun, observable, when } from "mobx"; import { update } from "serializr"; import { TokenStore } from "@client/state/TokenStore"; +import { UserStore } from "@client/state/UserStore"; import { ErrorCode } from "@common/ErrorCode"; import * as rpc from "@common/jsonRpc"; import logger from "@common/logger"; @@ -85,6 +86,7 @@ export class WebSocketRpcClient implements s.SprinklersRPC { authenticated: boolean = false; tokenStore: TokenStore; + userStore: UserStore; private nextRequestId = Math.round(Math.random() * 1000000); private responseCallbacks: ws.ServerResponseHandlers = {}; @@ -94,9 +96,10 @@ export class WebSocketRpcClient implements s.SprinklersRPC { return this.connectionState.isServerConnected || false; } - constructor(tokenStore: TokenStore, webSocketUrl: string = DEFAULT_URL) { + constructor(tokenStore: TokenStore, userStore: UserStore, webSocketUrl: string = DEFAULT_URL) { this.webSocketUrl = webSocketUrl; this.tokenStore = tokenStore; + this.userStore = userStore; this.connectionState.clientToServer = false; this.connectionState.serverToBroker = false; } @@ -138,8 +141,10 @@ export class WebSocketRpcClient implements s.SprinklersRPC { when(() => this.connectionState.clientToServer === true && this.tokenStore.accessToken.isValid, async () => { try { - await this.authenticate(this.tokenStore.accessToken.token!); - this.authenticated = true; + const res = await this.authenticate(this.tokenStore.accessToken.token!); + this.authenticated = res.authenticated; + logger.info({ user: res.user }, "authenticated websocket connection"); + this.userStore.userData = res.user; } catch (err) { logger.error({ err }, "error authenticating websocket connection"); // TODO message? diff --git a/client/state/AppState.ts b/client/state/AppState.ts index c59b6e5..14035ee 100644 --- a/client/state/AppState.ts +++ b/client/state/AppState.ts @@ -5,6 +5,7 @@ import { RouterStore, syncHistoryWithStore } from "mobx-react-router"; import { WebSocketRpcClient } from "@client/sprinklersRpc/WebSocketRpcClient"; import HttpApi from "@client/state/HttpApi"; import { UiStore } from "@client/state/UiStore"; +import { UserStore } from "@client/state/UserStore"; import ApiError from "@common/ApiError"; import { ErrorCode } from "@common/ErrorCode"; import log from "@common/logger"; @@ -13,9 +14,10 @@ export default class AppState { history: History = createBrowserHistory(); routerStore = new RouterStore(); uiStore = new UiStore(); + userStore = new UserStore(); httpApi = new HttpApi(); tokenStore = this.httpApi.tokenStore; - sprinklersRpc = new WebSocketRpcClient(this.tokenStore); + sprinklersRpc = new WebSocketRpcClient(this.tokenStore, this.userStore); @computed get isLoggedIn() { return this.tokenStore.accessToken.isValid; diff --git a/client/state/UserStore.ts b/client/state/UserStore.ts new file mode 100644 index 0000000..b5e4fff --- /dev/null +++ b/client/state/UserStore.ts @@ -0,0 +1,5 @@ +import { observable } from "mobx"; + +export class UserStore { + @observable userData: any = null; +} diff --git a/common/sprinklersRpc/websocketData.ts b/common/sprinklersRpc/websocketData.ts index d90f537..9850a57 100644 --- a/common/sprinklersRpc/websocketData.ts +++ b/common/sprinklersRpc/websocketData.ts @@ -25,7 +25,7 @@ export interface IClientRequestTypes { export interface IAuthenticateResponse { authenticated: boolean; message: string; - data?: any; + user: any; } export interface IDeviceSubscribeResponse { diff --git a/server/repositories/UserRepository.ts b/server/repositories/UserRepository.ts index 61873e2..8112800 100644 --- a/server/repositories/UserRepository.ts +++ b/server/repositories/UserRepository.ts @@ -1,4 +1,4 @@ -import { EntityRepository, Repository } from "typeorm"; +import { EntityRepository, FindOneOptions, Repository } from "typeorm"; import { User } from "../entities"; @@ -6,23 +6,27 @@ export interface FindUserOptions { devices: boolean; } -function applyDefaultOptions(options?: Partial): FindUserOptions { - return { devices: false, ...options }; +function applyDefaultOptions(options?: Partial): FindOneOptions { + const opts: FindUserOptions = { devices: false, ...options }; + const relations = [opts.devices && "devices"] + .filter(Boolean) as string[]; + return { relations }; } @EntityRepository(User) export class UserRepository extends Repository { findAll(options?: Partial) { const opts = applyDefaultOptions(options); - const relations = [ opts.devices && "devices" ] - .filter(Boolean) as string[]; - return super.find({ relations }); + return super.find(opts); + } + + findById(id: number, options?: Partial) { + const opts = applyDefaultOptions(options); + return super.findOne(id, opts); } findByUsername(username: string, options?: Partial) { const opts = applyDefaultOptions(options); - const relations = [ opts.devices && "devices" ] - .filter(Boolean) as string[]; - return this.findOne({ username }, { relations }); + return this.findOne({ username }, opts); } } diff --git a/server/sprinklersRpc/websocketServer.ts b/server/sprinklersRpc/websocketServer.ts index df1773d..b1a4849 100644 --- a/server/sprinklersRpc/websocketServer.ts +++ b/server/sprinklersRpc/websocketServer.ts @@ -8,8 +8,9 @@ import log from "@common/logger"; import * as deviceRequests from "@common/sprinklersRpc/deviceRequests"; import * as schema from "@common/sprinklersRpc/schema"; import * as ws from "@common/sprinklersRpc/websocketData"; -import { TokenClaims, verifyToken } from "../express/authentication"; -import { ServerState } from "../state"; +import { User } from "@server/entities"; +import { TokenClaims, verifyToken } from "@server/express/authentication"; +import { ServerState } from "@server/state"; // tslint:disable:member-ordering @@ -22,6 +23,7 @@ export class WebSocketClient { /// This shall be the user id if the client has been authenticated, null otherwise userId: number | null = null; + user: User | null = null; get state() { return this.api.state; @@ -52,12 +54,25 @@ export class WebSocketClient { } private checkAuthorization() { - if (!this.userId) { + if (!this.userId || !this.user) { throw new ws.RpcError("this WebSocket session has not been authenticated", ErrorCode.Unauthorized); } } + private checkDevice(devId: string) { + const userDevice = this.user!.devices!.find((dev) => dev.deviceId === devId); + if (userDevice == null) { + throw new ws.RpcError("you do not have permission to subscribe to this device", + ErrorCode.NoPermission); + } + const deviceId = userDevice.deviceId; + if (!deviceId) { + throw new ws.RpcError("device has no associated device prefix", ErrorCode.BadRequest); + } + return userDevice; + } + private requestHandlers: ws.ClientRequestHandlers = { authenticate: async (data: ws.IAuthenticateRequest) => { if (!data.accessToken) { @@ -73,27 +88,19 @@ export class WebSocketClient { throw new ws.RpcError("not an access token", ErrorCode.BadToken); } this.userId = decoded.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"); this.subscribeBrokerConnection(); return { result: "success", - data: { authenticated: true, message: "authenticated" }, + data: { authenticated: true, message: "authenticated", user: this.user!.toJSON() }, }; }, deviceSubscribe: async (data: ws.IDeviceSubscribeRequest) => { this.checkAuthorization(); - const userId = this.userId!; - const deviceId = data.deviceId; - const userDevice = await this.state.database.sprinklersDevices - .findUserDevice(userId, deviceId as any); // TODO: should be number device id - if (userDevice !== "grinklers") { - return { - result: "error", error: { - code: ErrorCode.NoPermission, - message: "you do not have permission to subscribe to this device", - }, - }; - } + const userDevice = this.checkDevice(data.deviceId); + const deviceId = userDevice.deviceId!; if (this.deviceSubscriptions.indexOf(deviceId) === -1) { this.deviceSubscriptions.push(deviceId); const device = this.state.mqttClient.getDevice(deviceId); @@ -204,10 +211,10 @@ export class WebSocketClient { } private async doDeviceCallRequest(requestData: ws.IDeviceCallRequest): Promise { - const { deviceId, data } = requestData; + const userDevice = this.checkDevice(requestData.deviceId); + const deviceId = userDevice.deviceId!; const device = this.state.mqttClient.getDevice(deviceId); - // TODO: authorize the requests - const request = schema.requests.deserializeRequest(data); + const request = schema.requests.deserializeRequest(requestData.data); return device.makeRequest(request); } }