Build auth API

This commit is contained in:
Robert Marshall 2020-04-12 13:50:39 +01:00
commit dafe603a06
43 changed files with 1153 additions and 0 deletions

48
.drone.yml Normal file
View file

@ -0,0 +1,48 @@
kind: pipeline
name: default
clone:
skip_verify: true
volumes:
- name: output
host:
path: /var/www/Api.Auth
steps:
- name: build and publish
image: mcr.microsoft.com/dotnet/core/sdk:3.1
volumes:
- name: output
path: /output
environment:
ConnectionString:
from_secret: ConnectionString
commands:
- chmod +x ./build.sh
- ./build.sh
- sed -i "s/<DatabaseConnectionString>/$ConnectionString/g" output/appsettings.json
- cp api.auth.service output/
- cp -r ./output/* /output
- name: restart service
privileged: true
image: appleboy/drone-ssh
settings:
host: 192.168.1.3
username:
from_secret: ssh_user
password:
from_secret: ssh_password
script:
- systemctl daemon-reload
- service api.auth restart
- name: notify
image: drillster/drone-email
settings:
host: 192.168.1.3
skip_verify: true
from: build@robware.uk
when:
status:
- changed
- failure

198
.editorconfig Normal file
View file

@ -0,0 +1,198 @@
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
[*]
indent_style = tab
# C# files
[*.cs]
max_line_length = 180
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Coding Conventions ####
# Organize usings
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
# this. and Me. preferences
dotnet_style_qualification_for_event = false:silent
dotnet_style_qualification_for_field = false:silent
dotnet_style_qualification_for_method = false:silent
dotnet_style_qualification_for_property = false:silent
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
dotnet_style_predefined_type_for_member_access = true:silent
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
# Expression-level preferences
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_object_initializer = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
# Field preferences
dotnet_style_readonly_field = true:suggestion
# Parameter preferences
dotnet_code_quality_unused_parameters = all:suggestion
#### C# Coding Conventions ####
# var preferences
csharp_style_var_elsewhere = false:silent
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = false:silent
# Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_prefer_switch_expression = true:suggestion
# Null-checking preferences
csharp_style_conditional_delegate_call = true:suggestion
# Modifier preferences
csharp_prefer_static_local_function = true:suggestion
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
# Code-block preferences
csharp_prefer_braces = false:silent
csharp_prefer_simple_using_statement = true:suggestion
# Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:silent
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = none
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
[*.{yaml,yml}]
indent_style = space
indent_size = 2

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
src/Robware.Api.Auth/appsettings.Development.json filter=clean-config

2
.gitconfig Normal file
View file

@ -0,0 +1,2 @@
[filter "clean-config"]
clean = ./clean-config.sh

56
.gitignore vendored Normal file
View file

@ -0,0 +1,56 @@
*.swp
*.*~
project.lock.json
.DS_Store
*.pyc
nupkg/
# Visual Studio Code
.vscode
# Rider
.idea
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
#build/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
msbuild.log
msbuild.err
msbuild.wrn
# Visual Studio 2015
.vs/
#lcov files
lcov.info
#compiled and minified assets
*.css
*.min.css
*.min.js
#nodejs
node_modules
output
.tmp
*.ncrunchsolution
/_NCrunch_Website
*.DotSettings
*.cache

1
.nuke Normal file
View file

@ -0,0 +1 @@
src/Robware.Api.Auth.sln

9
Readme.md Normal file
View file

@ -0,0 +1,9 @@
[![Build Status](https://build.robware.uk/api/badges/Robware/Api.Auth/status.svg)](https://build.robware.uk/Robware/Api.Auth)
# Auth microservice
Provides an API to authenticate users.
## Setup
After clone, please run `git config --local include.path ../.gitconfig`. This will set up the filters required to ignore local dev config.

16
api.auth.service Normal file
View file

@ -0,0 +1,16 @@
[Unit]
Description=Robware Auth API
[Service]
WorkingDirectory=/var/www/Api.Auth
ExecStart=/usr/bin/dotnet /var/www/Api.Auth/Robware.Api.Auth.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=Api.Auth
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.target

68
build.ps1 Normal file
View file

@ -0,0 +1,68 @@
[CmdletBinding()]
Param(
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
[string[]]$BuildArguments
)
Write-Output "Windows PowerShell $($Host.Version)"
Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { exit 1 }
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
###########################################################################
# CONFIGURATION
###########################################################################
$BuildProjectFile = "$PSScriptRoot\build\_build.csproj"
$TempDirectory = "$PSScriptRoot\\.tmp"
$DotNetGlobalFile = "$PSScriptRoot\\global.json"
$DotNetInstallUrl = "https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.ps1"
$DotNetChannel = "Current"
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1
$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1
###########################################################################
# EXECUTION
###########################################################################
function ExecSafe([scriptblock] $cmd) {
& $cmd
if ($LASTEXITCODE) { exit $LASTEXITCODE }
}
# If global.json exists, load expected version
if (Test-Path $DotNetGlobalFile) {
$DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json)
if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) {
$DotNetVersion = $DotNetGlobal.sdk.version
}
}
# If dotnet is installed locally, and expected version is not set or installation matches the expected version
if ((Get-Command "dotnet" -ErrorAction SilentlyContinue) -ne $null -and `
(!(Test-Path variable:DotNetVersion) -or $(& dotnet --version) -eq $DotNetVersion)) {
$env:DOTNET_EXE = (Get-Command "dotnet").Path
}
else {
$DotNetDirectory = "$TempDirectory\dotnet-win"
$env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
# Download install script
$DotNetInstallFile = "$TempDirectory\dotnet-install.ps1"
md -force $TempDirectory > $null
(New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile)
# Install by channel or version
if (!(Test-Path variable:DotNetVersion)) {
ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
} else {
ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
}
}
Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)"
ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false }
ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }

62
build.sh Normal file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env bash
echo $(bash --version 2>&1 | head -n 1)
set -eo pipefail
SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
###########################################################################
# CONFIGURATION
###########################################################################
BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj"
TEMP_DIRECTORY="$SCRIPT_DIR//.tmp"
DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json"
DOTNET_INSTALL_URL="https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.sh"
DOTNET_CHANNEL="Current"
export DOTNET_CLI_TELEMETRY_OPTOUT=1
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
###########################################################################
# EXECUTION
###########################################################################
function FirstJsonValue {
perl -nle 'print $1 if m{"'$1'": "([^"\-]+)",?}' <<< ${@:2}
}
# If global.json exists, load expected version
if [ -f "$DOTNET_GLOBAL_FILE" ]; then
DOTNET_VERSION=$(FirstJsonValue "version" $(cat "$DOTNET_GLOBAL_FILE"))
if [ "$DOTNET_VERSION" == "" ]; then
unset DOTNET_VERSION
fi
fi
# If dotnet is installed locally, and expected version is not set or installation matches the expected version
if [[ -x "$(command -v dotnet)" && (-z ${DOTNET_VERSION+x} || $(dotnet --version) == "$DOTNET_VERSION") ]]; then
export DOTNET_EXE="$(command -v dotnet)"
else
DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
# Download install script
DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
mkdir -p "$TEMP_DIRECTORY"
curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
chmod +x "$DOTNET_INSTALL_FILE"
# Install by channel or version
if [ -z ${DOTNET_VERSION+x} ]; then
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
else
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
fi
fi
echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)"
"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false
"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"

10
build/.editorconfig Normal file
View file

@ -0,0 +1,10 @@
[*.cs]
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning
dotnet_style_require_accessibility_modifiers = never:warning
csharp_style_expression_bodied_properties = true:warning
csharp_style_expression_bodied_indexers = true:warning
csharp_style_expression_bodied_accessors = true:warning

62
build/Build.cs Normal file
View file

@ -0,0 +1,62 @@
using Nuke.Common;
using Nuke.Common.Execution;
using Nuke.Common.IO;
using Nuke.Common.ProjectModel;
using Nuke.Common.Tools.DotNet;
using Nuke.Common.Utilities.Collections;
using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.Tools.DotNet.DotNetTasks;
[CheckBuildProjectConfigurations]
[UnsetVisualStudioEnvironmentVariables]
class Build : NukeBuild {
public static int Main() => Execute<Build>(x => x.Publish);
[Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;
[Solution] readonly Solution Solution;
AbsolutePath SourceDirectory => RootDirectory / "src";
AbsolutePath OutputDirectory => RootDirectory / "output";
Target Clean => _ => _
.Before(Restore)
.Executes(() => {
SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory);
EnsureCleanDirectory(OutputDirectory);
});
Target Restore => _ => _
.Executes(() => {
DotNetRestore(s => s
.SetProjectFile(Solution));
});
Target Compile => _ => _
.DependsOn(Restore)
.Executes(() => {
DotNetBuild(s => s
.SetProjectFile(Solution)
.SetConfiguration(Configuration)
.EnableNoRestore());
});
Target Test => _ => _
.DependsOn(Compile)
.Executes(() => {
DotNetTest(s => s
.SetProjectFile(Solution)
.EnableNoRestore());
});
Target Publish => _ => _
.DependsOn(Test)
.Executes(() => {
DotNetPublish(s => s
.SetProject(SourceDirectory / "Robware.Api.Auth/Robware.Api.Auth.csproj")
.SetConfiguration(Configuration)
.SetOutput(OutputDirectory));
});
}

34
build/_build.csproj Normal file
View file

@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<RootNamespace></RootNamespace>
<IsPackable>False</IsPackable>
<NoWarn>CS0649;CS0169</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nuke.Common" Version="0.24.8" />
</ItemGroup>
<ItemGroup>
<NukeSpecificationFiles Include="**\*.json" Exclude="bin\**;obj\**" />
<NukeExternalFiles Include="**\*.*.ext" Exclude="bin\**;obj\**" />
<None Remove="*.csproj.DotSettings;*.ref.*.txt" />
<!-- Common build related files -->
<None Include="..\build.ps1" />
<None Include="..\build.sh" />
<None Include="..\.nuke" />
<None Include="..\global.json" Condition="Exists('..\global.json')" />
<None Include="..\nuget.config" Condition="Exists('..\nuget.config')" />
<None Include="..\azure-pipelines.yml" Condition="Exists('..\azure-pipelines.yml')" />
<None Include="..\Jenkinsfile" Condition="Exists('..\Jenkinsfile')" />
<None Include="..\appveyor.yml" Condition="Exists('..\appveyor.yml')" />
<None Include="..\.travis.yml" Condition="Exists('..\.travis.yml')" />
<None Include="..\GitVersion.yml" Condition="Exists('..\GitVersion.yml')" />
</ItemGroup>
</Project>

5
clean-config.sh Normal file
View file

@ -0,0 +1,5 @@
#!/bin/sh
sed \
-e 's/"database": "Server=.*;User ID=.*;Password=.*;Database=.*"/"database": "Server=localhost;User ID=user;Password=pass;Database=db"/g' \
$1

View file

@ -0,0 +1,67 @@
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Robware.Api.Auth.Controllers;
using Robware.Api.Auth.Models;
using Robware.Auth;
using Xunit;
namespace Robware.Api.Auth.Tests {
public class AuthControllerTests {
private class TestUser : User {
public TestUser(string username, string password) {
Username = username;
Password = password;
}
}
[Fact]
public async Task Authenticate_WithSuccessfulLoginRequest_ReturnsUser() {
var logger = Substitute.For<ILogger<AuthController>>();
var authenticator = Substitute.For<IAuthenticator>();
authenticator.Authenticate("username", "password").Returns((AuthenticationResult.Success, new TestUser("username", "password")));
var request = new LoginRequest {
Username = "username",
Password = "password"
};
var expectation = new TestUser("username", "password");
var controller = new AuthController(logger, authenticator);
(await controller.Authenticate(request)).Value.Should().BeEquivalentTo(expectation);
}
[Fact]
public async Task Authenticate_WithIncorrectPassword_Returns401() {
var logger = Substitute.For<ILogger<AuthController>>();
var authenticator = Substitute.For<IAuthenticator>();
authenticator.Authenticate("username", "password").Returns((AuthenticationResult.IncorrectPassword, null));
var request = new LoginRequest {
Username = "username",
Password = "password"
};
var controller = new AuthController(logger, authenticator);
(await controller.Authenticate(request)).Result.Should().BeOfType<UnauthorizedResult>();
}
[Fact]
public async Task Authenticate_WithIncorrectPassword_Returns404() {
var logger = Substitute.For<ILogger<AuthController>>();
var authenticator = Substitute.For<IAuthenticator>();
authenticator.Authenticate("username", "password").Returns((AuthenticationResult.NotFound, null));
var request = new LoginRequest {
Username = "username",
Password = "password"
};
var controller = new AuthController(logger, authenticator);
(await controller.Authenticate(request)).Result.Should().BeOfType<NotFoundResult>();
}
}
}

View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="NSubstitute" Version="4.2.1" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Robware.Api.Auth\Robware.Api.Auth.csproj" />
</ItemGroup>
</Project>

53
src/Robware.Api.Auth.sln Normal file
View file

@ -0,0 +1,53 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29613.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robware.Api.Auth", "Robware.Api.Auth\Robware.Api.Auth.csproj", "{D21F1402-6526-4BD7-8ADA-F8DC626D6D5A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Api.Auth.Tests", "Robware.Api.Auth.Tests\Robware.Api.Auth.Tests.csproj", "{88716F73-0264-44D9-970D-4134C644C7EF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Auth", "Robware.Auth\Robware.Auth.csproj", "{8740FE72-12D7-4039-9EB3-0417E529A10E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Auth.Tests", "Robware.Auth.Tests\Robware.Auth.Tests.csproj", "{E229DE31-8DBB-4AED-9461-A04C8DE0F074}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Data", "Robware.Data\Robware.Data.csproj", "{69989FA2-BEE8-491D-97B9-856D4916D154}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "..\build\_build.csproj", "{19A36DA9-BFBF-4988-B7C7-4808D6B57246}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{19A36DA9-BFBF-4988-B7C7-4808D6B57246}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{19A36DA9-BFBF-4988-B7C7-4808D6B57246}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D21F1402-6526-4BD7-8ADA-F8DC626D6D5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D21F1402-6526-4BD7-8ADA-F8DC626D6D5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D21F1402-6526-4BD7-8ADA-F8DC626D6D5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D21F1402-6526-4BD7-8ADA-F8DC626D6D5A}.Release|Any CPU.Build.0 = Release|Any CPU
{88716F73-0264-44D9-970D-4134C644C7EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{88716F73-0264-44D9-970D-4134C644C7EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88716F73-0264-44D9-970D-4134C644C7EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88716F73-0264-44D9-970D-4134C644C7EF}.Release|Any CPU.Build.0 = Release|Any CPU
{8740FE72-12D7-4039-9EB3-0417E529A10E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8740FE72-12D7-4039-9EB3-0417E529A10E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8740FE72-12D7-4039-9EB3-0417E529A10E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8740FE72-12D7-4039-9EB3-0417E529A10E}.Release|Any CPU.Build.0 = Release|Any CPU
{E229DE31-8DBB-4AED-9461-A04C8DE0F074}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E229DE31-8DBB-4AED-9461-A04C8DE0F074}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E229DE31-8DBB-4AED-9461-A04C8DE0F074}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E229DE31-8DBB-4AED-9461-A04C8DE0F074}.Release|Any CPU.Build.0 = Release|Any CPU
{69989FA2-BEE8-491D-97B9-856D4916D154}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{69989FA2-BEE8-491D-97B9-856D4916D154}.Debug|Any CPU.Build.0 = Debug|Any CPU
{69989FA2-BEE8-491D-97B9-856D4916D154}.Release|Any CPU.ActiveCfg = Release|Any CPU
{69989FA2-BEE8-491D-97B9-856D4916D154}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {75A89B46-CAE8-45F8-9BEF-3B7A6FD0BC72}
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,35 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Robware.Api.Auth.Models;
using Robware.Auth;
namespace Robware.Api.Auth.Controllers {
[ApiController]
[Route("[controller]")]
public class AuthController : ControllerBase {
private readonly ILogger<AuthController> _logger;
private readonly IAuthenticator _authenticator;
public AuthController(ILogger<AuthController> logger, IAuthenticator authenticator) {
_logger = logger;
_authenticator = authenticator;
}
[HttpPost(nameof(Authenticate))]
public async Task<ActionResult<User>> Authenticate(LoginRequest request) {
var (result, user) = await _authenticator.Authenticate(request.Username, request.Password);
switch (result) {
case AuthenticationResult.Success:
return user;
case AuthenticationResult.NotFound:
return NotFound();
case AuthenticationResult.IncorrectPassword:
return Unauthorized();
default:
throw new ArgumentOutOfRangeException();
}
}
}
}

View file

@ -0,0 +1,6 @@
namespace Robware.Api.Auth.Models {
public class LoginRequest {
public string Username { get; set; }
public string Password { get; set; }
}
}

View file

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace Robware.Api.Auth {
public class Program {
public static void Main(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => {
webBuilder.UseStartup<Startup>();
}).Build().Run();
}
}

View file

@ -0,0 +1,28 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:61069",
"sslPort": 44309
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Robware.Api.Auth": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Robware.Auth\Robware.Auth.csproj" />
<ProjectReference Include="..\Robware.Data\Robware.Data.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,44 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Robware.Auth;
using Robware.Data;
namespace Robware.Api.Auth {
public class Startup {
public Startup(IConfiguration configuration) {
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) {
services.AddControllers();
services.AddSingleton<ICryptographyProvider, CryptographyProvider>()
.AddSingleton<IAuthenticator, Authenticator>()
.AddSingleton<IDatabaseProvider>(new MySQLDatabaseProvider(Configuration.GetConnectionString("database")))
.AddSingleton<IUsers, UserRepository>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
});
}
}
}

View file

@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
"database": "Server=localhost;User ID=user;Password=pass;Database=db"
}
}

View file

@ -0,0 +1,20 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Kestrel": {
"EndPoints": {
"Http": {
"Url": "http://0.0.0.0:5003"
}
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"database": "<DatabaseConnectionString>"
}
}

View file

@ -0,0 +1,50 @@
using System.Threading.Tasks;
using FluentAssertions;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Robware.Data;
using Xunit;
namespace Robware.Auth.Tests {
public class AuthenticatorTests {
[Fact]
public async Task Authenticate_ForUserThatExistsWithCorrectPassword_ReturnsOkResultWithUser() {
var users = Substitute.For<IUsers>();
var crypto = Substitute.For<ICryptographyProvider>();
crypto.Encrypt("password").Returns("password");
var user = new TestUser("test", "password");
users.GetByEmail("test").Returns(user);
var auth = new Authenticator(users, crypto);
(await auth.Authenticate("test", "password")).Should().BeEquivalentTo((AuthenticationResult.Success, user));
}
[Fact]
public async Task Authenticate_ForUserThatExistsWithIncorrectPassword_ReturnsIncorrectPassword() {
var users = Substitute.For<IUsers>();
var crypto = Substitute.For<ICryptographyProvider>();
crypto.Encrypt("password").Returns("password");
var user = new TestUser("test", "password");
users.GetByEmail("test").Returns(user);
var auth = new Authenticator(users, crypto);
(await auth.Authenticate("test", "wrong")).Should().BeEquivalentTo((AuthenticationResult.IncorrectPassword, null as User));
}
[Fact]
public async Task Authenticate_ForUserThatDoesntExist_ReturnsNotFound() {
var users = Substitute.For<IUsers>();
users.GetByEmail("test").Throws(new UserNotFoundException(""));
var crypto = Substitute.For<ICryptographyProvider>();
crypto.Encrypt("password").Returns("password");
var auth = new Authenticator(users, crypto);
(await auth.Authenticate("test", "password")).Should().BeEquivalentTo((AuthenticationResult.NotFound, null as User));
}
}
}

View file

@ -0,0 +1,12 @@
using FluentAssertions;
using Xunit;
namespace Robware.Auth.Tests {
public class CryoptographyProviderTests {
[Fact]
public void Encrypt_WithInput_ReturnsHash() {
var provider = new CryptographyProvider();
provider.Encrypt("password").Should().Be("5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8");
}
}
}

View file

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="NSubstitute" Version="4.2.1" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Robware.Auth\Robware.Auth.csproj" />
<ProjectReference Include="..\Robware.Data\Robware.Data.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,8 @@
namespace Robware.Auth.Tests {
internal class TestUser : User {
public TestUser(string username, string password) {
Username = username;
Password = password;
}
}
}

View file

@ -0,0 +1,31 @@
using System;
using System.Threading.Tasks;
namespace Robware.Auth {
public class Authenticator : IAuthenticator {
private readonly IUsers _users;
private readonly ICryptographyProvider _crypto;
public Authenticator(IUsers users, ICryptographyProvider crypto) {
_users = users;
_crypto = crypto;
}
public async Task<(AuthenticationResult Result, User User)> Authenticate(string username, string password) {
try {
var user = await _users.GetByEmail(username);
return _crypto.Encrypt(password) == user.Password ? (AuthenticationResult.Success, user) : (AuthenticationResult.IncorrectPassword, null);
}
catch (UserNotFoundException) {
return (AuthenticationResult.NotFound, null);
}
}
}
public enum AuthenticationResult {
Unknown,
Success,
NotFound,
IncorrectPassword
}
}

View file

@ -0,0 +1,19 @@
using System.Security.Cryptography;
using System.Text;
namespace Robware.Auth {
public class CryptographyProvider : ICryptographyProvider {
public string Encrypt(string input) {
using (var sha256 = SHA256.Create()) {
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(input));
var builder = new StringBuilder();
foreach (var b in hash)
builder.Append(b.ToString("x2"));
var hashString = builder.ToString();
return hashString;
}
}
}
}

View file

@ -0,0 +1,7 @@
using System.Threading.Tasks;
namespace Robware.Auth {
public interface IAuthenticator {
Task<(AuthenticationResult Result, User User)> Authenticate(string username, string password);
}
}

View file

@ -0,0 +1,5 @@
namespace Robware.Auth {
public interface ICryptographyProvider {
string Encrypt(string input);
}
}

View file

@ -0,0 +1,7 @@
using System.Threading.Tasks;
namespace Robware.Auth {
public interface IUsers {
Task<User> GetByEmail(string email);
}
}

View file

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
</Project>

6
src/Robware.Auth/User.cs Normal file
View file

@ -0,0 +1,6 @@
namespace Robware.Auth {
public class User {
public string Username { get; protected set; }
public string Password { get; protected set; }
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace Robware.Auth {
public class UserNotFoundException : Exception {
public UserNotFoundException(string username) : base("Could not find user " + username) {
}
}
}

View file

@ -0,0 +1,11 @@
using Robware.Auth;
using Robware.Data.States;
namespace Robware.Data {
public class DatabaseUser : User {
public DatabaseUser(UserState state) {
Username = state.User_Email;
Password = state.User_Password;
}
}
}

View file

@ -0,0 +1,7 @@
using System.Data;
namespace Robware.Data {
public interface IDatabaseProvider {
IDbConnection NewConnection();
}
}

View file

@ -0,0 +1,12 @@
using System.Data;
using MySql.Data.MySqlClient;
namespace Robware.Data {
public class MySQLDatabaseProvider : IDatabaseProvider {
private readonly string _connectionString;
public MySQLDatabaseProvider(string connectionString) => _connectionString = connectionString;
public IDbConnection NewConnection() => new MySqlConnection(_connectionString);
}
}

View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.0.35" />
<PackageReference Include="MySqlConnector" Version="0.63.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Robware.Auth\Robware.Auth.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,10 @@
namespace Robware.Data.States {
public class UserState {
public string User_Id { get; set; }
public string User_Email { get; set; }
public string User_Password { get; set; }
public string User_Created { get; set; }
public string User_Deleted { get; set; }
public string Group_Id { get; set; }
}
}

View file

@ -0,0 +1,29 @@
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using Robware.Auth;
using Robware.Data.States;
namespace Robware.Data {
public class UserRepository : IUsers {
private readonly IDatabaseProvider _dbProvider;
public UserRepository(IDatabaseProvider dbProvider) {
_dbProvider = dbProvider;
}
public async Task<User> GetByEmail(string email) {
const string query = "SELECT * FROM users WHERE user_email=@email";
using (var connection = _dbProvider.NewConnection()) {
connection.Open();
var result = await connection.QueryAsync<UserState>(query, new { email });
if (!result.Any())
throw new UserNotFoundException(email);
return new DatabaseUser(result.Single());
}
}
}
}