Browse Source

Fixed authentication and checking device authorization

update-deps
Alex Mikhalev 7 years ago
parent
commit
b59fbb456b
  1. 4
      client/components/DeviceView.tsx
  2. 11
      client/sprinklersRpc/WebSocketRpcClient.ts
  3. 4
      client/state/AppState.ts
  4. 5
      client/state/UserStore.ts
  5. 2
      common/sprinklersRpc/websocketData.ts
  6. 22
      server/repositories/UserRepository.ts
  7. 45
      server/sprinklersRpc/websocketServer.ts

4
client/components/DeviceView.tsx

@ -22,11 +22,11 @@ const ConnectionState = observer(({ connectionState, className }:
connectionText = "Connected"; connectionText = "Connected";
iconName = "linkify"; iconName = "linkify";
clazzName = "connected"; clazzName = "connected";
} else if (connected === false) {
connectionText = "Device Disconnected";
} else if (connectionState.noPermission) { } else if (connectionState.noPermission) {
connectionText = "No permission for this device"; connectionText = "No permission for this device";
iconName = "ban"; iconName = "ban";
} else if (connected === false) {
connectionText = "Device Disconnected";
} else if (connectionState.clientToServer === false) { } else if (connectionState.clientToServer === false) {
connectionText = "Disconnected from server"; connectionText = "Disconnected from server";
} else { } else {

11
client/sprinklersRpc/WebSocketRpcClient.ts

@ -2,6 +2,7 @@ import { action, autorun, observable, when } from "mobx";
import { update } from "serializr"; import { update } from "serializr";
import { TokenStore } from "@client/state/TokenStore"; import { TokenStore } from "@client/state/TokenStore";
import { UserStore } from "@client/state/UserStore";
import { ErrorCode } from "@common/ErrorCode"; import { ErrorCode } from "@common/ErrorCode";
import * as rpc from "@common/jsonRpc"; import * as rpc from "@common/jsonRpc";
import logger from "@common/logger"; import logger from "@common/logger";
@ -85,6 +86,7 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
authenticated: boolean = false; authenticated: boolean = false;
tokenStore: TokenStore; tokenStore: TokenStore;
userStore: UserStore;
private nextRequestId = Math.round(Math.random() * 1000000); private nextRequestId = Math.round(Math.random() * 1000000);
private responseCallbacks: ws.ServerResponseHandlers = {}; private responseCallbacks: ws.ServerResponseHandlers = {};
@ -94,9 +96,10 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
return this.connectionState.isServerConnected || false; 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.webSocketUrl = webSocketUrl;
this.tokenStore = tokenStore; this.tokenStore = tokenStore;
this.userStore = userStore;
this.connectionState.clientToServer = false; this.connectionState.clientToServer = false;
this.connectionState.serverToBroker = false; this.connectionState.serverToBroker = false;
} }
@ -138,8 +141,10 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
when(() => this.connectionState.clientToServer === true when(() => this.connectionState.clientToServer === true
&& this.tokenStore.accessToken.isValid, async () => { && this.tokenStore.accessToken.isValid, async () => {
try { try {
await this.authenticate(this.tokenStore.accessToken.token!); const res = await this.authenticate(this.tokenStore.accessToken.token!);
this.authenticated = true; this.authenticated = res.authenticated;
logger.info({ user: res.user }, "authenticated websocket connection");
this.userStore.userData = res.user;
} catch (err) { } catch (err) {
logger.error({ err }, "error authenticating websocket connection"); logger.error({ err }, "error authenticating websocket connection");
// TODO message? // TODO message?

4
client/state/AppState.ts

@ -5,6 +5,7 @@ import { RouterStore, syncHistoryWithStore } from "mobx-react-router";
import { WebSocketRpcClient } from "@client/sprinklersRpc/WebSocketRpcClient"; import { WebSocketRpcClient } from "@client/sprinklersRpc/WebSocketRpcClient";
import HttpApi from "@client/state/HttpApi"; import HttpApi from "@client/state/HttpApi";
import { UiStore } from "@client/state/UiStore"; import { UiStore } from "@client/state/UiStore";
import { UserStore } from "@client/state/UserStore";
import ApiError from "@common/ApiError"; import ApiError from "@common/ApiError";
import { ErrorCode } from "@common/ErrorCode"; import { ErrorCode } from "@common/ErrorCode";
import log from "@common/logger"; import log from "@common/logger";
@ -13,9 +14,10 @@ export default class AppState {
history: History = createBrowserHistory(); history: History = createBrowserHistory();
routerStore = new RouterStore(); routerStore = new RouterStore();
uiStore = new UiStore(); uiStore = new UiStore();
userStore = new UserStore();
httpApi = new HttpApi(); httpApi = new HttpApi();
tokenStore = this.httpApi.tokenStore; tokenStore = this.httpApi.tokenStore;
sprinklersRpc = new WebSocketRpcClient(this.tokenStore); sprinklersRpc = new WebSocketRpcClient(this.tokenStore, this.userStore);
@computed get isLoggedIn() { @computed get isLoggedIn() {
return this.tokenStore.accessToken.isValid; return this.tokenStore.accessToken.isValid;

5
client/state/UserStore.ts

@ -0,0 +1,5 @@
import { observable } from "mobx";
export class UserStore {
@observable userData: any = null;
}

2
common/sprinklersRpc/websocketData.ts

@ -25,7 +25,7 @@ export interface IClientRequestTypes {
export interface IAuthenticateResponse { export interface IAuthenticateResponse {
authenticated: boolean; authenticated: boolean;
message: string; message: string;
data?: any; user: any;
} }
export interface IDeviceSubscribeResponse { export interface IDeviceSubscribeResponse {

22
server/repositories/UserRepository.ts

@ -1,4 +1,4 @@
import { EntityRepository, Repository } from "typeorm"; import { EntityRepository, FindOneOptions, Repository } from "typeorm";
import { User } from "../entities"; import { User } from "../entities";
@ -6,23 +6,27 @@ export interface FindUserOptions {
devices: boolean; devices: boolean;
} }
function applyDefaultOptions(options?: Partial<FindUserOptions>): FindUserOptions { function applyDefaultOptions(options?: Partial<FindUserOptions>): FindOneOptions<User> {
return { devices: false, ...options }; const opts: FindUserOptions = { devices: false, ...options };
const relations = [opts.devices && "devices"]
.filter(Boolean) as string[];
return { relations };
} }
@EntityRepository(User) @EntityRepository(User)
export class UserRepository extends Repository<User> { export class UserRepository extends Repository<User> {
findAll(options?: Partial<FindUserOptions>) { findAll(options?: Partial<FindUserOptions>) {
const opts = applyDefaultOptions(options); const opts = applyDefaultOptions(options);
const relations = [ opts.devices && "devices" ] return super.find(opts);
.filter(Boolean) as string[]; }
return super.find({ relations });
findById(id: number, options?: Partial<FindUserOptions>) {
const opts = applyDefaultOptions(options);
return super.findOne(id, opts);
} }
findByUsername(username: string, options?: Partial<FindUserOptions>) { findByUsername(username: string, options?: Partial<FindUserOptions>) {
const opts = applyDefaultOptions(options); const opts = applyDefaultOptions(options);
const relations = [ opts.devices && "devices" ] return this.findOne({ username }, opts);
.filter(Boolean) as string[];
return this.findOne({ username }, { relations });
} }
} }

45
server/sprinklersRpc/websocketServer.ts

@ -8,8 +8,9 @@ import log from "@common/logger";
import * as deviceRequests from "@common/sprinklersRpc/deviceRequests"; 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 { TokenClaims, verifyToken } from "../express/authentication"; import { User } from "@server/entities";
import { ServerState } from "../state"; import { TokenClaims, verifyToken } from "@server/express/authentication";
import { ServerState } from "@server/state";
// tslint:disable:member-ordering // 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 /// This shall be the user id if the client has been authenticated, null otherwise
userId: number | null = null; userId: number | null = null;
user: User | null = null;
get state() { get state() {
return this.api.state; return this.api.state;
@ -52,12 +54,25 @@ export class WebSocketClient {
} }
private checkAuthorization() { private checkAuthorization() {
if (!this.userId) { if (!this.userId || !this.user) {
throw new ws.RpcError("this WebSocket session has not been authenticated", throw new ws.RpcError("this WebSocket session has not been authenticated",
ErrorCode.Unauthorized); 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 = { private requestHandlers: ws.ClientRequestHandlers = {
authenticate: async (data: ws.IAuthenticateRequest) => { authenticate: async (data: ws.IAuthenticateRequest) => {
if (!data.accessToken) { if (!data.accessToken) {
@ -73,27 +88,19 @@ export class WebSocketClient {
throw new ws.RpcError("not an access token", ErrorCode.BadToken); throw new ws.RpcError("not an access token", ErrorCode.BadToken);
} }
this.userId = decoded.aud; 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"); log.info({ userId: decoded.aud, name: decoded.name }, "authenticated websocket client");
this.subscribeBrokerConnection(); this.subscribeBrokerConnection();
return { return {
result: "success", result: "success",
data: { authenticated: true, message: "authenticated" }, data: { authenticated: true, message: "authenticated", user: this.user!.toJSON() },
}; };
}, },
deviceSubscribe: async (data: ws.IDeviceSubscribeRequest) => { deviceSubscribe: async (data: ws.IDeviceSubscribeRequest) => {
this.checkAuthorization(); this.checkAuthorization();
const userId = this.userId!; const userDevice = this.checkDevice(data.deviceId);
const deviceId = data.deviceId; const deviceId = userDevice.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",
},
};
}
if (this.deviceSubscriptions.indexOf(deviceId) === -1) { if (this.deviceSubscriptions.indexOf(deviceId) === -1) {
this.deviceSubscriptions.push(deviceId); this.deviceSubscriptions.push(deviceId);
const device = this.state.mqttClient.getDevice(deviceId); const device = this.state.mqttClient.getDevice(deviceId);
@ -204,10 +211,10 @@ export class WebSocketClient {
} }
private async doDeviceCallRequest(requestData: ws.IDeviceCallRequest): Promise<deviceRequests.Response> { private async doDeviceCallRequest(requestData: ws.IDeviceCallRequest): Promise<deviceRequests.Response> {
const { deviceId, data } = requestData; const userDevice = this.checkDevice(requestData.deviceId);
const deviceId = userDevice.deviceId!;
const device = this.state.mqttClient.getDevice(deviceId); const device = this.state.mqttClient.getDevice(deviceId);
// TODO: authorize the requests const request = schema.requests.deserializeRequest(requestData.data);
const request = schema.requests.deserializeRequest(data);
return device.makeRequest(request); return device.makeRequest(request);
} }
} }

Loading…
Cancel
Save