add authentication service

This commit is contained in:
Rick Watson 2019-05-18 19:35:27 +01:00
parent 7817010533
commit 04e852f7d9
10 changed files with 224 additions and 137 deletions

View File

@ -0,0 +1,52 @@
#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_

View File

@ -17,24 +17,25 @@
typedef std::function<void(AsyncWebServerRequest *request, JsonDocument &jsonDocument)> JsonRequestCallback;
class AsyncJsonRequestWebHandler: public AsyncWebHandler {
class AsyncJsonWebHandler: public AsyncWebHandler {
private:
String _uri;
WebRequestMethodComposite _method;
JsonRequestCallback _onRequest;
size_t _maxContentLength;
protected:
String _uri;
public:
AsyncJsonRequestWebHandler() :
_uri(),
AsyncJsonWebHandler() :
_method(HTTP_POST|HTTP_PUT|HTTP_PATCH),
_onRequest(NULL),
_maxContentLength(ASYNC_JSON_REQUEST_DEFAULT_MAX_SIZE) {}
_maxContentLength(ASYNC_JSON_REQUEST_DEFAULT_MAX_SIZE),
_uri() {}
~AsyncJsonRequestWebHandler() {}
~AsyncJsonWebHandler() {}
void setUri(const String& uri) { _uri = uri; }
void setMethod(WebRequestMethodComposite method) { _method = method; }
@ -61,7 +62,9 @@ class AsyncJsonRequestWebHandler: public AsyncWebHandler {
virtual void handleRequest(AsyncWebServerRequest *request) override final {
// no request configured
if(!_onRequest) {
request->send(404);
Serial.print("No request callback was configured for endpoint: ");
Serial.println(_uri);
request->send(500);
return;
}

View File

@ -0,0 +1,45 @@
#include <AuthenticationService.h>
AuthenticationService::AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager):
_server(server), _securityManager(securityManager) {
server->on(VERIFY_AUTHORIZATION_PATH, HTTP_GET, std::bind(&AuthenticationService::verifyAuthorization, this, std::placeholders::_1));
_signInHandler.setUri(SIGN_IN_PATH);
_signInHandler.setMethod(HTTP_POST);
_signInHandler.setMaxContentLength(MAX_SECURITY_MANAGER_SETTINGS_SIZE);
_signInHandler.onRequest(std::bind(&AuthenticationService::signIn, this, std::placeholders::_1, std::placeholders::_2));
server->addHandler(&_signInHandler);
}
AuthenticationService::~AuthenticationService() {}
/**
* Verifys that the request supplied a valid JWT.
*/
void AuthenticationService::verifyAuthorization(AsyncWebServerRequest *request) {
Authentication authentication = _securityManager->authenticateRequest(request);
request->send(authentication.isAuthenticated() ? 200: 401);
}
/**
* Signs in a user if the username and password match. Provides a JWT to be used in the Authorization header in subsequent requests.
*/
void AuthenticationService::signIn(AsyncWebServerRequest *request, JsonDocument &jsonDocument){
if (jsonDocument.is<JsonObject>()) {
String username = jsonDocument["username"];
String password = jsonDocument["password"];
Authentication authentication = _securityManager->authenticate(username, password);
if (authentication.isAuthenticated()) {
User* user = authentication.getUser();
AsyncJsonResponse * response = new AsyncJsonResponse(MAX_USERS_SIZE);
JsonObject jsonObject = response->getRoot();
jsonObject["access_token"] = _securityManager->generateJWT(user);
response->setLength();
request->send(response);
return;
}
}
AsyncWebServerResponse *response = request->beginResponse(401);
request->send(response);
}

View File

@ -0,0 +1,30 @@
#ifndef AuthenticationService_H_
#define AuthenticationService_H_
#include <SecurityManager.h>
#define VERIFY_AUTHORIZATION_PATH "/rest/verifyAuthorization"
#define SIGN_IN_PATH "/rest/signIn"
#define MAX_AUTHENTICATION_SIZE 256
class AuthenticationService {
public:
AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager) ;
~AuthenticationService();
private:
// server instance
AsyncWebServer* _server;
SecurityManager* _securityManager;
AsyncJsonWebHandler _signInHandler;
// endpoint functions
void signIn(AsyncWebServerRequest *request, JsonDocument &jsonDocument);
void verifyAuthorization(AsyncWebServerRequest *request);
};
#endif // end SecurityManager_h

View File

@ -1,23 +1,7 @@
#include <SecurityManager.h>
SecurityManager::SecurityManager(AsyncWebServer* server, FS* fs) : SettingsPersistence(fs, SECURITY_SETTINGS_FILE) {
// fetch users
server->on(USERS_PATH, HTTP_GET, std::bind(&SecurityManager::fetchUsers, this, std::placeholders::_1));
// sign in request
_signInRequestHandler.setUri(SIGN_IN_PATH);
_signInRequestHandler.setMethod(HTTP_POST);
_signInRequestHandler.setMaxContentLength(MAX_SECURITY_MANAGER_SETTINGS_SIZE);
_signInRequestHandler.onRequest(std::bind(&SecurityManager::signIn, this, std::placeholders::_1, std::placeholders::_2));
server->addHandler(&_signInRequestHandler);
// sign in request
_testVerifiction.setUri(TEST_VERIFICATION_PATH);
_testVerifiction.setMethod(HTTP_POST);
_testVerifiction.setMaxContentLength(MAX_SECURITY_MANAGER_SETTINGS_SIZE);
_testVerifiction.onRequest(std::bind(&SecurityManager::testVerification, this, std::placeholders::_1, std::placeholders::_2));
server->addHandler(&_testVerifiction);
}
SecurityManager::~SecurityManager() {}
@ -68,54 +52,6 @@ void SecurityManager::writeToJsonObject(JsonObject& root) {
}
}
// TODO - Decide about default role behaviour, don't over-engineer (multiple roles, boolean admin flag???).
void SecurityManager::signIn(AsyncWebServerRequest *request, JsonDocument &jsonDocument){
if (jsonDocument.is<JsonObject>()) {
// authenticate user
String username = jsonDocument["username"];
String password = jsonDocument["password"];
Authentication authentication = authenticate(username, password);
if (authentication.isAuthenticated()) {
User& user = authentication.getUser();
// create JWT
DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE);
JsonObject jwt = _jsonDocument.to<JsonObject>();
jwt["username"] = user.getUsername();
jwt["role"] = user.getRole();
// send JWT response
AsyncJsonResponse * response = new AsyncJsonResponse(MAX_USERS_SIZE);
JsonObject jsonObject = response->getRoot();
jsonObject["access_token"] = jwtHandler.buildJWT(jwt);
response->setLength();
request->send(response);
return;
}
}
// authentication failed
AsyncWebServerResponse *response = request->beginResponse(401);
request->send(response);
}
void SecurityManager::testVerification(AsyncWebServerRequest *request, JsonDocument &jsonDocument){
if (jsonDocument.is<JsonObject>()) {
String accessToken = jsonDocument["access_token"];
DynamicJsonDocument parsedJwt(MAX_JWT_SIZE);
jwtHandler.parseJWT(accessToken, parsedJwt);
if (parsedJwt.is<JsonObject>()){
AsyncWebServerResponse *response = request->beginResponse(200);
request->send(response);
return;
}
}
// authentication failed
AsyncWebServerResponse *response = request->beginResponse(401);
request->send(response);
}
void SecurityManager::fetchUsers(AsyncWebServerRequest *request) {
AsyncJsonResponse * response = new AsyncJsonResponse(MAX_USERS_SIZE);
JsonObject jsonObject = response->getRoot();
@ -132,36 +68,56 @@ void SecurityManager::begin() {
jwtHandler.setSecret(_jwtSecret);
}
/*
* TODO - VERIFY JWT IS CORRECT!
*/
Authentication SecurityManager::verify(String jwt) {
DynamicJsonDocument parsedJwt(MAX_JWT_SIZE);
jwtHandler.parseJWT(jwt, parsedJwt);
if (parsedJwt.is<JsonObject>()) {
String username = parsedJwt["username"];
Authentication SecurityManager::authenticateRequest(AsyncWebServerRequest *request) {
AsyncWebHeader* authorizationHeader = request->getHeader(AUTHORIZATION_HEADER);
if (authorizationHeader) {
String value = authorizationHeader->value();
value.startsWith(AUTHORIZATION_HEADER_PREFIX);
value = value.substring(AUTHORIZATION_HEADER_PREFIX_LEN);
return authenticateJWT(value);
}
return Authentication();
}
Authentication SecurityManager::authenticateJWT(String jwt) {
DynamicJsonDocument payloadDocument(MAX_JWT_SIZE);
jwtHandler.parseJWT(jwt, payloadDocument);
if (payloadDocument.is<JsonObject>()) {
JsonObject parsedPayload = payloadDocument.as<JsonObject>();
String username = parsedPayload["username"];
for (User _user : _users) {
if (_user.getUsername() == username){
return Authentication::forUser(_user);
if (_user.getUsername() == username && validatePayload(parsedPayload, &_user)){
return Authentication(_user);
}
}
}
return Authentication::notAuthenticated();
return Authentication();
}
Authentication SecurityManager::authenticate(String username, String password) {
for (User _user : _users) {
if (_user.getUsername() == username && _user.getPassword() == password){
return Authentication::forUser(_user);
return Authentication(_user);
}
}
return Authentication::notAuthenticated();
return Authentication();
}
String SecurityManager::generateJWT(User user) {
DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE);
JsonObject jwt = _jsonDocument.to<JsonObject>();
jwt["username"] = user.getUsername();
jwt["role"] = user.getRole();
return jwtHandler.buildJWT(jwt);
inline void populateJWTPayload(JsonObject &payload, User *user) {
payload["username"] = user->getUsername();
payload["role"] = user->getRole();
}
boolean SecurityManager::validatePayload(JsonObject &parsedPayload, User *user) {
DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE);
JsonObject payload = _jsonDocument.to<JsonObject>();
populateJWTPayload(payload, user);
return payload == parsedPayload;
}
String SecurityManager::generateJWT(User *user) {
DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE);
JsonObject payload = _jsonDocument.to<JsonObject>();
populateJWTPayload(payload, user);
return jwtHandler.buildJWT(payload);
}

View File

@ -13,18 +13,15 @@
#define SECURITY_SETTINGS_FILE "/config/securitySettings.json"
#define USERS_PATH "/rest/users"
#define AUTHENTICATE_PATH "/rest/authenticate"
#define SIGN_IN_PATH "/rest/signIn"
#define TEST_VERIFICATION_PATH "/rest/verification"
#define AUTHORIZATION_HEADER "Authorization"
#define AUTHORIZATION_HEADER_PREFIX "Bearer "
#define AUTHORIZATION_HEADER_PREFIX_LEN 7
#define MAX_JWT_SIZE 128
#define MAX_SECURITY_MANAGER_SETTINGS_SIZE 512
#define SECURITY_MANAGER_MAX_USERS 5
#define ANONYMOUS_USERNAME "_anonymous"
#define ANONYMOUS_PASSWORD ""
#define ANONYMOUS_ROLE ""
#define MAX_USERS_SIZE 1024
class User {
@ -45,31 +42,27 @@ class User {
}
};
const User NOT_AUTHENTICATED = User(ANONYMOUS_USERNAME, ANONYMOUS_PASSWORD, ANONYMOUS_ROLE);
class Authentication {
private:
User _user;
User *_user;
boolean _authenticated;
Authentication(User user, boolean authenticated) : _user(user), _authenticated(authenticated) {}
public:
// NOOP
~Authentication(){}
User& getUser() {
Authentication(User& user): _user(new User(user)), _authenticated(true) {}
Authentication() : _user(NULL), _authenticated(false) {}
~Authentication() {
if (_user != NULL){
delete(_user);
}
}
User* getUser() {
return _user;
}
bool isAuthenticated() {
return _authenticated;
}
static Authentication forUser(User user){
return Authentication(user, true);
}
static Authentication notAuthenticated(){
return Authentication(NOT_AUTHENTICATED, false);
}
};
class SecurityManager : public SettingsPersistence {
class SecurityManager : public SettingsPersistence {
public:
@ -79,19 +72,19 @@ class SecurityManager : public SettingsPersistence {
void begin();
/*
* Lookup the user by JWT
*/
Authentication verify(String jwt);
/*
* Authenticate, returning the user if found.
* Authenticate, returning the user if found
*/
Authentication authenticate(String username, String password);
/*
* Check the request header for the Authorization token
*/
Authentication authenticateRequest(AsyncWebServerRequest *request);
/*
* Generate a JWT for the user provided
*/
String generateJWT(User user);
String generateJWT(User *user);
protected:
@ -102,11 +95,6 @@ class SecurityManager : public SettingsPersistence {
// jwt handler
ArduinoJsonJWT jwtHandler = ArduinoJsonJWT(DEFAULT_JWT_SECRET);
// server instance
AsyncWebServer* _server;
AsyncJsonRequestWebHandler _signInRequestHandler;
AsyncJsonRequestWebHandler _testVerifiction;
// access point settings
String _jwtSecret;
std::list<String> _roles;
@ -114,8 +102,17 @@ class SecurityManager : public SettingsPersistence {
// endpoint functions
void fetchUsers(AsyncWebServerRequest *request);
void signIn(AsyncWebServerRequest *request, JsonDocument &jsonDocument);
void testVerification(AsyncWebServerRequest *request, JsonDocument &jsonDocument);
/*
* Lookup the user by JWT
*/
Authentication authenticateJWT(String jwt);
/*
* Verify the payload is correct
*/
boolean validatePayload(JsonObject &parsedPayload, User *user);
};
#endif // end SecurityManager_h

View File

@ -4,13 +4,13 @@
#include <ESPAsyncWebServer.h>
#include <FS.h>
#include <ArduinoJson.h>
#include <AsyncJsonRequestWebHandler.h>
#include <AsyncJsonWebHandler.h>
#include <AsyncArduinoJson6.h>
/**
* At the moment, not expecting settings service to have to deal with large JSON
* files this could be made configurable fairly simply, it's exposed on
* AsyncJsonRequestWebHandler with a setter.
* AsyncJsonWebHandler with a setter.
*/
#define MAX_SETTINGS_SIZE 1024

View File

@ -12,7 +12,7 @@
#include <SettingsPersistence.h>
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
#include <AsyncJsonRequestWebHandler.h>
#include <AsyncJsonWebHandler.h>
#include <AsyncArduinoJson6.h>
/*
@ -22,7 +22,7 @@ class SettingsService : public SettingsPersistence {
private:
AsyncJsonRequestWebHandler _updateHandler;
AsyncJsonWebHandler _updateHandler;
void fetchConfig(AsyncWebServerRequest *request){
AsyncJsonResponse * response = new AsyncJsonResponse(MAX_SETTINGS_SIZE);

View File

@ -12,12 +12,12 @@
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
#include <AsyncArduinoJson6.h>
#include <AsyncJsonRequestWebHandler.h>
#include <AsyncJsonWebHandler.h>
/**
* At the moment, not expecting services to have to deal with large JSON
* files this could be made configurable fairly simply, it's exposed on
* AsyncJsonRequestWebHandler with a setter.
* AsyncJsonWebHandler with a setter.
*/
#define MAX_SETTINGS_SIZE 1024
@ -31,7 +31,7 @@ class SimpleService {
private:
AsyncJsonRequestWebHandler _updateHandler;
AsyncJsonWebHandler _updateHandler;
void fetchConfig(AsyncWebServerRequest *request){
AsyncJsonResponse * response = new AsyncJsonResponse(MAX_SETTINGS_SIZE);

View File

@ -10,15 +10,18 @@
#endif
#include <FS.h>
#include <SecurityManager.h>
#include <WiFiSettingsService.h>
#include <WiFiStatus.h>
#include <WiFiScanner.h>
#include <APSettingsService.h>
#include <NTPSettingsService.h>
#include <NTPStatus.h>
#include <OTASettingsService.h>
#include <AuthenticationService.h>
#include <WiFiScanner.h>
#include <WiFiStatus.h>
#include <NTPStatus.h>
#include <APStatus.h>
#include <SecurityManager.h>
#define SERIAL_BAUD_RATE 115200
@ -30,6 +33,7 @@ WiFiSettingsService wifiSettingsService = WiFiSettingsService(&server, &SPIFFS);
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);