Skip to content
Merged
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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@
<PackageVersion Include="DxWorks.Hub.Sdk" Version="2.0.0" />
<PackageVersion Include="FluentValidation" Version="12.1.1" />
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="10.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.11" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.11" />
Expand Down
9 changes: 6 additions & 3 deletions Integrations/MCP/ScriptBee.MCP/Generated/GatewayApi.g.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -857,7 +857,7 @@ public partial interface IGatewayApi
/// <exception cref="ApiException">Thrown when the request returns a non-success status code.</exception>
[Headers("Accept: application/json")]
[Get("/api/config/auth")]
Task<AuthConfig> Config(CancellationToken cancellationToken = default);
Task<AuthConfig> Auth(CancellationToken cancellationToken = default);

/// <summary>Delete analysis</summary>
/// <remarks>Deletes a specific analysis and all its associated artifacts.</remarks>
Expand DownExpand Up@@ -1138,7 +1138,7 @@ namespace ScriptBee.MCP.Gateway.Generated.Contracts
{
using System = global::System;



[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class AllAvailablePluginsResponse
Expand DownExpand Up@@ -1278,6 +1278,9 @@ public partial class AuthConfig
[JsonPropertyName("authority")]
public string Authority { get; set; }

[JsonPropertyName("authWellknownEndpointUrl")]
public string AuthWellknownEndpointUrl { get; set; }

[JsonPropertyName("clientId")]
public string ClientId { get; set; }

Expand DownExpand Up@@ -2895,4 +2898,4 @@ public FileParameter(System.IO.Stream data, string fileName, string contentType)
#pragma warning restore 8603
#pragma warning restore 8604
#pragma warning restore 8625
#pragma warning restore 8765
#pragma warning restore 8765
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Authorization;

namespace ScriptBee.Web.Auth;

public class AllowAllAuthorizationHandler : IAuthorizationHandler
{
public Task HandleAsync(AuthorizationHandlerContext context)
{
foreach (var requirement in context.PendingRequirements.ToList())
{
context.Succeed(requirement);
}

return Task.CompletedTask;
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Authorization;

namespace ScriptBee.Web.Auth;

[AttributeUsage(AttributeTargets.Method)]
public class AuthorizeActionAttribute(string action)
: AuthorizeAttribute,
IAuthorizationRequirementData
{
public IEnumerable<IAuthorizationRequirement> GetRequirements() =>
[new OpaActionRequirement(action)];
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
namespace ScriptBee.Web.Auth;

public static class EndpointAuthorizationExtensions
{
public static RouteHandlerBuilder RequireAction(this RouteHandlerBuilder builder, string action)
{
return builder.RequireAuthorization(new AuthorizeActionAttribute(action));
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Authorization;

namespace ScriptBee.Web.Auth;

public class OpaActionRequirement(string action) : IAuthorizationRequirement
{
public string Action { get; } = action;
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,14 @@ namespace ScriptBee.Web.Config;
public class AuthenticationConfig
{
public string? AuthMode { get; init; }
public required bool RequireHttpsMetadata { get; init; }
public string? Authority { get; init; }
public string? Audience { get; init; }
public string? AuthWellknownEndpointUrl { get; init; }
public string? ClientId { get; init; }
public string? Scope { get; init; }
public string? OpaUrl { get; init; }

public bool IsDevelopment =>
AuthMode?.Equals("Development", StringComparison.OrdinalIgnoreCase) ?? false;
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,6 +28,7 @@ IOptions<AuthenticationConfig> authConfigOptions
{
AuthMode = config.AuthMode,
Authority = config.Authority,
AuthWellknownEndpointUrl = config.AuthWellknownEndpointUrl,
ClientId = config.ClientId,
Scope = config.Scope,
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ public class WebAuthConfig
{
public string? AuthMode { get; init; }
public string? Authority { get; init; }
public string? AuthWellknownEndpointUrl { get; init; }
public string? ClientId { get; init; }
public string? Scope { get; init; }
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,91 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using ScriptBee.Web.Auth;
using ScriptBee.Web.Config;

namespace ScriptBee.Web.Extensions;

public static class AuthenticationExtensions
{
public static IServiceCollection AddAuthenticationConfig(this IServiceCollection services)
private const string AuthenticationConfigSectionName = "Authentication";

public static IServiceCollection AddAuthenticationConfig(
this IServiceCollection services,
ConfigurationManager configurationManager
)
{
services.AddOptions<AuthenticationConfig>().BindConfiguration("Authentication");
services
.AddOptions<AuthenticationConfig>()
.BindConfiguration(AuthenticationConfigSectionName);

var authConfig = configurationManager
.GetSection(AuthenticationConfigSectionName)
.Get<AuthenticationConfig>()!;

services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = authConfig.Authority;
options.Audience = authConfig.Audience;
options.RequireHttpsMetadata = authConfig.RequireHttpsMetadata;

if (authConfig.IsDevelopment)
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = false,
ValidateLifetime = true,
};
}
else
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = authConfig.Authority,
ValidAudience = authConfig.Audience,
};
}
});

services.AddHttpContextAccessor();
services.AddHttpClient(
"OpaClient",
client => client.BaseAddress = new Uri(GetOpaUrl(authConfig))
);

if (authConfig.IsDevelopment)
{
services.AddSingleton<IAuthorizationHandler, AllowAllAuthorizationHandler>();
}
else
{
services.AddSingleton<IAuthorizationHandler, AllowAllAuthorizationHandler>();
// TODO FIXIT(#332): Add OPA authorization handler
// services.AddSingleton<IAuthorizationHandler, OpaActionAuthorizationHandler>();
}

services.AddAuthorization();

return services;
}

private static string GetOpaUrl(AuthenticationConfig config)
{
if (config.IsDevelopment)
{
return "";
}

return string.IsNullOrEmpty(config.OpaUrl)
? throw new InvalidOperationException(
"OpaUrl is not configured and is mandatory. Please set Authentication:OpaUrl in your configuration."
)
: config.OpaUrl;
}
}
5 changes: 4 additions & 1 deletion ScriptBeeWebApp/src/Gateway/Adapters/Web/Program.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,7 +41,7 @@
})
.AddValidatorsFromAssemblyContaining<IEndpointDefinitionMarker>()
.AddProblemDetailsDefaults()
.AddAuthenticationConfig()
.AddAuthenticationConfig(builder.Configuration)
.AddMongoDb(mongoConnectionString)
.AddCommonServices()
.AddArtifactFileAdapters()
Expand DownExpand Up@@ -80,6 +80,9 @@
app.UseAntiforgery();
app.UseAntiforgeryHeader();

app.UseAuthentication();
app.UseAuthorization();

app.MapHealthChecksEndpoint();

app.UseSerilogRequestLogging();
Expand Down
1 change: 1 addition & 0 deletions ScriptBeeWebApp/src/Gateway/Adapters/Web/Web.csproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
<ItemGroup>
<PackageReference Include="FluentValidation" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" />
</ItemGroup>
<ItemGroup>
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,13 @@
}
},
"Authentication": {
"AuthMode": "Development"
"AuthMode": "Development",
"RequireHttpsMetadata": false,
"Authority": "http://localhost:8080/default",
"Audience": "test-client-id",
"ClientId": "test-client-id",
"Scope": "openid profile email",
"OpaUrl": "http://localhost:8181/"
},
"ScriptBee": {
"Analysis": {
Expand Down
3 changes: 3 additions & 0 deletions ScriptBeeWebApp/src/Gateway/Adapters/Web/appsettings.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,9 @@
}
]
},
"Authentication": {
"RequireHttpsMetadata": true
},
"ConnectionStrings": {
"mongodb": "mongodb://root:example@localhost:27017/ScriptBee?authSource=admin"
},
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using ScriptBee.Web.Auth;

namespace ScriptBee.Web.Tests.Auth;

file record DummyRequirement : IAuthorizationRequirement;

public class AllowAllAuthorizationHandlerTests
{
private readonly AllowAllAuthorizationHandler _handler = new();

[Fact]
public async Task HandleAsync_ShouldSucceedAllPendingRequirements()
{
// Arrange
var requirement1 = new DummyRequirement();
var requirement2 = new DummyRequirement();

var user = new ClaimsPrincipal(new ClaimsIdentity());
var requirements = new IAuthorizationRequirement[] { requirement1, requirement2 };

var context = new AuthorizationHandlerContext(requirements, user, resource: null);

// Act
await _handler.HandleAsync(context);

// Assert
Assert.True(context.HasSucceeded);
Assert.False(context.HasFailed);
}
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
{
"authMode": "Development",
"authority": null,
"clientId": null,
"scope": null
"authority": "http://localhost:8080/default",
"clientId": "test-client-id",
"scope": "openid profile email"
}
27 changes: 27 additions & 0 deletions docs/architecture/configuration/gateway_configuration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,13 +10,34 @@

## Authentication

### `AUTHENTICATION__REQUIREHTTPSSECUREMETADATA`

- **Type:** `bool`
- **Default:** `true`
- **Description:** Whether to require HTTPS for the OpenID Connect metadata endpoint. This should be set to `true` in
production.

### `AUTHENTICATION__AUTHORITY`

- **Type:** `string`
- **Default:** _None_
- **Description:** The URL of the OpenID Connect authority (e.g., `https://login.microsoftonline.com/{tenantId}/v2.0`
for Azure AD).

### `AUTHENTICATION__AUDIENCE`

- **Type:** `string`
- **Default:** _None_
- **Description:** The audience for the OpenID Connect application. (e.g. `api://my-app-backend`)

### `AUTHENTICATION__AUTHWELLKNOWNENDPOINTURL`

- **Type:** `string`
- **Default:** _None_
- **Description:** An optional URL of the OpenID Connect well-known endpoint (e.g.,
`https://login.microsoftonline.com/{tenantId}/v2.0/.well-known/openid-configuration` for Azure AD). Normally the
authority URL is used to discover the well-known endpoint, but in some cases, you may want to override it.

### `AUTHENTICATION__CLIENTID`

- **Type:** `string`
Expand All@@ -30,6 +51,12 @@
- **Description:** The scope for the OpenID Connect application. (e.g.
`openid profile email api://my-app-backend/access_as_user`)

### `AUTHENTICATION__OPAURL`

- **Type:** `string`
- **Default:** _None_
- **Description:** The URL of the Open Policy Agent (OPA) server for authorization.

### `AUTHENTICATION__AUTHMODE`

- **Type:** `string`
Expand Down
3 changes: 3 additions & 0 deletions docs/public/gateway_swagger.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -2472,6 +2472,9 @@
"authority": {
"type": ["null", "string"]
},
"authWellknownEndpointUrl": {
"type": ["null", "string"]
},
"clientId": {
"type": ["null", "string"]
},
Expand Down
Loading