Compare commits
53 Commits
v2.0.0
...
archive/v2
| Author | SHA1 | Date | |
|---|---|---|---|
| f7bb16b85c | |||
| e530cf735a | |||
| 166134c6d8 | |||
| e613fa66e3 | |||
| 99d39be9ac | |||
| b8b0d571ab | |||
| a323da829f | |||
| ef4f05f0b6 | |||
| 11126e8080 | |||
| 0b9766f7db | |||
| 849ad649a8 | |||
| 3031dda710 | |||
| 73d89a241f | |||
| df68b6dbf8 | |||
| 20684ca40a | |||
| 20b82245d0 | |||
| 8db38183c2 | |||
| 5898ea8188 | |||
| 1bc48b0ba2 | |||
| 2308e1520d | |||
| 1ede337565 | |||
| a7d2f8031e | |||
| 4aab011224 | |||
| ae74745108 | |||
| 401dfc9909 | |||
| ffae1be340 | |||
| bee771a30e | |||
| 9b38a10797 | |||
| ba7584c771 | |||
| df89450745 | |||
| c6aca4baf6 | |||
| e47d4917df | |||
| 59c452ff73 | |||
| ba46147a74 | |||
| c087dbdf2b | |||
| 92afc85dba | |||
| 51c15eff4c | |||
| 422fd6c677 | |||
| 88c8fe612d | |||
| dce0471105 | |||
| c4ee8bb1e0 | |||
| 7c835ea49b | |||
| 5f746e0bc1 | |||
| ee7bf1e204 | |||
| 4d91ce1819 | |||
| a4d1d3227b | |||
| 14c82f4f06 | |||
| fca6ef4fa6 | |||
| da45a84f61 | |||
| b7eca1937c | |||
| 85031de3c2 | |||
| 1897428d00 | |||
| 4a5855250c |
@@ -17,18 +17,23 @@ build:
|
||||
artifacts:
|
||||
paths:
|
||||
- "**/bin/Release"
|
||||
expire_in: 10 minutes
|
||||
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- dotnet test --no-restore --verbosity normal
|
||||
- dotnet test --verbosity normal
|
||||
dependencies:
|
||||
- build
|
||||
|
||||
publish:
|
||||
stage: publish
|
||||
script:
|
||||
- dotnet pack -c Release -o .
|
||||
- for nupkg in *.nupkg; do dotnet nuget push $nupkg -k $NUGET_API_KEY -s https://api.nuget.org/v3/index.json; done
|
||||
- export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//')
|
||||
- dotnet pack -c Release -o . /p:Version=$VERSION
|
||||
- for nupkg in *.nupkg; do dotnet nuget push $nupkg -k ${NUGET_API_KEY} -s https://api.nuget.org/v3/index.json; done
|
||||
only:
|
||||
- main
|
||||
variables:
|
||||
NUGET_API_KEY: $NUGET_API_KEY
|
||||
- tags
|
||||
dependencies:
|
||||
- build
|
||||
- test
|
||||
|
||||
2
.idea/.idea.HopFrame/.idea/dataSources.xml
generated
2
.idea/.idea.HopFrame/.idea/dataSources.xml
generated
@@ -5,7 +5,7 @@
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/test/RestApiTest/bin/Debug/net8.0/test.db</jdbc-url>
|
||||
<jdbc-url>jdbc:sqlite:C:\Users\leon\Documents\Projekte\HopFrame\testing\HopFrame.Testing.Api\bin\Debug\net8.0\test.db</jdbc-url>
|
||||
<jdbc-additional-properties>
|
||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||
</jdbc-additional-properties>
|
||||
|
||||
14
.idea/.idea.HopFrame/.idea/discord.xml
generated
Normal file
14
.idea/.idea.HopFrame/.idea/discord.xml
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
<option name="applicationTheme" value="default" />
|
||||
<option name="iconsTheme" value="default" />
|
||||
<option name="button1Title" value="" />
|
||||
<option name="button1Url" value="" />
|
||||
<option name="button2Title" value="" />
|
||||
<option name="button2Url" value="" />
|
||||
<option name="customApplicationId" value="" />
|
||||
</component>
|
||||
</project>
|
||||
45
HopFrame.sln
45
HopFrame.sln
@@ -2,7 +2,7 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Database", "src\HopFrame.Database\HopFrame.Database.csproj", "{003120AE-F38B-4632-8497-BE4505189627}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RestApiTest", "test\RestApiTest\RestApiTest.csproj", "{921159CE-AF75-44C3-A3F9-6B9B1A4E85CF}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Testing.Api", "testing\HopFrame.Testing.Api\HopFrame.Testing.Api.csproj", "{921159CE-AF75-44C3-A3F9-6B9B1A4E85CF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Security", "src\HopFrame.Security\HopFrame.Security.csproj", "{7F82E1C6-4A42-4337-9E03-2EE6429D004F}"
|
||||
EndProject
|
||||
@@ -10,10 +10,24 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Api", "src\HopFram
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web", "src\HopFrame.Web\HopFrame.Web.csproj", "{3BE585BC-13A5-4BE4-A806-E9EC2D825956}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontendTest", "test\FrontendTest\FrontendTest.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Testing.Web", "testing\HopFrame.Testing.Web\HopFrame.Testing.Web.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web.Admin", "src\HopFrame.Web.Admin\HopFrame.Web.Admin.csproj", "{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{64EDCBED-A84F-4936-8697-78DC43CB2427}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{EEA20D27-D471-44AF-A287-C0E068D93182}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{1D98E5DE-CB8B-4C1C-A319-D49AC137441A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Database", "tests\HopFrame.Tests.Database\HopFrame.Tests.Database.csproj", "{1CAAC943-B8FE-48DD-9712-92699647DE18}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Security", "tests\HopFrame.Tests.Security\HopFrame.Tests.Security.csproj", "{6747753A-6059-48F1-B779-D73765A373A6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Api", "tests\HopFrame.Tests.Api\HopFrame.Tests.Api.csproj", "{25DE1510-47E5-46FF-89A4-B9F99542218E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Tests.Web", "tests\HopFrame.Tests.Web\HopFrame.Tests.Web.csproj", "{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -48,7 +62,34 @@ Global
|
||||
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1CAAC943-B8FE-48DD-9712-92699647DE18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1CAAC943-B8FE-48DD-9712-92699647DE18}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1CAAC943-B8FE-48DD-9712-92699647DE18}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1CAAC943-B8FE-48DD-9712-92699647DE18}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6747753A-6059-48F1-B779-D73765A373A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6747753A-6059-48F1-B779-D73765A373A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6747753A-6059-48F1-B779-D73765A373A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6747753A-6059-48F1-B779-D73765A373A6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{25DE1510-47E5-46FF-89A4-B9F99542218E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{25DE1510-47E5-46FF-89A4-B9F99542218E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{25DE1510-47E5-46FF-89A4-B9F99542218E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{25DE1510-47E5-46FF-89A4-B9F99542218E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{1E821490-AEDC-4F55-B758-52F4FADAB53A} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
||||
{003120AE-F38B-4632-8497-BE4505189627} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
||||
{7F82E1C6-4A42-4337-9E03-2EE6429D004F} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
||||
{3BE585BC-13A5-4BE4-A806-E9EC2D825956} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
||||
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB} = {64EDCBED-A84F-4936-8697-78DC43CB2427}
|
||||
{8F983A37-63CF-48D5-988D-58B78EF8AECD} = {EEA20D27-D471-44AF-A287-C0E068D93182}
|
||||
{921159CE-AF75-44C3-A3F9-6B9B1A4E85CF} = {EEA20D27-D471-44AF-A287-C0E068D93182}
|
||||
{1CAAC943-B8FE-48DD-9712-92699647DE18} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
|
||||
{6747753A-6059-48F1-B779-D73765A373A6} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
|
||||
{25DE1510-47E5-46FF-89A4-B9F99542218E} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
|
||||
{566C13B9-4ECA-48C4-8D02-FEB6CDF523E6} = {1D98E5DE-CB8B-4C1C-A319-D49AC137441A}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -1,8 +1,122 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAsyncValueTaskMethodBuilderT_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F1e8feaadf5c3fa14d36ea2a638c432a2e1a47b7837d8b83d88303c5d9c15cf_003FAsyncValueTaskMethodBuilderT_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationHandler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fca451c12d69fe026a0e7e9b1a0ddbf4cf6f6b8316cb2aec7984a7241813f648_003FAuthenticationHandler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationSchemeOptions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F8525b7a9e58c77f532f1a88d4f2897e3c2baf316b9eb2c391b242a3885fcce6_003FAuthenticationSchemeOptions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEditContextDataAnnotationsExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbc307cd57fb42fc4c7fb9795381958122734d3750f41b6c1735c7d132ecda70_003FEditContextDataAnnotationsExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIHttpContextAccessor_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffe1b239a13ce466b829e79538c654ff54e938_003Fa5_003F4093f165_003FIHttpContextAccessor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fb7208b3f72528d22781d25fde9a55271bdf2b5aade4f03b1324579a25493cd8_003FList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMvcCoreServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fe3d0c01ead7c1fbe35a3504e9fbf28f212ac59349851c852a28fa06d719e95_003FMvcCoreServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOpenIdConnectConfiguration_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F2820ca73717f4b1ab2b3b74fd61961bd1d5b0_003F7e_003F8be6b109_003FOpenIdConnectConfiguration_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARedirectResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2261f0cc7e883c3c89b8e6c7ea509ac7c52d0629e68690ed216e1cc0c8ac_003FRedirectResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASwaggerGenServiceCollectionExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F95fc571df74c88edcdd2fb9f2e804132aa893358f3e0d8bca643299aed8dbc_003FSwaggerGenServiceCollectionExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AThrowHelper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2c8e7ca976f350cba9836d5565dac56b11e0b56656fa786460eb1395857a6fa_003FThrowHelper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValidationMessageStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Ffc81648e473bb3cc818f71427c286ecddc3604d2f4c69c565205bb89e8b4ef4_003FValidationMessageStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue"><AssemblyExplorer>
|
||||
<Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" />
|
||||
<Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" />
|
||||
</AssemblyExplorer></s:String></wpf:ResourceDictionary>
|
||||
</AssemblyExplorer></s:String>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=027ac703_002Df1f3_002D42aa_002D9c67_002D7cbaeecdbead/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="Refresh_ShouldReturnConflict_WhenRefreshTokenNotValid" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>xUnit::25DE1510-47E5-46FF-89A4-B9F99542218E::net8.0::HopFrame.Tests.Api.Controllers.OpenIdControllerTests.Refresh_ShouldReturnConflict_WhenRefreshTokenNotValid</TestId>
|
||||
</TestAncestor>
|
||||
</SessionState></s:String>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</wpf:ResourceDictionary>
|
||||
@@ -6,6 +6,8 @@ A simple backend management api for ASP.NET Core Web APIs
|
||||
- [x] User authentication
|
||||
- [x] Permission management
|
||||
- [x] Generated frontend administration boards
|
||||
- [x] API token support
|
||||
- [x] OpenID authentication integration
|
||||
|
||||
# Usage
|
||||
There are two different versions of HopFrame, either the Web API version or the full Blazor web version.
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# HopFrame Endpoints
|
||||
HopFrame currently only supports endpoints for authentication out of the box.
|
||||
|
||||
> **Hint:** with the help of the [repositories](../repositories.md) you can very easily create missing endpoints for HopFrame components yourself.
|
||||
|
||||
## All currently supported endpoints
|
||||
|
||||
> **Hint:** you can use the build-in [swagger](https://swagger.io/) ui to explore and test all endpoints of your application __including__ HopFrame endpoints.
|
||||
|
||||
### SecurityController
|
||||
Base endpoint: `/api/v1/authentication`\
|
||||
**Important:** All primitive data types (including `string`) are return as a [`SingleValueResult`](./models.md#SingleValueResult)
|
||||
|
||||
|
||||
| Method | Endpoint | Payload | Returns |
|
||||
|--------|---------------|--------------------------------------------------------------|-----------------------|
|
||||
| PUT | /login | [UserLogin](../models.md#UserLogin) | access token (string) |
|
||||
| POST | /register | [UserRegister](../models.md#UserRegister) | access token (string) |
|
||||
| GET | /authenticate | | access token (string) |
|
||||
| DELETE | /logout | | |
|
||||
| DELETE | /delete | [UserPasswordValidation](./models.md#UserPasswordValidation) | |
|
||||
120
docs/api/endpoints/auth.md
Normal file
120
docs/api/endpoints/auth.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Auth Endpoints
|
||||
|
||||
## Used Models
|
||||
- [UserLogin](../../models.md#userlogin)
|
||||
- [UserRegister](../../models.md#userregister)
|
||||
- [SingleValueResult](../../models.md#singlevalueresult)
|
||||
- [UserPasswordValidation](../../models.md#userpasswordvalidation)
|
||||
|
||||
## API Endpoint: Login
|
||||
|
||||
**Endpoint:** `PUT /api/v1/auth/login`
|
||||
|
||||
**Description:** Authenticates a user and provides access and refresh tokens.
|
||||
|
||||
**Authorization Required:** No
|
||||
|
||||
**Parameters:**
|
||||
- **UserLogin** (required): The login credentials of the user.
|
||||
```json
|
||||
{
|
||||
"email": "string",
|
||||
"password": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
- **200 OK:** Returns the access token.
|
||||
```json
|
||||
{
|
||||
"value": "string"
|
||||
}
|
||||
```
|
||||
- **400 Bad Request:** HopFrame authentication scheme is disabled.
|
||||
- **404 Not Found:** The provided email address was not found.
|
||||
- **403 Forbidden:** The provided password is not correct.
|
||||
|
||||
## API Endpoint: Register
|
||||
|
||||
**Endpoint:** `POST /api/v1/auth/register`
|
||||
|
||||
**Description:** Registers a new user and provides access and refresh tokens.
|
||||
|
||||
**Authorization Required:** No
|
||||
|
||||
**Parameters:**
|
||||
- **UserRegister** (required): The registration details of the user.
|
||||
```json
|
||||
{
|
||||
"username": "string",
|
||||
"email": "string",
|
||||
"password": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
- **200 OK:** Returns the access token.
|
||||
```json
|
||||
{
|
||||
"value": "string"
|
||||
}
|
||||
```
|
||||
- **400 Bad Request:** HopFrame authentication scheme is disabled or the password is too short.
|
||||
- **409 Conflict:** Username or email is already registered.
|
||||
|
||||
## API Endpoint: Authenticate
|
||||
|
||||
**Endpoint:** `GET /api/v1/auth/authenticate`
|
||||
|
||||
**Description:** Authenticates the user using the refresh token and provides a new access token.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Parameters:**
|
||||
- None
|
||||
|
||||
**Response:**
|
||||
- **200 OK:** Returns the access token.
|
||||
```json
|
||||
{
|
||||
"value": "string"
|
||||
}
|
||||
```
|
||||
- **400 Bad Request:** HopFrame authentication scheme is disabled or refresh token not provided.
|
||||
- **404 Not Found:** The refresh token is not valid.
|
||||
- **403 Forbidden:** The refresh token is expired.
|
||||
- **409 Conflict:** The provided token is not a refresh token.
|
||||
|
||||
## API Endpoint: Logout
|
||||
|
||||
**Endpoint:** `DELETE /api/v1/auth/logout`
|
||||
|
||||
**Description:** Logs out the user and deletes the access and refresh tokens.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Parameters:**
|
||||
- None
|
||||
|
||||
**Response:**
|
||||
- **200 OK:** User is logged out successfully.
|
||||
|
||||
## API Endpoint: Delete
|
||||
|
||||
**Endpoint:** `DELETE /api/v1/auth/delete`
|
||||
|
||||
**Description:** Deletes the user account.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Parameters:**
|
||||
- **UserPasswordValidation** (required): The password validation for the user.
|
||||
```json
|
||||
{
|
||||
"password": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
- **200 OK:** User account is deleted successfully.
|
||||
- **403 Forbidden:** The provided password is not correct.
|
||||
237
docs/api/endpoints/group.md
Normal file
237
docs/api/endpoints/group.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Group Endpoints
|
||||
|
||||
## Used Models
|
||||
- [Group](../../models.md#permissiongroup)
|
||||
|
||||
## API Endpoint: GetGroups
|
||||
|
||||
**Endpoint:** `GET /api/v1/groups`
|
||||
|
||||
**Description:** Retrieves a list of all groups.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Required Permission:** `hopframe.admin.groups.read`
|
||||
|
||||
**Parameters:** None
|
||||
|
||||
**Response:**
|
||||
|
||||
- **200 OK:** Returns a list of groups.
|
||||
```json
|
||||
[
|
||||
{
|
||||
"Name": "string",
|
||||
"IsDefaultGroup": "boolean",
|
||||
"Description": "string",
|
||||
"CreatedAt": "2023-12-23T00:00:00Z",
|
||||
"Permissions": [
|
||||
{
|
||||
"Id": 1,
|
||||
"PermissionName": "string",
|
||||
"GrantedAt": "2023-12-23T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
- **401 Unauthorized:** User is not authorized to access this endpoint.
|
||||
|
||||
## API Endpoint: GetDefaultGroups
|
||||
|
||||
**Endpoint:** `GET /api/v1/groups/default`
|
||||
|
||||
**Description:** Retrieves a list of default groups.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Required Permission:** `hopframe.admin.groups.read`
|
||||
|
||||
**Parameters:** None
|
||||
|
||||
**Response:**
|
||||
|
||||
- **200 OK:** Returns a list of default groups.
|
||||
```json
|
||||
[
|
||||
{
|
||||
"Name": "string",
|
||||
"IsDefaultGroup": "boolean",
|
||||
"Description": "string",
|
||||
"CreatedAt": "2023-12-23T00:00:00Z",
|
||||
"Permissions": [
|
||||
{
|
||||
"Id": 1,
|
||||
"PermissionName": "string",
|
||||
"GrantedAt": "2023-12-23T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
- **401 Unauthorized:** User is not authorized to access this endpoint.
|
||||
|
||||
## API Endpoint: GetUserGroups
|
||||
|
||||
**Endpoint:** `GET /api/v1/groups/user/{userId}`
|
||||
|
||||
**Description:** Retrieves a list of groups for a specific user.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Required Permission:** `hopframe.admin.groups.read`
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- **userId:** `string` (path) - The ID of the user.
|
||||
|
||||
**Response:**
|
||||
|
||||
- **200 OK:** Returns a list of groups for the user.
|
||||
```json
|
||||
[
|
||||
{
|
||||
"Name": "string",
|
||||
"IsDefaultGroup": "boolean",
|
||||
"Description": "string",
|
||||
"CreatedAt": "2023-12-23T00:00:00Z",
|
||||
"Permissions": [
|
||||
{
|
||||
"Id": 1,
|
||||
"PermissionName": "string",
|
||||
"GrantedAt": "2023-12-23T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
- **400 Bad Request:** Invalid user ID.
|
||||
- **401 Unauthorized:** User is not authorized to access this endpoint.
|
||||
- **404 Not Found:** User does not exist.
|
||||
|
||||
## API Endpoint: GetGroup
|
||||
|
||||
**Endpoint:** `GET /api/v1/groups/{name}`
|
||||
|
||||
**Description:** Retrieves details of a specific group.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Required Permission:** `hopframe.admin.groups.read`
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- **name:** `string` (path) - The name of the group.
|
||||
|
||||
**Response:**
|
||||
|
||||
- **200 OK:** Returns the details of the group.
|
||||
```json
|
||||
{
|
||||
"Name": "string",
|
||||
"IsDefaultGroup": "boolean",
|
||||
"Description": "string",
|
||||
"CreatedAt": "2023-12-23T00:00:00Z",
|
||||
"Permissions": [
|
||||
{
|
||||
"Id": 1,
|
||||
"PermissionName": "string",
|
||||
"GrantedAt": "2023-12-23T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- **401 Unauthorized:** User is not authorized to access this endpoint.
|
||||
- **404 Not Found:** Group does not exist.
|
||||
|
||||
## API Endpoint: CreateGroup
|
||||
|
||||
**Endpoint:** `POST /api/v1/groups`
|
||||
|
||||
**Description:** Creates a new group.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Required Permission:** `hopframe.admin.groups.create`
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- **group:** `PermissionGroup` (body) - The group to be created.
|
||||
|
||||
**Response:**
|
||||
|
||||
- **200 OK:** Returns the created group.
|
||||
```json
|
||||
{
|
||||
"Name": "string",
|
||||
"IsDefaultGroup": "boolean",
|
||||
"Description": "string",
|
||||
"CreatedAt": "2023-12-23T00:00:00Z",
|
||||
"Permissions": [
|
||||
{
|
||||
"Id": 1,
|
||||
"PermissionName": "string",
|
||||
"GrantedAt": "2023-12-23T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- **400 Bad Request:** Provide a group.
|
||||
- **400 Bad Request:** Group names must start with 'group.'.
|
||||
- **401 Unauthorized:** User is not authorized to access this endpoint.
|
||||
- **409 Conflict:** Group already exists.
|
||||
|
||||
## API Endpoint: UpdateGroup
|
||||
|
||||
**Endpoint:** `PUT /api/v1/groups`
|
||||
|
||||
**Description:** Updates an existing group.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Required Permission:** `hopframe.admin.groups.update`
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- **group:** `PermissionGroup` (body) - The group to be updated.
|
||||
|
||||
**Response:**
|
||||
|
||||
- **200 OK:** Returns the updated group.
|
||||
```json
|
||||
{
|
||||
"Name": "string",
|
||||
"IsDefaultGroup": "boolean",
|
||||
"Description": "string",
|
||||
"CreatedAt": "2023-12-23T00:00:00Z",
|
||||
"Permissions": [
|
||||
{
|
||||
"Id": 1,
|
||||
"PermissionName": "string",
|
||||
"GrantedAt": "2023-12-23T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- **401 Unauthorized:** User is not authorized to access this endpoint.
|
||||
- **404 Not Found:** Group does not exist.
|
||||
|
||||
## API Endpoint: DeleteGroup
|
||||
|
||||
**Endpoint:** `DELETE /api/v1/groups/{name}`
|
||||
|
||||
**Description:** Deletes a specific group.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Required Permission:** `hopframe.admin.groups.delete`
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- **name:** `string` (path) - The name of the group to be deleted.
|
||||
|
||||
**Response:**
|
||||
|
||||
- **200 OK:** Group deleted successfully.
|
||||
- **401 Unauthorized:** User is not authorized to access this endpoint.
|
||||
- **404 Not Found:** Group does not exist.
|
||||
82
docs/api/endpoints/openId.md
Normal file
82
docs/api/endpoints/openId.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# OpenID Endpoints
|
||||
|
||||
## Used Models
|
||||
- [SingleValueResult](../../models.md#singlevalueresult)
|
||||
|
||||
## API Endpoint: RedirectToProvider
|
||||
|
||||
**Endpoint:** `GET /api/v1/openid/redirect`
|
||||
|
||||
**Description:** Redirects the user to the OpenID provider's authorization endpoint.
|
||||
|
||||
**Authorization Required:** No
|
||||
|
||||
**Parameters:**
|
||||
- **redirectAfter** (query, optional): The URL to redirect to after authentication.
|
||||
- **performRedirect** (query, optional): A flag to indicate if the user should be redirected (default is 1).
|
||||
|
||||
**Response:**
|
||||
- **302 Found:** Redirects the user to the OpenID provider's authorization endpoint.
|
||||
- **200 OK:** Returns the constructed authorization URI.
|
||||
```json
|
||||
{
|
||||
"value": "string"
|
||||
}
|
||||
```
|
||||
|
||||
## API Endpoint: Callback
|
||||
|
||||
**Endpoint:** `GET /api/v1/openid/callback`
|
||||
|
||||
**Description:** Handles the callback from the OpenID provider and exchanges the authorization code for tokens.
|
||||
|
||||
**Authorization Required:** No
|
||||
|
||||
**Parameters:**
|
||||
- **code** (query, required): The authorization code received from the OpenID provider.
|
||||
- **state** (query, optional): The state parameter to handle the redirect after authentication.
|
||||
|
||||
**Response:**
|
||||
- **200 OK:** Returns the access token.
|
||||
```json
|
||||
{
|
||||
"value": "string"
|
||||
}
|
||||
```
|
||||
- **400 Bad Request:** Authorization code is missing.
|
||||
- **403 Forbidden:** Authorization code is not valid.
|
||||
|
||||
## API Endpoint: Refresh
|
||||
|
||||
**Endpoint:** `GET /api/v1/openid/refresh`
|
||||
|
||||
**Description:** Refreshes the access token using the refresh token.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Parameters:**
|
||||
- None
|
||||
|
||||
**Response:**
|
||||
- **200 OK:** Returns the refreshed access token.
|
||||
```json
|
||||
{
|
||||
"value": "string"
|
||||
}
|
||||
```
|
||||
- **400 Bad Request:** Refresh token not provided.
|
||||
- **409 Conflict**: Refresh token not valid.
|
||||
|
||||
## API Endpoint: Logout
|
||||
|
||||
**Endpoint:** `DELETE /api/v1/openid/logout`
|
||||
|
||||
**Description:** Logs out the user by deleting the authentication cookies.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Parameters:**
|
||||
- None
|
||||
|
||||
**Response:**
|
||||
- **200 OK:** User is logged out successfully.
|
||||
316
docs/api/endpoints/user.md
Normal file
316
docs/api/endpoints/user.md
Normal file
@@ -0,0 +1,316 @@
|
||||
# User Endpoints
|
||||
|
||||
## Used Models
|
||||
- [User](../../models.md#user)
|
||||
- [UserCreator](../../models.md#usercreator)
|
||||
- [Permission](../../models.md#permission)
|
||||
|
||||
## API Endpoint: GetUsers
|
||||
|
||||
**Endpoint:** `GET /api/v1/users`
|
||||
|
||||
**Description:** Retrieves a list of users.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Required Permission:** `hopframe.admin.users.read`
|
||||
|
||||
**Parameters:** None
|
||||
|
||||
**Response:**
|
||||
|
||||
- **200 OK:** Returns a list of users.
|
||||
```json
|
||||
[
|
||||
{
|
||||
"Id": "guid",
|
||||
"Username": "string",
|
||||
"Email": "string",
|
||||
"CreatedAt": "2023-12-23T00:00:00Z",
|
||||
"Permissions": [
|
||||
{
|
||||
"Id": 1,
|
||||
"PermissionName": "string",
|
||||
"GrantedAt": "2023-12-23T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
- **401 Unauthorized:** User is not authorized to access this endpoint.
|
||||
|
||||
|
||||
## API Endpoint: GetUser
|
||||
|
||||
**Endpoint:** `GET /api/v1/users/{userId}`
|
||||
|
||||
**Description:** Retrieves a user by their ID.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Required Permission:** `hopframe.admin.users.read`
|
||||
|
||||
**Parameters:**
|
||||
- **userId:** `string` (Path) - The ID of the user to retrieve.
|
||||
|
||||
**Response:**
|
||||
|
||||
- **200 OK:** Returns the user details.
|
||||
```json
|
||||
{
|
||||
"Id": "guid",
|
||||
"Username": "string",
|
||||
"Email": "string",
|
||||
"CreatedAt": "2023-12-23T00:00:00Z",
|
||||
"Permissions": [
|
||||
{
|
||||
"Id": 1,
|
||||
"PermissionName": "string",
|
||||
"GrantedAt": "2023-12-23T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **400 Bad Request:** Invalid user ID format.
|
||||
|
||||
- **401 Unauthorized:** User is not authorized to access this endpoint.
|
||||
|
||||
- **404 Not Found:** User does not exist.
|
||||
|
||||
|
||||
## API Endpoint: GetUserByUsername
|
||||
|
||||
**Endpoint:** `GET /api/v1/users/username/{username}`
|
||||
|
||||
**Description:** Retrieves a user by their username.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Required Permission:** `hopframe.admin.users.read`
|
||||
|
||||
**Parameters:**
|
||||
- **username:** `string` (Path) - The username of the user to retrieve.
|
||||
|
||||
**Response:**
|
||||
|
||||
- **200 OK:** Returns the user details.
|
||||
```json
|
||||
{
|
||||
"Id": "guid",
|
||||
"Username": "string",
|
||||
"Email": "string",
|
||||
"CreatedAt": "2023-12-23T00:00:00Z",
|
||||
"Permissions": [
|
||||
{
|
||||
"Id": 1,
|
||||
"PermissionName": "string",
|
||||
"GrantedAt": "2023-12-23T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **401 Unauthorized:** User is not authorized to access this endpoint.
|
||||
|
||||
- **404 Not Found:** User does not exist.
|
||||
|
||||
|
||||
## API Endpoint: GetUserByEmail
|
||||
|
||||
**Endpoint:** `GET /api/v1/users/email/{email}`
|
||||
|
||||
**Description:** Retrieves a user by their email address.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Required Permission:** `hopframe.admin.users.read`
|
||||
|
||||
**Parameters:**
|
||||
- **email:** `string` (Path) - The email address of the user to retrieve.
|
||||
|
||||
**Response:**
|
||||
|
||||
- **200 OK:** Returns the user details.
|
||||
```json
|
||||
{
|
||||
"Id": "guid",
|
||||
"Username": "string",
|
||||
"Email": "string",
|
||||
"CreatedAt": "2023-12-23T00:00:00Z",
|
||||
"Permissions": [
|
||||
{
|
||||
"Id": 1,
|
||||
"PermissionName": "string",
|
||||
"GrantedAt": "2023-12-23T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **401 Unauthorized:** User is not authorized to access this endpoint.
|
||||
|
||||
- **404 Not Found:** User does not exist.
|
||||
|
||||
|
||||
## API Endpoint: CreateUser
|
||||
|
||||
**Endpoint:** `POST /api/v1/users`
|
||||
|
||||
**Description:** Creates a new user.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Required Permission:** `hopframe.admin.users.create`
|
||||
|
||||
**Parameters:**
|
||||
- **UserCreator:** (Body) - The user creation details.
|
||||
```json
|
||||
{
|
||||
"Username": "string",
|
||||
"Email": "string",
|
||||
"Password": "string",
|
||||
"Permissions": [
|
||||
"permission1",
|
||||
"permission2"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
- **200 OK:** Returns the created user.
|
||||
```json
|
||||
{
|
||||
"Id": "guid",
|
||||
"Username": "string",
|
||||
"Email": "string",
|
||||
"CreatedAt": "2023-12-23T00:00:00Z",
|
||||
"Permissions": [
|
||||
{
|
||||
"Id": 1,
|
||||
"PermissionName": "string",
|
||||
"GrantedAt": "2023-12-23T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **401 Unauthorized:** User is not authorized to access this endpoint.
|
||||
|
||||
- **409 Conflict:** The user already exists.
|
||||
|
||||
|
||||
## API Endpoint: UpdateUser
|
||||
|
||||
**Endpoint:** `PUT /api/v1/users/{userId}`
|
||||
|
||||
**Description:** Updates an existing user by their ID.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Required Permission:** `hopframe.admin.users.update`
|
||||
|
||||
**Parameters:**
|
||||
- **userId:** `string` (Path) - The ID of the user to update.
|
||||
- **User:** (Body) - The user details to update.
|
||||
```json
|
||||
{
|
||||
"Id": "guid",
|
||||
"Username": "string",
|
||||
"Email": "string",
|
||||
"CreatedAt": "2023-12-23T00:00:00Z",
|
||||
"Permissions": [
|
||||
{
|
||||
"Id": 1,
|
||||
"PermissionName": "string",
|
||||
"GrantedAt": "2023-12-23T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
- **200 OK:** Returns the updated user.
|
||||
```json
|
||||
{
|
||||
"Id": "guid",
|
||||
"Username": "string",
|
||||
"Email": "string",
|
||||
"CreatedAt": "2023-12-23T00:00:00Z",
|
||||
"Permissions": [
|
||||
{
|
||||
"Id": 1,
|
||||
"PermissionName": "string",
|
||||
"GrantedAt": "2023-12-23T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **400 Bad Request:** Invalid user ID format.
|
||||
|
||||
- **401 Unauthorized:** User is not authorized to access this endpoint.
|
||||
|
||||
- **404 Not Found:** User does not exist.
|
||||
|
||||
- **409 Conflict:** Cannot edit user with different user ID.
|
||||
|
||||
|
||||
## API Endpoint: DeleteUser
|
||||
|
||||
**Endpoint:** `DELETE /api/v1/users/{userId}`
|
||||
|
||||
**Description:** Deletes a user by their ID.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Required Permission:** `hopframe.admin.users.delete`
|
||||
|
||||
**Parameters:**
|
||||
- **userId:** `string` (Path) - The ID of the user to delete.
|
||||
|
||||
**Response:**
|
||||
|
||||
- **200 OK:** User successfully deleted.
|
||||
|
||||
- **400 Bad Request:** Invalid user ID format.
|
||||
|
||||
- **401 Unauthorized:** User is not authorized to access this endpoint.
|
||||
|
||||
- **404 Not Found:** User does not exist.
|
||||
|
||||
|
||||
## API Endpoint: ChangePassword
|
||||
|
||||
**Endpoint:** `PUT /api/v1/users/{userId}/password`
|
||||
|
||||
**Description:** Updates the password for a user by their ID.
|
||||
|
||||
**Authorization Required:** Yes
|
||||
|
||||
**Required Permission:** `hopframe.admin.users.update` (if the userId is not the id of the requesting user)
|
||||
|
||||
**Parameters:**
|
||||
- **userId:** `string` (Path) - The ID of the user whose password is being changed.
|
||||
- **UserPasswordChange:** (Body) - The password change details (note, if you change someone else's password the old password doesn't need to be correct).
|
||||
```json
|
||||
{
|
||||
"oldPassword": "string",
|
||||
"newPassword": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
- **200 OK:** Password successfully updated.
|
||||
|
||||
- **400 Bad Request:** Invalid user ID format.
|
||||
|
||||
- **401 Unauthorized:** User is not authorized to access this endpoint.
|
||||
|
||||
- **404 Not Found:** User does not exist.
|
||||
|
||||
- **409 Conflict:** Old password is incorrect.
|
||||
@@ -14,3 +14,66 @@ public sealed class UserPasswordValidation {
|
||||
public string Password { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
## OpenIdConfiguration
|
||||
```csharp
|
||||
public sealed class OpenIdConfiguration {
|
||||
public string Issuer { get; set; }
|
||||
public string AuthorizationEndpoint { get; set; }
|
||||
public string TokenEndpoint { get; set; }
|
||||
public string UserinfoEndpoint { get; set; }
|
||||
public string EndSessionEndpoint { get; set; }
|
||||
public string IntrospectionEndpoint { get; set; }
|
||||
public string RevocationEndpoint { get; set; }
|
||||
public string DeviceAuthorizationEndpoint { get; set; }
|
||||
public List<string> ResponseTypesSupported { get; set; }
|
||||
public List<string> ResponseModesSupported { get; set; }
|
||||
public string JwksUri { get; set; }
|
||||
public List<string> GrantTypesSupported { get; set; }
|
||||
public List<string> IdTokenSigningAlgValuesSupported { get; set; }
|
||||
public List<string> SubjectTypesSupported { get; set; }
|
||||
public List<string> TokenEndpointAuthMethodsSupported { get; set; }
|
||||
public List<string> AcrValuesSupported { get; set; }
|
||||
public List<string> ScopesSupported { get; set; }
|
||||
public bool RequestParameterSupported { get; set; }
|
||||
public List<string> ClaimsSupported { get; set; }
|
||||
public bool ClaimsParameterSupported { get; set; }
|
||||
public List<string> CodeChallengeMethodsSupported { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
## OpenIdIntrospection
|
||||
```csharp
|
||||
public sealed class OpenIdIntrospection {
|
||||
public string Issuer { get; set; }
|
||||
public string Subject { get; set; }
|
||||
public string Audience { get; set; }
|
||||
public long Expiration { get; set; }
|
||||
public long IssuedAt { get; set; }
|
||||
public long AuthTime { get; set; }
|
||||
public string Acr { get; set; }
|
||||
public List<string> AuthenticationMethods { get; set; }
|
||||
public string SessionId { get; set; }
|
||||
public string Email { get; set; }
|
||||
public bool EmailVerified { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string GivenName { get; set; }
|
||||
public string PreferredUsername { get; set; }
|
||||
public string Nickname { get; set; }
|
||||
public List<string> Groups { get; set; }
|
||||
public bool Active { get; set; }
|
||||
public string Scope { get; set; }
|
||||
public string ClientId { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
## OpenIdToken
|
||||
```csharp
|
||||
public sealed class OpenIdToken {
|
||||
public string AccessToken { get; set; }
|
||||
public string RefreshToken { get; set; }
|
||||
public string TokenType { get; set; }
|
||||
public int ExpiresIn { get; set; }
|
||||
public string IdToken { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
76
docs/authentication.md
Normal file
76
docs/authentication.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# HopFrame Authentication
|
||||
|
||||
HopFrame uses a token system with a short term access token and a long term refresh token for authenticating users.
|
||||
These tokens are usually provided to the endpoints of the API / Blazor Pages through Cookies:
|
||||
|
||||
| Cookie key | Cookie value sample | Description |
|
||||
|--------------------------------|----------------------------------------|-----------------------------|
|
||||
| HopFrame.Security.RefreshToken | `42047983-914d-418b-841a-4382614231be` | The long term refresh token |
|
||||
| HopFrame.Security.AccessToken | `d39c9432-0831-42df-8844-5e2b70f03eda` | The short term access token |
|
||||
|
||||
The advantage of these cookies is that they are automatically set by the backend and delete themselves, when they are
|
||||
no longer valid.
|
||||
|
||||
The access token can also be delivered through a header called `HopFrame.Authentication` or `Token`.
|
||||
It can also be delivered through a query parameter called `token`. This simplifies requests for images for example
|
||||
because you can directly specify the url in the img tag in html.
|
||||
|
||||
## Authentication configuration
|
||||
|
||||
You can also configure the time span that the tokens are valid using the `appsettings.json` or environment variables
|
||||
by configuring your configuration to load these.
|
||||
>**Hint**: Configuring your application to use environment variables works by simply adding
|
||||
> `builder.Configuration.AddEnvironmentVariables();` to your startup configuration before you add the
|
||||
> custom configurations / HopFrame services.
|
||||
|
||||
You can specify `Seconds`, `Minutes`, `Hours` and `Days` for either of the two token types.
|
||||
These get combined to a single time span. You can also completely disable the default authentication
|
||||
by setting the `DefaultAuthentication` to `false`. Note that you will no longer be able to login in any
|
||||
way unless you enabled the [OpenID](./openid.md) authentication.
|
||||
|
||||
#### Configuration example
|
||||
```json
|
||||
"HopFrame": {
|
||||
"Authentication": {
|
||||
"AccessToken": {
|
||||
"Minutes": 30
|
||||
},
|
||||
"RefreshToken": {
|
||||
"Days": 10,
|
||||
"Hours": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Environment variables example
|
||||
```dotenv
|
||||
HOPFRAME__AUTHENTICATION__ACCESSTOKEN__MINUTES=30
|
||||
HOPFRAME__AUTHENTICATION__REFRESHTOKEN__DAYS=10
|
||||
HOPFRAME__AUTHENTICATION__REFRESHTOKEN__HOURS=5
|
||||
```
|
||||
|
||||
## API tokens
|
||||
|
||||
API tokens are useful to use in automation environments that need to access an endpoint or page of your application.
|
||||
The HopFrame supports this natively and no further configuration is required in order to use them.
|
||||
|
||||
### Create an api token
|
||||
|
||||
You can create an api token via the `ITokenRepository`:
|
||||
```csharp
|
||||
tokens.CreateApiToken(user, DateTime.MaxValue);
|
||||
```
|
||||
|
||||
This creates a new api token that is valid until the provided DateTime has passed. Note that in the database and the token
|
||||
model the `CreatedAt` property represents the expiration date on an api token. For security reasons the api token by default
|
||||
has no permissions. This allows you to create tokens that are just permitted to perform a single action. Note that an api token
|
||||
can **never** have more permissions than the user associated with it.
|
||||
|
||||
### Add permissions to an api token
|
||||
|
||||
You can add permissions to an api token like you would to a normal user or group:
|
||||
|
||||
```csharp
|
||||
permissions.AddPermission(apiToken, "token.permission");
|
||||
```
|
||||
@@ -35,16 +35,18 @@ public class Permission {
|
||||
public DateTime GrantedAt { get; set; }
|
||||
public virtual User User { get; set; }
|
||||
public virtual PermissionGroup Group { get; set; }
|
||||
public virtual Token Token { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
## Token
|
||||
```csharp
|
||||
public class Token {
|
||||
public class Token : IPermissionOwner {
|
||||
public int Type { get; set; }
|
||||
public Guid Content { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public virtual User Owner { get; set; }
|
||||
public virtual List<Permission> Permissions { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -65,7 +67,31 @@ public class UserRegister {
|
||||
}
|
||||
```
|
||||
|
||||
## UserCreator
|
||||
```csharp
|
||||
public class UserCreator {
|
||||
public string Username { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Password { get; set; }
|
||||
public virtual List<string> Permissions { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
## IPermissionOwner
|
||||
```csharp
|
||||
public interface IPermissionOwner;
|
||||
```
|
||||
|
||||
## SingleValueResult
|
||||
```csharp
|
||||
public struct SingleValueResult<TValue>(TValue value) {
|
||||
public TValue Value { get; set; } = value;
|
||||
}
|
||||
```
|
||||
|
||||
## UserPasswordValidation
|
||||
```csharp
|
||||
public sealed class UserPasswordValidation {
|
||||
public string Password { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
120
docs/openid.md
Normal file
120
docs/openid.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# OpenID Authentication
|
||||
The HopFrame allows you to use an OpenID provider as your authentication provider for single sign on or better security
|
||||
etc. To use it, you just simply need to configure it through the `appsettings.json` or environment variables.
|
||||
|
||||
>**Note**: The Blazor module has not yet implemented endpoints for the login process, but the middleware is correctly
|
||||
> configured and the `IOpenIdAccessor` service is also provided for you to easily implement the endpoints yourself.
|
||||
|
||||
When you have enabled the integration, new endpoints will also be provided to perform the authentication.
|
||||
simply use the swagger explorer to look up how the endpoints function. They're all under the subroute
|
||||
`/api/v1/openid/`.
|
||||
|
||||
## Configure the HopFrame to use OpenID authentication
|
||||
|
||||
1. Create / Configure your OpenID provider:
|
||||
|
||||
- Save the ClientID and Client Secret from the provider, because you need it later.
|
||||
- The default redirect uri looks something like this: `https://example.com/api/v1/openid/callback`.
|
||||
- **Replace** the origin with the FQDN of your service.
|
||||
- In order for the HopFrame to automatically renew expired access tokens you need to enable the `offline_access` scope.
|
||||
- The integration also works without doing that, but then you need to reauthenticate every time your access token expires.
|
||||
|
||||
2. Configure the HopFrame integration:
|
||||
|
||||
>**Hint**: All of these configuration options can also be defined as environment variables. Use '__'
|
||||
> to separate the namespaces like so: `HOPFRAME__AUTHENTICATION__OPENID__ENABLED=true`
|
||||
|
||||
- Add the following lines to your `appsettings.json`:
|
||||
```json
|
||||
"HopFrame": {
|
||||
"Authentication": {
|
||||
"OpenID": {
|
||||
"Enabled": true,
|
||||
"Issuer": "your-issuer",
|
||||
"ClientId": "your-client-id",
|
||||
"ClientSecret": "your-client-secret"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
>**Hint**: If you are using Authentik, the issuer url looks something like this: `https://auth.example.com/application/o/application-name/`.
|
||||
> Just replace the FQDN and application-name with your configured application.
|
||||
|
||||
- **Optional**: You can also disable the default authentication via the config:
|
||||
|
||||
```json
|
||||
"HopFrame": {
|
||||
"Authentication": {
|
||||
"DefaultAuthentication": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Optional**: By default, the HopFrame will cache the api responses to reduce api latency. This can also be configured in the config (the cache can also be completely disabled here):
|
||||
|
||||
```json
|
||||
"HopFrame": {
|
||||
"Authentication": {
|
||||
"OpenID": {
|
||||
"Cache": {
|
||||
"Enabled": true,
|
||||
"Configuration": {
|
||||
"Hours": 5
|
||||
},
|
||||
"Auth": {
|
||||
"Seconds": 90
|
||||
},
|
||||
"Inspection": {
|
||||
"Minutes": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Optional**: You can also define your own callback endpoint like so (you also need to add / replace the endpoint in the provider settings):
|
||||
|
||||
```json
|
||||
"HopFrame": {
|
||||
"Authentication": {
|
||||
"OpenID": {
|
||||
"Callback": "https://example.com/auth/callback"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Optional**: You can also prevent new users from being created by disabling it in the config:
|
||||
|
||||
```json
|
||||
"HopFrame": {
|
||||
"Authentication": {
|
||||
"OpenID": {
|
||||
"GenerateUsers": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Use the abstraction to integrate OpenID yourself
|
||||
|
||||
The HopFrame has a service, that simplifies the communication with the OpenID provider called `IOpenIdAccessor`.
|
||||
You can inject it like every other service in your application.
|
||||
|
||||
```csharp
|
||||
public interface IOpenIdAccessor {
|
||||
|
||||
Task<OpenIdConfiguration> LoadConfiguration();
|
||||
|
||||
Task<OpenIdToken> RequestToken(string code, string defaultCallback);
|
||||
|
||||
Task<string> ConstructAuthUri(string defaultCallback, string state = null);
|
||||
|
||||
Task<OpenIdIntrospection> InspectToken(string token);
|
||||
|
||||
Task<OpenIdToken> RefreshAccessToken(string refreshToken);
|
||||
|
||||
}
|
||||
```
|
||||
80
docs/permissions.md
Normal file
80
docs/permissions.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# HopFrame Permissions
|
||||
|
||||
Permissions in the HopFrame are simple and effective to use.
|
||||
As discussed in the [repositories](./repositories.md) documentation, you can manage user / group permissions
|
||||
via the `IPermissionRepository` service.
|
||||
|
||||
## How do permissions work in the HopFrame
|
||||
|
||||
Permissions are defined using the . (dot) syntax. This enables you to nest permissions in namespaces.
|
||||
You can also give a user or a group the permission to every permission in a namespace by using the * (star) syntax.
|
||||
|
||||
| Permission | Example | Description |
|
||||
|----------------------|-------------------------------|-------------------------------------------------------|
|
||||
| `*` | `*` | all permissions |
|
||||
| `[namespace].[name]` | `hopframe.admin.users.create` | single permission |
|
||||
| `[namespace].*` | `hopframe.admin.*` | all permissions in that namespace (works recursively) |
|
||||
|
||||
### Reserved namespaces
|
||||
|
||||
| Namespace | Example | Description |
|
||||
|-----------|---------------|------------------------------------------|
|
||||
| `group` | `group.admin` | The user needs to be in a specific group |
|
||||
|
||||
### Permission Groups
|
||||
|
||||
You can manage them through the `IGroupRepository` as described in the [repositories](./repositories.md) documentation.
|
||||
You add permissions just like you would to a user with the `IPermissionRepository`.
|
||||
You can assign a user to a group by assigning the group permission to the user:
|
||||
```csharp
|
||||
permissionRepository.AddPermission(user, "group.admin");
|
||||
```
|
||||
|
||||
## Predefined Permissions
|
||||
|
||||
| Permission | Description |
|
||||
|--------------------------------|-------------------------------|
|
||||
| `hopframe.admin` | Access to the admin dashboard |
|
||||
| `hopframe.admin.users.read` | View all users |
|
||||
| `hopframe.admin.users.update` | Edit a user |
|
||||
| `hopframe.admin.users.delete` | Delete a user |
|
||||
| `hopframe.admin.users.create` | Add a group |
|
||||
| `hopframe.admin.groups.read` | View all groups |
|
||||
| `hopframe.admin.groups.update` | Edit a group |
|
||||
| `hopframe.admin.groups.delete` | Delete a group |
|
||||
| `hopframe.admin.groups.create` | Add a group |
|
||||
|
||||
### Configuring HopFrame permissions
|
||||
|
||||
You can also configure the predefined permissions using the `appsettings.json` or environment variables
|
||||
by configuring your configuration to load these.
|
||||
>**Hint**: Configuring your application to use environment variables works by simply adding
|
||||
> `builder.Configuration.AddEnvironmentVariables();` to your startup configuration before you add the
|
||||
> custom configurations / HopFrame services.
|
||||
|
||||
You can specify `Dashboard` for the dashboard permission and for `Users` and `Groups` you can specify
|
||||
`Create`, `Read`, `Update` and `Delete` permissions.
|
||||
|
||||
#### Configuration example
|
||||
```json
|
||||
"HopFrame": {
|
||||
"Permissions": {
|
||||
"Dashboard": "myapp.dashboard.view",
|
||||
"Users": {
|
||||
"Read": "myapp.read.users"
|
||||
},
|
||||
"Groups": {
|
||||
"Create": "myapp.create.groups",
|
||||
"Update": "myapp.update.groups"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Environment variables example
|
||||
```dotenv
|
||||
HOPFRAME__PERMISSIONS__DASHBOARD="myapp.dashboard.view"
|
||||
HOPFRAME__PERMISSIONS__USERS__READ="myapp.read.users"
|
||||
HOPFRAME__PERMISSIONS__GROUPS__CREATE="myapp.create.groups"
|
||||
HOPFRAME__PERMISSIONS__GROUPS__UPDATE="myapp.update.groups"
|
||||
```
|
||||
@@ -7,6 +7,9 @@ The HopFrame comes in two variations, you can eiter only use the backend with so
|
||||
- [Database](./database.md)
|
||||
- [Repositories](./repositories.md)
|
||||
- [Base Models](./models.md)
|
||||
- [Authentication](./authentication.md)
|
||||
- [Permissions](./permissions.md)
|
||||
- [OpenID Integration](./openid.md)
|
||||
|
||||
## HopFrame Web API
|
||||
|
||||
|
||||
@@ -71,5 +71,9 @@ public interface ITokenRepository {
|
||||
Task<Token> CreateToken(int type, User owner);
|
||||
|
||||
Task DeleteUserTokens(User owner);
|
||||
|
||||
Task DeleteToken(Token token);
|
||||
|
||||
Task<Token> CreateApiToken(User owner, DateTime expirationDate);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -7,8 +7,8 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace HopFrame.Api.Controller;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/v1/authentication")]
|
||||
public class SecurityController(IAuthLogic auth) : ControllerBase {
|
||||
[Route("api/v1/auth")]
|
||||
public class AuthController(IAuthLogic auth) : ControllerBase {
|
||||
|
||||
[HttpPut("login")]
|
||||
public async Task<ActionResult<SingleValueResult<string>>> Login([FromBody] UserLogin login) {
|
||||
74
src/HopFrame.Api/Controller/GroupController.cs
Normal file
74
src/HopFrame.Api/Controller/GroupController.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using HopFrame.Api.Logic;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authorization;
|
||||
using HopFrame.Security.Claims;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Api.Controller;
|
||||
|
||||
[ApiController, Route("api/v1/groups")]
|
||||
public class GroupController(IOptions<AdminPermissionOptions> permissions, IPermissionRepository perms, ITokenContext context, IGroupLogic groups) : ControllerBase {
|
||||
|
||||
private async Task<bool> AuthorizeRequest(string permission) {
|
||||
return await perms.HasPermission(context.AccessToken, permission);
|
||||
}
|
||||
|
||||
[HttpGet, Authorized]
|
||||
public async Task<ActionResult<IList<PermissionGroup>>> GetGroups() {
|
||||
if (!await AuthorizeRequest(permissions.Value.Groups.Read))
|
||||
return Unauthorized();
|
||||
|
||||
return await groups.GetGroups();
|
||||
}
|
||||
|
||||
[HttpGet("default"), Authorized]
|
||||
public async Task<ActionResult<IList<PermissionGroup>>> GetDefaultGroups() {
|
||||
if (!await AuthorizeRequest(permissions.Value.Groups.Read))
|
||||
return Unauthorized();
|
||||
|
||||
return await groups.GetDefaultGroups();
|
||||
}
|
||||
|
||||
[HttpGet("user/{userId}"), Authorized]
|
||||
public async Task<ActionResult<IList<PermissionGroup>>> GetUserGroups(string userId) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Groups.Read))
|
||||
return Unauthorized();
|
||||
|
||||
return await groups.GetUserGroups(userId);
|
||||
}
|
||||
|
||||
[HttpGet("{name}"), Authorized]
|
||||
public async Task<ActionResult<PermissionGroup>> GetGroup(string name) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Groups.Read))
|
||||
return Unauthorized();
|
||||
|
||||
return await groups.GetGroup(name);
|
||||
}
|
||||
|
||||
[HttpPost, Authorized]
|
||||
public async Task<ActionResult<PermissionGroup>> CreateGroup([FromBody] PermissionGroup group) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Groups.Create))
|
||||
return Unauthorized();
|
||||
|
||||
return await groups.CreateGroup(group);
|
||||
}
|
||||
|
||||
[HttpPut, Authorized]
|
||||
public async Task<ActionResult<PermissionGroup>> UpdateGroup([FromBody] PermissionGroup group) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Groups.Update))
|
||||
return Unauthorized();
|
||||
|
||||
return await groups.UpdateGroup(group);
|
||||
}
|
||||
|
||||
[HttpDelete("{name}"), Authorized]
|
||||
public async Task<ActionResult> DeleteGroup(string name) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Groups.Delete))
|
||||
return Unauthorized();
|
||||
|
||||
return await groups.DeleteGroup(name);
|
||||
}
|
||||
|
||||
}
|
||||
67
src/HopFrame.Api/Controller/OpenIdController.cs
Normal file
67
src/HopFrame.Api/Controller/OpenIdController.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using HopFrame.Api.Models;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Claims;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace HopFrame.Api.Controller;
|
||||
|
||||
[ApiController, Route("api/v1/openid")]
|
||||
public class OpenIdController(IOpenIdAccessor accessor) : ControllerBase {
|
||||
public const string DefaultCallback = "api/v1/openid/callback";
|
||||
|
||||
[HttpGet("redirect")]
|
||||
public async Task<IActionResult> RedirectToProvider([FromQuery] string redirectAfter, [FromQuery] int performRedirect = 1) {
|
||||
var uri = await accessor.ConstructAuthUri(redirectAfter);
|
||||
|
||||
if (performRedirect == 1) {
|
||||
return Redirect(uri);
|
||||
}
|
||||
|
||||
return Ok(new SingleValueResult<string>(uri));
|
||||
}
|
||||
|
||||
[HttpGet("callback")]
|
||||
public async Task<IActionResult> Callback([FromQuery] string code, [FromQuery] string state) {
|
||||
if (string.IsNullOrEmpty(code)) {
|
||||
return BadRequest("Authorization code is missing");
|
||||
}
|
||||
|
||||
var token = await accessor.RequestToken(code);
|
||||
|
||||
if (token is null) {
|
||||
return Forbid("Authorization code is not valid");
|
||||
}
|
||||
|
||||
accessor.SetAuthenticationCookies(token);
|
||||
|
||||
if (string.IsNullOrEmpty(state)) {
|
||||
return Ok(new SingleValueResult<string>(token.AccessToken));
|
||||
}
|
||||
|
||||
return Redirect(state.Replace("{token}", token.AccessToken));
|
||||
}
|
||||
|
||||
[HttpGet("refresh")]
|
||||
public async Task<IActionResult> Refresh() {
|
||||
var refreshToken = Request.Cookies[ITokenContext.RefreshTokenType];
|
||||
|
||||
if (string.IsNullOrEmpty(refreshToken))
|
||||
return BadRequest("Refresh token not provided");
|
||||
|
||||
var token = await accessor.RefreshAccessToken(refreshToken);
|
||||
|
||||
if (token is null)
|
||||
return Conflict("Refresh token not valid");
|
||||
|
||||
accessor.SetAuthenticationCookies(token);
|
||||
|
||||
return Ok(new SingleValueResult<string>(token.AccessToken));
|
||||
}
|
||||
|
||||
[HttpDelete("logout")]
|
||||
public IActionResult Logout() {
|
||||
accessor.Logout();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
83
src/HopFrame.Api/Controller/UserController.cs
Normal file
83
src/HopFrame.Api/Controller/UserController.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using HopFrame.Api.Logic;
|
||||
using HopFrame.Api.Models;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authorization;
|
||||
using HopFrame.Security.Claims;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Api.Controller;
|
||||
|
||||
[ApiController, Route("api/v1/users")]
|
||||
public class UserController(IOptions<AdminPermissionOptions> permissions, IPermissionRepository perms, ITokenContext context, IUserLogic logic) : ControllerBase {
|
||||
|
||||
private async Task<bool> AuthorizeRequest(string permission) {
|
||||
return await perms.HasPermission(context.AccessToken, permission);
|
||||
}
|
||||
|
||||
[HttpGet, Authorized]
|
||||
public async Task<ActionResult<IList<User>>> GetUsers() {
|
||||
if (!await AuthorizeRequest(permissions.Value.Users.Read))
|
||||
return Unauthorized();
|
||||
|
||||
return await logic.GetUsers();
|
||||
}
|
||||
|
||||
[HttpGet("{userId}"), Authorized]
|
||||
public async Task<ActionResult<User>> GetUser(string userId) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Users.Read))
|
||||
return Unauthorized();
|
||||
|
||||
return await logic.GetUser(userId);
|
||||
}
|
||||
|
||||
[HttpGet("username/{username}"), Authorized]
|
||||
public async Task<ActionResult<User>> GetUserByUsername(string username) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Users.Read))
|
||||
return Unauthorized();
|
||||
|
||||
return await logic.GetUserByUsername(username);
|
||||
}
|
||||
|
||||
[HttpGet("email/{email}"), Authorized]
|
||||
public async Task<ActionResult<User>> GetUserByEmail(string email) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Users.Read))
|
||||
return Unauthorized();
|
||||
|
||||
return await logic.GetUserByEmail(email);
|
||||
}
|
||||
|
||||
[HttpPost, Authorized]
|
||||
public async Task<ActionResult<User>> CreateUser([FromBody] UserCreator user) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Users.Create))
|
||||
return Unauthorized();
|
||||
|
||||
return await logic.CreateUser(user);
|
||||
}
|
||||
|
||||
[HttpPut("{userId}"), Authorized]
|
||||
public async Task<ActionResult<User>> UpdateUser(string userId, [FromBody] User user) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Users.Update))
|
||||
return Unauthorized();
|
||||
|
||||
return await logic.UpdateUser(userId, user);
|
||||
}
|
||||
|
||||
[HttpDelete("{userId}"), Authorized]
|
||||
public async Task<ActionResult> DeleteUser(string userId) {
|
||||
if (!await AuthorizeRequest(permissions.Value.Users.Delete))
|
||||
return Unauthorized();
|
||||
|
||||
return await logic.DeleteUser(userId);
|
||||
}
|
||||
|
||||
[HttpPut("{userId}/password"), Authorized]
|
||||
public async Task<ActionResult> ChangePassword(string userId, [FromBody] UserPasswordChange passwordChange) {
|
||||
if (context.User.Id.ToString() != userId && !await AuthorizeRequest(permissions.Value.Users.Update))
|
||||
return Unauthorized();
|
||||
|
||||
return await logic.UpdatePassword(userId, passwordChange.OldPassword, passwordChange.NewPassword);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
using HopFrame.Api.Controller;
|
||||
using HopFrame.Api.Logic;
|
||||
using HopFrame.Api.Logic.Implementation;
|
||||
using HopFrame.Api.Models;
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
@@ -15,23 +18,53 @@ public static class ServiceCollectionExtensions {
|
||||
/// Adds all HopFrame endpoints and services to the application
|
||||
/// </summary>
|
||||
/// <param name="services">The service provider to add the services to</param>
|
||||
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
|
||||
/// <param name="config">Configuration for how the HopFrame services are set up</param>
|
||||
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
|
||||
public static void AddHopFrame<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
|
||||
services.AddMvcCore().UseSpecificControllers(typeof(SecurityController));
|
||||
AddHopFrameNoEndpoints<TDbContext>(services);
|
||||
public static void AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration, HopFrameApiModuleConfig config = null) where TDbContext : HopDbContextBase {
|
||||
config ??= new();
|
||||
|
||||
var controllers = new List<Type>();
|
||||
|
||||
if (config.ExposeModelEndpoints)
|
||||
controllers.AddRange([typeof(UserController), typeof(GroupController)]);
|
||||
|
||||
var defaultAuthenticationSection = configuration.GetSection("HopFrame:Authentication:DefaultAuthentication");
|
||||
if ((!defaultAuthenticationSection.Exists() || configuration.GetValue<bool>("HopFrame:Authentication:DefaultAuthentication")) && config.ExposeAuthEndpoints)
|
||||
controllers.Add(typeof(AuthController));
|
||||
|
||||
if (configuration.GetValue<bool>("HopFrame:Authentication:OpenID:Enabled") && config.ExposeAuthEndpoints) {
|
||||
IOpenIdAccessor.DefaultCallback = OpenIdController.DefaultCallback;
|
||||
controllers.Add(typeof(OpenIdController));
|
||||
}
|
||||
|
||||
AddHopFrameNoEndpoints<TDbContext>(services, configuration, config);
|
||||
services.AddMvcCore().UseSpecificControllers(controllers.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all HopFrame services to the application
|
||||
/// </summary>
|
||||
/// <param name="services">The service provider to add the services to</param>
|
||||
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
|
||||
/// <param name="config">Configuration for how the HopFrame services are set up</param>
|
||||
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
|
||||
public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
|
||||
public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services, ConfigurationManager configuration, HopFrameApiModuleConfig config = null) where TDbContext : HopDbContextBase {
|
||||
config ??= new();
|
||||
|
||||
services.AddMvcCore().ConfigureApplicationPartManager(manager => {
|
||||
var endpoints = manager.ApplicationParts.SingleOrDefault(p => p.Name == typeof(ServiceCollectionExtensions).Namespace!.Replace(".Extensions", ""));
|
||||
manager.ApplicationParts.Remove(endpoints);
|
||||
});
|
||||
|
||||
services.AddSingleton(config);
|
||||
services.AddHopFrameRepositories<TDbContext>();
|
||||
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddScoped<IAuthLogic, AuthLogic>();
|
||||
services.AddScoped<IUserLogic, UserLogic>();
|
||||
services.AddScoped<IGroupLogic, GroupLogic>();
|
||||
|
||||
services.AddHopFrameAuthentication();
|
||||
services.AddHopFrameAuthentication(configuration, config);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<Nullable>disable</Nullable>
|
||||
|
||||
<PackageId>HopFrame.Api</PackageId>
|
||||
<Version>2.0.0</Version>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<IsPackable>true</IsPackable>
|
||||
@@ -22,4 +21,10 @@
|
||||
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>HopFrame.Tests.Api</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
14
src/HopFrame.Api/Logic/IGroupLogic.cs
Normal file
14
src/HopFrame.Api/Logic/IGroupLogic.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using HopFrame.Database.Models;
|
||||
|
||||
namespace HopFrame.Api.Logic;
|
||||
|
||||
public interface IGroupLogic {
|
||||
Task<LogicResult<IList<PermissionGroup>>> GetGroups();
|
||||
Task<LogicResult<IList<PermissionGroup>>> GetDefaultGroups();
|
||||
Task<LogicResult<IList<PermissionGroup>>> GetUserGroups(string userId);
|
||||
Task<LogicResult<PermissionGroup>> GetGroup(string name);
|
||||
|
||||
Task<LogicResult<PermissionGroup>> CreateGroup(PermissionGroup group);
|
||||
Task<LogicResult<PermissionGroup>> UpdateGroup(PermissionGroup group);
|
||||
Task<LogicResult> DeleteGroup(string name);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace HopFrame.Api.Logic;
|
||||
|
||||
|
||||
16
src/HopFrame.Api/Logic/IUserLogic.cs
Normal file
16
src/HopFrame.Api/Logic/IUserLogic.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using HopFrame.Api.Models;
|
||||
using HopFrame.Database.Models;
|
||||
|
||||
namespace HopFrame.Api.Logic;
|
||||
|
||||
public interface IUserLogic {
|
||||
Task<LogicResult<IList<User>>> GetUsers();
|
||||
Task<LogicResult<User>> GetUser(string id);
|
||||
Task<LogicResult<User>> GetUserByUsername(string username);
|
||||
Task<LogicResult<User>> GetUserByEmail(string email);
|
||||
|
||||
Task<LogicResult<User>> CreateUser(UserCreator user);
|
||||
Task<LogicResult<User>> UpdateUser(string id, User user);
|
||||
Task<LogicResult> DeleteUser(string id);
|
||||
Task<LogicResult> UpdatePassword(string id, string oldPassword, string newPassword);
|
||||
}
|
||||
@@ -5,12 +5,15 @@ using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Api.Logic.Implementation;
|
||||
|
||||
public class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor) : IAuthLogic {
|
||||
internal sealed class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor, IOptions<HopFrameAuthenticationOptions> options) : IAuthLogic {
|
||||
|
||||
public async Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login) {
|
||||
if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled");
|
||||
|
||||
var user = await users.GetUserByEmail(login.Email);
|
||||
|
||||
if (user is null)
|
||||
@@ -22,23 +25,25 @@ public class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenCon
|
||||
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
||||
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
|
||||
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.RefreshTokenTime,
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.RefreshTokenTime,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.AccessTokenTime,
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.AccessTokenTime,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString());
|
||||
}
|
||||
|
||||
public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) {
|
||||
if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled");
|
||||
|
||||
if (register.Password.Length < 8)
|
||||
return LogicResult<SingleValueResult<string>>.Conflict("Password needs to be at least 8 characters long");
|
||||
return LogicResult<SingleValueResult<string>>.BadRequest("Password needs to be at least 8 characters long");
|
||||
|
||||
var allUsers = await users.GetUsers();
|
||||
if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email))
|
||||
@@ -53,46 +58,48 @@ public class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenCon
|
||||
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
||||
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
|
||||
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.RefreshTokenTime,
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.RefreshTokenTime,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.AccessTokenTime,
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.AccessTokenTime,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString());
|
||||
}
|
||||
|
||||
public async Task<LogicResult<SingleValueResult<string>>> Authenticate() {
|
||||
if (!options.Value.DefaultAuthentication) return LogicResult<SingleValueResult<string>>.BadRequest("HopFrame authentication scheme is disabled");
|
||||
|
||||
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
|
||||
|
||||
if (string.IsNullOrEmpty(refreshToken))
|
||||
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token not provided");
|
||||
return LogicResult<SingleValueResult<string>>.BadRequest("Refresh token not provided");
|
||||
|
||||
var token = await tokens.GetToken(refreshToken);
|
||||
|
||||
if (token.Type != Token.RefreshTokenType)
|
||||
return LogicResult<SingleValueResult<string>>.BadRequest("The provided token is not a refresh token");
|
||||
|
||||
if (token is null)
|
||||
return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid");
|
||||
|
||||
if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now)
|
||||
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token is expired");
|
||||
if (token.Type != Token.RefreshTokenType)
|
||||
return LogicResult<SingleValueResult<string>>.Conflict("The provided token is not a refresh token");
|
||||
|
||||
if (token.CreatedAt + options.Value.RefreshTokenTime < DateTime.Now)
|
||||
return LogicResult<SingleValueResult<string>>.Forbidden("Refresh token is expired");
|
||||
|
||||
var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner);
|
||||
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.AccessTokenTime,
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.AccessTokenTime,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.TokenId.ToString());
|
||||
}
|
||||
|
||||
public async Task<LogicResult> Logout() {
|
||||
@@ -100,9 +107,7 @@ public class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenCon
|
||||
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
|
||||
|
||||
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
|
||||
return LogicResult.Conflict("access or refresh token not provided");
|
||||
|
||||
await tokens.DeleteUserTokens(tokenContext.User);
|
||||
await tokens.DeleteUserTokens(tokenContext.User);
|
||||
|
||||
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
|
||||
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType);
|
||||
|
||||
66
src/HopFrame.Api/Logic/Implementation/GroupLogic.cs
Normal file
66
src/HopFrame.Api/Logic/Implementation/GroupLogic.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
|
||||
namespace HopFrame.Api.Logic.Implementation;
|
||||
|
||||
internal sealed class GroupLogic(IGroupRepository groups, IUserRepository users) : IGroupLogic {
|
||||
public async Task<LogicResult<IList<PermissionGroup>>> GetGroups() {
|
||||
return LogicResult<IList<PermissionGroup>>.Ok(await groups.GetPermissionGroups());
|
||||
}
|
||||
|
||||
public async Task<LogicResult<IList<PermissionGroup>>> GetDefaultGroups() {
|
||||
return LogicResult<IList<PermissionGroup>>.Ok(await groups.GetDefaultGroups());
|
||||
}
|
||||
|
||||
public async Task<LogicResult<IList<PermissionGroup>>> GetUserGroups(string id) {
|
||||
if (!Guid.TryParse(id, out var userId))
|
||||
return LogicResult<IList<PermissionGroup>>.BadRequest("Invalid user id");
|
||||
|
||||
var user = await users.GetUser(userId);
|
||||
|
||||
if (user is null)
|
||||
return LogicResult<IList<PermissionGroup>>.NotFound("That user does not exist");
|
||||
|
||||
return LogicResult<IList<PermissionGroup>>.Ok(await groups.GetUserGroups(user));
|
||||
}
|
||||
|
||||
public async Task<LogicResult<PermissionGroup>> GetGroup(string name) {
|
||||
var group = await groups.GetPermissionGroup(name);
|
||||
|
||||
if (group is null)
|
||||
return LogicResult<PermissionGroup>.NotFound("That group does not exist");
|
||||
|
||||
return LogicResult<PermissionGroup>.Ok(group);
|
||||
}
|
||||
|
||||
public async Task<LogicResult<PermissionGroup>> CreateGroup(PermissionGroup group) {
|
||||
if (group is null)
|
||||
return LogicResult<PermissionGroup>.BadRequest("Provide a group");
|
||||
|
||||
if (!group.Name.StartsWith("group."))
|
||||
return LogicResult<PermissionGroup>.BadRequest("Group names must start with 'group.'");
|
||||
|
||||
if (await groups.GetPermissionGroup(group.Name) != null)
|
||||
return LogicResult<PermissionGroup>.Conflict("That group already exists");
|
||||
|
||||
return LogicResult<PermissionGroup>.Ok(await groups.CreatePermissionGroup(group));
|
||||
}
|
||||
|
||||
public async Task<LogicResult<PermissionGroup>> UpdateGroup(PermissionGroup group) {
|
||||
if (await groups.GetPermissionGroup(group.Name) == null)
|
||||
return LogicResult<PermissionGroup>.NotFound("That user does not exist");
|
||||
|
||||
await groups.EditPermissionGroup(group);
|
||||
return LogicResult<PermissionGroup>.Ok(group);
|
||||
}
|
||||
|
||||
public async Task<LogicResult> DeleteGroup(string name) {
|
||||
var group = await groups.GetPermissionGroup(name);
|
||||
|
||||
if (group is null)
|
||||
return LogicResult.NotFound("That group does not exist");
|
||||
|
||||
await groups.DeletePermissionGroup(group);
|
||||
return LogicResult.Ok();
|
||||
}
|
||||
}
|
||||
105
src/HopFrame.Api/Logic/Implementation/UserLogic.cs
Normal file
105
src/HopFrame.Api/Logic/Implementation/UserLogic.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using HopFrame.Api.Models;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Claims;
|
||||
|
||||
namespace HopFrame.Api.Logic.Implementation;
|
||||
|
||||
internal sealed class UserLogic(IUserRepository users, ITokenContext context) : IUserLogic {
|
||||
public async Task<LogicResult<IList<User>>> GetUsers() {
|
||||
return LogicResult<IList<User>>.Ok(await users.GetUsers());
|
||||
}
|
||||
|
||||
public async Task<LogicResult<User>> GetUser(string id) {
|
||||
if (!Guid.TryParse(id, out var userId))
|
||||
return LogicResult<User>.BadRequest("Invalid user id");
|
||||
|
||||
var user = await users.GetUser(userId);
|
||||
|
||||
if (user is null)
|
||||
return LogicResult<User>.NotFound("That user does not exist");
|
||||
|
||||
return LogicResult<User>.Ok(user);
|
||||
}
|
||||
|
||||
public async Task<LogicResult<User>> GetUserByUsername(string username) {
|
||||
var user = await users.GetUserByUsername(username);
|
||||
|
||||
if (user is null)
|
||||
return LogicResult<User>.NotFound("That user does not exist");
|
||||
|
||||
return LogicResult<User>.Ok(user);
|
||||
}
|
||||
|
||||
public async Task<LogicResult<User>> GetUserByEmail(string email) {
|
||||
var user = await users.GetUserByEmail(email);
|
||||
|
||||
if (user is null)
|
||||
return LogicResult<User>.NotFound("That user does not exist");
|
||||
|
||||
return LogicResult<User>.Ok(user);
|
||||
}
|
||||
|
||||
public async Task<LogicResult<User>> CreateUser(UserCreator user) {
|
||||
var createdUser = new User {
|
||||
Email = user.Email,
|
||||
Username = user.Username,
|
||||
Password = user.Password,
|
||||
};
|
||||
createdUser.Permissions = user.Permissions?.Select(p => new Permission {
|
||||
GrantedAt = DateTime.Now,
|
||||
PermissionName = p,
|
||||
User = createdUser
|
||||
}).ToList();
|
||||
|
||||
var newUser = await users.AddUser(createdUser);
|
||||
|
||||
if (newUser is null)
|
||||
return LogicResult<User>.Conflict("That user already exists");
|
||||
|
||||
return LogicResult<User>.Ok(newUser);
|
||||
}
|
||||
|
||||
public async Task<LogicResult<User>> UpdateUser(string id, User user) {
|
||||
if (!Guid.TryParse(id, out var userId))
|
||||
return LogicResult<User>.BadRequest("Invalid user id");
|
||||
|
||||
if (user.Id != userId)
|
||||
return LogicResult<User>.Conflict("Cannot edit user with different user id");
|
||||
|
||||
if (await users.GetUser(userId) is null)
|
||||
return LogicResult<User>.NotFound("That user does not exist");
|
||||
|
||||
await users.UpdateUser(user);
|
||||
return LogicResult<User>.Ok(user);
|
||||
}
|
||||
|
||||
public async Task<LogicResult> DeleteUser(string id) {
|
||||
if (!Guid.TryParse(id, out var userId))
|
||||
return LogicResult.BadRequest("Invalid user id");
|
||||
|
||||
var user = await users.GetUser(userId);
|
||||
|
||||
if (user is null)
|
||||
return LogicResult.NotFound("That user does not exist");
|
||||
|
||||
await users.DeleteUser(user);
|
||||
return LogicResult.Ok();
|
||||
}
|
||||
|
||||
public async Task<LogicResult> UpdatePassword(string id, string oldPassword, string newPassword) {
|
||||
if (!Guid.TryParse(id, out var userId))
|
||||
return LogicResult.BadRequest("Invalid user id");
|
||||
|
||||
var user = await users.GetUser(userId);
|
||||
|
||||
if (user is null)
|
||||
return LogicResult.NotFound("That user does not exist");
|
||||
|
||||
if (userId == context.User.Id && !await users.CheckUserPassword(user, oldPassword))
|
||||
return LogicResult.Conflict("Old password is not correct");
|
||||
|
||||
await users.ChangePassword(user, newPassword);
|
||||
return LogicResult.Ok();
|
||||
}
|
||||
}
|
||||
8
src/HopFrame.Api/Models/HopFrameApiModuleConfig.cs
Normal file
8
src/HopFrame.Api/Models/HopFrameApiModuleConfig.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using HopFrame.Security.Models;
|
||||
|
||||
namespace HopFrame.Api.Models;
|
||||
|
||||
public class HopFrameApiModuleConfig : HopFrameConfig {
|
||||
public bool ExposeModelEndpoints { get; set; } = true;
|
||||
public bool ExposeAuthEndpoints { get; set; } = true;
|
||||
}
|
||||
8
src/HopFrame.Api/Models/UserCreator.cs
Normal file
8
src/HopFrame.Api/Models/UserCreator.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace HopFrame.Api.Models;
|
||||
|
||||
public class UserCreator {
|
||||
public string Username { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Password { get; set; }
|
||||
public virtual List<string> Permissions { get; set; }
|
||||
}
|
||||
6
src/HopFrame.Api/Models/UserPasswordChange.cs
Normal file
6
src/HopFrame.Api/Models/UserPasswordChange.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace HopFrame.Api.Models;
|
||||
|
||||
public class UserPasswordChange {
|
||||
public string OldPassword { get; set; }
|
||||
public string NewPassword { get; set; }
|
||||
}
|
||||
@@ -8,6 +8,8 @@ namespace HopFrame.Database;
|
||||
/// </summary>
|
||||
public abstract class HopDbContextBase : DbContext {
|
||||
|
||||
public static IList<Action<HopDbContextBase>> SaveHandlers = new List<Action<HopDbContextBase>>();
|
||||
|
||||
public virtual DbSet<User> Users { get; set; }
|
||||
public virtual DbSet<Permission> Permissions { get; set; }
|
||||
public virtual DbSet<Token> Tokens { get; set; }
|
||||
@@ -30,5 +32,42 @@ public abstract class HopDbContextBase : DbContext {
|
||||
.HasMany(g => g.Permissions)
|
||||
.WithOne(p => p.Group)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<Token>()
|
||||
.HasMany(t => t.Permissions)
|
||||
.WithOne(t => t.Token)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
private void OnSaving() {
|
||||
var orphanedPermissions = Permissions
|
||||
.Where(p => p.UserId == null && p.GroupName == null && p.TokenId == null)
|
||||
.ToList();
|
||||
|
||||
foreach (var handler in SaveHandlers) {
|
||||
handler.Invoke(this);
|
||||
}
|
||||
|
||||
Permissions.RemoveRange(orphanedPermissions);
|
||||
}
|
||||
|
||||
public override int SaveChanges() {
|
||||
OnSaving();
|
||||
return base.SaveChanges();
|
||||
}
|
||||
|
||||
public override int SaveChanges(bool acceptAllChangesOnSuccess) {
|
||||
OnSaving();
|
||||
return base.SaveChanges(acceptAllChangesOnSuccess);
|
||||
}
|
||||
|
||||
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) {
|
||||
OnSaving();
|
||||
return base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken()) {
|
||||
OnSaving();
|
||||
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
<Nullable>disable</Nullable>
|
||||
|
||||
<PackageId>HopFrame.Database</PackageId>
|
||||
<Version>2.0.0</Version>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<IsPackable>true</IsPackable>
|
||||
@@ -22,4 +21,10 @@
|
||||
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>HopFrame.Tests.Database</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -18,9 +18,18 @@ public class Permission {
|
||||
[ForeignKey("UserId"), JsonIgnore]
|
||||
public virtual User User { get; set; }
|
||||
|
||||
public Guid? UserId { get; set; }
|
||||
|
||||
[ForeignKey("GroupName"), JsonIgnore]
|
||||
public virtual PermissionGroup Group { get; set; }
|
||||
|
||||
[MaxLength(255)]
|
||||
public string GroupName { get; set; }
|
||||
|
||||
[ForeignKey("TokenId"), JsonIgnore]
|
||||
public virtual Token Token { get; set; }
|
||||
|
||||
public Guid? TokenId { get; set; }
|
||||
}
|
||||
|
||||
public interface IPermissionOwner;
|
||||
|
||||
@@ -4,24 +4,33 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace HopFrame.Database.Models;
|
||||
|
||||
public class Token {
|
||||
public class Token : IPermissionOwner {
|
||||
public const int RefreshTokenType = 0;
|
||||
public const int AccessTokenType = 1;
|
||||
public const int ApiTokenType = 2;
|
||||
public const int OpenIdTokenType = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the Type of the stored Token
|
||||
/// 0: Refresh token
|
||||
/// 1: Access token
|
||||
/// 2: Api token
|
||||
/// </summary>
|
||||
[Required, MinLength(1), MaxLength(1)]
|
||||
public int Type { get; set; }
|
||||
|
||||
[Key, Required, MinLength(36), MaxLength(36)]
|
||||
public Guid Content { get; set; }
|
||||
public Guid TokenId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the creation date of the token
|
||||
/// In case of an api token it defines the date it becomes invalid
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[ForeignKey("UserId"), JsonIgnore]
|
||||
public virtual User Owner { get; set; }
|
||||
|
||||
public virtual List<Permission> Permissions { get; set; }
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace HopFrame.Database.Models;
|
||||
|
||||
public class User : IPermissionOwner {
|
||||
|
||||
[Key, Required, MinLength(36), MaxLength(36)]
|
||||
[Key, Required]
|
||||
public Guid Id { get; init; }
|
||||
|
||||
[Required, MaxLength(50)]
|
||||
@@ -14,7 +14,7 @@ public class User : IPermissionOwner {
|
||||
[Required, MaxLength(50), EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Required, MinLength(8), MaxLength(255), JsonIgnore]
|
||||
[MinLength(8), MaxLength(255), JsonIgnore]
|
||||
public string Password { get; set; }
|
||||
|
||||
[Required]
|
||||
|
||||
@@ -5,5 +5,7 @@ namespace HopFrame.Database.Repositories;
|
||||
public interface ITokenRepository {
|
||||
Task<Token> GetToken(string content);
|
||||
Task<Token> CreateToken(int type, User owner);
|
||||
Task DeleteUserTokens(User owner);
|
||||
Task DeleteUserTokens(User owner, bool includeApiTokens = false);
|
||||
Task DeleteToken(Token token);
|
||||
Task<Token> CreateApiToken(User owner, DateTime expirationDate);
|
||||
}
|
||||
@@ -33,19 +33,38 @@ internal sealed class GroupRepository<TDbContext>(TDbContext context) : IGroupRe
|
||||
}
|
||||
|
||||
public async Task EditPermissionGroup(PermissionGroup group) {
|
||||
var orig = await context.Groups.SingleOrDefaultAsync(g => g.Name == group.Name);
|
||||
var orig = await context.Groups
|
||||
.Include(g => g.Permissions) // Include related entities
|
||||
.SingleOrDefaultAsync(g => g.Name == group.Name);
|
||||
|
||||
if (orig is null) return;
|
||||
|
||||
var entity = context.Groups.Update(orig);
|
||||
// Update the main entity's properties
|
||||
orig.IsDefaultGroup = group.IsDefaultGroup;
|
||||
orig.Description = group.Description;
|
||||
|
||||
entity.Entity.IsDefaultGroup = group.IsDefaultGroup;
|
||||
entity.Entity.Description = group.Description;
|
||||
entity.Entity.Permissions = group.Permissions;
|
||||
// Update the permissions
|
||||
foreach (var permission in group.Permissions) {
|
||||
var existingPermission = orig.Permissions.FirstOrDefault(p => p.Id == permission.Id);
|
||||
if (existingPermission != null) {
|
||||
// Update existing permission
|
||||
context.Entry(existingPermission).CurrentValues.SetValues(permission);
|
||||
} else {
|
||||
// Add new permission
|
||||
orig.Permissions.Add(permission);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove deleted permissions
|
||||
foreach (var permission in orig.Permissions.ToList().Where(permission => group.Permissions.All(p => p.Id != permission.Id))) {
|
||||
orig.Permissions.Remove(permission);
|
||||
context.Permissions.Remove(permission); // Ensure it gets removed from the database
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
|
||||
public async Task<PermissionGroup> CreatePermissionGroup(PermissionGroup group) {
|
||||
group.CreatedAt = DateTime.Now;
|
||||
await context.Groups.AddAsync(group);
|
||||
|
||||
@@ -5,6 +5,10 @@ namespace HopFrame.Database.Repositories.Implementation;
|
||||
|
||||
internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGroupRepository groupRepository) : IPermissionRepository where TDbContext : HopDbContextBase {
|
||||
public async Task<bool> HasPermission(IPermissionOwner owner, params string[] permissions) {
|
||||
if (owner is Token { Type: Token.ApiTokenType } token) {
|
||||
if (!await HasPermission(token.Owner, permissions)) return false;
|
||||
}
|
||||
|
||||
var perms = (await GetFullPermissions(owner)).ToArray();
|
||||
|
||||
foreach (var permission in permissions) {
|
||||
@@ -24,6 +28,12 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
|
||||
entry.User = user;
|
||||
}else if (owner is PermissionGroup group) {
|
||||
entry.Group = group;
|
||||
}else if (owner is Token token) {
|
||||
if (token.Type != Token.ApiTokenType)
|
||||
throw new ArgumentException("Only API tokens can have permissions!");
|
||||
if (!await HasPermission(token.Owner, permission))
|
||||
throw new ArgumentException("An api token cannot have more permissions than the owner has!");
|
||||
entry.Token = token;
|
||||
}
|
||||
|
||||
await context.Permissions.AddAsync(entry);
|
||||
@@ -48,6 +58,13 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
|
||||
.Where(p =>p.Group.Name == group.Name)
|
||||
.Where(p => p.PermissionName == permission)
|
||||
.SingleOrDefaultAsync();
|
||||
}else if (owner is Token token) {
|
||||
entry = await context.Permissions
|
||||
.Include(p => p.Token)
|
||||
.Where(p => p.Token != null)
|
||||
.Where(p => p.Token.TokenId == token.TokenId)
|
||||
.Where(p => p.PermissionName == permission)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
if (entry is not null) {
|
||||
@@ -59,6 +76,10 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
|
||||
public async Task<IList<string>> GetFullPermissions(IPermissionOwner owner) {
|
||||
var permissions = new List<string>();
|
||||
|
||||
if (owner is Token token && token.Type != Token.ApiTokenType) {
|
||||
owner = token.Owner;
|
||||
}
|
||||
|
||||
if (owner is User user) {
|
||||
var perms = await context.Permissions
|
||||
.Include(p => p.User)
|
||||
@@ -74,6 +95,14 @@ internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGrou
|
||||
.Where(p =>p.Group.Name == group.Name)
|
||||
.ToListAsync();
|
||||
|
||||
permissions.AddRange(perms.Select(p => p.PermissionName));
|
||||
}else if (owner is Token apiToken) {
|
||||
var perms = await context.Permissions
|
||||
.Include(p => p.Token)
|
||||
.Where(p => p.Token != null)
|
||||
.Where(p =>p.Token.TokenId == apiToken.TokenId)
|
||||
.ToListAsync();
|
||||
|
||||
permissions.AddRange(perms.Select(p => p.PermissionName));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using HopFrame.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Database.Repositories.Implementation;
|
||||
|
||||
@@ -11,14 +12,14 @@ internal sealed class TokenRepository<TDbContext>(TDbContext context) : ITokenRe
|
||||
|
||||
return await context.Tokens
|
||||
.Include(t => t.Owner)
|
||||
.Where(t => t.Content == guid)
|
||||
.Where(t => t.TokenId == guid)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<Token> CreateToken(int type, User owner) {
|
||||
var token = new Token {
|
||||
CreatedAt = DateTime.Now,
|
||||
Content = Guid.NewGuid(),
|
||||
TokenId = Guid.NewGuid(),
|
||||
Type = type,
|
||||
Owner = owner
|
||||
};
|
||||
@@ -29,13 +30,37 @@ internal sealed class TokenRepository<TDbContext>(TDbContext context) : ITokenRe
|
||||
return token;
|
||||
}
|
||||
|
||||
public async Task DeleteUserTokens(User owner) {
|
||||
public async Task DeleteUserTokens(User owner, bool includeApiTokens = false) {
|
||||
var tokens = await context.Tokens
|
||||
.Include(t => t.Owner)
|
||||
.Where(t => t.Owner.Id == owner.Id)
|
||||
.ToListAsync();
|
||||
|
||||
if (!includeApiTokens)
|
||||
tokens = tokens
|
||||
.Where(t => t.Type != Token.ApiTokenType)
|
||||
.ToList();
|
||||
|
||||
context.Tokens.RemoveRange(tokens);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteToken(Token token) {
|
||||
context.Tokens.Remove(token);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<Token> CreateApiToken(User owner, DateTime expirationDate) {
|
||||
var token = new Token {
|
||||
CreatedAt = expirationDate,
|
||||
TokenId = Guid.NewGuid(),
|
||||
Type = Token.ApiTokenType,
|
||||
Owner = owner
|
||||
};
|
||||
|
||||
await context.Tokens.AddAsync(token);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return token;
|
||||
}
|
||||
}
|
||||
@@ -69,10 +69,45 @@ internal sealed class UserRepository<TDbContext>(TDbContext context, IGroupRepos
|
||||
.SingleOrDefaultAsync(entry => entry.Id == user.Id);
|
||||
if (entry is null) return;
|
||||
|
||||
// Update the main entity's properties
|
||||
entry.Email = user.Email;
|
||||
entry.Username = user.Username;
|
||||
entry.Permissions = user.Permissions;
|
||||
entry.Tokens = user.Tokens;
|
||||
|
||||
// Update Permissions
|
||||
foreach (var permission in user.Permissions) {
|
||||
var existingPermission = entry.Permissions.FirstOrDefault(p => p.Id == permission.Id);
|
||||
if (existingPermission != null) {
|
||||
// Update existing permission
|
||||
context.Entry(existingPermission).CurrentValues.SetValues(permission);
|
||||
} else {
|
||||
// Add new permission
|
||||
entry.Permissions.Add(permission);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove deleted permissions
|
||||
foreach (var permission in entry.Permissions.ToList().Where(permission => user.Permissions.All(p => p.Id != permission.Id))) {
|
||||
entry.Permissions.Remove(permission);
|
||||
context.Permissions.Remove(permission); // Ensure it gets removed from the database
|
||||
}
|
||||
|
||||
// Update Tokens
|
||||
foreach (var token in user.Tokens) {
|
||||
var existingToken = entry.Tokens.FirstOrDefault(t => t.TokenId == token.TokenId);
|
||||
if (existingToken != null) {
|
||||
// Update existing token
|
||||
context.Entry(existingToken).CurrentValues.SetValues(token);
|
||||
} else {
|
||||
// Add new token
|
||||
entry.Tokens.Add(token);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove deleted tokens
|
||||
foreach (var token in entry.Tokens.ToList().Where(token => user.Tokens.All(t => t.TokenId != token.TokenId))) {
|
||||
entry.Tokens.Remove(token);
|
||||
context.Tokens.Remove(token); // Ensure it gets removed from the database
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace HopFrame.Security;
|
||||
|
||||
public static class AdminPermissions {
|
||||
public const string IsAdmin = "hopframe.admin";
|
||||
|
||||
public const string ViewUsers = "hopframe.admin.users.view";
|
||||
public const string EditUser = "hopframe.admin.users.edit";
|
||||
public const string DeleteUser = "hopframe.admin.users.delete";
|
||||
public const string AddUser = "hopframe.admin.users.add";
|
||||
|
||||
public const string ViewGroups = "hopframe.admin.groups.view";
|
||||
public const string EditGroup = "hopframe.admin.groups.edit";
|
||||
public const string DeleteGroup = "hopframe.admin.groups.delete";
|
||||
public const string AddGroup = "hopframe.admin.groups.add";
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using HopFrame.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -17,39 +20,83 @@ public class HopFrameAuthentication(
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock,
|
||||
ITokenRepository tokens,
|
||||
IPermissionRepository perms,
|
||||
IOptions<HopFrameAuthenticationOptions> tokenOptions,
|
||||
IOptions<OpenIdOptions> openIdOptions,
|
||||
IUserRepository users,
|
||||
IPermissionRepository perms)
|
||||
IOpenIdAccessor accessor)
|
||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) {
|
||||
|
||||
public const string SchemeName = "HopCore.Authentication";
|
||||
public static readonly TimeSpan AccessTokenTime = new(0, 0, 5, 0);
|
||||
public static readonly TimeSpan RefreshTokenTime = new(30, 0, 0, 0);
|
||||
public const string SchemeName = "HopFrame.Authentication";
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
|
||||
var accessToken = Request.Cookies[ITokenContext.AccessTokenType];
|
||||
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers[SchemeName];
|
||||
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"];
|
||||
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Query["token"];
|
||||
if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided");
|
||||
|
||||
var tokenEntry = await tokens.GetToken(accessToken);
|
||||
|
||||
if (tokenEntry?.Type != Token.ApiTokenType && openIdOptions.Value.Enabled && !Guid.TryParse(accessToken, out _)) {
|
||||
var result = await accessor.InspectToken(accessToken);
|
||||
|
||||
if (result is null || !result.Active)
|
||||
return AuthenticateResult.Fail("Invalid OpenID Connect token");
|
||||
|
||||
var email = result.Email;
|
||||
if (string.IsNullOrEmpty(email))
|
||||
return AuthenticateResult.Fail("OpenID user has no email associated to it");
|
||||
|
||||
var user = await users.GetUserByEmail(email);
|
||||
if (user is null) {
|
||||
if (!openIdOptions.Value.GenerateUsers)
|
||||
return AuthenticateResult.Fail("OpenID user does not exist");
|
||||
|
||||
var username = result.PreferredUsername;
|
||||
user = await users.AddUser(new User {
|
||||
Email = email,
|
||||
Username = username
|
||||
});
|
||||
}
|
||||
|
||||
var token = new Token {
|
||||
Owner = user,
|
||||
CreatedAt = DateTime.Now,
|
||||
Type = Token.OpenIdTokenType
|
||||
};
|
||||
var identity = await GenerateClaims(token, perms);
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(identity, Scheme.Name));
|
||||
}
|
||||
|
||||
if (!tokenOptions.Value.DefaultAuthentication)
|
||||
return AuthenticateResult.Fail("HopFrame authentication scheme is disabled");
|
||||
|
||||
if (tokenEntry is null) return AuthenticateResult.Fail("The provided Access Token does not exist");
|
||||
if (tokenEntry.CreatedAt + AccessTokenTime < DateTime.Now) return AuthenticateResult.Fail("The provided Access Token is expired");
|
||||
|
||||
if (tokenEntry.Type == Token.ApiTokenType) {
|
||||
if (tokenEntry.CreatedAt < DateTime.Now) return AuthenticateResult.Fail("The provided API Token is expired");
|
||||
}else if (tokenEntry.CreatedAt + tokenOptions.Value.AccessTokenTime < DateTime.Now) return AuthenticateResult.Fail("The provided Access Token is expired");
|
||||
|
||||
if (tokenEntry.Owner is null)
|
||||
return AuthenticateResult.Fail("The provided Access Token does not match any user");
|
||||
|
||||
var principal = await GenerateClaims(tokenEntry, perms);
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
|
||||
}
|
||||
|
||||
public static async Task<ClaimsPrincipal> GenerateClaims(Token token, IPermissionRepository perms) {
|
||||
var claims = new List<Claim> {
|
||||
new(HopFrameClaimTypes.AccessTokenId, accessToken),
|
||||
new(HopFrameClaimTypes.UserId, tokenEntry.Owner.Id.ToString())
|
||||
new(HopFrameClaimTypes.AccessTokenId, token.TokenId.ToString()),
|
||||
new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString())
|
||||
};
|
||||
|
||||
var permissions = await perms.GetFullPermissions(tokenEntry.Owner);
|
||||
var permissions = await perms.GetFullPermissions(token);
|
||||
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
|
||||
|
||||
var principal = new ClaimsPrincipal();
|
||||
principal.AddIdentity(new ClaimsIdentity(claims, SchemeName));
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
|
||||
return principal;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +1,66 @@
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Authentication.OpenID.Implementation;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using HopFrame.Security.Authorization;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Models;
|
||||
using HopFrame.Security.Options;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace HopFrame.Security.Authentication;
|
||||
|
||||
public static class HopFrameAuthenticationExtensions {
|
||||
|
||||
/// <summary>
|
||||
/// Configures the WebApplication to use the authentication and authorization of the HopFrame API
|
||||
/// </summary>
|
||||
/// <param name="service">The service provider to add the services to</param>
|
||||
/// <typeparam name="TDbContext">The database object that saves all entities that are important for the security api</typeparam>
|
||||
/// <param name="services">The service provider to add the services to</param>
|
||||
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
|
||||
/// <param name="config">Configuration for how the HopFrame services are set up</param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service) {
|
||||
service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
service.AddScoped<ITokenContext, TokenContextImplementor>();
|
||||
public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection services, ConfigurationManager configuration, HopFrameConfig config = null) {
|
||||
config ??= new HopFrameConfig();
|
||||
|
||||
service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {});
|
||||
service.AddAuthorization();
|
||||
services.AddSingleton(config);
|
||||
services.AddScoped(typeof(ICacheProvider), config.CacheProvider);
|
||||
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddScoped<ITokenContext, TokenContextImplementor>();
|
||||
|
||||
return service;
|
||||
if (config.CacheProvider == typeof(MemoryCacheProvider))
|
||||
services.AddMemoryCache();
|
||||
|
||||
services.AddHttpClient<OpenIdAccessor>();
|
||||
services.AddScoped<IOpenIdAccessor, OpenIdAccessor>();
|
||||
|
||||
services.AddOptionsFromConfiguration<HopFrameAuthenticationOptions>(configuration);
|
||||
services.AddOptionsFromConfiguration<AdminPermissionOptions>(configuration);
|
||||
services.AddOptionsFromConfiguration<OpenIdOptions>(configuration);
|
||||
|
||||
services.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {});
|
||||
services.AddAuthorization();
|
||||
|
||||
HopDbContextBase.SaveHandlers.Add(context => {
|
||||
var section = configuration.GetSection("HopFrame:Authentication");
|
||||
var accessToken = section?.GetSection("AccessToken")?.Get<HopFrameAuthenticationOptions.TokenTime>()?.ConstructTimeSpan ?? new HopFrameAuthenticationOptions().AccessTokenTime;
|
||||
var refreshToken = section?.GetSection("RefreshToken")?.Get<HopFrameAuthenticationOptions.TokenTime>()?.ConstructTimeSpan ?? new HopFrameAuthenticationOptions().RefreshTokenTime;
|
||||
|
||||
var now = DateTime.Now;
|
||||
var accessTokenExpiry = now - accessToken;
|
||||
var refreshTokenExpiry = now - refreshToken;
|
||||
var invalidTokens = context.Tokens
|
||||
.Where(t =>
|
||||
(t.Type == Token.AccessTokenType && t.CreatedAt < accessTokenExpiry) ||
|
||||
(t.Type == Token.RefreshTokenType && t.CreatedAt < refreshTokenExpiry))
|
||||
.ToList();
|
||||
context.Tokens.RemoveRange(invalidTokens);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using HopFrame.Security.Options;
|
||||
|
||||
namespace HopFrame.Security.Authentication;
|
||||
|
||||
public class HopFrameAuthenticationOptions : OptionsFromConfiguration {
|
||||
public override string Position { get; } = "HopFrame:Authentication";
|
||||
|
||||
public TimeSpan AccessTokenTime => AccessToken is null ? new(0, 0, 5, 0) : AccessToken.ConstructTimeSpan;
|
||||
public TimeSpan RefreshTokenTime => RefreshToken is null ? new(30, 0, 0, 0) : RefreshToken.ConstructTimeSpan;
|
||||
|
||||
public bool DefaultAuthentication { get; set; } = true;
|
||||
|
||||
public TokenTime AccessToken { get; set; }
|
||||
public TokenTime RefreshToken { get; set; }
|
||||
|
||||
public class TokenTime {
|
||||
public int Days { get; set; }
|
||||
public int Hours { get; set; }
|
||||
public int Minutes { get; set; }
|
||||
public int Seconds { get; set; }
|
||||
|
||||
public TimeSpan ConstructTimeSpan => new(Days, Hours, Minutes, Seconds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace HopFrame.Security.Authentication.OpenID;
|
||||
|
||||
public interface ICacheProvider {
|
||||
Task<TItem> GetOrCreate<TItem>(string key, Func<Task<TItem>> factory) where TItem : class;
|
||||
Task Set<TItem>(string key, TItem value, TimeSpan ttl);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using HopFrame.Security.Authentication.OpenID.Models;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID;
|
||||
|
||||
public interface IOpenIdAccessor {
|
||||
public static string DefaultCallback;
|
||||
|
||||
Task<OpenIdConfiguration> LoadConfiguration();
|
||||
Task<OpenIdToken> RequestToken(string code);
|
||||
Task<string> ConstructAuthUri(string state = null);
|
||||
Task<OpenIdIntrospection> InspectToken(string token);
|
||||
Task<OpenIdToken> RefreshAccessToken(string refreshToken);
|
||||
void SetAuthenticationCookies(OpenIdToken token);
|
||||
void Logout();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID.Implementation;
|
||||
|
||||
public class MemoryCacheProvider(IMemoryCache cache) : ICacheProvider {
|
||||
public Task<TItem> GetOrCreate<TItem>(string key, Func<Task<TItem>> factory) where TItem : class {
|
||||
if (cache.TryGetValue(key, out var value)) {
|
||||
return Task.FromResult(value as TItem);
|
||||
}
|
||||
|
||||
return factory.Invoke();
|
||||
}
|
||||
|
||||
public Task Set<TItem>(string key, TItem value, TimeSpan ttl) {
|
||||
cache.Set(key, value, ttl);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using System.Text.Json;
|
||||
using HopFrame.Security.Authentication.OpenID.Models;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using HopFrame.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID.Implementation;
|
||||
|
||||
internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdOptions> options, IHttpContextAccessor accessor, ICacheProvider cache) : IOpenIdAccessor {
|
||||
private const string ConfigurationCacheKey = "HopFrame:OpenID:Configuration";
|
||||
private const string AuthCodeCacheKey = "HopFrame:OpenID:Code:";
|
||||
private const string TokenCacheKey = "HopFrame:OpenID:Token:";
|
||||
|
||||
public Task<OpenIdConfiguration> LoadConfiguration() {
|
||||
if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled) {
|
||||
return cache.GetOrCreate(ConfigurationCacheKey, LoadConfigurationInCache);
|
||||
}
|
||||
|
||||
return LoadConfigurationInCache();
|
||||
}
|
||||
|
||||
internal async Task<OpenIdConfiguration> LoadConfigurationInCache() {
|
||||
var client = clientFactory.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, Path.Combine(options.Value.Issuer, ".well-known/openid-configuration").Replace("\\", "/"));
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
var config = await JsonSerializer.DeserializeAsync<OpenIdConfiguration>(await response.Content.ReadAsStreamAsync());
|
||||
|
||||
if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled)
|
||||
await cache.Set(ConfigurationCacheKey, config, options.Value.Cache.Configuration.TTL.ConstructTimeSpan);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public Task<OpenIdToken> RequestToken(string code) {
|
||||
if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled) {
|
||||
return cache.GetOrCreate(AuthCodeCacheKey + code, () => RequestTokenInCache(code));
|
||||
}
|
||||
|
||||
return RequestTokenInCache(code);
|
||||
}
|
||||
|
||||
internal async Task<OpenIdToken> RequestTokenInCache(string code) {
|
||||
var protocol = accessor.HttpContext!.Request.IsHttps ? "https" : "http";
|
||||
var callback = options.Value.Callback ?? Path.Combine($"{protocol}://{accessor.HttpContext!.Request.Host.Value}", IOpenIdAccessor.DefaultCallback).Replace("\\", "/");
|
||||
|
||||
var configuration = await LoadConfiguration();
|
||||
|
||||
var client = clientFactory.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, configuration.TokenEndpoint) {
|
||||
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
|
||||
{ "grant_type", "authorization_code" },
|
||||
{ "code", code },
|
||||
{ "redirect_uri", callback },
|
||||
{ "client_id", options.Value.ClientId },
|
||||
{ "client_secret", options.Value.ClientSecret }
|
||||
})
|
||||
};
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
var token = await JsonSerializer.DeserializeAsync<OpenIdToken>(await response.Content.ReadAsStreamAsync());
|
||||
|
||||
if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled)
|
||||
await cache.Set(AuthCodeCacheKey + code, token, options.Value.Cache.Auth.TTL.ConstructTimeSpan);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
public async Task<string> ConstructAuthUri(string state = null) {
|
||||
var protocol = accessor.HttpContext!.Request.IsHttps ? "https" : "http";
|
||||
var callback = options.Value.Callback ?? Path.Combine($"{protocol}://{accessor.HttpContext!.Request.Host.Value}", IOpenIdAccessor.DefaultCallback).Replace("\\", "/");
|
||||
|
||||
var configuration = await LoadConfiguration();
|
||||
return $"{configuration.AuthorizationEndpoint}?response_type=code&client_id={options.Value.ClientId}&redirect_uri={callback}&scope=openid%20profile%20email%20offline_access&state={state}";
|
||||
}
|
||||
|
||||
public Task<OpenIdIntrospection> InspectToken(string token) {
|
||||
if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled) {
|
||||
return cache.GetOrCreate(TokenCacheKey + token, () => InspectTokenInCache(token));
|
||||
}
|
||||
|
||||
return InspectTokenInCache(token);
|
||||
}
|
||||
|
||||
internal async Task<OpenIdIntrospection> InspectTokenInCache(string token) {
|
||||
var configuration = await LoadConfiguration();
|
||||
|
||||
var client = clientFactory.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, configuration.IntrospectionEndpoint) {
|
||||
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
|
||||
{ "token", token },
|
||||
{ "client_id", options.Value.ClientId },
|
||||
{ "client_secret", options.Value.ClientSecret }
|
||||
})
|
||||
};
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
var introspection = await JsonSerializer.DeserializeAsync<OpenIdIntrospection>(await response.Content.ReadAsStreamAsync());
|
||||
|
||||
if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled)
|
||||
await cache.Set(TokenCacheKey + token, introspection, options.Value.Cache.Inspection.TTL.ConstructTimeSpan);
|
||||
|
||||
return introspection;
|
||||
}
|
||||
|
||||
public async Task<OpenIdToken> RefreshAccessToken(string refreshToken) {
|
||||
var configuration = await LoadConfiguration();
|
||||
|
||||
var client = clientFactory.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, configuration.TokenEndpoint) {
|
||||
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
|
||||
{ "grant_type", "refresh_token" },
|
||||
{ "refresh_token", refreshToken },
|
||||
{ "client_id", options.Value.ClientId },
|
||||
{ "client_secret", options.Value.ClientSecret }
|
||||
})
|
||||
};
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
return await JsonSerializer.DeserializeAsync<OpenIdToken>(await response.Content.ReadAsStreamAsync());
|
||||
}
|
||||
|
||||
public void SetAuthenticationCookies(OpenIdToken token) {
|
||||
if (token.AccessToken is not null)
|
||||
accessor.HttpContext!.Response.Cookies.Append(ITokenContext.AccessTokenType, token.AccessToken, new CookieOptions {
|
||||
MaxAge = TimeSpan.FromSeconds(token.ExpiresIn),
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
|
||||
if (token.RefreshToken is not null)
|
||||
accessor.HttpContext!.Response.Cookies.Append(ITokenContext.RefreshTokenType, token.RefreshToken, new CookieOptions {
|
||||
MaxAge = options.Value.RefreshToken.ConstructTimeSpan,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
}
|
||||
|
||||
public void Logout() {
|
||||
accessor.HttpContext!.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
|
||||
accessor.HttpContext!.Response.Cookies.Delete(ITokenContext.AccessTokenType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID.Models;
|
||||
|
||||
public sealed class OpenIdConfiguration {
|
||||
[JsonPropertyName("issuer")]
|
||||
public string Issuer { get; set; }
|
||||
|
||||
[JsonPropertyName("authorization_endpoint")]
|
||||
public string AuthorizationEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("token_endpoint")]
|
||||
public string TokenEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("userinfo_endpoint")]
|
||||
public string UserinfoEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("end_session_endpoint")]
|
||||
public string EndSessionEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("introspection_endpoint")]
|
||||
public string IntrospectionEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("revocation_endpoint")]
|
||||
public string RevocationEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("device_authorization_endpoint")]
|
||||
public string DeviceAuthorizationEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("response_types_supported")]
|
||||
public List<string> ResponseTypesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("response_modes_supported")]
|
||||
public List<string> ResponseModesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("jwks_uri")]
|
||||
public string JwksUri { get; set; }
|
||||
|
||||
[JsonPropertyName("grant_types_supported")]
|
||||
public List<string> GrantTypesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("id_token_signing_alg_values_supported")]
|
||||
public List<string> IdTokenSigningAlgValuesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("subject_types_supported")]
|
||||
public List<string> SubjectTypesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("token_endpoint_auth_methods_supported")]
|
||||
public List<string> TokenEndpointAuthMethodsSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("acr_values_supported")]
|
||||
public List<string> AcrValuesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("scopes_supported")]
|
||||
public List<string> ScopesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("request_parameter_supported")]
|
||||
public bool RequestParameterSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("claims_supported")]
|
||||
public List<string> ClaimsSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("claims_parameter_supported")]
|
||||
public bool ClaimsParameterSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("code_challenge_methods_supported")]
|
||||
public List<string> CodeChallengeMethodsSupported { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID.Models;
|
||||
|
||||
public sealed class OpenIdIntrospection {
|
||||
[JsonPropertyName("iss")]
|
||||
public string Issuer { get; set; }
|
||||
|
||||
[JsonPropertyName("sub")]
|
||||
public string Subject { get; set; }
|
||||
|
||||
[JsonPropertyName("aud")]
|
||||
public string Audience { get; set; }
|
||||
|
||||
[JsonPropertyName("exp")]
|
||||
public long Expiration { get; set; }
|
||||
|
||||
[JsonPropertyName("iat")]
|
||||
public long IssuedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("auth_time")]
|
||||
public long AuthTime { get; set; }
|
||||
|
||||
[JsonPropertyName("acr")]
|
||||
public string Acr { get; set; }
|
||||
|
||||
[JsonPropertyName("amr")]
|
||||
public List<string> AuthenticationMethods { get; set; }
|
||||
|
||||
[JsonPropertyName("sid")]
|
||||
public string SessionId { get; set; }
|
||||
|
||||
[JsonPropertyName("email")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[JsonPropertyName("email_verified")]
|
||||
public bool EmailVerified { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("given_name")]
|
||||
public string GivenName { get; set; }
|
||||
|
||||
[JsonPropertyName("preferred_username")]
|
||||
public string PreferredUsername { get; set; }
|
||||
|
||||
[JsonPropertyName("nickname")]
|
||||
public string Nickname { get; set; }
|
||||
|
||||
[JsonPropertyName("groups")]
|
||||
public List<string> Groups { get; set; }
|
||||
|
||||
[JsonPropertyName("active")]
|
||||
public bool Active { get; set; }
|
||||
|
||||
[JsonPropertyName("scope")]
|
||||
public string Scope { get; set; }
|
||||
|
||||
[JsonPropertyName("client_id")]
|
||||
public string ClientId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID.Models;
|
||||
|
||||
public sealed class OpenIdToken {
|
||||
[JsonPropertyName("access_token")]
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
[JsonPropertyName("refresh_token")]
|
||||
public string RefreshToken { get; set; }
|
||||
|
||||
[JsonPropertyName("token_type")]
|
||||
public string TokenType { get; set; }
|
||||
|
||||
[JsonPropertyName("expires_in")]
|
||||
public int ExpiresIn { get; set; }
|
||||
|
||||
[JsonPropertyName("id_token")]
|
||||
public string IdToken { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using HopFrame.Security.Options;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID.Options;
|
||||
|
||||
public sealed class OpenIdOptions : OptionsFromConfiguration {
|
||||
public override string Position { get; } = "HopFrame:Authentication:OpenID";
|
||||
|
||||
public bool Enabled { get; set; } = false;
|
||||
public bool GenerateUsers { get; set; } = true;
|
||||
|
||||
public string Issuer { get; set; }
|
||||
public string ClientId { get; set; }
|
||||
public string ClientSecret { get; set; }
|
||||
public string Callback { get; set; }
|
||||
|
||||
public HopFrameAuthenticationOptions.TokenTime RefreshToken { get; set; } = new() {
|
||||
Days = 30
|
||||
};
|
||||
|
||||
public CachingOptions Cache { get; set; } = new() {
|
||||
Enabled = true,
|
||||
Configuration = new() {
|
||||
Enabled = true,
|
||||
TTL = new() {
|
||||
Hours = 24
|
||||
}
|
||||
},
|
||||
Auth = new() {
|
||||
Enabled = true,
|
||||
TTL = new() {
|
||||
Seconds = 30
|
||||
}
|
||||
},
|
||||
Inspection = new() {
|
||||
Enabled = true,
|
||||
TTL = new() {
|
||||
Minutes = 2
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public class CachingTypeOptions {
|
||||
public bool Enabled { get; set; }
|
||||
public HopFrameAuthenticationOptions.TokenTime TTL { get; set; }
|
||||
}
|
||||
|
||||
public class CachingOptions {
|
||||
public bool Enabled { get; set; }
|
||||
public CachingTypeOptions Configuration { get; set; }
|
||||
public CachingTypeOptions Auth { get; set; }
|
||||
public CachingTypeOptions Inspection { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using HopFrame.Security.Options;
|
||||
|
||||
namespace HopFrame.Security.Authorization;
|
||||
|
||||
public class AdminPermissionOptions : OptionsFromConfiguration {
|
||||
public override string Position { get; } = "HopFrame:Permissions";
|
||||
|
||||
public string Dashboard { get; set; } = "hopframe.admin";
|
||||
|
||||
public CrudPermission Users { get; set; } = new() {
|
||||
Read = "hopframe.admin.users.read",
|
||||
Update = "hopframe.admin.users.update",
|
||||
Delete = "hopframe.admin.users.delete",
|
||||
Create = "hopframe.admin.users.create"
|
||||
};
|
||||
|
||||
public CrudPermission Groups { get; set; } = new() {
|
||||
Read = "hopframe.admin.groups.read",
|
||||
Update = "hopframe.admin.groups.update",
|
||||
Delete = "hopframe.admin.groups.delete",
|
||||
Create = "hopframe.admin.groups.create"
|
||||
};
|
||||
|
||||
public class CrudPermission {
|
||||
public string Create { get; set; }
|
||||
public string Read { get; set; }
|
||||
public string Update { get; set; }
|
||||
public string Delete { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Security.Claims;
|
||||
|
||||
internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens) : ITokenContext {
|
||||
internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens, IOptions<OpenIdOptions> options) : ITokenContext {
|
||||
public bool IsAuthenticated => !string.IsNullOrEmpty(accessor.HttpContext?.User.GetAccessTokenId());
|
||||
|
||||
public User User => users.GetUser(Guid.Parse(accessor.HttpContext?.User.GetUserId() ?? Guid.Empty.ToString())).GetAwaiter().GetResult();
|
||||
|
||||
public Token AccessToken => tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult();
|
||||
public Token AccessToken => options.Value.Enabled ? new Token {
|
||||
Owner = User,
|
||||
Type = Token.OpenIdTokenType,
|
||||
CreatedAt = DateTime.Now
|
||||
} : tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult();
|
||||
}
|
||||
@@ -8,7 +8,6 @@
|
||||
<RootNamespace>HopFrame.Security</RootNamespace>
|
||||
|
||||
<PackageId>HopFrame.Security</PackageId>
|
||||
<Version>2.0.0</Version>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
7
src/HopFrame.Security/Models/HopFrameConfig.cs
Normal file
7
src/HopFrame.Security/Models/HopFrameConfig.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using HopFrame.Security.Authentication.OpenID.Implementation;
|
||||
|
||||
namespace HopFrame.Security.Models;
|
||||
|
||||
public class HopFrameConfig {
|
||||
public Type CacheProvider { get; set; } = typeof(MemoryCacheProvider);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace HopFrame.Security.Options;
|
||||
|
||||
public abstract class OptionsFromConfiguration {
|
||||
public abstract string Position { get; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HopFrame.Security.Options;
|
||||
|
||||
public static class OptionsFromConfigurationExtensions {
|
||||
public static void AddOptionsFromConfiguration<T>(this IServiceCollection services, IConfiguration configuration) where T : OptionsFromConfiguration {
|
||||
T optionsInstance = (T)Activator.CreateInstance(typeof(T));
|
||||
string position = optionsInstance?.Position;
|
||||
if (position is null) {
|
||||
throw new ArgumentException($"""Configuration "{typeof(T).Name}" has no position configured!""");
|
||||
}
|
||||
|
||||
services.Configure((Action<T>)(options => {
|
||||
IConfigurationSection section = configuration.GetSection(position);
|
||||
section.Bind(options);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,6 @@ public sealed class AdminPermissionsAttribute(string view = null, string create
|
||||
Create = create,
|
||||
Update = update,
|
||||
Delete = delete,
|
||||
View = view
|
||||
Read = view
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public interface IAdminPageGenerator<TModel> {
|
||||
/// </summary>
|
||||
/// <param name="permission">the specified permission</param>
|
||||
/// <returns></returns>
|
||||
IAdminPageGenerator<TModel> ViewPermission(string permission);
|
||||
IAdminPageGenerator<TModel> ReadPermission(string permission);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the permission needed to create a new Entry
|
||||
|
||||
@@ -48,8 +48,8 @@ internal sealed class AdminPageGenerator<TModel> : IAdminPageGenerator<TModel>,
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPageGenerator<TModel> ViewPermission(string permission) {
|
||||
Page.Permissions.View = permission;
|
||||
public IAdminPageGenerator<TModel> ReadPermission(string permission) {
|
||||
Page.Permissions.Read = permission;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ internal sealed class AdminPageGenerator<TModel> : IAdminPageGenerator<TModel>,
|
||||
var attribute = attributes.Single(a => a is AdminPermissionsAttribute) as AdminPermissionsAttribute;
|
||||
CreatePermission(attribute?.Permissions.Create);
|
||||
UpdatePermission(attribute?.Permissions.Update);
|
||||
ViewPermission(attribute?.Permissions.View);
|
||||
ReadPermission(attribute?.Permissions.Read);
|
||||
DeletePermission(attribute?.Permissions.Delete);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<Nullable>disable</Nullable>
|
||||
|
||||
<PackageId>HopFrame.Web.Admin</PackageId>
|
||||
<Version>2.0.0</Version>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace HopFrame.Web.Admin.Models;
|
||||
|
||||
public sealed class AdminPagePermissions {
|
||||
public string View { get; set; }
|
||||
public string Read { get; set; }
|
||||
public string Create { get; set; }
|
||||
public string Update { get; set; }
|
||||
public string Delete { get; set; }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Web.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
@@ -21,15 +20,9 @@ public sealed class AuthMiddleware(IAuthService auth, IPermissionRepository perm
|
||||
return;
|
||||
}
|
||||
|
||||
var claims = new List<Claim> {
|
||||
new(HopFrameClaimTypes.AccessTokenId, token.Content.ToString()),
|
||||
new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString())
|
||||
};
|
||||
|
||||
var permissions = await perms.GetFullPermissions(token.Owner);
|
||||
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
|
||||
|
||||
context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName));
|
||||
var principal = await HopFrameAuthentication.GenerateClaims(token, perms);
|
||||
if (principal?.Identity is ClaimsIdentity identity)
|
||||
context.User.AddIdentity(identity);
|
||||
}
|
||||
|
||||
await next?.Invoke(context);
|
||||
|
||||
@@ -321,7 +321,7 @@
|
||||
|
||||
private async void Save() {
|
||||
if (_isEdit && _currentPage.Permissions.Update is not null) {
|
||||
if (!await Permissions.HasPermission(Auth.User, _currentPage.Permissions.Update)) {
|
||||
if (!await Permissions.HasPermission(Auth.AccessToken, _currentPage.Permissions.Update)) {
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Unauthorized!",
|
||||
Text = "You don't have the required permissions to edit an entry!",
|
||||
@@ -330,7 +330,7 @@
|
||||
return;
|
||||
}
|
||||
}else if (_currentPage.Permissions.Create is not null) {
|
||||
if (!await Permissions.HasPermission(Auth.User, _currentPage.Permissions.Create)) {
|
||||
if (!await Permissions.HasPermission(Auth.AccessToken, _currentPage.Permissions.Create)) {
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Unauthorized!",
|
||||
Text = "You don't have the required permissions to add an entry!",
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Security;
|
||||
using HopFrame.Security.Authorization;
|
||||
using HopFrame.Web.Admin;
|
||||
using HopFrame.Web.Admin.Attributes;
|
||||
using HopFrame.Web.Admin.Generators;
|
||||
using HopFrame.Web.Admin.Models;
|
||||
using HopFrame.Web.Provider;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Web;
|
||||
|
||||
internal class HopAdminContext : AdminPagesContext {
|
||||
internal class HopAdminContext(IOptions<AdminPermissionOptions> options) : AdminPagesContext {
|
||||
|
||||
[AdminPageUrl("users")]
|
||||
public AdminPage<User> Users { get; set; }
|
||||
@@ -21,10 +23,10 @@ internal class HopAdminContext : AdminPagesContext {
|
||||
generator.Page<User>()
|
||||
.Description("On this page you can manage all user accounts.")
|
||||
.ConfigureProvider<UserProvider>()
|
||||
.ViewPermission(AdminPermissions.ViewUsers)
|
||||
.CreatePermission(AdminPermissions.AddUser)
|
||||
.UpdatePermission(AdminPermissions.EditUser)
|
||||
.DeletePermission(AdminPermissions.DeleteUser);
|
||||
.ReadPermission(options.Value.Users.Read)
|
||||
.CreatePermission(options.Value.Users.Create)
|
||||
.UpdatePermission(options.Value.Users.Update)
|
||||
.DeletePermission(options.Value.Users.Delete);
|
||||
|
||||
generator.Page<User>().Property(u => u.Password)
|
||||
.DisplayInListing(false)
|
||||
@@ -64,10 +66,10 @@ internal class HopAdminContext : AdminPagesContext {
|
||||
generator.Page<PermissionGroup>()
|
||||
.Description("On this page you can view, create, edit and delete permission groups.")
|
||||
.ConfigureProvider<GroupProvider>()
|
||||
.ViewPermission(AdminPermissions.ViewGroups)
|
||||
.CreatePermission(AdminPermissions.AddGroup)
|
||||
.UpdatePermission(AdminPermissions.EditGroup)
|
||||
.DeletePermission(AdminPermissions.DeleteGroup)
|
||||
.ReadPermission(options.Value.Groups.Read)
|
||||
.CreatePermission(options.Value.Groups.Create)
|
||||
.UpdatePermission(options.Value.Groups.Update)
|
||||
.DeletePermission(options.Value.Groups.Delete)
|
||||
.ListingProperty(g => g.Name);
|
||||
|
||||
generator.Page<PermissionGroup>().Property(g => g.Name)
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||
|
||||
<PackageId>HopFrame.Web</PackageId>
|
||||
<Version>2.0.0</Version>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<IsPackable>true</IsPackable>
|
||||
@@ -35,4 +34,10 @@
|
||||
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>HopFrame.Tests.Web</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
7
src/HopFrame.Web/Models/HopFrameWebModuleConfig.cs
Normal file
7
src/HopFrame.Web/Models/HopFrameWebModuleConfig.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using HopFrame.Security.Models;
|
||||
|
||||
namespace HopFrame.Web.Models;
|
||||
|
||||
public class HopFrameWebModuleConfig : HopFrameConfig {
|
||||
public string AdminLoginPageUri { get; set; } = "/administration/login";
|
||||
}
|
||||
@@ -5,25 +5,27 @@
|
||||
@using BlazorStrap
|
||||
@using HopFrame.Web.Pages.Administration.Layout
|
||||
@using BlazorStrap.V5
|
||||
@using HopFrame.Security
|
||||
@using HopFrame.Security.Authorization
|
||||
@using HopFrame.Web.Admin.Providers
|
||||
@using HopFrame.Web.Components
|
||||
@using HopFrame.Web.Models
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.Extensions.Options
|
||||
@layout AdminLayout
|
||||
|
||||
<AuthorizedView Permission="@AdminPermissions.IsAdmin" RedirectIfUnauthorized="/administration/login" />
|
||||
<AuthorizedView Permission="@Options.Value.Dashboard" RedirectIfUnauthorized="@ConstructRedirectUri()" />
|
||||
|
||||
<PageTitle>Admin Dashboard</PageTitle>
|
||||
|
||||
<BSContainer>
|
||||
<BSRow Justify="Justify.Center">
|
||||
@foreach (var adminPage in Pages.LoadRegisteredAdminPages()) {
|
||||
<AuthorizedView Permission="@adminPage.Permissions.View">
|
||||
<AuthorizedView Permission="@adminPage.Permissions.Read">
|
||||
<BSCol Column="4" style="margin-bottom: 10px">
|
||||
<BSCard CardType="CardType.Card" Color="BSColor.Dark" style="min-height: 200px; min-width: 200px">
|
||||
<BSCard CardType="CardType.Body" style="display: flex; flex-direction: column">
|
||||
<BSCard CardType="CardType.Title">@adminPage.Title</BSCard>
|
||||
<BSCard CardType="CardType.Subtitle"><span style="color: gray">@adminPage.Permissions.View</span></BSCard>
|
||||
<BSCard CardType="CardType.Subtitle"><span style="color: gray">@adminPage.Permissions.Read</span></BSCard>
|
||||
<BSCard CardType="CardType.Text">@adminPage.Description</BSCard>
|
||||
<BSButton IsOutlined="true" MarginTop="Margins.Auto" style="width: max-content; align-self: center" OnClick="() => NavigateTo(adminPage.Url)" Color="BSColor.Light">Open</BSButton>
|
||||
</BSCard>
|
||||
@@ -36,11 +38,17 @@
|
||||
|
||||
@inject NavigationManager Navigator
|
||||
@inject IAdminPagesProvider Pages
|
||||
@inject IOptions<AdminPermissionOptions> Options
|
||||
@inject HopFrameWebModuleConfig Config
|
||||
|
||||
@code {
|
||||
|
||||
public void NavigateTo(string url) {
|
||||
Navigator.NavigateTo("administration/" + url, true);
|
||||
Navigator.NavigateTo("/administration/" + url, true);
|
||||
}
|
||||
|
||||
public string ConstructRedirectUri() {
|
||||
return Config.AdminLoginPageUri + "?redirect=/administration";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
private UserLogin UserLogin { get; set; }
|
||||
|
||||
[SupplyParameterFromQuery(Name = "redirect")]
|
||||
private string RedirectAfter { get; set; }
|
||||
public string RedirectAfter { get; set; }
|
||||
|
||||
private const string DefaultRedirect = "/administration";
|
||||
|
||||
@@ -65,6 +65,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.NavigateTo(string.IsNullOrEmpty(RedirectAfter) ? DefaultRedirect : "/administration/" + RedirectAfter, true);
|
||||
Navigator.NavigateTo(string.IsNullOrEmpty(RedirectAfter) ? DefaultRedirect : RedirectAfter, true);
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,10 @@
|
||||
@using HopFrame.Security.Claims
|
||||
@using HopFrame.Web.Admin
|
||||
@using HopFrame.Web.Components
|
||||
@using HopFrame.Web.Models
|
||||
|
||||
<PageTitle>@_pageData.Title</PageTitle>
|
||||
<AuthorizedView Permission="@_pageData.Permissions.View" RedirectIfUnauthorized="@GenerateRedirectString()" />
|
||||
<AuthorizedView Permission="@_pageData.Permissions.Read" RedirectIfUnauthorized="@GenerateRedirectString()" />
|
||||
|
||||
<AdminPageModal ReloadDelegate="Reload" @ref="_modal"/>
|
||||
|
||||
@@ -33,7 +34,7 @@
|
||||
<div class="d-flex" role="search" id="search">
|
||||
<input class="form-control me-2 input-dark" type="search" placeholder="Search" aria-label="Search" @oninput="TriggerSearch">
|
||||
</div>
|
||||
<AuthorizedView Permission="@Security.AdminPermissions.AddGroup">
|
||||
<AuthorizedView Permission="@_pageData.Permissions.Create">
|
||||
<BSButton IsSubmit="false" Color="BSColor.Success" @onclick="Create">Add Entry</BSButton>
|
||||
</AuthorizedView>
|
||||
</div>
|
||||
@@ -107,6 +108,7 @@
|
||||
@inject IPermissionRepository Permissions
|
||||
@inject SweetAlertService Alerts
|
||||
@inject NavigationManager Navigator
|
||||
@inject HopFrameWebModuleConfig Config
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
@@ -140,8 +142,8 @@
|
||||
throw new ArgumentException($"AdminPage '{_pageData.Title}' does not specify a model repository!'");
|
||||
_modelProvider = _pageData.LoadModelProvider(Provider);
|
||||
|
||||
_hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Update);
|
||||
_hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Delete);
|
||||
_hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.AccessToken, _pageData.Permissions.Update);
|
||||
_hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.AccessToken, _pageData.Permissions.Delete);
|
||||
|
||||
await Reload();
|
||||
}
|
||||
@@ -251,6 +253,6 @@
|
||||
}
|
||||
|
||||
private string GenerateRedirectString() {
|
||||
return "/administration/login?redirect=" + _pageData?.Url;
|
||||
return Config.AdminLoginPageUri + "?redirect=/administration/" + _pageData?.Url;
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@
|
||||
<BSNavItem IsActive="IsDashboardActive()" OnClick="NavigateToDashboard">Dashboard</BSNavItem>
|
||||
|
||||
@foreach (var adminPage in Pages.LoadRegisteredAdminPages()) {
|
||||
<AuthorizedView Permission="@adminPage.Permissions.View">
|
||||
<AuthorizedView Permission="@adminPage.Permissions.Read">
|
||||
<BSNavItem IsActive="IsNavItemActive(adminPage.Url)" OnClick="() => Navigate(adminPage.Url)">@adminPage.Title</BSNavItem>
|
||||
</AuthorizedView>
|
||||
}
|
||||
|
||||
@@ -3,26 +3,29 @@ using CurrieTechnologies.Razor.SweetAlert2;
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Web.Admin;
|
||||
using HopFrame.Web.Models;
|
||||
using HopFrame.Web.Services;
|
||||
using HopFrame.Web.Services.Implementation;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HopFrame.Web;
|
||||
|
||||
public static class ServiceCollectionExtensions {
|
||||
public static IServiceCollection AddHopFrame<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
|
||||
services.AddHttpClient();
|
||||
public static IServiceCollection AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration, HopFrameWebModuleConfig config = null) where TDbContext : HopDbContextBase {
|
||||
config ??= new HopFrameWebModuleConfig();
|
||||
services.AddHopFrameRepositories<TDbContext>();
|
||||
services.AddScoped<IAuthService, AuthService>();
|
||||
services.AddTransient<AuthMiddleware>();
|
||||
services.AddAdminContext<HopAdminContext>();
|
||||
services.AddSingleton(config);
|
||||
|
||||
// Component library's
|
||||
services.AddSweetAlert2();
|
||||
services.AddBlazorStrap();
|
||||
|
||||
services.AddHopFrameAuthentication();
|
||||
services.AddHopFrameAuthentication(configuration, config);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Web.Services.Implementation;
|
||||
|
||||
@@ -11,10 +14,16 @@ internal class AuthService(
|
||||
IUserRepository userService,
|
||||
IHttpContextAccessor httpAccessor,
|
||||
ITokenRepository tokens,
|
||||
ITokenContext context)
|
||||
ITokenContext context,
|
||||
IOptions<HopFrameAuthenticationOptions> options,
|
||||
IOptions<OpenIdOptions> openIdOptions,
|
||||
IOpenIdAccessor accessor,
|
||||
IUserRepository users)
|
||||
: IAuthService {
|
||||
|
||||
public async Task Register(UserRegister register) {
|
||||
if (!options.Value.DefaultAuthentication) return;
|
||||
|
||||
var user = await userService.AddUser(new User {
|
||||
Username = register.Username,
|
||||
Email = register.Email,
|
||||
@@ -26,19 +35,21 @@ internal class AuthService(
|
||||
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
||||
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
|
||||
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.RefreshTokenTime,
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.RefreshTokenTime,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.AccessTokenTime,
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.AccessTokenTime,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<bool> Login(UserLogin login) {
|
||||
if (!options.Value.DefaultAuthentication) return false;
|
||||
|
||||
var user = await userService.GetUserByEmail(login.Email);
|
||||
|
||||
if (user == null) return false;
|
||||
@@ -47,13 +58,13 @@ internal class AuthService(
|
||||
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
||||
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
|
||||
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.RefreshTokenTime,
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.RefreshTokenTime,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.AccessTokenTime,
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.AccessTokenTime,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
@@ -73,16 +84,51 @@ internal class AuthService(
|
||||
|
||||
if (string.IsNullOrWhiteSpace(refreshToken)) return null;
|
||||
|
||||
if (openIdOptions.Value.Enabled && !Guid.TryParse(refreshToken, out _)) {
|
||||
var openIdToken = await accessor.RefreshAccessToken(refreshToken);
|
||||
|
||||
if (openIdToken is null)
|
||||
return null;
|
||||
|
||||
var inspection = await accessor.InspectToken(openIdToken.AccessToken);
|
||||
|
||||
var email = inspection.Email;
|
||||
if (string.IsNullOrEmpty(email))
|
||||
return null;
|
||||
|
||||
var user = await users.GetUserByEmail(email);
|
||||
if (user is null) {
|
||||
if (!openIdOptions.Value.GenerateUsers)
|
||||
return null;
|
||||
|
||||
var username = inspection.PreferredUsername;
|
||||
user = await users.AddUser(new User {
|
||||
Email = email,
|
||||
Username = username
|
||||
});
|
||||
}
|
||||
|
||||
accessor.SetAuthenticationCookies(openIdToken);
|
||||
return new() {
|
||||
Owner = user,
|
||||
CreatedAt = DateTime.Now,
|
||||
Type = Token.OpenIdTokenType
|
||||
};
|
||||
}
|
||||
|
||||
if (!options.Value.DefaultAuthentication)
|
||||
return null;
|
||||
|
||||
var token = await tokens.GetToken(refreshToken);
|
||||
|
||||
if (token is null || token.Type != Token.RefreshTokenType) return null;
|
||||
|
||||
if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now) return null;
|
||||
if (token.CreatedAt + options.Value.RefreshTokenTime < DateTime.Now) return null;
|
||||
|
||||
var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner);
|
||||
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.AccessTokenTime,
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.TokenId.ToString(), new CookieOptions {
|
||||
MaxAge = options.Value.AccessTokenTime,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
@@ -91,15 +137,12 @@ internal class AuthService(
|
||||
}
|
||||
|
||||
public async Task<bool> IsLoggedIn() {
|
||||
var accessToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.AccessTokenType];
|
||||
if (string.IsNullOrEmpty(accessToken)) return false;
|
||||
var accessToken = context.AccessToken;
|
||||
|
||||
var tokenEntry = await tokens.GetToken(accessToken);
|
||||
|
||||
if (tokenEntry is null) return false;
|
||||
if (tokenEntry.Type != Token.AccessTokenType) return false;
|
||||
if (tokenEntry.CreatedAt + HopFrameAuthentication.AccessTokenTime < DateTime.Now) return false;
|
||||
if (tokenEntry.Owner is null) return false;
|
||||
if (accessToken is null) return false;
|
||||
if (accessToken.Type != Token.AccessTokenType && accessToken.Type != Token.OpenIdTokenType) return false;
|
||||
if (accessToken.CreatedAt + options.Value.AccessTokenTime < DateTime.Now) return false;
|
||||
if (accessToken.Owner is null) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
using HopFrame.Api.Logic;
|
||||
using HopFrame.Api.Models;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Authorization;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Testing.Api.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RestApiTest.Models;
|
||||
|
||||
namespace RestApiTest.Controllers;
|
||||
namespace HopFrame.Testing.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("test")]
|
||||
public class TestController(ITokenContext userContext, DatabaseContext context) : ControllerBase {
|
||||
public class TestController(ITokenContext userContext, DatabaseContext context, ITokenRepository tokens, IPermissionRepository permissions) : ControllerBase {
|
||||
|
||||
[HttpGet("permissions"), Authorized]
|
||||
public ActionResult<IList<Permission>> Permissions() {
|
||||
return new ActionResult<IList<Permission>>(userContext.User.Permissions);
|
||||
public async Task<ActionResult<IList<string>>> Permissions() {
|
||||
return new ActionResult<IList<string>>(await permissions.GetFullPermissions(userContext.AccessToken));
|
||||
}
|
||||
|
||||
[HttpGet("generate")]
|
||||
@@ -51,4 +54,23 @@ public class TestController(ITokenContext userContext, DatabaseContext context)
|
||||
return LogicResult<IList<Address>>.Ok(await context.Addresses.Include(e => e.Employee).ToListAsync());
|
||||
}
|
||||
|
||||
[HttpGet("token"), Authorized]
|
||||
public async Task<ActionResult<SingleValueResult<string>>> GetApiToken() {
|
||||
var token = await tokens.CreateApiToken(userContext.User, DateTime.MaxValue);
|
||||
await permissions.AddPermission(token, "hopframe.admin");
|
||||
await permissions.AddPermission(token, "hopframe.admin.users.read");
|
||||
return LogicResult<SingleValueResult<string>>.Ok(token.TokenId.ToString());
|
||||
}
|
||||
|
||||
[HttpDelete("token/{tokenId}")]
|
||||
public async Task DeleteToken(string tokenId) {
|
||||
var token = await tokens.GetToken(tokenId);
|
||||
await tokens.DeleteToken(token);
|
||||
}
|
||||
|
||||
[HttpGet("url")]
|
||||
public ActionResult<string> GetUrl() {
|
||||
return Ok(IOpenIdAccessor.DefaultCallback ?? "Not set");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Testing.Api.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RestApiTest.Models;
|
||||
|
||||
namespace RestApiTest;
|
||||
namespace HopFrame.Testing.Api;
|
||||
|
||||
public class DatabaseContext : HopDbContextBase {
|
||||
|
||||
@@ -12,7 +12,7 @@ public class DatabaseContext : HopDbContextBase {
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
|
||||
optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\test\RestApiTest\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
|
||||
optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\testing\HopFrame.Testing.Api\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||
@@ -4,6 +4,7 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace RestApiTest.Models;
|
||||
namespace HopFrame.Testing.Api.Models;
|
||||
|
||||
public class Address {
|
||||
[ForeignKey("Employee")]
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace RestApiTest.Models;
|
||||
namespace HopFrame.Testing.Api.Models;
|
||||
|
||||
public class Employee {
|
||||
public int EmployeeId { get; set; }
|
||||
@@ -1,4 +1,4 @@
|
||||
using RestApiTest;
|
||||
using HopFrame.Testing.Api;
|
||||
using HopFrame.Api.Extensions;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
@@ -6,8 +6,9 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddHopFrame<DatabaseContext>();
|
||||
builder.Services.AddHopFrame<DatabaseContext>(builder.Configuration);
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
@@ -18,7 +19,7 @@ builder.Services.AddSwaggerGen(c => {
|
||||
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
|
||||
Description = @"JWT Authorization header using the Bearer scheme. \r\n\r\n
|
||||
Enter 'Bearer' [space] and then your token in the text input below.",
|
||||
Name = "Authorization",
|
||||
Name = "Token",
|
||||
In = ParameterLocation.Header,
|
||||
Type = SecuritySchemeType.ApiKey,
|
||||
Scheme = "Bearer"
|
||||
@@ -1,10 +1,10 @@
|
||||
using FrontendTest.Providers;
|
||||
using HopFrame.Web.Admin;
|
||||
using HopFrame.Web.Admin.Generators;
|
||||
using HopFrame.Web.Admin.Models;
|
||||
using RestApiTest.Models;
|
||||
using HopFrame.Testing.Api.Models;
|
||||
using HopFrame.Testing.Web.Providers;
|
||||
|
||||
namespace FrontendTest;
|
||||
namespace HopFrame.Testing.Web;
|
||||
|
||||
public class AdminContext : AdminPagesContext {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<base href="/"/>
|
||||
<link rel="stylesheet" href="bootstrap/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="app.css"/>
|
||||
<link rel="stylesheet" href="FrontendTest.styles.css"/>
|
||||
<link rel="stylesheet" href="HopFrame.Testing.Web.styles.css"/>
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
<HeadOutlet/>
|
||||
</head>
|
||||
@@ -6,5 +6,5 @@
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using FrontendTest
|
||||
@using FrontendTest.Components
|
||||
@using HopFrame.Testing.Web
|
||||
@using HopFrame.Testing.Web.Components
|
||||
@@ -1,8 +1,8 @@
|
||||
using HopFrame.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RestApiTest.Models;
|
||||
using HopFrame.Testing.Api.Models;
|
||||
|
||||
namespace FrontendTest;
|
||||
namespace HopFrame.Testing.Web;
|
||||
|
||||
public class DatabaseContext : HopDbContextBase {
|
||||
public DbSet<Employee> Employees { get; set; }
|
||||
@@ -11,7 +11,7 @@ public class DatabaseContext : HopDbContextBase {
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
|
||||
optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\test\RestApiTest\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
|
||||
optionsBuilder.UseSqlite(@"Data Source=C:\Users\leon\Documents\Projekte\HopFrame\testing\HopFrame.Testing.Api\bin\Debug\net8.0\test.db;Mode=ReadWrite;");
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user