add security to all admin endpoints
This commit is contained in:
parent
71e5830d6c
commit
73433586b6
@ -47,7 +47,7 @@ export function redirectingAuthorizedFetch(url, params) {
|
|||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
authorizedFetch(url, params).then(response => {
|
authorizedFetch(url, params).then(response => {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
history.go("/");
|
history.push("/");
|
||||||
} else {
|
} else {
|
||||||
resolve(response);
|
resolve(response);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {withNotifier} from '../components/SnackbarNotification';
|
import { withNotifier } from '../components/SnackbarNotification';
|
||||||
|
import { redirectingAuthorizedFetch } from '../authentication/Authentication';
|
||||||
/*
|
/*
|
||||||
* It is unlikely this application will grow complex enough to require redux.
|
* It is unlikely this application will grow complex enough to require redux.
|
||||||
*
|
*
|
||||||
@ -16,11 +16,11 @@ export const restComponent = (endpointUrl, FormComponent) => {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state={
|
this.state = {
|
||||||
data:null,
|
data: null,
|
||||||
fetched: false,
|
fetched: false,
|
||||||
errorMessage:null
|
errorMessage: null
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setState = this.setState.bind(this);
|
this.setState = this.setState.bind(this);
|
||||||
this.loadData = this.loadData.bind(this);
|
this.loadData = this.loadData.bind(this);
|
||||||
@ -30,78 +30,78 @@ export const restComponent = (endpointUrl, FormComponent) => {
|
|||||||
|
|
||||||
setData(data) {
|
setData(data) {
|
||||||
this.setState({
|
this.setState({
|
||||||
data:data,
|
data: data,
|
||||||
fetched: true,
|
fetched: true,
|
||||||
errorMessage:null
|
errorMessage: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadData() {
|
loadData() {
|
||||||
this.setState({
|
this.setState({
|
||||||
data:null,
|
data: null,
|
||||||
fetched: false,
|
fetched: false,
|
||||||
errorMessage:null
|
errorMessage: null
|
||||||
});
|
});
|
||||||
fetch(endpointUrl)
|
redirectingAuthorizedFetch(endpointUrl)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
throw Error("Invalid status code: " + response.status);
|
throw Error("Invalid status code: " + response.status);
|
||||||
})
|
})
|
||||||
.then(json => {this.setState({data: json, fetched:true})})
|
.then(json => { this.setState({ data: json, fetched: true }) })
|
||||||
.catch(error =>{
|
.catch(error => {
|
||||||
this.props.raiseNotification("Problem fetching: " + error.message);
|
this.props.raiseNotification("Problem fetching: " + error.message);
|
||||||
this.setState({data: null, fetched:true, errorMessage:error.message});
|
this.setState({ data: null, fetched: true, errorMessage: error.message });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
saveData(e) {
|
saveData(e) {
|
||||||
this.setState({fetched: false});
|
this.setState({ fetched: false });
|
||||||
fetch(endpointUrl, {
|
redirectingAuthorizedFetch(endpointUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(this.state.data),
|
body: JSON.stringify(this.state.data),
|
||||||
headers: new Headers({
|
headers: new Headers({
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
throw Error("Invalid status code: " + response.status);
|
throw Error("Invalid status code: " + response.status);
|
||||||
})
|
})
|
||||||
.then(json => {
|
.then(json => {
|
||||||
this.props.raiseNotification("Changes successfully applied.");
|
this.props.raiseNotification("Changes successfully applied.");
|
||||||
this.setState({data: json, fetched:true});
|
this.setState({ data: json, fetched: true });
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
this.props.raiseNotification("Problem saving: " + error.message);
|
this.props.raiseNotification("Problem saving: " + error.message);
|
||||||
this.setState({data: null, fetched:true, errorMessage:error.message});
|
this.setState({ data: null, fetched: true, errorMessage: error.message });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleValueChange = name => event => {
|
handleValueChange = name => event => {
|
||||||
const { data } = this.state;
|
const { data } = this.state;
|
||||||
data[name] = event.target.value;
|
data[name] = event.target.value;
|
||||||
this.setState({data});
|
this.setState({ data });
|
||||||
};
|
};
|
||||||
|
|
||||||
handleCheckboxChange = name => event => {
|
handleCheckboxChange = name => event => {
|
||||||
const { data } = this.state;
|
const { data } = this.state;
|
||||||
data[name] = event.target.checked;
|
data[name] = event.target.checked;
|
||||||
this.setState({data});
|
this.setState({ data });
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <FormComponent
|
return <FormComponent
|
||||||
handleValueChange={this.handleValueChange}
|
handleValueChange={this.handleValueChange}
|
||||||
handleCheckboxChange={this.handleCheckboxChange}
|
handleCheckboxChange={this.handleCheckboxChange}
|
||||||
setData={this.setData}
|
setData={this.setData}
|
||||||
saveData={this.saveData}
|
saveData={this.saveData}
|
||||||
loadData={this.loadData}
|
loadData={this.loadData}
|
||||||
{...this.state}
|
{...this.state}
|
||||||
{...this.props}
|
{...this.props}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { SCAN_NETWORKS_ENDPOINT, LIST_NETWORKS_ENDPOINT } from '../constants/E
|
|||||||
import SectionContent from '../components/SectionContent';
|
import SectionContent from '../components/SectionContent';
|
||||||
import WiFiNetworkSelector from '../forms/WiFiNetworkSelector';
|
import WiFiNetworkSelector from '../forms/WiFiNetworkSelector';
|
||||||
import {withNotifier} from '../components/SnackbarNotification';
|
import {withNotifier} from '../components/SnackbarNotification';
|
||||||
|
import { redirectingAuthorizedFetch } from '../authentication/Authentication';
|
||||||
|
|
||||||
const NUM_POLLS = 10
|
const NUM_POLLS = 10
|
||||||
const POLLING_FREQUENCY = 500
|
const POLLING_FREQUENCY = 500
|
||||||
@ -38,7 +39,7 @@ class WiFiNetworkScanner extends Component {
|
|||||||
scanNetworks() {
|
scanNetworks() {
|
||||||
this.pollCount = 0;
|
this.pollCount = 0;
|
||||||
this.setState({scanningForNetworks:true, networkList: null, errorMessage:null});
|
this.setState({scanningForNetworks:true, networkList: null, errorMessage:null});
|
||||||
fetch(SCAN_NETWORKS_ENDPOINT).then(response => {
|
redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT).then(response => {
|
||||||
if (response.status === 202) {
|
if (response.status === 202) {
|
||||||
this.schedulePollTimeout();
|
this.schedulePollTimeout();
|
||||||
return;
|
return;
|
||||||
@ -70,7 +71,7 @@ class WiFiNetworkScanner extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pollNetworkList() {
|
pollNetworkList() {
|
||||||
fetch(LIST_NETWORKS_ENDPOINT)
|
redirectingAuthorizedFetch(LIST_NETWORKS_ENDPOINT)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#include <APSettingsService.h>
|
#include <APSettingsService.h>
|
||||||
|
|
||||||
APSettingsService::APSettingsService(AsyncWebServer* server, FS* fs) : SettingsService(server, fs, AP_SETTINGS_SERVICE_PATH, AP_SETTINGS_FILE) {
|
APSettingsService::APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : AdminSettingsService(server, fs, securityManager, AP_SETTINGS_SERVICE_PATH, AP_SETTINGS_FILE) {
|
||||||
onConfigUpdated();
|
onConfigUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,11 +19,11 @@
|
|||||||
#define AP_SETTINGS_FILE "/config/apSettings.json"
|
#define AP_SETTINGS_FILE "/config/apSettings.json"
|
||||||
#define AP_SETTINGS_SERVICE_PATH "/rest/apSettings"
|
#define AP_SETTINGS_SERVICE_PATH "/rest/apSettings"
|
||||||
|
|
||||||
class APSettingsService : public SettingsService {
|
class APSettingsService : public AdminSettingsService {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
APSettingsService(AsyncWebServer* server, FS* fs);
|
APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager);
|
||||||
~APSettingsService();
|
~APSettingsService();
|
||||||
|
|
||||||
void loop();
|
void loop();
|
||||||
|
@ -3,7 +3,11 @@
|
|||||||
ArduinoJsonJWT::ArduinoJsonJWT(String secret) : _secret(secret) { }
|
ArduinoJsonJWT::ArduinoJsonJWT(String secret) : _secret(secret) { }
|
||||||
|
|
||||||
void ArduinoJsonJWT::setSecret(String secret){
|
void ArduinoJsonJWT::setSecret(String secret){
|
||||||
_secret = secret;
|
_secret = secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
String ArduinoJsonJWT::getSecret(){
|
||||||
|
return _secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -31,6 +31,8 @@ public:
|
|||||||
ArduinoJsonJWT(String secret);
|
ArduinoJsonJWT(String secret);
|
||||||
|
|
||||||
void setSecret(String secret);
|
void setSecret(String secret);
|
||||||
|
String getSecret();
|
||||||
|
|
||||||
String buildJWT(JsonObject &payload);
|
String buildJWT(JsonObject &payload);
|
||||||
void parseJWT(String jwt, JsonDocument &jsonDocument);
|
void parseJWT(String jwt, JsonDocument &jsonDocument);
|
||||||
};
|
};
|
||||||
|
@ -1,52 +0,0 @@
|
|||||||
#ifndef AsyncAuthJsonWebHandler_H_
|
|
||||||
#define AsyncAuthJsonWebHandler_H_
|
|
||||||
|
|
||||||
#include <ESPAsyncWebServer.h>
|
|
||||||
#include <AsyncJsonWebHandler.h>
|
|
||||||
#include <ArduinoJson.h>
|
|
||||||
#include <SecurityManager.h>
|
|
||||||
|
|
||||||
typedef std::function<void(AsyncWebServerRequest *request, JsonDocument &jsonDocument, Authentication &authentication)> AuthenticationJsonRequestCallback;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extends AsyncJsonWebHandler with a wrapper which verifies the user is authenticated.
|
|
||||||
*
|
|
||||||
* TODO - Extend with role checking support, possibly with a callback to verify the user.
|
|
||||||
*/
|
|
||||||
class AsyncAuthJsonWebHandler: public AsyncJsonWebHandler {
|
|
||||||
|
|
||||||
private:
|
|
||||||
SecurityManager *_securityManager;
|
|
||||||
using AsyncJsonWebHandler::onRequest;
|
|
||||||
|
|
||||||
public:
|
|
||||||
|
|
||||||
AsyncAuthJsonWebHandler() :
|
|
||||||
AsyncJsonWebHandler(), _securityManager(NULL) {}
|
|
||||||
|
|
||||||
~AsyncAuthJsonWebHandler() {}
|
|
||||||
|
|
||||||
void setSecurityManager(SecurityManager *securityManager) {
|
|
||||||
_securityManager = securityManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onRequest(AuthenticationJsonRequestCallback callback) {
|
|
||||||
AsyncJsonWebHandler::onRequest([this, callback](AsyncWebServerRequest *request, JsonDocument &jsonDocument) {
|
|
||||||
if(!_securityManager) {
|
|
||||||
Serial.print("Security manager not configured for endpoint: ");
|
|
||||||
Serial.println(_uri);
|
|
||||||
request->send(500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Authentication authentication = _securityManager->authenticateRequest(request);
|
|
||||||
if (!authentication.isAuthenticated()) {
|
|
||||||
request->send(401);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
callback(request, jsonDocument, authentication);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // end AsyncAuthJsonWebHandler_H_
|
|
@ -6,7 +6,7 @@ AuthenticationService::AuthenticationService(AsyncWebServer* server, SecurityMan
|
|||||||
|
|
||||||
_signInHandler.setUri(SIGN_IN_PATH);
|
_signInHandler.setUri(SIGN_IN_PATH);
|
||||||
_signInHandler.setMethod(HTTP_POST);
|
_signInHandler.setMethod(HTTP_POST);
|
||||||
_signInHandler.setMaxContentLength(MAX_SECURITY_MANAGER_SETTINGS_SIZE);
|
_signInHandler.setMaxContentLength(MAX_AUTHENTICATION_SIZE);
|
||||||
_signInHandler.onRequest(std::bind(&AuthenticationService::signIn, this, std::placeholders::_1, std::placeholders::_2));
|
_signInHandler.onRequest(std::bind(&AuthenticationService::signIn, this, std::placeholders::_1, std::placeholders::_2));
|
||||||
server->addHandler(&_signInHandler);
|
server->addHandler(&_signInHandler);
|
||||||
}
|
}
|
||||||
@ -31,7 +31,7 @@ void AuthenticationService::signIn(AsyncWebServerRequest *request, JsonDocument
|
|||||||
Authentication authentication = _securityManager->authenticate(username, password);
|
Authentication authentication = _securityManager->authenticate(username, password);
|
||||||
if (authentication.isAuthenticated()) {
|
if (authentication.isAuthenticated()) {
|
||||||
User* user = authentication.getUser();
|
User* user = authentication.getUser();
|
||||||
AsyncJsonResponse * response = new AsyncJsonResponse(MAX_USERS_SIZE);
|
AsyncJsonResponse * response = new AsyncJsonResponse(MAX_AUTHENTICATION_SIZE);
|
||||||
JsonObject jsonObject = response->getRoot();
|
JsonObject jsonObject = response->getRoot();
|
||||||
jsonObject["access_token"] = _securityManager->generateJWT(user);
|
jsonObject["access_token"] = _securityManager->generateJWT(user);
|
||||||
response->setLength();
|
response->setLength();
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
#define AuthenticationService_H_
|
#define AuthenticationService_H_
|
||||||
|
|
||||||
#include <SecurityManager.h>
|
#include <SecurityManager.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <AsyncJsonWebHandler.h>
|
||||||
|
#include <AsyncArduinoJson6.h>
|
||||||
|
|
||||||
#define VERIFY_AUTHORIZATION_PATH "/rest/verifyAuthorization"
|
#define VERIFY_AUTHORIZATION_PATH "/rest/verifyAuthorization"
|
||||||
#define SIGN_IN_PATH "/rest/signIn"
|
#define SIGN_IN_PATH "/rest/signIn"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#include <NTPSettingsService.h>
|
#include <NTPSettingsService.h>
|
||||||
|
|
||||||
NTPSettingsService::NTPSettingsService(AsyncWebServer* server, FS* fs) : SettingsService(server, fs, NTP_SETTINGS_SERVICE_PATH, NTP_SETTINGS_FILE) {
|
NTPSettingsService::NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : AdminSettingsService(server, fs, securityManager, NTP_SETTINGS_SERVICE_PATH, NTP_SETTINGS_FILE) {
|
||||||
|
|
||||||
#if defined(ESP8266)
|
#if defined(ESP8266)
|
||||||
_onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected(std::bind(&NTPSettingsService::onStationModeDisconnected, this, std::placeholders::_1));
|
_onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected(std::bind(&NTPSettingsService::onStationModeDisconnected, this, std::placeholders::_1));
|
||||||
|
@ -17,11 +17,11 @@
|
|||||||
#define NTP_SETTINGS_FILE "/config/ntpSettings.json"
|
#define NTP_SETTINGS_FILE "/config/ntpSettings.json"
|
||||||
#define NTP_SETTINGS_SERVICE_PATH "/rest/ntpSettings"
|
#define NTP_SETTINGS_SERVICE_PATH "/rest/ntpSettings"
|
||||||
|
|
||||||
class NTPSettingsService : public SettingsService {
|
class NTPSettingsService : public AdminSettingsService {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
NTPSettingsService(AsyncWebServer* server, FS* fs);
|
NTPSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager);
|
||||||
~NTPSettingsService();
|
~NTPSettingsService();
|
||||||
|
|
||||||
void loop();
|
void loop();
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#include <OTASettingsService.h>
|
#include <OTASettingsService.h>
|
||||||
|
|
||||||
OTASettingsService::OTASettingsService(AsyncWebServer* server, FS* fs) : SettingsService(server, fs, OTA_SETTINGS_SERVICE_PATH, OTA_SETTINGS_FILE) {
|
OTASettingsService::OTASettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : AdminSettingsService(server, fs, securityManager, OTA_SETTINGS_SERVICE_PATH, OTA_SETTINGS_FILE) {
|
||||||
#if defined(ESP8266)
|
#if defined(ESP8266)
|
||||||
_onStationModeGotIPHandler = WiFi.onStationModeGotIP(std::bind(&OTASettingsService::onStationModeGotIP, this, std::placeholders::_1));
|
_onStationModeGotIPHandler = WiFi.onStationModeGotIP(std::bind(&OTASettingsService::onStationModeGotIP, this, std::placeholders::_1));
|
||||||
#elif defined(ESP_PLATFORM)
|
#elif defined(ESP_PLATFORM)
|
||||||
|
@ -19,11 +19,11 @@
|
|||||||
#define OTA_SETTINGS_FILE "/config/otaSettings.json"
|
#define OTA_SETTINGS_FILE "/config/otaSettings.json"
|
||||||
#define OTA_SETTINGS_SERVICE_PATH "/rest/otaSettings"
|
#define OTA_SETTINGS_SERVICE_PATH "/rest/otaSettings"
|
||||||
|
|
||||||
class OTASettingsService : public SettingsService {
|
class OTASettingsService : public AdminSettingsService {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
OTASettingsService(AsyncWebServer* server, FS* fs);
|
OTASettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager);
|
||||||
~OTASettingsService();
|
~OTASettingsService();
|
||||||
|
|
||||||
void loop();
|
void loop();
|
||||||
|
@ -1,41 +1,5 @@
|
|||||||
#include <SecurityManager.h>
|
#include <SecurityManager.h>
|
||||||
|
|
||||||
SecurityManager::SecurityManager(AsyncWebServer* server, FS* fs) : SettingsService(server, fs, SECURITY_SETTINGS_PATH, SECURITY_SETTINGS_FILE) {}
|
|
||||||
SecurityManager::~SecurityManager() {}
|
|
||||||
|
|
||||||
void SecurityManager::readFromJsonObject(JsonObject& root) {
|
|
||||||
// secret
|
|
||||||
_jwtSecret = root["jwt_secret"] | DEFAULT_JWT_SECRET;
|
|
||||||
_jwtHandler.setSecret(_jwtSecret);
|
|
||||||
|
|
||||||
// users
|
|
||||||
_users.clear();
|
|
||||||
if (root["users"].is<JsonArray>()) {
|
|
||||||
for (JsonVariant user : root["users"].as<JsonArray>()) {
|
|
||||||
_users.push_back(User(user["username"], user["password"], user["admin"]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void SecurityManager::writeToJsonObject(JsonObject& root) {
|
|
||||||
// secret
|
|
||||||
root["jwt_secret"] = _jwtSecret;
|
|
||||||
|
|
||||||
// users
|
|
||||||
JsonArray users = root.createNestedArray("users");
|
|
||||||
for (User _user : _users) {
|
|
||||||
JsonObject user = users.createNestedObject();
|
|
||||||
user["username"] = _user.getUsername();
|
|
||||||
user["password"] = _user.getPassword();
|
|
||||||
user["admin"] = _user.isAdmin();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SecurityManager::begin() {
|
|
||||||
readFromFS();
|
|
||||||
}
|
|
||||||
|
|
||||||
Authentication SecurityManager::authenticateRequest(AsyncWebServerRequest *request) {
|
Authentication SecurityManager::authenticateRequest(AsyncWebServerRequest *request) {
|
||||||
AsyncWebHeader* authorizationHeader = request->getHeader(AUTHORIZATION_HEADER);
|
AsyncWebHeader* authorizationHeader = request->getHeader(AUTHORIZATION_HEADER);
|
||||||
if (authorizationHeader) {
|
if (authorizationHeader) {
|
||||||
@ -90,3 +54,15 @@ String SecurityManager::generateJWT(User *user) {
|
|||||||
populateJWTPayload(payload, user);
|
populateJWTPayload(payload, user);
|
||||||
return _jwtHandler.buildJWT(payload);
|
return _jwtHandler.buildJWT(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ArRequestHandlerFunction SecurityManager::wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate) {
|
||||||
|
return [this, onRequest, predicate](AsyncWebServerRequest *request){
|
||||||
|
Authentication authentication = authenticateRequest(request);
|
||||||
|
if (!predicate(authentication)) {
|
||||||
|
request->send(401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onRequest(request);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -2,27 +2,16 @@
|
|||||||
#define SecurityManager_h
|
#define SecurityManager_h
|
||||||
|
|
||||||
#include <list>
|
#include <list>
|
||||||
|
|
||||||
#include <SettingsService.h>
|
|
||||||
#include <DNSServer.h>
|
|
||||||
#include <IPAddress.h>
|
|
||||||
#include <ArduinoJsonJWT.h>
|
#include <ArduinoJsonJWT.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
|
||||||
#define DEFAULT_JWT_SECRET "esp8266-react"
|
#define DEFAULT_JWT_SECRET "esp8266-react"
|
||||||
|
|
||||||
#define SECURITY_SETTINGS_FILE "/config/securitySettings.json"
|
|
||||||
|
|
||||||
#define SECURITY_SETTINGS_PATH "/rest/securitySettings"
|
|
||||||
|
|
||||||
#define AUTHORIZATION_HEADER "Authorization"
|
#define AUTHORIZATION_HEADER "Authorization"
|
||||||
#define AUTHORIZATION_HEADER_PREFIX "Bearer "
|
#define AUTHORIZATION_HEADER_PREFIX "Bearer "
|
||||||
#define AUTHORIZATION_HEADER_PREFIX_LEN 7
|
#define AUTHORIZATION_HEADER_PREFIX_LEN 7
|
||||||
|
|
||||||
#define MAX_JWT_SIZE 128
|
#define MAX_JWT_SIZE 128
|
||||||
#define MAX_SECURITY_MANAGER_SETTINGS_SIZE 512
|
|
||||||
#define SECURITY_MANAGER_MAX_USERS 5
|
|
||||||
|
|
||||||
#define MAX_USERS_SIZE 1024
|
|
||||||
|
|
||||||
class User {
|
class User {
|
||||||
private:
|
private:
|
||||||
@ -62,15 +51,25 @@ class Authentication {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class SecurityManager : public SettingsService {
|
typedef std::function<boolean(Authentication &authentication)> AuthenticationPredicate;
|
||||||
|
|
||||||
|
class AuthenticationPredicates {
|
||||||
|
public:
|
||||||
|
static bool NONE_REQUIRED(Authentication &authentication) {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
static bool IS_AUTHENTICATED(Authentication &authentication) {
|
||||||
|
return authentication.isAuthenticated();
|
||||||
|
};
|
||||||
|
static bool IS_ADMIN(Authentication &authentication) {
|
||||||
|
return authentication.isAuthenticated() && authentication.getUser()->isAdmin();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
class SecurityManager {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
SecurityManager(AsyncWebServer* server, FS* fs);
|
|
||||||
~SecurityManager();
|
|
||||||
|
|
||||||
void begin();
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Authenticate, returning the user if found
|
* Authenticate, returning the user if found
|
||||||
*/
|
*/
|
||||||
@ -86,21 +85,17 @@ class SecurityManager : public SettingsService {
|
|||||||
*/
|
*/
|
||||||
String generateJWT(User *user);
|
String generateJWT(User *user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap the provided request to provide validation against an AuthenticationPredicate.
|
||||||
|
*/
|
||||||
|
ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
||||||
void readFromJsonObject(JsonObject& root);
|
|
||||||
void writeToJsonObject(JsonObject& root);
|
|
||||||
|
|
||||||
private:
|
|
||||||
// jwt handler
|
|
||||||
ArduinoJsonJWT _jwtHandler = ArduinoJsonJWT(DEFAULT_JWT_SECRET);
|
ArduinoJsonJWT _jwtHandler = ArduinoJsonJWT(DEFAULT_JWT_SECRET);
|
||||||
|
|
||||||
// access point settings
|
|
||||||
String _jwtSecret;
|
|
||||||
std::list<User> _users;
|
std::list<User> _users;
|
||||||
|
|
||||||
// endpoint functions
|
private:
|
||||||
void fetchUsers(AsyncWebServerRequest *request);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Lookup the user by JWT
|
* Lookup the user by JWT
|
||||||
|
35
src/SecuritySettingsService.cpp
Normal file
35
src/SecuritySettingsService.cpp
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#include <SecuritySettingsService.h>
|
||||||
|
|
||||||
|
SecuritySettingsService::SecuritySettingsService(AsyncWebServer* server, FS* fs) : AdminSettingsService(server, fs, this, SECURITY_SETTINGS_PATH, SECURITY_SETTINGS_FILE), SecurityManager() {}
|
||||||
|
SecuritySettingsService::~SecuritySettingsService() {}
|
||||||
|
|
||||||
|
void SecuritySettingsService::readFromJsonObject(JsonObject& root) {
|
||||||
|
// secret
|
||||||
|
_jwtHandler.setSecret(root["jwt_secret"] | DEFAULT_JWT_SECRET);
|
||||||
|
|
||||||
|
// users
|
||||||
|
_users.clear();
|
||||||
|
if (root["users"].is<JsonArray>()) {
|
||||||
|
for (JsonVariant user : root["users"].as<JsonArray>()) {
|
||||||
|
_users.push_back(User(user["username"], user["password"], user["admin"]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SecuritySettingsService::writeToJsonObject(JsonObject& root) {
|
||||||
|
// secret
|
||||||
|
root["jwt_secret"] = _jwtHandler.getSecret();
|
||||||
|
|
||||||
|
// users
|
||||||
|
JsonArray users = root.createNestedArray("users");
|
||||||
|
for (User _user : _users) {
|
||||||
|
JsonObject user = users.createNestedObject();
|
||||||
|
user["username"] = _user.getUsername();
|
||||||
|
user["password"] = _user.getPassword();
|
||||||
|
user["admin"] = _user.isAdmin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SecuritySettingsService::begin() {
|
||||||
|
readFromFS();
|
||||||
|
}
|
26
src/SecuritySettingsService.h
Normal file
26
src/SecuritySettingsService.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#ifndef SecuritySettingsService_h
|
||||||
|
#define SecuritySettingsService_h
|
||||||
|
|
||||||
|
#include <SettingsService.h>
|
||||||
|
#include <SecurityManager.h>
|
||||||
|
|
||||||
|
#define SECURITY_SETTINGS_FILE "/config/securitySettings.json"
|
||||||
|
#define SECURITY_SETTINGS_PATH "/rest/securitySettings"
|
||||||
|
|
||||||
|
class SecuritySettingsService : public AdminSettingsService, public SecurityManager {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
SecuritySettingsService(AsyncWebServer* server, FS* fs);
|
||||||
|
~SecuritySettingsService();
|
||||||
|
|
||||||
|
void begin();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
|
||||||
|
void readFromJsonObject(JsonObject& root);
|
||||||
|
void writeToJsonObject(JsonObject& root);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // end SecuritySettingsService_h
|
@ -9,54 +9,19 @@
|
|||||||
#include <AsyncTCP.h>
|
#include <AsyncTCP.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <SecurityManager.h>
|
||||||
#include <SettingsPersistence.h>
|
#include <SettingsPersistence.h>
|
||||||
#include <ESPAsyncWebServer.h>
|
#include <ESPAsyncWebServer.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <AsyncJsonWebHandler.h>
|
#include <AsyncJsonWebHandler.h>
|
||||||
#include <AsyncArduinoJson6.h>
|
#include <AsyncArduinoJson6.h>
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Abstraction of a service which stores it's settings as JSON in a file system.
|
* Abstraction of a service which stores it's settings as JSON in a file system.
|
||||||
*/
|
*/
|
||||||
class SettingsService : public SettingsPersistence {
|
class SettingsService : public SettingsPersistence {
|
||||||
|
|
||||||
private:
|
|
||||||
|
|
||||||
AsyncJsonWebHandler _updateHandler;
|
|
||||||
|
|
||||||
void fetchConfig(AsyncWebServerRequest *request){
|
|
||||||
AsyncJsonResponse * response = new AsyncJsonResponse(MAX_SETTINGS_SIZE);
|
|
||||||
JsonObject jsonObject = response->getRoot();
|
|
||||||
writeToJsonObject(jsonObject);
|
|
||||||
response->setLength();
|
|
||||||
request->send(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateConfig(AsyncWebServerRequest *request, JsonDocument &jsonDocument) {
|
|
||||||
if (jsonDocument.is<JsonObject>()){
|
|
||||||
JsonObject newConfig = jsonDocument.as<JsonObject>();
|
|
||||||
readFromJsonObject(newConfig);
|
|
||||||
writeToFS();
|
|
||||||
|
|
||||||
// write settings back with a callback to reconfigure the wifi
|
|
||||||
AsyncJsonCallbackResponse * response = new AsyncJsonCallbackResponse([this] () {onConfigUpdated();}, MAX_SETTINGS_SIZE);
|
|
||||||
JsonObject jsonObject = response->getRoot();
|
|
||||||
writeToJsonObject(jsonObject);
|
|
||||||
response->setLength();
|
|
||||||
request->send(response);
|
|
||||||
} else {
|
|
||||||
request->send(400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
|
|
||||||
// will serve setting endpoints from here
|
|
||||||
AsyncWebServer* _server;
|
|
||||||
|
|
||||||
// implement to perform action when config has been updated
|
|
||||||
virtual void onConfigUpdated(){}
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
SettingsService(AsyncWebServer* server, FS* fs, char const* servicePath, char const* filePath):
|
SettingsService(AsyncWebServer* server, FS* fs, char const* servicePath, char const* filePath):
|
||||||
@ -79,6 +44,81 @@ private:
|
|||||||
readFromFS();
|
readFromFS();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// will serve setting endpoints from here
|
||||||
|
AsyncWebServer* _server;
|
||||||
|
|
||||||
|
AsyncJsonWebHandler _updateHandler;
|
||||||
|
|
||||||
|
virtual void fetchConfig(AsyncWebServerRequest *request) {
|
||||||
|
// handle the request
|
||||||
|
AsyncJsonResponse * response = new AsyncJsonResponse(MAX_SETTINGS_SIZE);
|
||||||
|
JsonObject jsonObject = response->getRoot();
|
||||||
|
writeToJsonObject(jsonObject);
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void updateConfig(AsyncWebServerRequest *request, JsonDocument &jsonDocument) {
|
||||||
|
// handle the request
|
||||||
|
if (jsonDocument.is<JsonObject>()){
|
||||||
|
JsonObject newConfig = jsonDocument.as<JsonObject>();
|
||||||
|
readFromJsonObject(newConfig);
|
||||||
|
writeToFS();
|
||||||
|
|
||||||
|
// write settings back with a callback to reconfigure the wifi
|
||||||
|
AsyncJsonCallbackResponse * response = new AsyncJsonCallbackResponse([this] () {onConfigUpdated();}, MAX_SETTINGS_SIZE);
|
||||||
|
JsonObject jsonObject = response->getRoot();
|
||||||
|
writeToJsonObject(jsonObject);
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
} else {
|
||||||
|
request->send(400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// implement to perform action when config has been updated
|
||||||
|
virtual void onConfigUpdated(){}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class AdminSettingsService : public SettingsService {
|
||||||
|
public:
|
||||||
|
AdminSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager, char const* servicePath, char const* filePath):
|
||||||
|
SettingsService(server, fs, servicePath, filePath), _securityManager(securityManager) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// will validate the requests with the security manager
|
||||||
|
SecurityManager* _securityManager;
|
||||||
|
|
||||||
|
void fetchConfig(AsyncWebServerRequest *request) {
|
||||||
|
// verify the request against the predicate
|
||||||
|
Authentication authentication = _securityManager->authenticateRequest(request);
|
||||||
|
if (!getAuthenticationPredicate()(authentication)) {
|
||||||
|
request->send(401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// delegate to underlying implemetation
|
||||||
|
SettingsService::fetchConfig(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateConfig(AsyncWebServerRequest *request, JsonDocument &jsonDocument) {
|
||||||
|
// verify the request against the predicate
|
||||||
|
Authentication authentication = _securityManager->authenticateRequest(request);
|
||||||
|
if (!getAuthenticationPredicate()(authentication)) {
|
||||||
|
request->send(401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// delegate to underlying implemetation
|
||||||
|
SettingsService::updateConfig(request, jsonDocument);
|
||||||
|
}
|
||||||
|
|
||||||
|
// override to override the default authentication predicate, IS_ADMIN
|
||||||
|
AuthenticationPredicate getAuthenticationPredicate() {
|
||||||
|
return AuthenticationPredicates::IS_ADMIN;
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // end SettingsService
|
#endif // end SettingsService
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
#include <WiFiScanner.h>
|
#include <WiFiScanner.h>
|
||||||
|
|
||||||
WiFiScanner::WiFiScanner(AsyncWebServer *server) : _server(server) {
|
WiFiScanner::WiFiScanner(AsyncWebServer *server, SecurityManager* securityManager) : _server(server) {
|
||||||
_server->on(SCAN_NETWORKS_SERVICE_PATH, HTTP_GET, std::bind(&WiFiScanner::scanNetworks, this, std::placeholders::_1));
|
_server->on(SCAN_NETWORKS_SERVICE_PATH, HTTP_GET,
|
||||||
_server->on(LIST_NETWORKS_SERVICE_PATH, HTTP_GET, std::bind(&WiFiScanner::listNetworks, this, std::placeholders::_1));
|
securityManager->wrapRequest(std::bind(&WiFiScanner::scanNetworks, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN)
|
||||||
|
);
|
||||||
|
_server->on(LIST_NETWORKS_SERVICE_PATH, HTTP_GET,
|
||||||
|
securityManager->wrapRequest(std::bind(&WiFiScanner::listNetworks, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WiFiScanner::scanNetworks(AsyncWebServerRequest *request) {
|
void WiFiScanner::scanNetworks(AsyncWebServerRequest *request) {
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <AsyncArduinoJson6.h>
|
#include <AsyncArduinoJson6.h>
|
||||||
#include <TimeLib.h>
|
#include <TimeLib.h>
|
||||||
|
#include <SecurityManager.h>
|
||||||
|
|
||||||
#define SCAN_NETWORKS_SERVICE_PATH "/rest/scanNetworks"
|
#define SCAN_NETWORKS_SERVICE_PATH "/rest/scanNetworks"
|
||||||
#define LIST_NETWORKS_SERVICE_PATH "/rest/listNetworks"
|
#define LIST_NETWORKS_SERVICE_PATH "/rest/listNetworks"
|
||||||
@ -23,7 +24,7 @@ class WiFiScanner {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
WiFiScanner(AsyncWebServer *server);
|
WiFiScanner(AsyncWebServer *server, SecurityManager* securityManager);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#include <WiFiSettingsService.h>
|
#include <WiFiSettingsService.h>
|
||||||
|
|
||||||
WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs) : SettingsService(server, fs, WIFI_SETTINGS_SERVICE_PATH, WIFI_SETTINGS_FILE) {}
|
WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : AdminSettingsService(server, fs, securityManager, WIFI_SETTINGS_SERVICE_PATH, WIFI_SETTINGS_FILE) {}
|
||||||
|
|
||||||
WiFiSettingsService::~WiFiSettingsService() {}
|
WiFiSettingsService::~WiFiSettingsService() {}
|
||||||
|
|
||||||
|
@ -7,11 +7,11 @@
|
|||||||
#define WIFI_SETTINGS_FILE "/config/wifiSettings.json"
|
#define WIFI_SETTINGS_FILE "/config/wifiSettings.json"
|
||||||
#define WIFI_SETTINGS_SERVICE_PATH "/rest/wifiSettings"
|
#define WIFI_SETTINGS_SERVICE_PATH "/rest/wifiSettings"
|
||||||
|
|
||||||
class WiFiSettingsService : public SettingsService {
|
class WiFiSettingsService : public AdminSettingsService {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
WiFiSettingsService(AsyncWebServer* server, FS* fs);
|
WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager);
|
||||||
~WiFiSettingsService();
|
~WiFiSettingsService();
|
||||||
|
|
||||||
void begin();
|
void begin();
|
||||||
|
21
src/main.cpp
21
src/main.cpp
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
#include <FS.h>
|
#include <FS.h>
|
||||||
|
|
||||||
#include <SecurityManager.h>
|
#include <SecuritySettingsService.h>
|
||||||
#include <WiFiSettingsService.h>
|
#include <WiFiSettingsService.h>
|
||||||
#include <APSettingsService.h>
|
#include <APSettingsService.h>
|
||||||
#include <NTPSettingsService.h>
|
#include <NTPSettingsService.h>
|
||||||
@ -27,15 +27,14 @@
|
|||||||
|
|
||||||
AsyncWebServer server(80);
|
AsyncWebServer server(80);
|
||||||
|
|
||||||
SecurityManager securityManager = SecurityManager(&server, &SPIFFS);
|
SecuritySettingsService securitySettingsService = SecuritySettingsService(&server, &SPIFFS);
|
||||||
|
WiFiSettingsService wifiSettingsService = WiFiSettingsService(&server, &SPIFFS, &securitySettingsService);
|
||||||
|
APSettingsService apSettingsService = APSettingsService(&server, &SPIFFS, &securitySettingsService);
|
||||||
|
NTPSettingsService ntpSettingsService = NTPSettingsService(&server, &SPIFFS, &securitySettingsService);
|
||||||
|
OTASettingsService otaSettingsService = OTASettingsService(&server, &SPIFFS, &securitySettingsService);
|
||||||
|
AuthenticationService authenticationService = AuthenticationService(&server, &securitySettingsService);
|
||||||
|
|
||||||
WiFiSettingsService wifiSettingsService = WiFiSettingsService(&server, &SPIFFS);
|
WiFiScanner wifiScanner = WiFiScanner(&server, &securitySettingsService);
|
||||||
APSettingsService apSettingsService = APSettingsService(&server, &SPIFFS);
|
|
||||||
NTPSettingsService ntpSettingsService = NTPSettingsService(&server, &SPIFFS);
|
|
||||||
OTASettingsService otaSettingsService = OTASettingsService(&server, &SPIFFS);
|
|
||||||
AuthenticationService authenticationService = AuthenticationService(&server, &securityManager);
|
|
||||||
|
|
||||||
WiFiScanner wifiScanner = WiFiScanner(&server);
|
|
||||||
WiFiStatus wifiStatus = WiFiStatus(&server);
|
WiFiStatus wifiStatus = WiFiStatus(&server);
|
||||||
NTPStatus ntpStatus = NTPStatus(&server);
|
NTPStatus ntpStatus = NTPStatus(&server);
|
||||||
APStatus apStatus = APStatus(&server);
|
APStatus apStatus = APStatus(&server);
|
||||||
@ -48,8 +47,8 @@ void setup() {
|
|||||||
Serial.begin(SERIAL_BAUD_RATE);
|
Serial.begin(SERIAL_BAUD_RATE);
|
||||||
SPIFFS.begin();
|
SPIFFS.begin();
|
||||||
|
|
||||||
// start security manager
|
// start security settings service first
|
||||||
securityManager.begin();
|
securitySettingsService.begin();
|
||||||
|
|
||||||
// start services
|
// start services
|
||||||
ntpSettingsService.begin();
|
ntpSettingsService.begin();
|
||||||
|
Loading…
Reference in New Issue
Block a user