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.
```Json
"Auth0": {
"Domain": "[domain].auth0.com",
"ApiIdentifier": "https://[identifier]/api"
},
"domain": "[domain].auth0.com",
"api_identifier": "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.
@@ -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:
```Json
{
"root_music_path": "/home/dev/null/music/",
"temp_root_path": "/home/dev/null/music/temp/",
"cover_root_path": "/home/dev/null/music/coverArt/",
"archive_root_path": "/home/dev/null/music/archive/"
"root_music_path": "/dev/null/music/",
"temp_root_path": "/dev/null/music/temp/",
"cover_root_path": "/dev/null/music/coverArt/",
"archive_root_path": "/dev/null/music/archive/"
}
```
* `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
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
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=my_db;Uid=admin;Pwd=toughpassword;"
}
"server": "localhost",
"database": "my_db",
"username": "admin",
"password": "toughpassword"
}
```
* Server - The address or domain name of the MySQL server
* Database - The database name
* Uid - Username
* Password - Self-explanatory
* server - The address or domain name of the MySQL server
* database - The database name
* user - Username
* 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.
### Migrations
### Database
Prior to starting the API, the Migrations must be applied. There are 6 tables with migrations being applied and thy are:
* Users
Prior to starting the API, the database must be created. The following tables are required:
* User
* Song
* Album
* Artist
* Year
* 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
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
```
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
cd build
conan install ..
cmake ..
make
bin/icarus
```
Runs the server on localhost port 5002
+18 -3
View File
@@ -11,10 +11,10 @@
#include "dto/LoginResultDto.hpp"
#include "manager/TokenManager.h"
#include "manager/UserManager.h"
#include "model/Models.h"
namespace controller
{
namespace controller {
class LoginController : public oatpp::web::server::api::ApiController
{
public:
@@ -31,11 +31,26 @@ namespace controller
ENDPOINT("POST", "/api/v1/login", data, BODY_DTO(dto::UserDto::ObjectWrapper, usr)) {
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;
auto token = tok.retrieveToken(m_bConf);
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->token = token.accessToken.c_str();
logRes->token_type = token.tokenType.c_str();
+15
View File
@@ -7,6 +7,7 @@
#include "database/BaseRepository.h"
#include "model/Models.h"
#include "type/SaltFilter.h"
#include "type/UserFilter.h"
namespace database {
@@ -15,10 +16,16 @@ namespace database {
UserRepository(const model::BinaryPath&);
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 saveUserSalt(const model::PassSec&);
private:
struct UserLengths;
struct SaltLengths;
struct UserLengths
{
unsigned long firstnameLength;
@@ -28,14 +35,22 @@ namespace database {
unsigned long usernameLength;
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> 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> saltValueBind(model::PassSec&, char*);
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();
model::User parseRecord(MYSQL_STMT*);
model::PassSec parseSaltRecord(MYSQL_STMT*);
};
}
+5 -3
View File
@@ -6,17 +6,19 @@
#include "dto/LoginResultDto.hpp"
#include "model/Models.h"
namespace manager
{
namespace manager {
class UserManager
{
public:
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 registerUser(model::User&);
static model::User userDtoConv(dto::UserDto::ObjectWrapper&);
private:
model::BinaryPath m_bConf;
};
+3 -2
View File
@@ -4,8 +4,7 @@
#include <string>
#include <vector>
namespace model
{
namespace model {
struct Song
{
Song() = default;
@@ -129,8 +128,10 @@ namespace model
struct PassSec
{
int id;
std::string hashPassword;
std::string salt;
int userId;
};
struct AuthCredentials
+2 -1
View File
@@ -5,7 +5,8 @@ namespace type {
enum class SaltFilter
{
id = 0,
salt
salt,
userId
};
}
+1 -2
View File
@@ -6,8 +6,7 @@
#include "model/Models.h"
namespace utility
{
namespace utility {
class MetadataRetriever
{
public:
+2
View File
@@ -9,6 +9,8 @@ namespace utility {
class PasswordEncryption {
public:
model::PassSec hashPassword(const model::User&);
bool isPasswordValid(const model::User&, const model::PassSec&);
private:
int saltSize();
};
+159
View File
@@ -38,6 +38,8 @@ model::User UserRepository::retrieveUserRecord(model::User& user,
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);
@@ -51,7 +53,86 @@ model::User UserRepository::retrieveUserRecord(model::User& 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)
{
std::cout << "inserting user record" << std::endl;
@@ -75,6 +156,28 @@ void UserRepository::saveUserRecord(const model::User& user)
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<UserRepository::UserLengths> lengths)
@@ -114,6 +217,22 @@ std::shared_ptr<MYSQL_BIND> UserRepository::insertUserValues(const model::User&
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::tuple<char*, char*, char*, char*, char*, char*>& us)
{
@@ -150,6 +269,24 @@ std::shared_ptr<MYSQL_BIND> UserRepository::valueBind(model::User& user,
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)
{
auto userLen = std::make_shared<UserLengths>();
@@ -163,6 +300,14 @@ std::shared_ptr<UserRepository::UserLengths> UserRepository::fetchUserLengths(co
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()
{
@@ -195,4 +340,18 @@ model::User UserRepository::parseRecord(MYSQL_STMT *stmt)
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 "type/UserFilter.h"
#include "type/SaltFilter.h"
#include "utility/MetadataRetriever.h"
#include "utility/PasswordEncryption.h"
namespace manager {
UserManager::UserManager(const model::BinaryPath& bConf) : m_bConf(bConf) { }
model::User UserManager::userDtoConv(dto::UserDto::ObjectWrapper& userDto)
bool UserManager::doesUserExist(const model::User& user)
{
model::User user;
database::UserRepository userRepo(m_bConf);
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 userRepo.doesUserRecordExist(user, type::UserFilter::username);
}
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;
usr = usrRepo.retrieveUserRecord(usr, type::UserFilter::username);
hashed.hashPassword = usr.password;
hashed.userId = usr.id;
usrRepo.saveUserSalt(hashed);
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;
}
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()
{
constexpr auto size = 10000;