Merge branch 'feature/openid' into 'dev'
Resolve "OAuth" See merge request leon.hoppe/hopframe!4
This commit was merged in pull request #42.
This commit is contained in:
@@ -1,10 +1,13 @@
|
|||||||
<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">
|
<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_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_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_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_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_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_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_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_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_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/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>
|
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue"><AssemblyExplorer>
|
||||||
@@ -71,6 +74,9 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ A simple backend management api for ASP.NET Core Web APIs
|
|||||||
- [x] User authentication
|
- [x] User authentication
|
||||||
- [x] Permission management
|
- [x] Permission management
|
||||||
- [x] Generated frontend administration boards
|
- [x] Generated frontend administration boards
|
||||||
|
- [x] API token support
|
||||||
|
- [x] OpenID authentication integration
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
There are two different versions of HopFrame, either the Web API version or the full Blazor web version.
|
There are two different versions of HopFrame, either the Web API version or the full Blazor web version.
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ by configuring your configuration to load these.
|
|||||||
> custom configurations / HopFrame services.
|
> custom configurations / HopFrame services.
|
||||||
|
|
||||||
You can specify `Seconds`, `Minutes`, `Hours` and `Days` for either of the two token types.
|
You can specify `Seconds`, `Minutes`, `Hours` and `Days` for either of the two token types.
|
||||||
These get combined to a single time span.
|
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
|
#### Configuration example
|
||||||
```json
|
```json
|
||||||
|
|||||||
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);
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -9,6 +9,7 @@ The HopFrame comes in two variations, you can eiter only use the backend with so
|
|||||||
- [Base Models](./models.md)
|
- [Base Models](./models.md)
|
||||||
- [Authentication](./authentication.md)
|
- [Authentication](./authentication.md)
|
||||||
- [Permissions](./permissions.md)
|
- [Permissions](./permissions.md)
|
||||||
|
- [OpenID Integration](./openid.md)
|
||||||
|
|
||||||
## HopFrame Web API
|
## HopFrame Web API
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
namespace HopFrame.Api.Controller;
|
namespace HopFrame.Api.Controller;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/v1/authentication")]
|
[Route("api/v1/auth")]
|
||||||
public class SecurityController(IAuthLogic auth) : ControllerBase {
|
public class AuthController(IAuthLogic auth) : ControllerBase {
|
||||||
|
|
||||||
[HttpPut("login")]
|
[HttpPut("login")]
|
||||||
public async Task<ActionResult<SingleValueResult<string>>> Login([FromBody] UserLogin login) {
|
public async Task<ActionResult<SingleValueResult<string>>> Login([FromBody] UserLogin login) {
|
||||||
84
src/HopFrame.Api/Controller/OpenIdController.cs
Normal file
84
src/HopFrame.Api/Controller/OpenIdController.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using HopFrame.Api.Models;
|
||||||
|
using HopFrame.Security.Authentication.OpenID;
|
||||||
|
using HopFrame.Security.Authentication.OpenID.Options;
|
||||||
|
using HopFrame.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace HopFrame.Api.Controller;
|
||||||
|
|
||||||
|
[ApiController, Route("api/v1/openid")]
|
||||||
|
public class OpenIdController(IOpenIdAccessor accessor, IOptions<OpenIdOptions> options) : 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(DefaultCallback, 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, DefaultCallback);
|
||||||
|
|
||||||
|
if (token is null) {
|
||||||
|
return Forbid("Authorization code is not valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
Response.Cookies.Append(ITokenContext.AccessTokenType, token.AccessToken, new CookieOptions {
|
||||||
|
MaxAge = TimeSpan.FromSeconds(token.ExpiresIn),
|
||||||
|
HttpOnly = false,
|
||||||
|
Secure = true
|
||||||
|
});
|
||||||
|
Response.Cookies.Append(ITokenContext.RefreshTokenType, token.RefreshToken, new CookieOptions {
|
||||||
|
MaxAge = options.Value.RefreshToken.ConstructTimeSpan,
|
||||||
|
HttpOnly = false,
|
||||||
|
Secure = true
|
||||||
|
});
|
||||||
|
|
||||||
|
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 NotFound("Refresh token not valid");
|
||||||
|
|
||||||
|
Response.Cookies.Append(ITokenContext.AccessTokenType, token.AccessToken, new CookieOptions {
|
||||||
|
MaxAge = TimeSpan.FromSeconds(token.ExpiresIn),
|
||||||
|
HttpOnly = false,
|
||||||
|
Secure = true
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(new SingleValueResult<string>(token.AccessToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("logout")]
|
||||||
|
public IActionResult Logout() {
|
||||||
|
Response.Cookies.Delete(ITokenContext.RefreshTokenType);
|
||||||
|
Response.Cookies.Delete(ITokenContext.AccessTokenType);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -83,4 +83,4 @@ public static class MvcExtensions {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,16 @@ public static class ServiceCollectionExtensions {
|
|||||||
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
|
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
|
||||||
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
|
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
|
||||||
public static void AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase {
|
public static void AddHopFrame<TDbContext>(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase {
|
||||||
services.AddMvcCore().UseSpecificControllers(typeof(SecurityController));
|
var controllers = new List<Type>();
|
||||||
|
|
||||||
|
if (configuration.GetValue<bool>("HopFrame:Authentication:DefaultAuthentication"))
|
||||||
|
controllers.Add(typeof(AuthController));
|
||||||
|
|
||||||
|
if (configuration.GetValue<bool>("HopFrame:Authentication:OpenID:Enabled"))
|
||||||
|
controllers.Add(typeof(OpenIdController));
|
||||||
|
|
||||||
AddHopFrameNoEndpoints<TDbContext>(services, configuration);
|
AddHopFrameNoEndpoints<TDbContext>(services, configuration);
|
||||||
|
services.AddMvcCore().UseSpecificControllers(controllers.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -30,6 +38,11 @@ public static class ServiceCollectionExtensions {
|
|||||||
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
|
/// <param name="configuration">The configuration used to configure HopFrame authentication</param>
|
||||||
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
|
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
|
||||||
public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase {
|
public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase {
|
||||||
|
services.AddMvcCore().ConfigureApplicationPartManager(manager => {
|
||||||
|
var endpoints = manager.ApplicationParts.SingleOrDefault(p => p.Name == typeof(ServiceCollectionExtensions).Namespace!.Replace(".Extensions", ""));
|
||||||
|
manager.ApplicationParts.Remove(endpoints);
|
||||||
|
});
|
||||||
|
|
||||||
services.AddHopFrameRepositories<TDbContext>();
|
services.AddHopFrameRepositories<TDbContext>();
|
||||||
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
services.AddScoped<IAuthLogic, AuthLogic>();
|
services.AddScoped<IAuthLogic, AuthLogic>();
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ namespace HopFrame.Api.Logic.Implementation;
|
|||||||
internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor, IOptions<HopFrameAuthenticationOptions> options) : IAuthLogic {
|
internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor, IOptions<HopFrameAuthenticationOptions> options) : IAuthLogic {
|
||||||
|
|
||||||
public async Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login) {
|
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);
|
var user = await users.GetUserByEmail(login.Email);
|
||||||
|
|
||||||
if (user is null)
|
if (user is null)
|
||||||
@@ -38,6 +40,8 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) {
|
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)
|
if (register.Password.Length < 8)
|
||||||
return LogicResult<SingleValueResult<string>>.BadRequest("Password needs to be at least 8 characters long");
|
return LogicResult<SingleValueResult<string>>.BadRequest("Password needs to be at least 8 characters long");
|
||||||
|
|
||||||
@@ -69,6 +73,8 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<LogicResult<SingleValueResult<string>>> Authenticate() {
|
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];
|
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(refreshToken))
|
if (string.IsNullOrEmpty(refreshToken))
|
||||||
@@ -101,9 +107,7 @@ internal class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenC
|
|||||||
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
|
var refreshToken = accessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
|
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.RefreshTokenType);
|
||||||
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType);
|
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ public class Token : IPermissionOwner {
|
|||||||
public const int RefreshTokenType = 0;
|
public const int RefreshTokenType = 0;
|
||||||
public const int AccessTokenType = 1;
|
public const int AccessTokenType = 1;
|
||||||
public const int ApiTokenType = 2;
|
public const int ApiTokenType = 2;
|
||||||
|
public const int OpenIdTokenType = 3;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines the Type of the stored Token
|
/// Defines the Type of the stored Token
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using HopFrame.Database.Models;
|
using HopFrame.Database.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace HopFrame.Database.Repositories.Implementation;
|
namespace HopFrame.Database.Repositories.Implementation;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ using System.Security.Claims;
|
|||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using HopFrame.Database.Models;
|
using HopFrame.Database.Models;
|
||||||
using HopFrame.Database.Repositories;
|
using HopFrame.Database.Repositories;
|
||||||
|
using HopFrame.Security.Authentication.OpenID;
|
||||||
|
using HopFrame.Security.Authentication.OpenID.Options;
|
||||||
using HopFrame.Security.Claims;
|
using HopFrame.Security.Claims;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -19,7 +21,10 @@ public class HopFrameAuthentication(
|
|||||||
ISystemClock clock,
|
ISystemClock clock,
|
||||||
ITokenRepository tokens,
|
ITokenRepository tokens,
|
||||||
IPermissionRepository perms,
|
IPermissionRepository perms,
|
||||||
IOptions<HopFrameAuthenticationOptions> tokenOptions)
|
IOptions<HopFrameAuthenticationOptions> tokenOptions,
|
||||||
|
IOptions<OpenIdOptions> openIdOptions,
|
||||||
|
IUserRepository users,
|
||||||
|
IOpenIdAccessor accessor)
|
||||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) {
|
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) {
|
||||||
|
|
||||||
public const string SchemeName = "HopFrame.Authentication";
|
public const string SchemeName = "HopFrame.Authentication";
|
||||||
@@ -30,8 +35,42 @@ public class HopFrameAuthentication(
|
|||||||
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"];
|
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"];
|
||||||
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Query["token"];
|
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Query["token"];
|
||||||
if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided");
|
if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided");
|
||||||
|
|
||||||
var tokenEntry = await tokens.GetToken(accessToken);
|
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 is null) return AuthenticateResult.Fail("The provided Access Token does not exist");
|
||||||
|
|
||||||
@@ -42,17 +81,22 @@ public class HopFrameAuthentication(
|
|||||||
if (tokenEntry.Owner is null)
|
if (tokenEntry.Owner is null)
|
||||||
return AuthenticateResult.Fail("The provided Access Token does not match any user");
|
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> {
|
var claims = new List<Claim> {
|
||||||
new(HopFrameClaimTypes.AccessTokenId, accessToken),
|
new(HopFrameClaimTypes.AccessTokenId, token.TokenId.ToString()),
|
||||||
new(HopFrameClaimTypes.UserId, tokenEntry.Owner.Id.ToString())
|
new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString())
|
||||||
};
|
};
|
||||||
|
|
||||||
var permissions = await perms.GetFullPermissions(tokenEntry);
|
var permissions = await perms.GetFullPermissions(token);
|
||||||
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
|
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
|
||||||
|
|
||||||
var principal = new ClaimsPrincipal();
|
var principal = new ClaimsPrincipal();
|
||||||
principal.AddIdentity(new ClaimsIdentity(claims, SchemeName));
|
principal.AddIdentity(new ClaimsIdentity(claims, SchemeName));
|
||||||
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
|
return principal;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using HopFrame.Security.Authentication.OpenID;
|
||||||
|
using HopFrame.Security.Authentication.OpenID.Implementation;
|
||||||
|
using HopFrame.Security.Authentication.OpenID.Options;
|
||||||
using HopFrame.Security.Authorization;
|
using HopFrame.Security.Authorization;
|
||||||
using HopFrame.Security.Claims;
|
using HopFrame.Security.Claims;
|
||||||
using HopFrame.Security.Options;
|
using HopFrame.Security.Options;
|
||||||
@@ -20,8 +23,13 @@ public static class HopFrameAuthenticationExtensions {
|
|||||||
service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
service.AddScoped<ITokenContext, TokenContextImplementor>();
|
service.AddScoped<ITokenContext, TokenContextImplementor>();
|
||||||
|
|
||||||
|
service.AddHttpClient();
|
||||||
|
service.AddMemoryCache();
|
||||||
|
service.AddScoped<IOpenIdAccessor, OpenIdAccessor>();
|
||||||
|
|
||||||
service.AddOptionsFromConfiguration<HopFrameAuthenticationOptions>(configuration);
|
service.AddOptionsFromConfiguration<HopFrameAuthenticationOptions>(configuration);
|
||||||
service.AddOptionsFromConfiguration<AdminPermissionOptions>(configuration);
|
service.AddOptionsFromConfiguration<AdminPermissionOptions>(configuration);
|
||||||
|
service.AddOptionsFromConfiguration<OpenIdOptions>(configuration);
|
||||||
|
|
||||||
service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {});
|
service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {});
|
||||||
service.AddAuthorization();
|
service.AddAuthorization();
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ namespace HopFrame.Security.Authentication;
|
|||||||
public class HopFrameAuthenticationOptions : OptionsFromConfiguration {
|
public class HopFrameAuthenticationOptions : OptionsFromConfiguration {
|
||||||
public override string Position { get; } = "HopFrame:Authentication";
|
public override string Position { get; } = "HopFrame:Authentication";
|
||||||
|
|
||||||
public TimeSpan AccessTokenTime => AccessToken is null ? new(0, 0, 5, 0) : new(AccessToken.Days, AccessToken.Hours, AccessToken.Minutes, AccessToken.Seconds);
|
public TimeSpan AccessTokenTime => AccessToken is null ? new(0, 0, 5, 0) : AccessToken.ConstructTimeSpan;
|
||||||
public TimeSpan RefreshTokenTime => RefreshToken is null ? new(30, 0, 0, 0) : new(RefreshToken.Days, RefreshToken.Hours, RefreshToken.Minutes, RefreshToken.Seconds);
|
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 AccessToken { get; set; }
|
||||||
public TokenTime RefreshToken { get; set; }
|
public TokenTime RefreshToken { get; set; }
|
||||||
@@ -16,5 +18,7 @@ public class HopFrameAuthenticationOptions : OptionsFromConfiguration {
|
|||||||
public int Hours { get; set; }
|
public int Hours { get; set; }
|
||||||
public int Minutes { get; set; }
|
public int Minutes { get; set; }
|
||||||
public int Seconds { get; set; }
|
public int Seconds { get; set; }
|
||||||
|
|
||||||
|
public TimeSpan ConstructTimeSpan => new(Days, Hours, Minutes, Seconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using HopFrame.Security.Authentication.OpenID.Models;
|
||||||
|
|
||||||
|
namespace HopFrame.Security.Authentication.OpenID;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using HopFrame.Security.Authentication.OpenID.Models;
|
||||||
|
using HopFrame.Security.Authentication.OpenID.Options;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace HopFrame.Security.Authentication.OpenID.Implementation;
|
||||||
|
|
||||||
|
internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdOptions> options, IHttpContextAccessor accessor, IMemoryCache cache) : IOpenIdAccessor {
|
||||||
|
private const string ConfigurationCacheKey = "HopFrame:OpenID:Configuration";
|
||||||
|
private const string AuthCodeCacheKey = "HopFrame:OpenID:Code:";
|
||||||
|
private const string TokenCacheKey = "HopFrame:OpenID:Token:";
|
||||||
|
|
||||||
|
public async Task<OpenIdConfiguration> LoadConfiguration() {
|
||||||
|
if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled && cache.TryGetValue(ConfigurationCacheKey, out object cachedConfiguration)) {
|
||||||
|
return cachedConfiguration as OpenIdConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = clientFactory.CreateClient();
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, Path.Combine(options.Value.Issuer, ".well-known/openid-configuration"));
|
||||||
|
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)
|
||||||
|
cache.Set(ConfigurationCacheKey, config, options.Value.Cache.Configuration.TTL.ConstructTimeSpan);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OpenIdToken> RequestToken(string code, string defaultCallback) {
|
||||||
|
if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled && cache.TryGetValue(AuthCodeCacheKey + code, out object cachedToken)) {
|
||||||
|
return cachedToken as OpenIdToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
var protocol = accessor.HttpContext!.Request.IsHttps ? "https" : "http";
|
||||||
|
var callback = options.Value.Callback ?? $"{protocol}://{accessor.HttpContext!.Request.Host.Value}/{defaultCallback}";
|
||||||
|
|
||||||
|
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)
|
||||||
|
cache.Set(AuthCodeCacheKey + code, token, options.Value.Cache.Auth.TTL.ConstructTimeSpan);
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> ConstructAuthUri(string defaultCallback, string state = null) {
|
||||||
|
var protocol = accessor.HttpContext!.Request.IsHttps ? "https" : "http";
|
||||||
|
var callback = options.Value.Callback ?? $"{protocol}://{accessor.HttpContext!.Request.Host.Value}/{defaultCallback}";
|
||||||
|
|
||||||
|
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 async Task<OpenIdIntrospection> InspectToken(string token) {
|
||||||
|
if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled && cache.TryGetValue(TokenCacheKey + token, out object cachedToken)) {
|
||||||
|
return cachedToken as OpenIdIntrospection;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
Minutes = 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,4 @@ public interface ITokenContext {
|
|||||||
/// The access token the user provided
|
/// The access token the user provided
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Token AccessToken { get; }
|
Token AccessToken { get; }
|
||||||
|
|
||||||
IList<string> ContextualPermissions { get; }
|
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
using HopFrame.Database.Models;
|
using HopFrame.Database.Models;
|
||||||
using HopFrame.Database.Repositories;
|
using HopFrame.Database.Repositories;
|
||||||
|
using HopFrame.Security.Authentication.OpenID.Options;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace HopFrame.Security.Claims;
|
namespace HopFrame.Security.Claims;
|
||||||
|
|
||||||
internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens, IPermissionRepository permissions) : 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 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 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,
|
||||||
public IList<string> ContextualPermissions => permissions.GetFullPermissions(AccessToken).GetAwaiter().GetResult();
|
Type = Token.OpenIdTokenType,
|
||||||
|
CreatedAt = DateTime.Now
|
||||||
|
} : tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using HopFrame.Database.Repositories;
|
using HopFrame.Database.Repositories;
|
||||||
using HopFrame.Security.Authentication;
|
using HopFrame.Security.Authentication;
|
||||||
using HopFrame.Security.Claims;
|
|
||||||
using HopFrame.Web.Services;
|
using HopFrame.Web.Services;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
@@ -20,16 +19,10 @@ public sealed class AuthMiddleware(IAuthService auth, IPermissionRepository perm
|
|||||||
next?.Invoke(context);
|
next?.Invoke(context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var claims = new List<Claim> {
|
|
||||||
new(HopFrameClaimTypes.AccessTokenId, token.TokenId.ToString()),
|
|
||||||
new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString())
|
|
||||||
};
|
|
||||||
|
|
||||||
var permissions = await perms.GetFullPermissions(token);
|
var principal = await HopFrameAuthentication.GenerateClaims(token, perms);
|
||||||
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
|
if (principal?.Identity is ClaimsIdentity identity)
|
||||||
|
context.User.AddIdentity(identity);
|
||||||
context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await next?.Invoke(context);
|
await next?.Invoke(context);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using HopFrame.Database.Models;
|
using HopFrame.Database.Models;
|
||||||
using HopFrame.Database.Repositories;
|
using HopFrame.Database.Repositories;
|
||||||
using HopFrame.Security.Authentication;
|
using HopFrame.Security.Authentication;
|
||||||
|
using HopFrame.Security.Authentication.OpenID;
|
||||||
|
using HopFrame.Security.Authentication.OpenID.Options;
|
||||||
using HopFrame.Security.Claims;
|
using HopFrame.Security.Claims;
|
||||||
using HopFrame.Security.Models;
|
using HopFrame.Security.Models;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@@ -13,10 +15,15 @@ internal class AuthService(
|
|||||||
IHttpContextAccessor httpAccessor,
|
IHttpContextAccessor httpAccessor,
|
||||||
ITokenRepository tokens,
|
ITokenRepository tokens,
|
||||||
ITokenContext context,
|
ITokenContext context,
|
||||||
IOptions<HopFrameAuthenticationOptions> options)
|
IOptions<HopFrameAuthenticationOptions> options,
|
||||||
|
IOptions<OpenIdOptions> openIdOptions,
|
||||||
|
IOpenIdAccessor accessor,
|
||||||
|
IUserRepository users)
|
||||||
: IAuthService {
|
: IAuthService {
|
||||||
|
|
||||||
public async Task Register(UserRegister register) {
|
public async Task Register(UserRegister register) {
|
||||||
|
if (!options.Value.DefaultAuthentication) return;
|
||||||
|
|
||||||
var user = await userService.AddUser(new User {
|
var user = await userService.AddUser(new User {
|
||||||
Username = register.Username,
|
Username = register.Username,
|
||||||
Email = register.Email,
|
Email = register.Email,
|
||||||
@@ -41,6 +48,8 @@ internal class AuthService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> Login(UserLogin login) {
|
public async Task<bool> Login(UserLogin login) {
|
||||||
|
if (!options.Value.DefaultAuthentication) return false;
|
||||||
|
|
||||||
var user = await userService.GetUserByEmail(login.Email);
|
var user = await userService.GetUserByEmail(login.Email);
|
||||||
|
|
||||||
if (user == null) return false;
|
if (user == null) return false;
|
||||||
@@ -75,6 +84,45 @@ internal class AuthService(
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(refreshToken)) return null;
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, openIdToken.AccessToken, new CookieOptions {
|
||||||
|
MaxAge = TimeSpan.FromSeconds(openIdToken.ExpiresIn),
|
||||||
|
HttpOnly = false,
|
||||||
|
Secure = true
|
||||||
|
});
|
||||||
|
return new() {
|
||||||
|
Owner = user,
|
||||||
|
CreatedAt = DateTime.Now,
|
||||||
|
Type = Token.OpenIdTokenType
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.Value.DefaultAuthentication)
|
||||||
|
return null;
|
||||||
|
|
||||||
var token = await tokens.GetToken(refreshToken);
|
var token = await tokens.GetToken(refreshToken);
|
||||||
|
|
||||||
if (token is null || token.Type != Token.RefreshTokenType) return null;
|
if (token is null || token.Type != Token.RefreshTokenType) return null;
|
||||||
@@ -96,7 +144,7 @@ internal class AuthService(
|
|||||||
var accessToken = context.AccessToken;
|
var accessToken = context.AccessToken;
|
||||||
|
|
||||||
if (accessToken is null) return false;
|
if (accessToken is null) return false;
|
||||||
if (accessToken.Type != Token.AccessTokenType) 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.CreatedAt + options.Value.AccessTokenTime < DateTime.Now) return false;
|
||||||
if (accessToken.Owner is null) return false;
|
if (accessToken.Owner is null) return false;
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ namespace HopFrame.Testing.Api.Controllers;
|
|||||||
public class TestController(ITokenContext userContext, DatabaseContext context, ITokenRepository tokens, IPermissionRepository permissions) : ControllerBase {
|
public class TestController(ITokenContext userContext, DatabaseContext context, ITokenRepository tokens, IPermissionRepository permissions) : ControllerBase {
|
||||||
|
|
||||||
[HttpGet("permissions"), Authorized]
|
[HttpGet("permissions"), Authorized]
|
||||||
public ActionResult<IList<string>> Permissions() {
|
public async Task<ActionResult<IList<string>>> Permissions() {
|
||||||
return new ActionResult<IList<string>>(userContext.ContextualPermissions);
|
return new ActionResult<IList<string>>(await permissions.GetFullPermissions(userContext.AccessToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("generate")]
|
[HttpGet("generate")]
|
||||||
@@ -66,5 +66,11 @@ public class TestController(ITokenContext userContext, DatabaseContext context,
|
|||||||
var token = await tokens.GetToken(tokenId);
|
var token = await tokens.GetToken(tokenId);
|
||||||
await tokens.DeleteToken(token);
|
await tokens.DeleteToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("url")]
|
||||||
|
public async Task<ActionResult<SingleValueResult<string>>> GetUrl() {
|
||||||
|
var protocol = Request.IsHttps ? "https" : "http";
|
||||||
|
return Ok($"{protocol}://{Request.Host.Value}/auth/callback");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient();
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddHopFrame<DatabaseContext>(builder.Configuration);
|
builder.Services.AddHopFrame<DatabaseContext>(builder.Configuration);
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ public class AuthLogicTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Logout_With_NoAccessToken_Should_Fail() {
|
public async Task Logout_With_NoAccessToken_Should_Succeed() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var (auth, context) = SetupEnvironment(provideAccessToken: false);
|
var (auth, context) = SetupEnvironment(provideAccessToken: false);
|
||||||
context.Response.Cookies.Append(ITokenContext.AccessTokenType, _accessToken.ToString());
|
context.Response.Cookies.Append(ITokenContext.AccessTokenType, _accessToken.ToString());
|
||||||
@@ -339,14 +339,13 @@ public class AuthLogicTests {
|
|||||||
var result = await auth.Logout();
|
var result = await auth.Logout();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(result.IsSuccessful);
|
Assert.True(result.IsSuccessful);
|
||||||
Assert.Equal(HttpStatusCode.Conflict, result.State);
|
Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
|
||||||
Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
|
Assert.Null(context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
|
||||||
Assert.Equal(_refreshToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Logout_With_NoRefreshToken_Should_Fail() {
|
public async Task Logout_With_NoRefreshToken_Should_Succeed() {
|
||||||
// Arrange
|
// Arrange
|
||||||
var (auth, context) = SetupEnvironment();
|
var (auth, context) = SetupEnvironment();
|
||||||
context.Response.Cookies.Append(ITokenContext.AccessTokenType, _accessToken.ToString());
|
context.Response.Cookies.Append(ITokenContext.AccessTokenType, _accessToken.ToString());
|
||||||
@@ -356,10 +355,9 @@ public class AuthLogicTests {
|
|||||||
var result = await auth.Logout();
|
var result = await auth.Logout();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(result.IsSuccessful);
|
Assert.True(result.IsSuccessful);
|
||||||
Assert.Equal(HttpStatusCode.Conflict, result.State);
|
Assert.Null(context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
|
||||||
Assert.Equal(_accessToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.AccessTokenType));
|
Assert.Null(context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
|
||||||
Assert.Equal(_refreshToken.ToString(), context.Response.Headers.FindCookie(ITokenContext.RefreshTokenType));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ using System.Text.Encodings.Web;
|
|||||||
using HopFrame.Database.Models;
|
using HopFrame.Database.Models;
|
||||||
using HopFrame.Database.Repositories;
|
using HopFrame.Database.Repositories;
|
||||||
using HopFrame.Security.Authentication;
|
using HopFrame.Security.Authentication;
|
||||||
|
using HopFrame.Security.Authentication.OpenID;
|
||||||
|
using HopFrame.Security.Authentication.OpenID.Options;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -46,7 +48,17 @@ public class AuthenticationTests {
|
|||||||
.Setup(x => x.GetFullPermissions(It.IsAny<Token>()))
|
.Setup(x => x.GetFullPermissions(It.IsAny<Token>()))
|
||||||
.ReturnsAsync(new List<string>());
|
.ReturnsAsync(new List<string>());
|
||||||
|
|
||||||
var auth = new HopFrameAuthentication(options.Object, logger.Object, encoder.Object, clock.Object, tokens.Object, perms.Object, new OptionsWrapper<HopFrameAuthenticationOptions>(new HopFrameAuthenticationOptions()));
|
var auth = new HopFrameAuthentication(
|
||||||
|
options.Object,
|
||||||
|
logger.Object,
|
||||||
|
encoder.Object,
|
||||||
|
clock.Object,
|
||||||
|
tokens.Object,
|
||||||
|
perms.Object,
|
||||||
|
new OptionsWrapper<HopFrameAuthenticationOptions>(new HopFrameAuthenticationOptions()),
|
||||||
|
new OptionsWrapper<OpenIdOptions>(new OpenIdOptions()),
|
||||||
|
new Mock<IUserRepository>().Object,
|
||||||
|
new Mock<IOpenIdAccessor>().Object);
|
||||||
var context = new DefaultHttpContext();
|
var context = new DefaultHttpContext();
|
||||||
if (provideCorrectToken)
|
if (provideCorrectToken)
|
||||||
context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, correctToken.TokenId.ToString());
|
context.HttpContext.Request.Headers.Append(HopFrameAuthentication.SchemeName, correctToken.TokenId.ToString());
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using HopFrame.Database.Models;
|
using HopFrame.Database.Models;
|
||||||
using HopFrame.Database.Repositories;
|
using HopFrame.Database.Repositories;
|
||||||
using HopFrame.Security.Authentication;
|
using HopFrame.Security.Authentication;
|
||||||
|
using HopFrame.Security.Authentication.OpenID;
|
||||||
|
using HopFrame.Security.Authentication.OpenID.Options;
|
||||||
using HopFrame.Security.Claims;
|
using HopFrame.Security.Claims;
|
||||||
using HopFrame.Security.Models;
|
using HopFrame.Security.Models;
|
||||||
using HopFrame.Tests.Web.Extensions;
|
using HopFrame.Tests.Web.Extensions;
|
||||||
@@ -68,7 +70,16 @@ public class AuthServiceTests {
|
|||||||
.Setup(c => c.AccessToken)
|
.Setup(c => c.AccessToken)
|
||||||
.Returns(providedAccessToken);
|
.Returns(providedAccessToken);
|
||||||
|
|
||||||
return (new AuthService(users.Object, accessor, tokens.Object, context.Object, new OptionsWrapper<HopFrameAuthenticationOptions>(new HopFrameAuthenticationOptions())), accessor.HttpContext);
|
return (new AuthService(
|
||||||
|
users.Object,
|
||||||
|
accessor,
|
||||||
|
tokens.Object,
|
||||||
|
context.Object,
|
||||||
|
new OptionsWrapper<HopFrameAuthenticationOptions>(new HopFrameAuthenticationOptions()),
|
||||||
|
new OptionsWrapper<OpenIdOptions>(new OpenIdOptions()),
|
||||||
|
new Mock<IOpenIdAccessor>().Object,
|
||||||
|
users.Object
|
||||||
|
), accessor.HttpContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
private User CreateDummyUser() => new() {
|
private User CreateDummyUser() => new() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Security.Claims;
|
|||||||
using Bunit;
|
using Bunit;
|
||||||
using Bunit.TestDoubles;
|
using Bunit.TestDoubles;
|
||||||
using HopFrame.Security.Authentication;
|
using HopFrame.Security.Authentication;
|
||||||
|
using HopFrame.Security.Authentication.OpenID;
|
||||||
using HopFrame.Security.Claims;
|
using HopFrame.Security.Claims;
|
||||||
using HopFrame.Web.Components;
|
using HopFrame.Web.Components;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|||||||
Reference in New Issue
Block a user