Added OpenID endpoints tests and documentation

This commit is contained in:
2024-12-24 11:58:35 +01:00
parent e613fa66e3
commit 166134c6d8
7 changed files with 469 additions and 1 deletions

View File

@@ -14,6 +14,18 @@
<Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" />
 <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" />
 <Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" />
&lt;/AssemblyExplorer&gt;</s:String> &lt;/AssemblyExplorer&gt;</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=027ac703_002Df1f3_002D42aa_002D9c67_002D7cbaeecdbead/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="Refresh_ShouldReturnConflict_WhenRefreshTokenNotValid" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;TestAncestor&gt;&#xD;
&lt;TestId&gt;xUnit::25DE1510-47E5-46FF-89A4-B9F99542218E::net8.0::HopFrame.Tests.Api.Controllers.OpenIdControllerTests.Refresh_ShouldReturnConflict_WhenRefreshTokenNotValid&lt;/TestId&gt;&#xD;
&lt;/TestAncestor&gt;&#xD;
&lt;/SessionState&gt;</s:String>

120
docs/api/endpoints/auth.md Normal file
View 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.

View 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.

View File

@@ -14,3 +14,66 @@ public sealed class UserPasswordValidation {
public string Password { get; set; } 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; }
}
```

View File

@@ -81,3 +81,17 @@ public class UserCreator {
```csharp ```csharp
public interface IPermissionOwner; 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; }
}
```

View File

@@ -51,7 +51,7 @@ public class OpenIdController(IOpenIdAccessor accessor) : ControllerBase {
var token = await accessor.RefreshAccessToken(refreshToken); var token = await accessor.RefreshAccessToken(refreshToken);
if (token is null) if (token is null)
return NotFound("Refresh token not valid"); return Conflict("Refresh token not valid");
accessor.SetAuthenticationCookies(token); accessor.SetAuthenticationCookies(token);

View File

@@ -0,0 +1,177 @@
using HopFrame.Api.Controller;
using HopFrame.Api.Models;
using HopFrame.Security.Authentication.OpenID;
using HopFrame.Security.Authentication.OpenID.Models;
using HopFrame.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moq;
namespace HopFrame.Tests.Api.Controllers;
public class OpenIdControllerTests {
private (Mock<IOpenIdAccessor>, OpenIdController) SetupEnvironment(out HttpContext httpContext) {
var mockAccessor = new Mock<IOpenIdAccessor>();
var controller = new OpenIdController(mockAccessor.Object);
httpContext = new DefaultHttpContext();
controller.ControllerContext = new ControllerContext {
HttpContext = httpContext
};
return (mockAccessor, controller);
}
[Fact]
public async Task RedirectToProvider_ShouldRedirect_WhenPerformRedirectIsTrue() {
// Arrange
var (mockAccessor, controller) = SetupEnvironment(out _);
var uri = "https://example.com/auth";
mockAccessor.Setup(a => a.ConstructAuthUri(It.IsAny<string>())).ReturnsAsync(uri);
// Act
var result = await controller.RedirectToProvider("https://redirectafter.com", 1);
// Assert
var redirectResult = Assert.IsType<RedirectResult>(result);
Assert.Equal(uri, redirectResult.Url);
}
[Fact]
public async Task RedirectToProvider_ShouldReturnOk_WhenPerformRedirectIsFalse() {
// Arrange
var (mockAccessor, controller) = SetupEnvironment(out _);
var uri = "https://example.com/auth";
mockAccessor.Setup(a => a.ConstructAuthUri(It.IsAny<string>())).ReturnsAsync(uri);
// Act
var result = await controller.RedirectToProvider("https://redirectafter.com", 0);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var singleValueResult = Assert.IsType<SingleValueResult<string>>(okResult.Value);
Assert.Equal(uri, singleValueResult.Value);
}
[Fact]
public async Task Callback_ShouldReturnBadRequest_WhenAuthorizationCodeIsMissing() {
// Arrange
var (_, controller) = SetupEnvironment(out _);
// Act
var result = await controller.Callback(string.Empty, "state");
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.Equal("Authorization code is missing", badRequestResult.Value);
}
[Fact]
public async Task Callback_ShouldReturnForbidden_WhenAuthorizationCodeIsNotValid() {
// Arrange
var (mockAccessor, controller) = SetupEnvironment(out _);
mockAccessor.Setup(a => a.RequestToken(It.IsAny<string>())).ReturnsAsync((OpenIdToken)null);
// Act
var result = await controller.Callback("invalid_code", "state");
// Assert
var forbidResult = Assert.IsType<ForbidResult>(result);
Assert.Equal("Authorization code is not valid", forbidResult.AuthenticationSchemes.First());
}
[Fact]
public async Task Callback_ShouldReturnOk_WhenStateIsNull() {
// Arrange
var (mockAccessor, controller) = SetupEnvironment(out _);
var token = new OpenIdToken { AccessToken = "valid_token" };
mockAccessor.Setup(a => a.RequestToken(It.IsAny<string>())).ReturnsAsync(token);
// Act
var result = await controller.Callback("valid_code", null);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var singleValueResult = Assert.IsType<SingleValueResult<string>>(okResult.Value);
Assert.Equal("valid_token", singleValueResult.Value);
}
[Fact]
public async Task Callback_ShouldRedirect_WhenStateIsProvided() {
// Arrange
var (mockAccessor, controller) = SetupEnvironment(out _);
var token = new OpenIdToken { AccessToken = "valid_token" };
mockAccessor.Setup(a => a.RequestToken(It.IsAny<string>())).ReturnsAsync(token);
// Act
var result = await controller.Callback("valid_code", "https://redirect.com/{token}");
// Assert
var redirectResult = Assert.IsType<RedirectResult>(result);
Assert.Equal("https://redirect.com/valid_token", redirectResult.Url);
}
[Fact]
public async Task Refresh_ShouldReturnBadRequest_WhenRefreshTokenNotProvided() {
// Arrange
var (_, controller) = SetupEnvironment(out _);
// Act
var result = await controller.Refresh();
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.Equal("Refresh token not provided", badRequestResult.Value);
}
[Fact]
public async Task Refresh_ShouldReturnConflict_WhenRefreshTokenNotValid() {
// Arrange
var (mockAccessor, controller) = SetupEnvironment(out var httpContext);
var cookies = new Mock<IRequestCookieCollection>();
cookies
.SetupGet(c => c[ITokenContext.RefreshTokenType])
.Returns("invalid_token");
httpContext.Request.Cookies = cookies.Object;
mockAccessor.Setup(a => a.RefreshAccessToken(It.IsAny<string>())).ReturnsAsync((OpenIdToken)null);
// Act
var result = await controller.Refresh();
// Assert
var conflictResult = Assert.IsType<ConflictObjectResult>(result);
Assert.Equal("Refresh token not valid", conflictResult.Value);
}
[Fact]
public async Task Refresh_ShouldReturnOk_WhenRefreshTokenIsValid() {
// Arrange
var (mockAccessor, controller) = SetupEnvironment(out var httpContext);
var cookies = new Mock<IRequestCookieCollection>();
cookies
.SetupGet(c => c[ITokenContext.RefreshTokenType])
.Returns("valid_token");
httpContext.Request.Cookies = cookies.Object;
var token = new OpenIdToken { AccessToken = "new_access_token", RefreshToken = "new_refresh_token", ExpiresIn = 3600 };
mockAccessor.Setup(a => a.RefreshAccessToken(It.IsAny<string>())).ReturnsAsync(token);
// Act
var result = await controller.Refresh();
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var singleValueResult = Assert.IsType<SingleValueResult<string>>(okResult.Value);
Assert.Equal("new_access_token", singleValueResult.Value);
}
[Fact]
public void Logout_ShouldReturnOk() {
// Arrange
var (_, controller) = SetupEnvironment(out _);
// Act
var result = controller.Logout();
// Assert
Assert.IsType<OkResult>(result);
}
}