From 04e852f7d96d0a4cc4cb89687d0b8d04e136dd0b Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Sat, 18 May 2019 19:35:27 +0100 Subject: [PATCH] add authentication service --- src/AsyncAuthJsonWebHandler.h | 52 ++++++++ ...uestWebHandler.h => AsyncJsonWebHandler.h} | 19 +-- src/AuthenticationService.cpp | 45 +++++++ src/AuthenticationService.h | 30 +++++ src/SecurityManager.cpp | 120 ++++++------------ src/SecurityManager.h | 65 +++++----- src/SettingsPersistence.h | 4 +- src/SettingsService.h | 4 +- src/SimpleService.h | 6 +- src/main.cpp | 12 +- 10 files changed, 222 insertions(+), 135 deletions(-) create mode 100644 src/AsyncAuthJsonWebHandler.h rename src/{AsyncJsonRequestWebHandler.h => AsyncJsonWebHandler.h} (90%) create mode 100644 src/AuthenticationService.cpp create mode 100644 src/AuthenticationService.h diff --git a/src/AsyncAuthJsonWebHandler.h b/src/AsyncAuthJsonWebHandler.h new file mode 100644 index 0000000..a767a9a --- /dev/null +++ b/src/AsyncAuthJsonWebHandler.h @@ -0,0 +1,52 @@ +#ifndef AsyncAuthJsonWebHandler_H_ +#define AsyncAuthJsonWebHandler_H_ + +#include +#include +#include +#include + +typedef std::function 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_ \ No newline at end of file diff --git a/src/AsyncJsonRequestWebHandler.h b/src/AsyncJsonWebHandler.h similarity index 90% rename from src/AsyncJsonRequestWebHandler.h rename to src/AsyncJsonWebHandler.h index e149129..8489f76 100644 --- a/src/AsyncJsonRequestWebHandler.h +++ b/src/AsyncJsonWebHandler.h @@ -17,24 +17,25 @@ typedef std::function 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; } diff --git a/src/AuthenticationService.cpp b/src/AuthenticationService.cpp new file mode 100644 index 0000000..1a9e321 --- /dev/null +++ b/src/AuthenticationService.cpp @@ -0,0 +1,45 @@ +#include + +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()) { + 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); +} + diff --git a/src/AuthenticationService.h b/src/AuthenticationService.h new file mode 100644 index 0000000..71d44f9 --- /dev/null +++ b/src/AuthenticationService.h @@ -0,0 +1,30 @@ +#ifndef AuthenticationService_H_ +#define AuthenticationService_H_ + +#include + +#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 \ No newline at end of file diff --git a/src/SecurityManager.cpp b/src/SecurityManager.cpp index 96091b5..7d6b854 100644 --- a/src/SecurityManager.cpp +++ b/src/SecurityManager.cpp @@ -1,23 +1,7 @@ #include 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()) { - // 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(); - 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()) { - String accessToken = jsonDocument["access_token"]; - DynamicJsonDocument parsedJwt(MAX_JWT_SIZE); - jwtHandler.parseJWT(accessToken, parsedJwt); - if (parsedJwt.is()){ - 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()) { - 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 parsedPayload = payloadDocument.as(); + 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(); +} + +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(); + populateJWTPayload(payload, user); + return payload == parsedPayload; } -String SecurityManager::generateJWT(User user) { +String SecurityManager::generateJWT(User *user) { DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE); - JsonObject jwt = _jsonDocument.to(); - jwt["username"] = user.getUsername(); - jwt["role"] = user.getRole(); - return jwtHandler.buildJWT(jwt); + JsonObject payload = _jsonDocument.to(); + populateJWTPayload(payload, user); + return jwtHandler.buildJWT(payload); } diff --git a/src/SecurityManager.h b/src/SecurityManager.h index 6bb4d0a..853571e 100644 --- a/src/SecurityManager.h +++ b/src/SecurityManager.h @@ -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 + * Authenticate, returning the user if found */ - Authentication verify(String jwt); + Authentication authenticate(String username, String password); /* - * Authenticate, returning the user if found. + * Check the request header for the Authorization token */ - Authentication authenticate(String username, String password); - + 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 _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 \ No newline at end of file diff --git a/src/SettingsPersistence.h b/src/SettingsPersistence.h index 5b50126..03ca73d 100644 --- a/src/SettingsPersistence.h +++ b/src/SettingsPersistence.h @@ -4,13 +4,13 @@ #include #include #include -#include +#include #include /** * 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 diff --git a/src/SettingsService.h b/src/SettingsService.h index 8adc4da..5d7cab0 100644 --- a/src/SettingsService.h +++ b/src/SettingsService.h @@ -12,7 +12,7 @@ #include #include #include -#include +#include #include /* @@ -22,7 +22,7 @@ class SettingsService : public SettingsPersistence { private: - AsyncJsonRequestWebHandler _updateHandler; + AsyncJsonWebHandler _updateHandler; void fetchConfig(AsyncWebServerRequest *request){ AsyncJsonResponse * response = new AsyncJsonResponse(MAX_SETTINGS_SIZE); diff --git a/src/SimpleService.h b/src/SimpleService.h index 9144dfc..83534b1 100644 --- a/src/SimpleService.h +++ b/src/SimpleService.h @@ -12,12 +12,12 @@ #include #include #include -#include +#include /** * 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); diff --git a/src/main.cpp b/src/main.cpp index 7544857..7c9ec3c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,15 +10,18 @@ #endif #include + +#include #include -#include -#include #include #include -#include #include +#include +#include +#include +#include #include -#include + #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);