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);