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
16 changes: 7 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "9.0.x"
dotnet-version: "10.0.x"

- name: Restore
run: dotnet restore AuthService.sln
Expand Down Expand Up @@ -47,7 +47,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "9.0.x"
dotnet-version: "10.0.x"

# The codebase is consistently formatted today and nothing enforced it, so the first
# outside contribution was where drift would start.
Expand All @@ -63,7 +63,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "9.0.x"
dotnet-version: "10.0.x"

- name: Install dotnet-ef
run: |
Expand All @@ -80,12 +80,10 @@ jobs:
# matching migration is invisible under EnsureCreated and fails at runtime under
# Migrate, which is the failure mode issue #17 is about.
#
# It is a no-op until a set is committed, deliberately. Generating one needs the
# startup project to reference the migrations assembly so `dotnet ef` can load it, and
# the migrations assembly already references the startup project for the DbContext
# type — a cycle that cannot be broken while ApplicationDbContext lives in the
# application project. See docs/schema/README.md; the fix is a structural change, not
# a CI flag, and pretending otherwise here would just leave a job that always fails.
# The reference cycle that used to make generating a set impossible is gone —
# ApplicationDbContext now lives in AuthService.Data, which the migration assemblies
# reference and AuthService references back. `scripts/generate-migrations.sh` works.
# This stays a no-op only until someone runs it and commits the result.
- name: Migrations match the model
run: |
set -euo pipefail
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "9.0.x"
dotnet-version: "10.0.x"

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/publish-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ jobs:
with:
context: .
file: src/AuthService/Dockerfile
platforms: linux/amd64
# arm64 as well as amd64 so the image runs natively on Apple Silicon and on arm
# instances without emulation. The Dockerfile cross-compiles rather than building
# under QEMU, so the second platform costs little.
platforms: linux/amd64,linux/arm64
push: true
provenance: false
tags: |
Expand Down
6 changes: 6 additions & 0 deletions AuthService.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuthService", "src\AuthServ
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuthService.Tests", "tests\AuthService.Tests\AuthService.Tests.csproj", "{1F2E3D4C-5B6A-4978-8B12-3C4D5E6F7A8B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuthService.Data", "src\AuthService.Data\AuthService.Data.csproj", "{7C4E1A56-9D38-4B27-A150-2E6F8B0D4C79}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuthService.Migrations.PostgreSQL", "src\AuthService.Migrations.PostgreSQL\AuthService.Migrations.PostgreSQL.csproj", "{3A7C2D91-6E4B-4F58-9A21-7C5D8E1F2B34}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuthService.Migrations.SqlServer", "src\AuthService.Migrations.SqlServer\AuthService.Migrations.SqlServer.csproj", "{5B8D3E02-7F5C-4069-8B32-8D6E9F203C45}"
Expand All @@ -33,6 +35,10 @@ Global
{5B8D3E02-7F5C-4069-8B32-8D6E9F203C45}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5B8D3E02-7F5C-4069-8B32-8D6E9F203C45}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5B8D3E02-7F5C-4069-8B32-8D6E9F203C45}.Release|Any CPU.Build.0 = Release|Any CPU
{7C4E1A56-9D38-4B27-A150-2E6F8B0D4C79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C4E1A56-9D38-4B27-A150-2E6F8B0D4C79}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C4E1A56-9D38-4B27-A150-2E6F8B0D4C79}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7C4E1A56-9D38-4B27-A150-2E6F8B0D4C79}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ If you are unsure, open an issue before writing the code. That is cheaper for bo

## Getting set up

Requires the .NET 9 SDK, plus Docker if you want a real database.
Requires the .NET 10 SDK, plus Docker if you want a real database.

```bash
git clone https://github.com/konradcinkusz/authservice.git
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ beyond your own machine.

### Prerequisites

- [.NET 9 SDK](https://dotnet.microsoft.com/download)
- [.NET 10 SDK](https://dotnet.microsoft.com/download)
- PostgreSQL (default) or SQL Server
- Docker + Docker Compose (only needed for the quick start above)

Expand Down Expand Up @@ -202,7 +202,14 @@ schema. The service logs a warning on every startup where this happens.

Migrations are not committed, because one migration set cannot serve both PostgreSQL and SQL
Server — the generated DDL and the filtered-index expressions differ. Generate a set per
provider; a design-time factory is included so no database is needed:
provider; the projects are already wired up and a design-time factory is included, so no
database is needed:

```bash
scripts/generate-migrations.sh InitialCreate
```

or, one provider at a time:

```bash
DATABASE_PROVIDER=PostgreSQL \
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/DEVIATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Last reviewed in full on **2026-08-14**.
|---|---|---|---|
| 2026-08-14 | **No OTLP traces, metrics or logs.** Observability is the console logger, plus the structured audit log this service keeps for its own security events. | Anything beyond a single log line has to be reconstructed by hand | **To fix.** Less acute here than in a multi-service pipeline — this is one service and one database — but a sign-in that fails somewhere between OAuth callback, account linking and token issuance is exactly what a trace answers. |
| 2026-08-14 | **No orchestrator manifest** declaring this service's dependencies, health checks and wiring for local composition. | A consumer has to infer the topology from the docs | **Accepted.** This repository publishes an image and is deployed by whichever project consumes it (ADR 0001). Such a manifest describes a system; this is one component of somebody else's. The consuming project owns that declaration. |
| 2026-08-14 | **No committed migration set.** `Database:SchemaMode` defaults to `EnsureCreated`, which creates a schema and then never changes it. | Without migrations there is no upgrade path: a model change is silently not applied, and the app fails at runtime against a stale schema | **To fix, and blocked on a refactor.** The two migration assemblies exist; generating into them fails because `dotnet ef` loads the migrations assembly from the startup project's output, so `AuthService` must reference it — and it already references `AuthService`, for the `ApplicationDbContext` type. Breaking that cycle means moving the context into a class library. See [`docs/schema/README.md`](../schema/README.md). |
| 2026-08-14 | **No committed migration set.** `Database:SchemaMode` defaults to `EnsureCreated`, which creates a schema and then never changes it. | Without migrations there is no upgrade path: a model change is silently not applied, and the app fails at runtime against a stale schema | **To fix; no longer blocked.** The refactor landed on 2026-08-15 — `ApplicationDbContext` now lives in `AuthService.Data`, the migration assemblies reference it, and `AuthService` references them back, so `scripts/generate-migrations.sh` works. What remains is running it and committing the result. See [`docs/schema/README.md`](../schema/README.md). |
| 2026-08-14 | **Two providers, one model.** PostgreSQL and SQL Server are both supported, doubling the migration surface, and the SQL Server path has no integration coverage — the test suite runs against SQLite. | Untested support is a promise the project cannot keep | **Open question, not yet decided.** Issue #30 raises whether SQL Server support earns its keep. ADR 0003's "stay small" argues against it. Dropping it would unblock some of the row above. |
| 2026-08-14 | **`Program.cs` is ~500 lines and does its own service wiring** rather than delegating to composition extensions. | The file every change touches is the file hardest to review | **To fix.** Mechanical, and it has grown steadily — which is the argument for doing it soon rather than never. |
| 2026-08-14 | **`iss` is a bare string, not a URL.** Tokens carry `iss: "AuthService"` (or the deployment's override), so the discovery document reports that rather than the service's origin. | Consumers expecting the OIDC convention of a URL issuer have to be told otherwise | **Accepted.** Changing it invalidates every issued token and every consumer's `ValidIssuer`, for a cosmetic gain. ADR 0002 records the reasoning; the discovery document is self-consistent as it stands. |
Expand Down
2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ <h1>Auth, handled. Once.</h1>
<a class="btn secondary" href="https://github.com/konradcinkusz/authservice/blob/main/DEMO.md">Demo walkthrough</a>
</div>
<div class="badges">
<span class="badge">.NET 9</span>
<span class="badge">.NET 10</span>
<span class="badge">PostgreSQL / SQL Server</span>
<span class="badge">JWT</span>
<span class="badge">OAuth2 · Google / GitHub</span>
Expand Down
51 changes: 23 additions & 28 deletions docs/schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,15 @@ locally, which is what the wiring below supports.

## Generating migrations

Create a migrations project per provider, referencing the service project:
Both migration projects already exist and are wired up. Generate a set into each with:

```bash
dotnet new classlib -o src/AuthService.Migrations.PostgreSQL
dotnet add src/AuthService.Migrations.PostgreSQL reference src/AuthService/AuthService.csproj
dotnet sln add src/AuthService.Migrations.PostgreSQL

dotnet new classlib -o src/AuthService.Migrations.SqlServer
dotnet add src/AuthService.Migrations.SqlServer reference src/AuthService/AuthService.csproj
dotnet sln add src/AuthService.Migrations.SqlServer
scripts/generate-migrations.sh InitialCreate
```

Generate the initial migration for each. `DesignTimeDbContextFactory` reads the same
`DATABASE_PROVIDER` the runtime does and needs no reachable database:
That runs, for each provider, what you would otherwise type by hand.
`DesignTimeDbContextFactory` reads the same `DATABASE_PROVIDER` the runtime does and needs no
reachable database, so nothing has to be running:

```bash
DATABASE_PROVIDER=PostgreSQL \
Expand All @@ -58,29 +53,29 @@ dotnet ef migrations add InitialCreate \
--startup-project src/AuthService
```

### The reference cycle you will hit first
Review the generated DDL and commit it. CI's `Migrations` job turns from a no-op into a real
guard — `has-pending-model-changes` — as soon as a set is present.

`dotnet ef migrations add --project src/AuthService.Migrations.PostgreSQL --startup-project
src/AuthService` fails with:
### The project layout, and the cycle it exists to avoid

```
File '.../src/AuthService/bin/Debug/net9.0/AuthService.Migrations.PostgreSQL.dll' not found.
```
`dotnet ef` loads the migrations assembly out of the **startup project's** output directory,
so `AuthService` has to reference `AuthService.Migrations.*`. Those projects in turn need the
`ApplicationDbContext` type their `[DbContext(...)]` attributes name. While the context lived
in `AuthService` that was a cycle, and no combination of flags resolved it — every attempt
failed with `File '.../AuthService.Migrations.PostgreSQL.dll' not found.`

This is not a mistake in the command. `dotnet ef` loads the migrations assembly out of the
**startup project's** output directory, so `AuthService` has to reference
`AuthService.Migrations.PostgreSQL` — and that project already references `AuthService`, for
the `ApplicationDbContext` type its `[DbContext(...)]` attributes name. That is a cycle, and
no combination of flags resolves it.
`src/AuthService.Data/` is the fix. It holds the entity types, `ApplicationDbContext`,
`DesignTimeDbContextFactory` and the provider wiring, and depends on nothing else in the
repository:

The standard fix is structural: move `ApplicationDbContext` and the entity types into a
class library that the application and both migration projects reference. The application
then references the migration assemblies without a cycle. That is a real refactor and is
deliberately not done here — it touches every `using` in the service, and `EnsureCreated`
remains correct for a first deploy against an empty database in the meantime.
```
AuthService.Data ← AuthService.Migrations.PostgreSQL ←┐
← AuthService.Migrations.SqlServer ←┤
←──────────────────────────────────── AuthService
```

Until it happens, the two migration projects exist as the destination, and CI's migration
check is a no-op that turns into a real guard the moment a set is committed.
Namespaces did not change with the move (`AuthService.Data`, `AuthService.Models`,
`AuthService.Extensions`), so no `using` anywhere in the service or the tests was affected.

Then run with:

Expand Down
43 changes: 43 additions & 0 deletions src/AuthService.Data/AuthService.Data.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">

<!--
The model, the DbContext, and the provider wiring — everything `dotnet ef` needs and
nothing else.

This exists to break a reference cycle. `dotnet ef` loads the migrations assembly out of
the *startup* project's output directory, so AuthService has to reference
AuthService.Migrations.*; those projects in turn need the ApplicationDbContext type their
[DbContext(...)] attributes name. While the context lived in AuthService that was a cycle
and no combination of flags resolved it — which is why no migration set could be generated
(issue #17).

With the context here, the graph is acyclic:

AuthService.Data ← AuthService.Migrations.PostgreSQL ←┐
← AuthService.Migrations.SqlServer ←┤
←──────────────────────────────────── AuthService

Namespaces are deliberately unchanged (AuthService.Data, AuthService.Models,
AuthService.Extensions), so moving the files changed no `using` anywhere.
-->

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>AuthService</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.11" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<!-- DesignTimeDbContextFactory builds its own configuration; outside the Web SDK these
two providers are not implicit. (IDesignTimeDbContextFactory itself lives in
Microsoft.EntityFrameworkCore, so the Design package belongs in the startup project
where `dotnet ef` looks for it, not here.) -->
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.11" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.11" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@
Keeping each provider's set in its own assembly is the standard answer, and is what
Database:MigrationsAssembly selects between at runtime.

The EF Core provider packages arrive transitively from the service project; nothing is
This references AuthService.Data rather than AuthService, so that AuthService can
reference it back without a cycle — which is what `dotnet ef` requires to load a
migrations assembly at all.

The EF Core provider packages arrive transitively from AuthService.Data; nothing is
referenced here that is not already referenced there.
-->

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>AuthService.Migrations.PostgreSQL</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\AuthService\AuthService.csproj" />
<ProjectReference Include="..\AuthService.Data\AuthService.Data.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
-->

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>AuthService.Migrations.SqlServer</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\AuthService\AuthService.csproj" />
<ProjectReference Include="..\AuthService.Data\AuthService.Data.csproj" />
</ItemGroup>

</Project>
28 changes: 20 additions & 8 deletions src/AuthService/AuthService.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
Expand All @@ -11,19 +11,31 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="9.0.19" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.19" />
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="9.4.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.19" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.19">
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="10.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.11" />
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.11">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.19" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="SendGrid" Version="9.29.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.22.0" />
</ItemGroup>

<ItemGroup>
<!-- The model, the DbContext and the provider wiring. -->
<ProjectReference Include="..\AuthService.Data\AuthService.Data.csproj" />

<!--
Referenced so the migration assemblies land in this project's output directory, which is
where `dotnet ef` loads them from and where Database:MigrationsAssembly resolves them at
runtime. Neither project references this one back — see AuthService.Data.csproj for why
that used to be a cycle.
-->
<ProjectReference Include="..\AuthService.Migrations.PostgreSQL\AuthService.Migrations.PostgreSQL.csproj" />
<ProjectReference Include="..\AuthService.Migrations.SqlServer\AuthService.Migrations.SqlServer.csproj" />
</ItemGroup>

</Project>
Loading
Loading