diff --git a/docs/configuration.md b/docs/configuration.md index 9effe1e59..a5421abec 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -185,6 +185,12 @@ a valid login to your IDP creates the tenant on the RelayServer. No cleanup mech available, thus automatically created tenants need to be manually deleted when they are not needed/wanted anymore. +### Require authentication + +If a tenant has `RequireAuthentication` enabled in the database, the RelayServer only relays +when the request contains an access token from it's own issuer and audience (e.g., it comes +from a connector). In any other case it returns 401. + ## Connector The `RelayConnectorOptions` type provides the main configuration for the connector. These diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 4c10822d9..aaaa43646 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -8,7 +8,7 @@ enable 3.0.0 - alpha.5 + alpha.6 $(VersionPrefix)-$(VersionSuffix)-$(BuildNumber) diff --git a/src/Thinktecture.Relay.Server.Abstractions/Persistence/Models/Tenant.cs b/src/Thinktecture.Relay.Server.Abstractions/Persistence/Models/Tenant.cs index ca99fc3cf..10b4a90ce 100644 --- a/src/Thinktecture.Relay.Server.Abstractions/Persistence/Models/Tenant.cs +++ b/src/Thinktecture.Relay.Server.Abstractions/Persistence/Models/Tenant.cs @@ -27,6 +27,11 @@ public class Tenant /// The maximum length is 1000 unicode characters. public string? Description { get; set; } + /// + /// Enable the requirement that only an authenticated request can use this tenant to relay requests. + /// + public bool RequireAuthentication { get; set; } + /// /// The normalized (e.g. ToUpperInvariant()) name of the tenant. Use this for case-insensitive comparison in the database. /// diff --git a/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.PostgreSql/Migrations/ConfigurationDb/20231109143956_Add_RequireAuthentication.Designer.cs b/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.PostgreSql/Migrations/ConfigurationDb/20231109143956_Add_RequireAuthentication.Designer.cs new file mode 100644 index 000000000..f6f6613dc --- /dev/null +++ b/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.PostgreSql/Migrations/ConfigurationDb/20231109143956_Add_RequireAuthentication.Designer.cs @@ -0,0 +1,293 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Thinktecture.Relay.Server.Persistence.EntityFrameworkCore; + +#nullable disable + +namespace Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.PostgreSql.Migrations.ConfigurationDb +{ + [DbContext(typeof(RelayDbContext))] + [Migration("20231109143956_Add_RequireAuthentication")] + partial class Add_RequireAuthentication + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.21") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.ClientSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Expiration") + .HasColumnType("timestamp with time zone"); + + b.Property("TenantName") + .IsRequired() + .HasColumnType("character varying(100)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.HasKey("Id"); + + b.HasIndex("TenantName"); + + b.ToTable("ClientSecrets"); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Config", b => + { + b.Property("TenantName") + .HasColumnType("text"); + + b.Property("EnableTracing") + .HasColumnType("boolean"); + + b.Property("KeepAliveInterval") + .HasColumnType("interval"); + + b.Property("ReconnectMaximumDelay") + .HasColumnType("interval"); + + b.Property("ReconnectMinimumDelay") + .HasColumnType("interval"); + + b.HasKey("TenantName"); + + b.ToTable("Configs"); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Connection", b => + { + b.Property("Id") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ConnectTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DisconnectTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSeenTime") + .HasColumnType("timestamp with time zone"); + + b.Property("OriginId") + .HasColumnType("uuid"); + + b.Property("RemoteIpAddress") + .HasColumnType("text"); + + b.Property("TenantName") + .IsRequired() + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("OriginId"); + + b.HasIndex("TenantName"); + + b.ToTable("Connections"); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Origin", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("LastSeenTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ShutdownTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Origins"); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Request", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Aborted") + .HasColumnType("boolean"); + + b.Property("Errored") + .HasColumnType("boolean"); + + b.Property("Expired") + .HasColumnType("boolean"); + + b.Property("Failed") + .HasColumnType("boolean"); + + b.Property("HttpMethod") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("HttpStatusCode") + .HasColumnType("integer"); + + b.Property("RequestBodySize") + .HasColumnType("bigint"); + + b.Property("RequestDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RequestDuration") + .HasColumnType("bigint"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("RequestOriginalBodySize") + .HasColumnType("bigint"); + + b.Property("RequestUrl") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ResponseBodySize") + .HasColumnType("bigint"); + + b.Property("ResponseOriginalBodySize") + .HasColumnType("bigint"); + + b.Property("Target") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantName") + .IsRequired() + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("TenantName"); + + b.ToTable("Requests"); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Tenant", b => + { + b.Property("NormalizedName") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ConfigTenantName") + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RequireAuthentication") + .HasColumnType("boolean"); + + b.HasKey("NormalizedName"); + + b.HasIndex("ConfigTenantName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.ClientSecret", b => + { + b.HasOne("Thinktecture.Relay.Server.Persistence.Models.Tenant", null) + .WithMany("ClientSecrets") + .HasForeignKey("TenantName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Connection", b => + { + b.HasOne("Thinktecture.Relay.Server.Persistence.Models.Origin", null) + .WithMany("Connections") + .HasForeignKey("OriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Thinktecture.Relay.Server.Persistence.Models.Tenant", null) + .WithMany("Connections") + .HasForeignKey("TenantName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Request", b => + { + b.HasOne("Thinktecture.Relay.Server.Persistence.Models.Tenant", null) + .WithMany("Requests") + .HasForeignKey("TenantName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Tenant", b => + { + b.HasOne("Thinktecture.Relay.Server.Persistence.Models.Config", "Config") + .WithMany() + .HasForeignKey("ConfigTenantName"); + + b.Navigation("Config"); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Origin", b => + { + b.Navigation("Connections"); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Tenant", b => + { + b.Navigation("ClientSecrets"); + + b.Navigation("Connections"); + + b.Navigation("Requests"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.PostgreSql/Migrations/ConfigurationDb/20231109143956_Add_RequireAuthentication.cs b/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.PostgreSql/Migrations/ConfigurationDb/20231109143956_Add_RequireAuthentication.cs new file mode 100644 index 000000000..68b5d1a3d --- /dev/null +++ b/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.PostgreSql/Migrations/ConfigurationDb/20231109143956_Add_RequireAuthentication.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.PostgreSql.Migrations.ConfigurationDb +{ + public partial class Add_RequireAuthentication : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RequireAuthentication", + table: "Tenants", + type: "boolean", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "RequireAuthentication", + table: "Tenants"); + } + } +} diff --git a/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.PostgreSql/Migrations/ConfigurationDb/RelayDbContextModelSnapshot.cs b/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.PostgreSql/Migrations/ConfigurationDb/RelayDbContextModelSnapshot.cs index 5bbe4b615..8e463b6f1 100644 --- a/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.PostgreSql/Migrations/ConfigurationDb/RelayDbContextModelSnapshot.cs +++ b/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.PostgreSql/Migrations/ConfigurationDb/RelayDbContextModelSnapshot.cs @@ -217,6 +217,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(100) .HasColumnType("character varying(100)"); + b.Property("RequireAuthentication") + .HasColumnType("boolean"); + b.HasKey("NormalizedName"); b.HasIndex("ConfigTenantName"); diff --git a/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.SqlServer/Migrations/ConfigurationDb/20231109143959_Add_RequireAuthentication.Designer.cs b/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.SqlServer/Migrations/ConfigurationDb/20231109143959_Add_RequireAuthentication.Designer.cs new file mode 100644 index 000000000..aa61d826c --- /dev/null +++ b/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.SqlServer/Migrations/ConfigurationDb/20231109143959_Add_RequireAuthentication.Designer.cs @@ -0,0 +1,293 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Thinktecture.Relay.Server.Persistence.EntityFrameworkCore; + +#nullable disable + +namespace Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.SqlServer.Migrations.ConfigurationDb +{ + [DbContext(typeof(RelayDbContext))] + [Migration("20231109143959_Add_RequireAuthentication")] + partial class Add_RequireAuthentication + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.21") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.ClientSecret", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Expiration") + .HasColumnType("datetime2"); + + b.Property("TenantName") + .IsRequired() + .HasColumnType("nvarchar(100)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.HasKey("Id"); + + b.HasIndex("TenantName"); + + b.ToTable("ClientSecrets"); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Config", b => + { + b.Property("TenantName") + .HasColumnType("nvarchar(450)"); + + b.Property("EnableTracing") + .HasColumnType("bit"); + + b.Property("KeepAliveInterval") + .HasColumnType("time"); + + b.Property("ReconnectMaximumDelay") + .HasColumnType("time"); + + b.Property("ReconnectMinimumDelay") + .HasColumnType("time"); + + b.HasKey("TenantName"); + + b.ToTable("Configs"); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Connection", b => + { + b.Property("Id") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ConnectTime") + .HasColumnType("datetimeoffset"); + + b.Property("DisconnectTime") + .HasColumnType("datetimeoffset"); + + b.Property("LastSeenTime") + .HasColumnType("datetimeoffset"); + + b.Property("OriginId") + .HasColumnType("uniqueidentifier"); + + b.Property("RemoteIpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("TenantName") + .IsRequired() + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("OriginId"); + + b.HasIndex("TenantName"); + + b.ToTable("Connections"); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Origin", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("LastSeenTime") + .HasColumnType("datetimeoffset"); + + b.Property("ShutdownTime") + .HasColumnType("datetimeoffset"); + + b.Property("StartupTime") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.ToTable("Origins"); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Request", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Aborted") + .HasColumnType("bit"); + + b.Property("Errored") + .HasColumnType("bit"); + + b.Property("Expired") + .HasColumnType("bit"); + + b.Property("Failed") + .HasColumnType("bit"); + + b.Property("HttpMethod") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("HttpStatusCode") + .HasColumnType("int"); + + b.Property("RequestBodySize") + .HasColumnType("bigint"); + + b.Property("RequestDate") + .HasColumnType("datetimeoffset"); + + b.Property("RequestDuration") + .HasColumnType("bigint"); + + b.Property("RequestId") + .HasColumnType("uniqueidentifier"); + + b.Property("RequestOriginalBodySize") + .HasColumnType("bigint"); + + b.Property("RequestUrl") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ResponseBodySize") + .HasColumnType("bigint"); + + b.Property("ResponseOriginalBodySize") + .HasColumnType("bigint"); + + b.Property("Target") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("TenantName") + .IsRequired() + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("TenantName"); + + b.ToTable("Requests"); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Tenant", b => + { + b.Property("NormalizedName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ConfigTenantName") + .HasColumnType("nvarchar(450)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RequireAuthentication") + .HasColumnType("bit"); + + b.HasKey("NormalizedName"); + + b.HasIndex("ConfigTenantName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Tenants"); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.ClientSecret", b => + { + b.HasOne("Thinktecture.Relay.Server.Persistence.Models.Tenant", null) + .WithMany("ClientSecrets") + .HasForeignKey("TenantName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Connection", b => + { + b.HasOne("Thinktecture.Relay.Server.Persistence.Models.Origin", null) + .WithMany("Connections") + .HasForeignKey("OriginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Thinktecture.Relay.Server.Persistence.Models.Tenant", null) + .WithMany("Connections") + .HasForeignKey("TenantName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Request", b => + { + b.HasOne("Thinktecture.Relay.Server.Persistence.Models.Tenant", null) + .WithMany("Requests") + .HasForeignKey("TenantName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Tenant", b => + { + b.HasOne("Thinktecture.Relay.Server.Persistence.Models.Config", "Config") + .WithMany() + .HasForeignKey("ConfigTenantName"); + + b.Navigation("Config"); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Origin", b => + { + b.Navigation("Connections"); + }); + + modelBuilder.Entity("Thinktecture.Relay.Server.Persistence.Models.Tenant", b => + { + b.Navigation("ClientSecrets"); + + b.Navigation("Connections"); + + b.Navigation("Requests"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.SqlServer/Migrations/ConfigurationDb/20231109143959_Add_RequireAuthentication.cs b/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.SqlServer/Migrations/ConfigurationDb/20231109143959_Add_RequireAuthentication.cs new file mode 100644 index 000000000..93880a3ec --- /dev/null +++ b/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.SqlServer/Migrations/ConfigurationDb/20231109143959_Add_RequireAuthentication.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.SqlServer.Migrations.ConfigurationDb +{ + public partial class Add_RequireAuthentication : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "RequireAuthentication", + table: "Tenants", + type: "bit", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "RequireAuthentication", + table: "Tenants"); + } + } +} diff --git a/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.SqlServer/Migrations/ConfigurationDb/RelayDbContextModelSnapshot.cs b/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.SqlServer/Migrations/ConfigurationDb/RelayDbContextModelSnapshot.cs index 298d69a61..8630cbffd 100644 --- a/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.SqlServer/Migrations/ConfigurationDb/RelayDbContextModelSnapshot.cs +++ b/src/Thinktecture.Relay.Server.Persistence.EntityFrameworkCore.SqlServer/Migrations/ConfigurationDb/RelayDbContextModelSnapshot.cs @@ -217,6 +217,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(100) .HasColumnType("nvarchar(100)"); + b.Property("RequireAuthentication") + .HasColumnType("bit"); + b.HasKey("NormalizedName"); b.HasIndex("ConfigTenantName"); diff --git a/src/Thinktecture.Relay.Server/Controllers/DiscoveryDocumentController.cs b/src/Thinktecture.Relay.Server/Controllers/DiscoveryDocumentController.cs index 7c85233b1..64dfe14d2 100644 --- a/src/Thinktecture.Relay.Server/Controllers/DiscoveryDocumentController.cs +++ b/src/Thinktecture.Relay.Server/Controllers/DiscoveryDocumentController.cs @@ -34,6 +34,6 @@ public DiscoveryDocumentController(ILogger logger) public IActionResult GetDiscoveryDocument([FromServices] DiscoveryDocumentBuilder documentBuilder) { LogReturnDiscoveryDocument(); - return Ok(documentBuilder.BuildDiscoveryDocument(Request)); + return Ok(documentBuilder.Build(Request)); } } diff --git a/src/Thinktecture.Relay.Server/Middleware/RelayMiddleware.cs b/src/Thinktecture.Relay.Server/Middleware/RelayMiddleware.cs index e5f816469..02c8e4115 100644 --- a/src/Thinktecture.Relay.Server/Middleware/RelayMiddleware.cs +++ b/src/Thinktecture.Relay.Server/Middleware/RelayMiddleware.cs @@ -3,14 +3,17 @@ using System.IO; using System.Linq; using System.Net; +using System.Security.Claims; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.JsonWebTokens; using Thinktecture.Relay.Acknowledgement; using Thinktecture.Relay.Server.Diagnostics; using Thinktecture.Relay.Server.Interceptor; @@ -42,6 +45,7 @@ public partial class RelayMiddleware : IMiddl private readonly IRelayRequestLogger _relayRequestLogger; private readonly IDistributedCache _cache; private readonly RelayServerOptions _relayServerOptions; + private readonly JwtBearerOptions? _jwtBearerOptions; private readonly IRequestCoordinator _requestCoordinator; private readonly IRelayClientRequestFactory _requestFactory; private readonly IResponseCoordinator _responseCoordinator; @@ -71,6 +75,7 @@ public partial class RelayMiddleware : IMiddl /// /// An . /// An implementation of + /// An . public RelayMiddleware(ILogger> logger, IRelayClientRequestFactory requestFactory, ConnectorRegistry connectorRegistry, ITenantService tenantService, IBodyStore bodyStore, IRequestCoordinator requestCoordinator, @@ -79,7 +84,8 @@ public RelayMiddleware(ILogger relayServerOptions, IEnumerable> clientRequestInterceptors, IEnumerable> targetResponseInterceptors, - IRelayRequestLogger relayRequestLogger, IDistributedCache cache) + IRelayRequestLogger relayRequestLogger, IDistributedCache cache, + IOptionsSnapshot? jwtBearerOptions) { if (relayServerOptions == null) throw new ArgumentNullException(nameof(relayServerOptions)); if (tenantTransport == null) throw new ArgumentNullException(nameof(tenantTransport)); @@ -102,6 +108,8 @@ public RelayMiddleware(ILogger c.DisconnectTime == null) ?? false + HasActiveConnections = tenant.Connections?.Any(c => c.DisconnectTime == null) ?? false, + RequireAuthentication = tenant.RequireAuthentication, }; } @@ -414,17 +441,19 @@ public static TenantState FromSpan(Span span) { Unknown = span[0] == byte.MaxValue, HasActiveConnections = span[1] == byte.MaxValue, - TenantName = Encoding.Unicode.GetString(span[2..]), + RequireAuthentication = span[2] == byte.MaxValue, + TenantName = Encoding.Unicode.GetString(span[3..]), }; public static Span AsSpan(TenantState tenantState) { var tenantName = Encoding.Unicode.GetBytes(tenantState.TenantName); - var buffer = new byte[2 + tenantName.Length]; + var buffer = new byte[3 + tenantName.Length]; buffer[0] = tenantState.Unknown ? byte.MaxValue : byte.MinValue; buffer[1] = tenantState.HasActiveConnections ? byte.MaxValue : byte.MinValue; - tenantName.CopyTo(buffer, 2); + buffer[2] = tenantState.RequireAuthentication ? byte.MaxValue : byte.MinValue; + tenantName.CopyTo(buffer, 3); return buffer; } diff --git a/src/Thinktecture.Relay.Server/Services/DiscoveryDocumentBuilder.cs b/src/Thinktecture.Relay.Server/Services/DiscoveryDocumentBuilder.cs index 302bf026c..21f604095 100644 --- a/src/Thinktecture.Relay.Server/Services/DiscoveryDocumentBuilder.cs +++ b/src/Thinktecture.Relay.Server/Services/DiscoveryDocumentBuilder.cs @@ -6,6 +6,7 @@ namespace Thinktecture.Relay.Server.Services; +// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global /// /// An implementation that creates a discovery document. /// @@ -32,7 +33,7 @@ public DiscoveryDocumentBuilder(IServiceProvider serviceProvider, IOptions /// A . /// A new instance of the discovery document. - public DiscoveryDocument BuildDiscoveryDocument(HttpRequest request) + public DiscoveryDocument Build(HttpRequest request) { var baseUri = BuildBaseUri(request);