Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/Ignis.Api/Controllers/AuthorizationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,22 @@ namespace Ignis.Api.Controllers;
[ApiController]
public class AuthorizationController(AuthorizationHandler handler) : ControllerBase
{
/// <summary>Exchange credentials for an access token (OAuth 2.0 client_credentials grant).</summary>
/// <summary>Authorization endpoint.</summary>
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[ApiExplorerSettings(IgnoreApi = true)]
public Task<IActionResult> Authorize() => handler.AuthorizeAsync(HttpContext);

/// <summary>Exchange credentials for an access token (OAuth 2.0 client_credentials or authorization_code grant).</summary>
[HttpPost("~/connect/token")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK, "application/json")]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public Task<IActionResult> Exchange() => handler.ExchangeAsync(HttpContext);

/// <summary>Logout endpoint.</summary>
[HttpGet("~/connect/logout")]
[HttpPost("~/connect/logout")]
[ApiExplorerSettings(IgnoreApi = true)]
public Task<IActionResult> Logout() => handler.LogoutAsync(HttpContext);
}
3 changes: 2 additions & 1 deletion src/Ignis.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors();

await app.SyncOAuthClientsAsync();
app.UseAuthentication();
app.UseAuthorization();

await app.SyncOAuthClientsAsync();
app.MapControllers();
app.MapGet("/healthz", () => Results.Ok("ok"));

Expand Down
5 changes: 4 additions & 1 deletion src/Ignis.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
"DisplayName": "Ignis Client",
"AllowedGrantTypes": ["client_credentials"]
}
]
],
"Endpoints": {
"LoginPath": "/connect/login"
}
},
"SparkSettings": {
"Endpoint": "https://localhost:5201/fhir",
Expand Down
12 changes: 7 additions & 5 deletions src/Ignis.Auth/AuthSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ public class AuthSettings
public AuthCertificateSettings Certificates { get; set; } = new();
}

public class AuthEndpointSettings
{
public string LoginPath { get; set; } = "/connect/login";
}

public class AuthCertificateSettings
{
public string SigningCertificatePath { get; set; } = "";
Expand All @@ -16,15 +21,12 @@ public class AuthCertificateSettings
public string EncryptionCertificatePassword { get; set; } = "";
}

public class AuthEndpointSettings
{
public string TokenEndpointPath { get; set; } = "connect/token";
}

public class ClientDefinition
{
public string ClientId { get; set; } = "";
public string ClientSecret { get; set; } = "";
public string DisplayName { get; set; } = "";
public List<string> AllowedGrantTypes { get; set; } = [];
public List<string> RedirectUris { get; set; } = [];
public List<string> PostLogoutRedirectUris { get; set; } = [];
}
59 changes: 58 additions & 1 deletion src/Ignis.Auth/AuthorizationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
namespace Ignis.Auth;

/// <summary>
/// Contains the OpenIddict token endpoint logic.
/// Contains the OpenIddict authorization, token and logout endpoint logic.
/// Designed to be called from a thin controller in the host application.
/// </summary>
public class AuthorizationHandler
Expand All @@ -25,11 +25,62 @@ public AuthorizationHandler(IOpenIddictApplicationManager applicationManager)
_applicationManager = applicationManager;
}

public async Task<IActionResult> AuthorizeAsync(HttpContext httpContext)
{
var request = httpContext.GetOpenIddictServerRequest()
?? throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

var result = await httpContext.AuthenticateAsync(AuthConstants.SessionScheme);
if (!result.Succeeded || result.Principal?.Identity?.IsAuthenticated != true)
{
var httpRequest = httpContext.Request;
var parameters = httpRequest.HasFormContentType
? httpRequest.Form.Where(p => p.Key != "__RequestVerificationToken").ToList()
: httpRequest.Query.ToList();

return new ChallengeResult(
[AuthConstants.SessionScheme],
new AuthenticationProperties
{
RedirectUri = httpRequest.PathBase + httpRequest.Path + QueryString.Create(parameters),
});
}

var identity = new ClaimsIdentity(
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
Claims.Name, Claims.Role);

identity.SetClaim(Claims.Subject,
result.Principal.FindFirst(Claims.Subject)?.Value ??
result.Principal.FindFirst(ClaimTypes.NameIdentifier)?.Value);
identity.SetClaim(Claims.Name,
result.Principal.FindFirst(Claims.Name)?.Value ??
result.Principal.Identity?.Name);

identity.SetScopes(request.GetScopes());
identity.SetDestinations(static claim => [Destinations.AccessToken]);

return new SignInResult(
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
new ClaimsPrincipal(identity));
}

public async Task<IActionResult> ExchangeAsync(HttpContext httpContext)
{
var request = httpContext.GetOpenIddictServerRequest()
?? throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");

if (request.IsAuthorizationCodeGrantType())
{
var principal = (await httpContext.AuthenticateAsync(
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal
?? throw new InvalidOperationException("The authorization code principal cannot be retrieved.");

return new SignInResult(
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
principal);
}

if (request.IsClientCredentialsGrantType())
{
return await ExchangeClientCredentialsAsync(request);
Expand All @@ -38,6 +89,12 @@ public async Task<IActionResult> ExchangeAsync(HttpContext httpContext)
return ForbidWithError(Errors.UnsupportedGrantType, "The specified grant type is not supported.");
}

public async Task<IActionResult> LogoutAsync(HttpContext httpContext)
{
await httpContext.SignOutAsync(AuthConstants.SessionScheme);
return new RedirectResult("/");
}

private async Task<IActionResult> ExchangeClientCredentialsAsync(OpenIddictRequest request)
{
if (string.IsNullOrEmpty(request.ClientId))
Expand Down
68 changes: 53 additions & 15 deletions src/Ignis.Auth/Extensions/AuthServerExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
using System.Security.Cryptography.X509Certificates;

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.DependencyInjection;

using MongoDB.Driver;

using OpenIddict.Server;

namespace Ignis.Auth.Extensions;

public static class AuthServerExtensions
{
/// <summary>
/// Registers the OpenIddict authorization server and certificates.
/// Registers the OpenIddict authorization server, token validation,
/// session cookie authentication and certificates.
/// Use this when the application acts as an authorization server.
/// </summary>
public static IServiceCollection AddIgnisAuthServer(
Expand All @@ -20,7 +20,7 @@ public static IServiceCollection AddIgnisAuthServer(
bool useDevelopmentCertificates)
{
ArgumentNullException.ThrowIfNull(settings);
ArgumentException.ThrowIfNullOrWhiteSpace(settings.ConnectionString);
ArgumentException.ThrowIfNullOrWhiteSpace(settings.ConnectionString, "AuthSettings:ConnectionString is required");

services.Configure<AuthSettings>(options =>
{
Expand All @@ -30,20 +30,38 @@ public static IServiceCollection AddIgnisAuthServer(
options.Certificates = settings.Certificates;
});

services.AddOpenIddictServer(settings, useDevelopmentCertificates);
services.AddOpenIddict()
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
services
.AddSessionCookieAuthentication(settings.Endpoints.LoginPath)
.AddOpenIddictServer(settings, useDevelopmentCertificates)
.AddOpenIddictValidation();

services.AddTransient<AuthorizationHandler>();

return services;
}

private static void AddOpenIddictServer(
private static IServiceCollection AddSessionCookieAuthentication(
this IServiceCollection services,
string loginPath)
{
services.AddAuthentication()
.AddCookie(AuthConstants.SessionScheme, options =>
{
options.LoginPath = loginPath;
// Always issue a 302 redirect; the default cookie handler
// returns 401 for non-browser requests (missing Accept: text/html),
// which breaks the OAuth 2.0 authorization endpoint flow.
options.Events.OnRedirectToLogin = context =>
{
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
});

return services;
}

private static IServiceCollection AddOpenIddictServer(
this IServiceCollection services,
AuthSettings settings,
bool useDevelopmentCertificates)
Expand All @@ -58,18 +76,38 @@ private static void AddOpenIddictServer(
.AddServer(options =>
{
options
.SetTokenEndpointUris(settings.Endpoints.TokenEndpointPath)
.AllowClientCredentialsFlow();
.SetTokenEndpointUris("connect/token")
.SetAuthorizationEndpointUris("connect/authorize")
.SetPushedAuthorizationEndpointUris("connect/par")
.AllowClientCredentialsFlow()
.AllowAuthorizationCodeFlow()
.RequireProofKeyForCodeExchange()
.RequirePushedAuthorizationRequests();

ConfigureCertificates(options, settings.Certificates, useDevelopmentCertificates);

var aspNetCoreBuilder = options
.UseAspNetCore()
.EnableTokenEndpointPassthrough();
.EnableTokenEndpointPassthrough()
.EnableAuthorizationEndpointPassthrough();

if (useDevelopmentCertificates)
aspNetCoreBuilder.DisableTransportSecurityRequirement();
});

return services;
}

private static IServiceCollection AddOpenIddictValidation(this IServiceCollection services)
{
services.AddOpenIddict()
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});

return services;
}

private static void ConfigureCertificates(
Expand Down
Loading
Loading