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 }: @@ -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 {

11
client/sprinklersRpc/WebSocketRpcClient.ts

@ -2,6 +2,7 @@ import { action, autorun, observable, when } from "mobx"; @@ -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 { @@ -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 { @@ -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 { @@ -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?

4
client/state/AppState.ts

@ -5,6 +5,7 @@ import { RouterStore, syncHistoryWithStore } from "mobx-react-router"; @@ -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 { @@ -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;

5
client/state/UserStore.ts

@ -0,0 +1,5 @@ @@ -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 { @@ -25,7 +25,7 @@ export interface IClientRequestTypes {
export interface IAuthenticateResponse {
authenticated: boolean;
message: string;
data?: any;
user: any;
}
export interface IDeviceSubscribeResponse {

22
server/repositories/UserRepository.ts

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

45
server/sprinklersRpc/websocketServer.ts

@ -8,8 +8,9 @@ import log from "@common/logger"; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -204,10 +211,10 @@ export class WebSocketClient {
}
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);
// TODO: authorize the requests
const request = schema.requests.deserializeRequest(data);
const request = schema.requests.deserializeRequest(requestData.data);
return device.makeRequest(request);
}
}

Loading…
Cancel
Save