added user authentication and updated README.md

This commit is contained in:
kdeng00
2019-09-24 22:29:09 -04:00
parent e936c2da17
commit 1b8c28e67b
11 changed files with 492 additions and 258 deletions
+32 -31
View File
@@ -43,10 +43,10 @@ Create the API and enter an approrpiate name and identified. For the identified,
Replace [domain] with the domain name of the created tenant. This can be found in the Default App from the Application menu. Replace [identifier] with the identifer root name in the appsettings environment file. Not the friendly name but the root name of the identifier, omitting the http protocol and the *api* path. Replace [domain] with the domain name of the created tenant. This can be found in the Default App from the Application menu. Replace [identifier] with the identifer root name in the appsettings environment file. Not the friendly name but the root name of the identifier, omitting the http protocol and the *api* path.
```Json ```Json
"Auth0": { "domain": "[domain].auth0.com",
"Domain": "[domain].auth0.com", "api_identifier": "https://[identifier]/api",
"ApiIdentifier": "https://[identifier]/api" "client_id": "iamunique",
}, "client_secret": "Icankeepasecret"
``` ```
For the sake of this section, I will not go over configuring the API to accept the signing algorithm since it has already been configured in the [Startip](Startup.cs).cs file. Click on permissions to create the permissions for the API. For the sake of this section, I will not go over configuring the API to accept the signing algorithm since it has already been configured in the [Startip](Startup.cs).cs file. Click on permissions to create the permissions for the API.
@@ -88,10 +88,10 @@ Enter the information in the corresponding ``authcredentials.json`` file
For the purposes of properly uploading, downloading, updating, deleting, and streaming songs the API filesystem paths must be configured. For that purpose you have to open the ``paths.json`` file. What is meant by this is that the `root_music_path` directory where all music will be stored must exist. The `cover_root_path`, `archive_root_path`, and `temp_root_path` paths must exist and be accessible. An example on a Linux system: For the purposes of properly uploading, downloading, updating, deleting, and streaming songs the API filesystem paths must be configured. For that purpose you have to open the ``paths.json`` file. What is meant by this is that the `root_music_path` directory where all music will be stored must exist. The `cover_root_path`, `archive_root_path`, and `temp_root_path` paths must exist and be accessible. An example on a Linux system:
```Json ```Json
{ {
"root_music_path": "/home/dev/null/music/", "root_music_path": "/dev/null/music/",
"temp_root_path": "/home/dev/null/music/temp/", "temp_root_path": "/dev/null/music/temp/",
"cover_root_path": "/home/dev/null/music/coverArt/", "cover_root_path": "/dev/null/music/coverArt/",
"archive_root_path": "/home/dev/null/music/archive/" "archive_root_path": "/dev/null/music/archive/"
} }
``` ```
* `root_music_path` - Where music will be stored in the following convention: *`Artist/Album/Songs`* * `root_music_path` - Where music will be stored in the following convention: *`Artist/Album/Songs`*
@@ -104,55 +104,56 @@ For the purposes of properly uploading, downloading, updating, deleting, and str
### Database connection string ### Database connection string
In order for Database functionality to be operable, there must be a valid connection string and credentials with appropriate permissions. **At the moment there is only support for MySQL**. Depending on your environment `Release` or `Debug` you will need to edit the appsettings.json or appsettings.Development.json accordingly. An example of the fields to change are below: In order for Database functionality to be operable, there must be a valid connection string and MySQL credentials with appropriate permissions. **At the moment there is only support for MySQL**. Edit the database.json file accordingly. An example of the fields to change are below:
```Json ```Json
{ {
"ConnectionStrings": { "server": "localhost",
"DefaultConnection": "Server=localhost;Database=my_db;Uid=admin;Pwd=toughpassword;" "database": "my_db",
} "username": "admin",
"password": "toughpassword"
} }
``` ```
* Server - The address or domain name of the MySQL server * server - The address or domain name of the MySQL server
* Database - The database name * database - The database name
* Uid - Username * user - Username
* Password - Self-explanatory * password - Self-explanatory
The only requirement of the User is that the user should have full permissions to the database as well as permissions to create a database. Other than that, that is all that is required. The only requirement of the User is that the user should have full permissions to the database as well as permissions to create a database. Other than that, that is all that is required.
### Migrations ### Database
Prior to starting the API, the Migrations must be applied. There are 6 tables with migrations being applied and thy are: Prior to starting the API, the database must be created. The following tables are required:
* Users * User
* Song * Song
* Album * Album
* Artist * Artist
* Year * Year
* Genre * Genre
* CoverArt
There is a script for Linux systems to apply these migrations, it can be found in the [Scripts/Migrations/Linux](https://github.com/amazing-username/Icarus/blob/master/Scripts/Migrations/Linux/AddUpdate.sh) directory. Just merely execute: There is a MySQL script to create these tables, it can be found in the [Scripts/MySQL/](https://github.com/amazing-username/Icarus/blob/master/Scripts/MySQL/create_database.sql) directory. Just merely execute:
```shell ```shell
scripts/Migrations/Linux/AddUpdate.sh mysql -u ** -p < Scripts/MySQL/create_database.sql
``` ```
Or you can manually add the migrations like so for each migration:
```shell
dotnet ef migrations Add [Migration] --context [Migration]Context
```
Then update the migrations to the database like so<sup>*</sup>:
```shell
dotnet ef database update --context [Migration]Context
```
From this point the database has been successfully configured. Metadata and song filesystem locations can be saved.
<sup>*</sup> Will only need to execute this for UserContext and SongContext because the Song table has relational constraints with Album, Artist, Year, and Genre. From this point the database has been successfully created. Metadata and song filesystem locations can be saved.
## Building and Running ## Building and Running
``` ```
git clone --recursive https://github.com/kdeng00/icarus git clone --recursive https://github.com/kdeng00/icarus
cd 3rdparty/libbcrypt/
make
cp bcrypt.a libbcrypt.a
cp bcrypt.o libbcrypt.o
cd ../..
mkdir build mkdir build
cd build cd build
conan install ..
cmake .. cmake ..
make
bin/icarus bin/icarus
``` ```
Runs the server on localhost port 5002 Runs the server on localhost port 5002
+18 -3
View File
@@ -11,10 +11,10 @@
#include "dto/LoginResultDto.hpp" #include "dto/LoginResultDto.hpp"
#include "manager/TokenManager.h" #include "manager/TokenManager.h"
#include "manager/UserManager.h"
#include "model/Models.h" #include "model/Models.h"
namespace controller namespace controller {
{
class LoginController : public oatpp::web::server::api::ApiController class LoginController : public oatpp::web::server::api::ApiController
{ {
public: public:
@@ -31,11 +31,26 @@ namespace controller
ENDPOINT("POST", "/api/v1/login", data, BODY_DTO(dto::UserDto::ObjectWrapper, usr)) { ENDPOINT("POST", "/api/v1/login", data, BODY_DTO(dto::UserDto::ObjectWrapper, usr)) {
OATPP_LOGI("icarus", "logging in"); OATPP_LOGI("icarus", "logging in");
manager::UserManager usrMgr(m_bConf);
auto user = manager::UserManager::userDtoConv(usr);
if (!usrMgr.doesUserExist(user)) {
std::cout << "user does not exist" << std::endl;
return createResponse(Status::CODE_401, "invalid credentials");
}
std::cout << "user exists" << std::endl;
if (!usrMgr.validatePassword(user)) {
std::cout << "password is not valid" << std::endl;
return createResponse(Status::CODE_401, "invalid credentials");
}
manager::TokenManager tok; manager::TokenManager tok;
auto token = tok.retrieveToken(m_bConf); auto token = tok.retrieveToken(m_bConf);
auto logRes = dto::LoginResultDto::createShared(); auto logRes = dto::LoginResultDto::createShared();
logRes->id = 0; // TODO: change this later on to something meaningful logRes->id = 0; // TODO: change this later on to something meaningful or remove it
logRes->username = usr->username->c_str(); logRes->username = usr->username->c_str();
logRes->token = token.accessToken.c_str(); logRes->token = token.accessToken.c_str();
logRes->token_type = token.tokenType.c_str(); logRes->token_type = token.tokenType.c_str();
+15
View File
@@ -7,6 +7,7 @@
#include "database/BaseRepository.h" #include "database/BaseRepository.h"
#include "model/Models.h" #include "model/Models.h"
#include "type/SaltFilter.h"
#include "type/UserFilter.h" #include "type/UserFilter.h"
namespace database { namespace database {
@@ -15,10 +16,16 @@ namespace database {
UserRepository(const model::BinaryPath&); UserRepository(const model::BinaryPath&);
model::User retrieveUserRecord(model::User&, type::UserFilter); model::User retrieveUserRecord(model::User&, type::UserFilter);
model::PassSec retrieverUserSaltRecord(model::PassSec&, type::SaltFilter);
bool doesUserRecordExist(const model::User&, type::UserFilter);
void saveUserRecord(const model::User&); void saveUserRecord(const model::User&);
void saveUserSalt(const model::PassSec&);
private: private:
struct UserLengths; struct UserLengths;
struct SaltLengths;
struct UserLengths struct UserLengths
{ {
unsigned long firstnameLength; unsigned long firstnameLength;
@@ -28,14 +35,22 @@ namespace database {
unsigned long usernameLength; unsigned long usernameLength;
unsigned long passwordLength; unsigned long passwordLength;
}; };
struct SaltLengths
{
unsigned long saltLength;
};
std::shared_ptr<MYSQL_BIND> insertUserValues(const model::User&, std::shared_ptr<UserLengths>); std::shared_ptr<MYSQL_BIND> insertUserValues(const model::User&, std::shared_ptr<UserLengths>);
std::shared_ptr<MYSQL_BIND> insertSaltValues(const model::PassSec&, std::shared_ptr<SaltLengths>);
std::shared_ptr<MYSQL_BIND> valueBind(model::User&, std::tuple<char*, char*, char*, char*, char*, char*>&); std::shared_ptr<MYSQL_BIND> valueBind(model::User&, std::tuple<char*, char*, char*, char*, char*, char*>&);
std::shared_ptr<MYSQL_BIND> saltValueBind(model::PassSec&, char*);
std::shared_ptr<UserLengths> fetchUserLengths(const model::User&); std::shared_ptr<UserLengths> fetchUserLengths(const model::User&);
std::shared_ptr<SaltLengths> fetchSaltLengths(const model::PassSec&);
std::tuple<char*, char*, char*, char*, char*, char*> fetchUV(); std::tuple<char*, char*, char*, char*, char*, char*> fetchUV();
model::User parseRecord(MYSQL_STMT*); model::User parseRecord(MYSQL_STMT*);
model::PassSec parseSaltRecord(MYSQL_STMT*);
}; };
} }
+5 -3
View File
@@ -6,17 +6,19 @@
#include "dto/LoginResultDto.hpp" #include "dto/LoginResultDto.hpp"
#include "model/Models.h" #include "model/Models.h"
namespace manager namespace manager {
{
class UserManager class UserManager
{ {
public: public:
UserManager(const model::BinaryPath&); UserManager(const model::BinaryPath&);
model::User userDtoConv(dto::UserDto::ObjectWrapper&); bool doesUserExist(const model::User&);
bool validatePassword(const model::User&);
void printUser(const model::User&); void printUser(const model::User&);
void registerUser(model::User&); void registerUser(model::User&);
static model::User userDtoConv(dto::UserDto::ObjectWrapper&);
private: private:
model::BinaryPath m_bConf; model::BinaryPath m_bConf;
}; };
+3 -2
View File
@@ -4,8 +4,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
namespace model namespace model {
{
struct Song struct Song
{ {
Song() = default; Song() = default;
@@ -129,8 +128,10 @@ namespace model
struct PassSec struct PassSec
{ {
int id;
std::string hashPassword; std::string hashPassword;
std::string salt; std::string salt;
int userId;
}; };
struct AuthCredentials struct AuthCredentials
+2 -1
View File
@@ -5,7 +5,8 @@ namespace type {
enum class SaltFilter enum class SaltFilter
{ {
id = 0, id = 0,
salt salt,
userId
}; };
} }
+1 -2
View File
@@ -6,8 +6,7 @@
#include "model/Models.h" #include "model/Models.h"
namespace utility namespace utility {
{
class MetadataRetriever class MetadataRetriever
{ {
public: public:
+2
View File
@@ -9,6 +9,8 @@ namespace utility {
class PasswordEncryption { class PasswordEncryption {
public: public:
model::PassSec hashPassword(const model::User&); model::PassSec hashPassword(const model::User&);
bool isPasswordValid(const model::User&, const model::PassSec&);
private: private:
int saltSize(); int saltSize();
}; };
+159
View File
@@ -38,6 +38,8 @@ model::User UserRepository::retrieveUserRecord(model::User& user,
break; break;
} }
qry << " LIMIT 1";
const auto query = qry.str(); const auto query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size()); auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params); status = mysql_stmt_bind_param(stmt, params);
@@ -51,7 +53,86 @@ model::User UserRepository::retrieveUserRecord(model::User& user,
return user; return user;
} }
model::PassSec UserRepository::retrieverUserSaltRecord(model::PassSec& userSec, type::SaltFilter filter)
{
std::stringstream qry;
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
qry << "SELECT * FROM Salt WHERE ";
MYSQL_BIND params[1];
std::memset(params, 0, sizeof(params));
switch (filter) {
case type::SaltFilter::userId:
qry << "UserId = ?";
params[0].buffer_type = MYSQL_TYPE_LONG;
params[0].buffer = (char*)&userSec.userId;
params[0].length = 0;
params[0].is_null = 0;
break;
default:
break;
}
qry << " LIMIT 1";
const auto query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
userSec = parseSaltRecord(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
return userSec;
}
bool UserRepository::doesUserRecordExist(const model::User& user, type::UserFilter filter)
{
std::stringstream qry;
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
qry << "SELECT * FROM User WHERE ";
MYSQL_BIND params[1];
std::memset(params, 0, sizeof(params));
auto userLength = user.username.size();
switch (filter) {
case type::UserFilter::username:
qry << "Username = ?";
params[0].buffer_type = MYSQL_TYPE_STRING;
params[0].buffer = (char*)user.username.c_str();
params[0].length = &userLength;
params[0].is_null = 0;
break;
default:
break;
}
qry << " LIMIT 1";
const auto query = qry.str();
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, params);
status = mysql_stmt_execute(stmt);
mysql_stmt_store_result(stmt);
const auto rowCount = mysql_stmt_num_rows(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
return (rowCount > 0) ? true : false;
}
void UserRepository::saveUserRecord(const model::User& user) void UserRepository::saveUserRecord(const model::User& user)
{ {
std::cout << "inserting user record" << std::endl; std::cout << "inserting user record" << std::endl;
@@ -75,6 +156,28 @@ void UserRepository::saveUserRecord(const model::User& user)
mysql_close(conn); mysql_close(conn);
} }
void UserRepository::saveUserSalt(const model::PassSec& userSec)
{
std::cout << "inserting user salt record" << std::endl;
auto conn = setupMysqlConnection();
auto stmt = mysql_stmt_init(conn);
std::stringstream qry;
qry << "INSERT INTO Salt(Salt, UserId) VALUES(?,?)";
const auto query = qry.str();
auto sizes = fetchSaltLengths(userSec);
auto values = insertSaltValues(userSec, sizes);
auto status = mysql_stmt_prepare(stmt, query.c_str(), query.size());
status = mysql_stmt_bind_param(stmt, values.get());
status = mysql_stmt_execute(stmt);
mysql_stmt_close(stmt);
mysql_close(conn);
}
std::shared_ptr<MYSQL_BIND> UserRepository::insertUserValues(const model::User& user, std::shared_ptr<MYSQL_BIND> UserRepository::insertUserValues(const model::User& user,
std::shared_ptr<UserRepository::UserLengths> lengths) std::shared_ptr<UserRepository::UserLengths> lengths)
@@ -114,6 +217,22 @@ std::shared_ptr<MYSQL_BIND> UserRepository::insertUserValues(const model::User&
return values; return values;
} }
std::shared_ptr<MYSQL_BIND> UserRepository::insertSaltValues(const model::PassSec& passSec,
std::shared_ptr<UserRepository::SaltLengths> lengths)
{
std::shared_ptr<MYSQL_BIND> values((MYSQL_BIND*) std::calloc(6, sizeof(MYSQL_BIND)));
values.get()[0].buffer_type = MYSQL_TYPE_STRING;
values.get()[0].buffer = (char*)passSec.hashPassword.c_str();
values.get()[0].length = &(lengths->saltLength);
values.get()[1].buffer_type = MYSQL_TYPE_LONG;
values.get()[1].buffer = (char*)&passSec.userId;
return values;
}
std::shared_ptr<MYSQL_BIND> UserRepository::valueBind(model::User& user, std::shared_ptr<MYSQL_BIND> UserRepository::valueBind(model::User& user,
std::tuple<char*, char*, char*, char*, char*, char*>& us) std::tuple<char*, char*, char*, char*, char*, char*>& us)
{ {
@@ -150,6 +269,24 @@ std::shared_ptr<MYSQL_BIND> UserRepository::valueBind(model::User& user,
return values; return values;
} }
std::shared_ptr<MYSQL_BIND> UserRepository::saltValueBind(model::PassSec& userSalt, char *salt)
{
std::shared_ptr<MYSQL_BIND> values((MYSQL_BIND*) std::calloc(3, sizeof(MYSQL_BIND)));
constexpr auto strLen = 1024;
values.get()[0].buffer_type = MYSQL_TYPE_LONG;
values.get()[0].buffer = (char*)&userSalt.id;
values.get()[1].buffer_type = MYSQL_TYPE_STRING;
values.get()[1].buffer = (char*)salt;
values.get()[1].buffer_length = strLen;
values.get()[2].buffer_type = MYSQL_TYPE_LONG;
values.get()[2].buffer = (char*)&userSalt.userId;
return values;
}
std::shared_ptr<UserRepository::UserLengths> UserRepository::fetchUserLengths(const model::User& user) std::shared_ptr<UserRepository::UserLengths> UserRepository::fetchUserLengths(const model::User& user)
{ {
auto userLen = std::make_shared<UserLengths>(); auto userLen = std::make_shared<UserLengths>();
@@ -163,6 +300,14 @@ std::shared_ptr<UserRepository::UserLengths> UserRepository::fetchUserLengths(co
return userLen; return userLen;
} }
std::shared_ptr<UserRepository::SaltLengths> UserRepository::fetchSaltLengths(const model::PassSec& passSec)
{
auto saltLen = std::make_shared<SaltLengths>();
saltLen->saltLength = passSec.salt.size();
return saltLen;
}
std::tuple<char*, char*, char*, char*, char*, char*> UserRepository::fetchUV() std::tuple<char*, char*, char*, char*, char*, char*> UserRepository::fetchUV()
{ {
@@ -195,4 +340,18 @@ model::User UserRepository::parseRecord(MYSQL_STMT *stmt)
return user; return user;
} }
model::PassSec UserRepository::parseSaltRecord(MYSQL_STMT* stmt)
{
model::PassSec userSalt;
char saltKey[1024];
auto bindedValues = saltValueBind(userSalt, saltKey);
auto status = mysql_stmt_bind_result(stmt, bindedValues.get());
status = mysql_stmt_fetch(stmt);
userSalt.salt = saltKey;
return userSalt;
}
} }
+39 -11
View File
@@ -2,26 +2,35 @@
#include "database/UserRepository.h" #include "database/UserRepository.h"
#include "type/UserFilter.h" #include "type/UserFilter.h"
#include "type/SaltFilter.h"
#include "utility/MetadataRetriever.h" #include "utility/MetadataRetriever.h"
#include "utility/PasswordEncryption.h" #include "utility/PasswordEncryption.h"
namespace manager { namespace manager {
UserManager::UserManager(const model::BinaryPath& bConf) : m_bConf(bConf) { } UserManager::UserManager(const model::BinaryPath& bConf) : m_bConf(bConf) { }
bool UserManager::doesUserExist(const model::User& user)
model::User UserManager::userDtoConv(dto::UserDto::ObjectWrapper& userDto)
{ {
model::User user; database::UserRepository userRepo(m_bConf);
user.id = (userDto->userId.getPtr() == nullptr) ? 0 : userDto->userId->getValue(); return userRepo.doesUserRecordExist(user, type::UserFilter::username);
user.firstname = (userDto->firstname == nullptr) ? "" : userDto->firstname->c_str(); }
user.lastname = (userDto->lastname == nullptr) ? "" : userDto->lastname->c_str();
user.phone = (userDto->phone == nullptr) ? "" : userDto->phone->c_str();
user.email = (userDto->email == nullptr) ? "" : userDto->email->c_str();
user.username = (userDto->username == nullptr) ? "" : userDto->username->c_str();
user.password = (userDto->password == nullptr) ? "" : userDto->password->c_str();
return user; bool UserManager::validatePassword(const model::User& user)
{
database::UserRepository userRepo(m_bConf);
model::User usr;
usr.username = user.username;
usr = userRepo.retrieveUserRecord(usr, type::UserFilter::username);
model::PassSec userSec;
userSec.userId = usr.id;
userSec = userRepo.retrieverUserSaltRecord(userSec, type::SaltFilter::userId);
userSec.hashPassword = usr.password;
utility::PasswordEncryption passEnc;
return passEnc.isPasswordValid(user, userSec);
} }
@@ -53,7 +62,26 @@ void UserManager::registerUser(model::User& user)
std::cout << usr.username << std::endl; std::cout << usr.username << std::endl;
usr = usrRepo.retrieveUserRecord(usr, type::UserFilter::username); usr = usrRepo.retrieveUserRecord(usr, type::UserFilter::username);
hashed.hashPassword = usr.password;
hashed.userId = usr.id;
usrRepo.saveUserSalt(hashed);
printUser(usr); printUser(usr);
} }
model::User UserManager::userDtoConv(dto::UserDto::ObjectWrapper& userDto)
{
model::User user;
user.id = (userDto->userId.getPtr() == nullptr) ? 0 : userDto->userId->getValue();
user.firstname = (userDto->firstname == nullptr) ? "" : userDto->firstname->c_str();
user.lastname = (userDto->lastname == nullptr) ? "" : userDto->lastname->c_str();
user.phone = (userDto->phone == nullptr) ? "" : userDto->phone->c_str();
user.email = (userDto->email == nullptr) ? "" : userDto->email->c_str();
user.username = (userDto->username == nullptr) ? "" : userDto->username->c_str();
user.password = (userDto->password == nullptr) ? "" : userDto->password->c_str();
return user;
}
} }
+11
View File
@@ -28,6 +28,17 @@ model::PassSec PasswordEncryption::hashPassword(const model::User& user)
return passSec; return passSec;
} }
bool PasswordEncryption::isPasswordValid(const model::User& user, const model::PassSec& userSalt)
{
std::unique_ptr<char[]> passwordHashed(new char[BCRYPT_HASHSIZE]);
bcrypt_hashpw(user.password.c_str(), userSalt.salt.c_str(), passwordHashed.get());
return (userSalt.hashPassword.compare(passwordHashed.get()) == 0) ? true : false;
}
int PasswordEncryption::saltSize() int PasswordEncryption::saltSize()
{ {
constexpr auto size = 10000; constexpr auto size = 10000;