diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..0470bf3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,27 @@ +name: Build + +on: + push: + branches: [main, master] + pull_request: + +jobs: + core: + name: ASP.NET Core sample (Linux) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - run: dotnet build src/CastleDemo/CastleDemo.csproj -c Release + + framework: + name: .NET Framework sample (Windows) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - run: dotnet build src/CastleDemo.Framework/CastleDemo.Framework.csproj -c Release diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d0e138e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# Build and run the ASP.NET Core CastleDemo sample. +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY src/CastleDemo/CastleDemo.csproj CastleDemo/ +RUN dotnet restore CastleDemo/CastleDemo.csproj +COPY src/CastleDemo/ CastleDemo/ +RUN dotnet publish CastleDemo/CastleDemo.csproj -c Release -o /app/publish + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime +WORKDIR /app +COPY --from=build /app/publish . +ENV ASPNETCORE_URLS=http://+:8080 +EXPOSE 8080 +ENTRYPOINT ["dotnet", "CastleDemo.dll"] diff --git a/README.md b/README.md index ce02278..ddc14bc 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,70 @@ # Castle .NET Example -This is an example of integrating Castle with a standard ASP.NET Core Razor Pages application. +Examples of integrating Castle into .NET applications. The repository contains two samples: -## Foundation +| Sample | Path | Target | Notes | +| --- | --- | --- | --- | +| ASP.NET Core Razor Pages | `src/CastleDemo` | `net8.0` | The main, cross-platform sample. | +| .NET Framework console | `src/CastleDemo.Framework` | `net48` | Exercises the `System.Web` code path; builds and runs on Windows. | -The example is almost fully from the default Visual Studio template for a Razor Pages app with _Invididual user accounts_ for authentication. +## ASP.NET Core sample (`src/CastleDemo`) -### Framework +A Razor Pages app based on the default template with _Individual user accounts_ for +authentication. The home page lists the available demos (see `Demos/DemoCatalog.cs`), +each of which triggers a Castle API call. -NET Core 6.0 +### Highlights -### Template modifications - -- The database runs in-memory +- Targets `net8.0` and uses minimal hosting (`Program.cs`); there is no `Startup.cs`. +- The database runs in-memory: ```csharp -services.AddDbContext(options => - options.UseInMemoryDatabase("CastleDemo") -); +builder.Services.AddDbContext(options => + options.UseInMemoryDatabase("CastleDemo")); ``` -- Visual Studio scaffolding has been used to create a Login page we can alter, as described by Microsoft [here](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/scaffold-identity). -- We use [Microsoft.VisualStudio.Threading](https://www.nuget.org/packages/Microsoft.VisualStudio.Threading/) to get access to the `Forget()` extension method, which is useful for fire-and-forget calls to async methods, like **Track** or **Authenticate** in Monitor mode. +- Fire-and-forget Castle calls (e.g. **Track** / **Authenticate** in monitor mode) use a + discard (`_ = client.Track(...)`) rather than an external dependency. +- Client-side fingerprinting and secure mode are wired up in `Pages/Shared/_Layout.cshtml`. + +All Castle-related changes are marked with comments containing the word _Castle_ for easy +searching, and primarily affect: + +- `Program.cs` — service registration +- `Areas/Identity/Pages/Account/Login.cshtml.cs` and the `Risk` / `Filter` / `Log` pages — SDK calls +- `Pages/Shared/_Layout.cshtml` — client-side Castle +- `appsettings.json` — your Castle API secret and App ID -## The integration +### Run -The example application applies the steps described for the [Castle Baseline Integration](https://castle.io/docs/baseline), with the addition of [secure requests](https://castle.io/docs/securing_requests). All Castle-related changes are marked with comments containing the word _Castle_ for easy searching, and affect the following files: +```bash +dotnet run --project src/CastleDemo/CastleDemo.csproj +``` + +Set your credentials in `appsettings.json` (or via environment / user secrets): -- `Startup.cs` Ioc -- `Areas/Identity/Pages/Account/Login.cshtml.cs` Castle SDK calls -- `Pages/Shared/_Layout.cshtml` Client-side Castle -- `appsettings.json` Your Castle API secret and App ID +```json +"Castle": { + "ApiSecret": "YOUR API SECRET", + "AppId": "YOUR APP ID" +} +``` -## Development testing +### Docker -# nuget add pathtonugetpackage.nupkg -source sourceDir +```bash +docker build -t castle-dotnet-example . +docker run -p 8080:8080 -e Castle__ApiSecret=YOUR_API_SECRET castle-dotnet-example +``` + +## .NET Framework sample (`src/CastleDemo.Framework`) + +A minimal `net48` console app that adapts a `System.Web` request to +`Castle.Context.FromHttpRequest(HttpRequestBase)` and sends a Risk request (with +`DoNotTrack` enabled so it runs without a real secret). It builds and runs on Windows: + +```bash +dotnet run --project src/CastleDemo.Framework/CastleDemo.Framework.csproj +``` -# dotnet add package Castle.Sdk -s sourceDir +This sample requires `Castle.Sdk` **2.4.0** or newer (the first version with a `net48` target). diff --git a/src/CastleDemo.Framework/CastleDemo.Framework.csproj b/src/CastleDemo.Framework/CastleDemo.Framework.csproj new file mode 100644 index 0000000..c2c6800 --- /dev/null +++ b/src/CastleDemo.Framework/CastleDemo.Framework.csproj @@ -0,0 +1,14 @@ + + + Exe + net48 + latest + CastleDemo.Framework + CastleDemo.Framework + + + + + + + diff --git a/src/CastleDemo.Framework/DemoHttpRequest.cs b/src/CastleDemo.Framework/DemoHttpRequest.cs new file mode 100644 index 0000000..789204b --- /dev/null +++ b/src/CastleDemo.Framework/DemoHttpRequest.cs @@ -0,0 +1,29 @@ +using System.Collections.Specialized; +using System.Web; + +namespace CastleDemo.Framework +{ + /// + /// A tiny stand-in so the console sample can build a + /// request context the same way an ASP.NET (System.Web) application would. + /// + internal sealed class DemoHttpRequest : HttpRequestBase + { + private readonly NameValueCollection _headers; + private readonly HttpCookieCollection _cookies; + private readonly string _userHostAddress; + + public DemoHttpRequest(NameValueCollection headers, HttpCookieCollection cookies, string userHostAddress) + { + _headers = headers; + _cookies = cookies; + _userHostAddress = userHostAddress; + } + + public override NameValueCollection Headers => _headers; + + public override HttpCookieCollection Cookies => _cookies; + + public override string UserHostAddress => _userHostAddress; + } +} diff --git a/src/CastleDemo.Framework/Program.cs b/src/CastleDemo.Framework/Program.cs new file mode 100644 index 0000000..d928bdc --- /dev/null +++ b/src/CastleDemo.Framework/Program.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Threading.Tasks; +using System.Web; +using Castle; +using Castle.Config; +using Castle.Messages.Requests; + +namespace CastleDemo.Framework +{ + /// + /// Minimal .NET Framework 4.8 sample. It exercises the System.Web code path of the + /// SDK — — which only builds and + /// runs on Windows. The Castle call uses DoNotTrack so the sample runs without a + /// real API secret or network access. + /// + internal static class Program + { + private static async Task Main() + { + var apiSecret = Environment.GetEnvironmentVariable("CASTLE_API_SECRET") ?? "YOUR API SECRET"; + + var client = new CastleClient(new CastleConfiguration(apiSecret) + { + DoNotTrack = true + }); + + var headers = new NameValueCollection + { + ["X-Castle-Client-ID"] = "the-client-id-from-castle-js", + ["X-Forwarded-For"] = "1.2.3.4" + }; + + // Adapt a System.Web request to the SDK's .NET Framework context overload. + HttpRequestBase request = new DemoHttpRequest(headers, new HttpCookieCollection(), "1.2.3.4"); + var context = Context.FromHttpRequest(request); + + var actionRequest = new ActionRequest + { + Event = "$login.succeeded", + UserId = "user-123", + UserTraits = new Dictionary + { + ["email"] = "user@example.com" + }, + Context = context + }; + + var verdict = await client.Risk(actionRequest); + + Console.WriteLine("Castle .NET Framework sample"); + Console.WriteLine($" Resolved client id: {context.ClientId}"); + Console.WriteLine($" Resolved ip: {context.Ip}"); + Console.WriteLine($" Verdict action: {verdict?.Action}"); + Console.WriteLine($" Failover: {verdict?.Failover}"); + } + } +} diff --git a/src/CastleDemo.sln b/src/CastleDemo.sln index d97f8a2..9b910bd 100644 --- a/src/CastleDemo.sln +++ b/src/CastleDemo.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 15.0.28307.438 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CastleDemo", "CastleDemo\CastleDemo.csproj", "{155A2766-3EE6-42EC-9A73-A835C190E89E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CastleDemo.Framework", "CastleDemo.Framework\CastleDemo.Framework.csproj", "{A9116005-AC8D-4F84-8375-0C41D9F5EF3D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {155A2766-3EE6-42EC-9A73-A835C190E89E}.Debug|Any CPU.Build.0 = Debug|Any CPU {155A2766-3EE6-42EC-9A73-A835C190E89E}.Release|Any CPU.ActiveCfg = Release|Any CPU {155A2766-3EE6-42EC-9A73-A835C190E89E}.Release|Any CPU.Build.0 = Release|Any CPU + {A9116005-AC8D-4F84-8375-0C41D9F5EF3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9116005-AC8D-4F84-8375-0C41D9F5EF3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9116005-AC8D-4F84-8375-0C41D9F5EF3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9116005-AC8D-4F84-8375-0C41D9F5EF3D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/CastleDemo/Areas/Identity/Pages/Account/Login.cshtml.cs b/src/CastleDemo/Areas/Identity/Pages/Account/Login.cshtml.cs index ba0ff19..b1efe83 100644 --- a/src/CastleDemo/Areas/Identity/Pages/Account/Login.cshtml.cs +++ b/src/CastleDemo/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -11,7 +11,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.Threading; namespace CastleDemo.Areas.Identity.Pages.Account { @@ -90,8 +89,8 @@ public async Task OnPostAsync(string returnUrl = null) { _logger.LogInformation("User logged in."); - // Castle Authenticate $login.succeeded - _castleClient.Authenticate(CreateCastleActionRequest("$login.succeeded")).Forget(); + // Castle Authenticate $login.succeeded (fire-and-forget) + _ = _castleClient.Authenticate(CreateCastleActionRequest("$login.succeeded")); return LocalRedirect(returnUrl); } @@ -106,8 +105,8 @@ public async Task OnPostAsync(string returnUrl = null) } else { - // Castle Track $login.failed - _castleClient.Track(CreateCastleActionRequest("$login.failed")).Forget(); + // Castle Track $login.failed (fire-and-forget) + _ = _castleClient.Track(CreateCastleActionRequest("$login.failed")); ModelState.AddModelError(string.Empty, "Invalid login attempt."); return Page(); diff --git a/src/CastleDemo/Areas/Identity/Pages/Filter/Filter.cshtml.cs b/src/CastleDemo/Areas/Identity/Pages/Filter/Filter.cshtml.cs index 1aa72cb..ccfd37d 100644 --- a/src/CastleDemo/Areas/Identity/Pages/Filter/Filter.cshtml.cs +++ b/src/CastleDemo/Areas/Identity/Pages/Filter/Filter.cshtml.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Castle.Messages.Requests; using System.Collections.Generic; -using Microsoft.VisualStudio.Threading; using System.Threading.Tasks; using System; using Castle.Infrastructure.Exceptions; diff --git a/src/CastleDemo/Areas/Identity/Pages/Log/Log.cshtml.cs b/src/CastleDemo/Areas/Identity/Pages/Log/Log.cshtml.cs index 5da9010..03431e5 100644 --- a/src/CastleDemo/Areas/Identity/Pages/Log/Log.cshtml.cs +++ b/src/CastleDemo/Areas/Identity/Pages/Log/Log.cshtml.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Castle.Messages.Requests; using System.Collections.Generic; -using Microsoft.VisualStudio.Threading; using System.Threading.Tasks; using System; diff --git a/src/CastleDemo/Areas/Identity/Pages/Risk/Risk.cshtml.cs b/src/CastleDemo/Areas/Identity/Pages/Risk/Risk.cshtml.cs index c29a1bf..1bb32e5 100644 --- a/src/CastleDemo/Areas/Identity/Pages/Risk/Risk.cshtml.cs +++ b/src/CastleDemo/Areas/Identity/Pages/Risk/Risk.cshtml.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Castle.Messages.Requests; using System.Collections.Generic; -using Microsoft.VisualStudio.Threading; using System.Threading.Tasks; using System; using Castle.Infrastructure.Exceptions; diff --git a/src/CastleDemo/Areas/Identity/Pages/_ViewImports.cshtml b/src/CastleDemo/Areas/Identity/Pages/_ViewImports.cshtml index 71d0bf6..0138dd5 100644 --- a/src/CastleDemo/Areas/Identity/Pages/_ViewImports.cshtml +++ b/src/CastleDemo/Areas/Identity/Pages/_ViewImports.cshtml @@ -1,5 +1,4 @@ @using Microsoft.AspNetCore.Identity @using CastleDemo.Areas.Identity -@using Microsoft.AspNetCore.Identity @namespace CastleDemo.Areas.Identity.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/CastleDemo/CastleDemo.csproj b/src/CastleDemo/CastleDemo.csproj index fc8d3dc..0e64a0e 100644 --- a/src/CastleDemo/CastleDemo.csproj +++ b/src/CastleDemo/CastleDemo.csproj @@ -1,20 +1,15 @@ - net9.0 + net8.0 + enable + disable aspnet-CastleDemo-708B0CB0-0EAA-415A-87AA-60C6A98D774C - - - - - - - - - - - - + + + + + - \ No newline at end of file + diff --git a/src/CastleDemo/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs b/src/CastleDemo/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs deleted file mode 100644 index 0482a08..0000000 --- a/src/CastleDemo/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs +++ /dev/null @@ -1,236 +0,0 @@ -// -using System; -using CastleDemo.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace CastleDemo.Data.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("00000000000000_CreateIdentitySchema")] - partial class CreateIdentitySchema - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.0-preview1") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken(); - - b.Property("Name") - .HasMaxLength(256); - - b.Property("NormalizedName") - .HasMaxLength(256); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasName("RoleNameIndex") - .HasFilter("[NormalizedName] IS NOT NULL"); - - b.ToTable("AspNetRoles"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("ClaimType"); - - b.Property("ClaimValue"); - - b.Property("RoleId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("AccessFailedCount"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken(); - - b.Property("Email") - .HasMaxLength(256); - - b.Property("EmailConfirmed"); - - b.Property("LockoutEnabled"); - - b.Property("LockoutEnd"); - - b.Property("NormalizedEmail") - .HasMaxLength(256); - - b.Property("NormalizedUserName") - .HasMaxLength(256); - - b.Property("PasswordHash"); - - b.Property("PhoneNumber"); - - b.Property("PhoneNumberConfirmed"); - - b.Property("SecurityStamp"); - - b.Property("TwoFactorEnabled"); - - b.Property("UserName") - .HasMaxLength(256); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasName("UserNameIndex") - .HasFilter("[NormalizedUserName] IS NOT NULL"); - - b.ToTable("AspNetUsers"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("ClaimType"); - - b.Property("ClaimValue"); - - b.Property("UserId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasMaxLength(128); - - b.Property("ProviderKey") - .HasMaxLength(128); - - b.Property("ProviderDisplayName"); - - b.Property("UserId") - .IsRequired(); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId"); - - b.Property("RoleId"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId"); - - b.Property("LoginProvider") - .HasMaxLength(128); - - b.Property("Name") - .HasMaxLength(128); - - b.Property("Value"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/CastleDemo/Data/Migrations/00000000000000_CreateIdentitySchema.cs b/src/CastleDemo/Data/Migrations/00000000000000_CreateIdentitySchema.cs deleted file mode 100644 index e6b3f84..0000000 --- a/src/CastleDemo/Data/Migrations/00000000000000_CreateIdentitySchema.cs +++ /dev/null @@ -1,220 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace CastleDemo.Data.Migrations -{ - public partial class CreateIdentitySchema : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "AspNetRoles", - columns: table => new - { - Id = table.Column(nullable: false), - Name = table.Column(maxLength: 256, nullable: true), - NormalizedName = table.Column(maxLength: 256, nullable: true), - ConcurrencyStamp = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetRoles", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "AspNetUsers", - columns: table => new - { - Id = table.Column(nullable: false), - UserName = table.Column(maxLength: 256, nullable: true), - NormalizedUserName = table.Column(maxLength: 256, nullable: true), - Email = table.Column(maxLength: 256, nullable: true), - NormalizedEmail = table.Column(maxLength: 256, nullable: true), - EmailConfirmed = table.Column(nullable: false), - PasswordHash = table.Column(nullable: true), - SecurityStamp = table.Column(nullable: true), - ConcurrencyStamp = table.Column(nullable: true), - PhoneNumber = table.Column(nullable: true), - PhoneNumberConfirmed = table.Column(nullable: false), - TwoFactorEnabled = table.Column(nullable: false), - LockoutEnd = table.Column(nullable: true), - LockoutEnabled = table.Column(nullable: false), - AccessFailedCount = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUsers", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "AspNetRoleClaims", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), - RoleId = table.Column(nullable: false), - ClaimType = table.Column(nullable: true), - ClaimValue = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); - table.ForeignKey( - name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", - column: x => x.RoleId, - principalTable: "AspNetRoles", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AspNetUserClaims", - columns: table => new - { - Id = table.Column(nullable: false) - .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), - UserId = table.Column(nullable: false), - ClaimType = table.Column(nullable: true), - ClaimValue = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); - table.ForeignKey( - name: "FK_AspNetUserClaims_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AspNetUserLogins", - columns: table => new - { - LoginProvider = table.Column(maxLength: 128, nullable: false), - ProviderKey = table.Column(maxLength: 128, nullable: false), - ProviderDisplayName = table.Column(nullable: true), - UserId = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); - table.ForeignKey( - name: "FK_AspNetUserLogins_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AspNetUserRoles", - columns: table => new - { - UserId = table.Column(nullable: false), - RoleId = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); - table.ForeignKey( - name: "FK_AspNetUserRoles_AspNetRoles_RoleId", - column: x => x.RoleId, - principalTable: "AspNetRoles", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_AspNetUserRoles_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AspNetUserTokens", - columns: table => new - { - UserId = table.Column(nullable: false), - LoginProvider = table.Column(maxLength: 128, nullable: false), - Name = table.Column(maxLength: 128, nullable: false), - Value = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); - table.ForeignKey( - name: "FK_AspNetUserTokens_AspNetUsers_UserId", - column: x => x.UserId, - principalTable: "AspNetUsers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_AspNetRoleClaims_RoleId", - table: "AspNetRoleClaims", - column: "RoleId"); - - migrationBuilder.CreateIndex( - name: "RoleNameIndex", - table: "AspNetRoles", - column: "NormalizedName", - unique: true, - filter: "[NormalizedName] IS NOT NULL"); - - migrationBuilder.CreateIndex( - name: "IX_AspNetUserClaims_UserId", - table: "AspNetUserClaims", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_AspNetUserLogins_UserId", - table: "AspNetUserLogins", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_AspNetUserRoles_RoleId", - table: "AspNetUserRoles", - column: "RoleId"); - - migrationBuilder.CreateIndex( - name: "EmailIndex", - table: "AspNetUsers", - column: "NormalizedEmail"); - - migrationBuilder.CreateIndex( - name: "UserNameIndex", - table: "AspNetUsers", - column: "NormalizedUserName", - unique: true, - filter: "[NormalizedUserName] IS NOT NULL"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AspNetRoleClaims"); - - migrationBuilder.DropTable( - name: "AspNetUserClaims"); - - migrationBuilder.DropTable( - name: "AspNetUserLogins"); - - migrationBuilder.DropTable( - name: "AspNetUserRoles"); - - migrationBuilder.DropTable( - name: "AspNetUserTokens"); - - migrationBuilder.DropTable( - name: "AspNetRoles"); - - migrationBuilder.DropTable( - name: "AspNetUsers"); - } - } -} diff --git a/src/CastleDemo/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/src/CastleDemo/Data/Migrations/ApplicationDbContextModelSnapshot.cs deleted file mode 100644 index ca45eaf..0000000 --- a/src/CastleDemo/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ /dev/null @@ -1,234 +0,0 @@ -// -using System; -using CastleDemo.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace CastleDemo.Data.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - partial class ApplicationDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.0-preview1") - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken(); - - b.Property("Name") - .HasMaxLength(256); - - b.Property("NormalizedName") - .HasMaxLength(256); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasName("RoleNameIndex") - .HasFilter("[NormalizedName] IS NOT NULL"); - - b.ToTable("AspNetRoles"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("ClaimType"); - - b.Property("ClaimValue"); - - b.Property("RoleId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => - { - b.Property("Id") - .ValueGeneratedOnAdd(); - - b.Property("AccessFailedCount"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken(); - - b.Property("Email") - .HasMaxLength(256); - - b.Property("EmailConfirmed"); - - b.Property("LockoutEnabled"); - - b.Property("LockoutEnd"); - - b.Property("NormalizedEmail") - .HasMaxLength(256); - - b.Property("NormalizedUserName") - .HasMaxLength(256); - - b.Property("PasswordHash"); - - b.Property("PhoneNumber"); - - b.Property("PhoneNumberConfirmed"); - - b.Property("SecurityStamp"); - - b.Property("TwoFactorEnabled"); - - b.Property("UserName") - .HasMaxLength(256); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasName("UserNameIndex") - .HasFilter("[NormalizedUserName] IS NOT NULL"); - - b.ToTable("AspNetUsers"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("ClaimType"); - - b.Property("ClaimValue"); - - b.Property("UserId") - .IsRequired(); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasMaxLength(128); - - b.Property("ProviderKey") - .HasMaxLength(128); - - b.Property("ProviderDisplayName"); - - b.Property("UserId") - .IsRequired(); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId"); - - b.Property("RoleId"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId"); - - b.Property("LoginProvider") - .HasMaxLength(128); - - b.Property("Name") - .HasMaxLength(128); - - b.Property("Value"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/CastleDemo/Demos/DemoCatalog.cs b/src/CastleDemo/Demos/DemoCatalog.cs new file mode 100644 index 0000000..eea7958 --- /dev/null +++ b/src/CastleDemo/Demos/DemoCatalog.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; + +namespace CastleDemo.Demos +{ + /// + /// A single Castle demo scenario surfaced on the home page. + /// + public class DemoInfo + { + public string Title { get; set; } + + /// The Castle API used by the demo (Risk, Filter, Log, ...). + public string Api { get; set; } + + /// The Castle event/type the demo sends. + public string Event { get; set; } + + public string Blurb { get; set; } + + /// Razor Page path used with asp-page. + public string Page { get; set; } + + /// Razor Page area used with asp-area (empty for the root area). + public string Area { get; set; } + } + + /// + /// Central registry of the demos shipped with this sample so they can be + /// listed in one place and linked from the home page. + /// + public static class DemoCatalog + { + public static readonly IReadOnlyList Demos = new List + { + new DemoInfo + { + Title = "Login", + Api = "Authenticate / Track", + Event = "$login.succeeded / $login.failed", + Blurb = "Sign in to send an Authenticate request on success and a Track request on failure.", + Area = "Identity", + Page = "/Account/Login" + }, + new DemoInfo + { + Title = "Risk", + Api = "Risk", + Event = "$profile_update", + Blurb = "Send a synchronous Risk request and inspect the verdict.", + Area = "Identity", + Page = "/Risk/Risk" + }, + new DemoInfo + { + Title = "Filter", + Api = "Filter", + Event = "$custom", + Blurb = "Evaluate a pre-authentication event such as a custom abuse signal.", + Area = "Identity", + Page = "/Filter/Filter" + }, + new DemoInfo + { + Title = "Log", + Api = "Log", + Event = "$challenge", + Blurb = "Record an event for monitoring without affecting a verdict.", + Area = "Identity", + Page = "/Log/Log" + } + }; + } +} diff --git a/src/CastleDemo/Pages/Index.cshtml b/src/CastleDemo/Pages/Index.cshtml index b5f0c15..80bbf2a 100644 --- a/src/CastleDemo/Pages/Index.cshtml +++ b/src/CastleDemo/Pages/Index.cshtml @@ -1,10 +1,26 @@ @page @model IndexModel @{ - ViewData["Title"] = "Home page"; + ViewData["Title"] = "Castle .NET demos"; }
-

Welcome

-

Learn about building Web apps with ASP.NET Core.

+

Castle .NET SDK demo

+

Each card below triggers a Castle API call so you can see the SDK in action.

+
+ +
+ @foreach (var demo in Model.Demos) + { +
+
+
+
@demo.Title
+
@demo.Api — @demo.Event
+

@demo.Blurb

+ Open demo +
+
+
+ }
diff --git a/src/CastleDemo/Pages/Index.cshtml.cs b/src/CastleDemo/Pages/Index.cshtml.cs index a1562c7..fec9cd2 100644 --- a/src/CastleDemo/Pages/Index.cshtml.cs +++ b/src/CastleDemo/Pages/Index.cshtml.cs @@ -1,17 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using CastleDemo.Demos; using Microsoft.AspNetCore.Mvc.RazorPages; namespace CastleDemo.Pages { public class IndexModel : PageModel { + public IReadOnlyList Demos => DemoCatalog.Demos; + public void OnGet() { - } } } diff --git a/src/CastleDemo/Program.cs b/src/CastleDemo/Program.cs index bb8553a..e9e89d6 100644 --- a/src/CastleDemo/Program.cs +++ b/src/CastleDemo/Program.cs @@ -1,24 +1,50 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace CastleDemo +using Castle; +using Castle.Config; +using CastleDemo.Data; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.Configure(options => +{ + options.CheckConsentNeeded = _ => true; + options.MinimumSameSitePolicy = SameSiteMode.None; +}); + +builder.Services.AddDbContext(options => + options.UseInMemoryDatabase("CastleDemo")); + +builder.Services.AddDefaultIdentity() + .AddEntityFrameworkStores(); + +builder.Services.AddRazorPages(); + +// Castle: register a single client from the configured API secret. +builder.Services.AddSingleton(new CastleClient( + new CastleConfiguration(builder.Configuration["Castle:ApiSecret"]))); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); +} +else { - public class Program - { - public static void Main(string[] args) - { - CreateWebHostBuilder(args).Build().Run(); - } - - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup(); - } + app.UseExceptionHandler("/Error"); + app.UseHsts(); } + +app.UseHttpsRedirection(); +app.UseStaticFiles(); +app.UseCookiePolicy(); + +app.UseRouting(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapRazorPages(); + +app.Run(); diff --git a/src/CastleDemo/ScaffoldingReadme.txt b/src/CastleDemo/ScaffoldingReadme.txt deleted file mode 100644 index 980e996..0000000 --- a/src/CastleDemo/ScaffoldingReadme.txt +++ /dev/null @@ -1,18 +0,0 @@ -Support for ASP.NET Core Identity was added to your project -- The code for adding Identity to your project was generated under Areas/Identity. - -Configuration of the Identity related services can be found in the Areas/Identity/IdentityHostingStartup.cs file. - - -The generated UI requires support for static files. To add static files to your app: -1. Call app.UseStaticFiles() from your Configure method - -To use ASP.NET Core Identity you also need to enable authentication. To authentication to your app: -1. Call app.UseAuthentication() from your Configure method (after static files) - -The generated UI requires MVC. To add MVC to your app: -1. Call services.AddMvc() from your ConfigureServices method -2. Call app.UseMvc() from your Configure method (after authentication) - -Apps that use ASP.NET Core Identity should also use HTTPS. To enable HTTPS see https://go.microsoft.com/fwlink/?linkid=848054. - diff --git a/src/CastleDemo/Startup.cs b/src/CastleDemo/Startup.cs deleted file mode 100644 index e18d393..0000000 --- a/src/CastleDemo/Startup.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Castle; -using Castle.Config; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; -using CastleDemo.Data; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace CastleDemo -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - public void ConfigureServices(IServiceCollection services) - { - services.Configure(options => - { - options.CheckConsentNeeded = context => true; - options.MinimumSameSitePolicy = SameSiteMode.None; - }); - - services.AddDbContext(options => - options.UseInMemoryDatabase("CastleDemo") - ); - services.AddIdentityCore() - .AddDefaultUI() - .AddEntityFrameworkStores(); - - services.AddRazorPages(); - - services.AddSingleton(new CastleClient(new CastleConfiguration(Configuration["Castle:ApiSecret"]))); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - app.UseMigrationsEndPoint(); - } - else - { - app.UseExceptionHandler("/Error"); - app.UseHsts(); - } - - app.UseHttpsRedirection(); - app.UseStaticFiles(); - app.UseCookiePolicy(); - - app.UseRouting(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapRazorPages(); - }); - } - } -}