Fixed authentication and checking device authorization
This commit is contained in:
parent
ab0756d01e
commit
b59fbb456b
@ -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 {
|
||||||
|
@ -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?
|
||||||
|
@ -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
Normal file
5
client/state/UserStore.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { observable } from "mobx";
|
||||||
|
|
||||||
|
export class UserStore {
|
||||||
|
@observable userData: any = null;
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user