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
33 changes: 33 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ env:
DOTNET_VERSION: "10.0.x"
DOTNET_CACHE: "dotnet-cache-${{ github.sha }}"
FRONTEND_CACHE: "frontend-cache-${{ github.sha }}"
NUGET_OUTPUT: ./Artifacts/NuGet

on:
workflow_dispatch:
Expand All @@ -30,6 +31,7 @@ on:
permissions:
contents: write
packages: write
id-token: write

jobs:
release:
Expand Down Expand Up @@ -155,3 +157,34 @@ jobs:
tags: ${{ steps.docker-tags.outputs.tags }}
build-args: |
VERSION=${{ needs.release.outputs.version }}

publish-nuget:
if: needs.release.outputs.publish == 'true'
runs-on: ubuntu-latest
needs: [release]
permissions:
id-token: write

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Remove any existing artifacts
run: rm -rf ${{ env.NUGET_OUTPUT }}

- name: Pack Aspire hosting package
run: dotnet pack Source/Aspire/Aspire.csproj --configuration Release -o ${{ env.NUGET_OUTPUT }} -p:PackageVersion=${{ needs.release.outputs.version }}

- name: NuGet login (OIDC → temp API key)
uses: NuGet/login@v1
id: login
with:
user: ${{ secrets.NUGET_USER }}

- name: Push NuGet package
run: dotnet nuget push --skip-duplicate '${{ env.NUGET_OUTPUT }}/*.nupkg' --timeout 900 --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
1 change: 1 addition & 0 deletions AuthProxy.slnx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Solution>
<Folder Name="/Source/">
<Project Path="Source/AuthProxy/AuthProxy.csproj" />
<Project Path="Source/Aspire/Aspire.csproj" />
<Project Path="Source/Composition/Composition.csproj" />
<Project Path="Source/AuthProxy.Specs/AuthProxy.Specs.csproj" />
<Project Path="Source/TestApp/TestApp.csproj" />
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<!-- Cratis -->
<PackageVersion Include="Cratis.Arc.Core" Version="20.33.1" />
<!-- Microsoft -->
<PackageVersion Include="Aspire.Hosting" Version="9.5.2" />
<PackageVersion Include="Aspire.Hosting.NodeJs" Version="9.5.2" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.8" />
Expand Down
260 changes: 260 additions & 0 deletions Documentation/aspire/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
# Aspire Hosting Integration

The `Cratis.AuthProxy.Aspire` NuGet package adds first-class .NET Aspire support for AuthProxy.
Instead of configuring environment variables by hand, you wire up authentication, tenancy, and
service routing with a concise fluent API in your `AppHost`.

## Installation

```bash
dotnet add package Cratis.AuthProxy.Aspire
```

## Adding AuthProxy as a container resource

This is the typical path for external consumers who run AuthProxy from Docker Hub:

```csharp
var authproxy = builder.AddAuthProxy("authproxy", tag: "latest")
.WithHttpEndpoint(port: 8080)
.WithBackend("main", apiResource)
.WithFrontend("main", webResource)
.WithOidcProvider(
"Microsoft",
OidcProviderType.Microsoft,
authority: "https://login.microsoftonline.com/<tenant-id>/v2.0",
clientId: "<client-id>",
clientSecret: "<client-secret>")
.WithHostTenantResolution();
```

`AddAuthProxy` creates an `AuthProxyResource` backed by the `cratis/authproxy` Docker Hub image.
Pin `tag` to a specific release in production environments — the default `"latest"` is convenient
for local development.

## Adding AuthProxy as a project resource

When working inside the AuthProxy repository itself (or in a monorepo that includes AuthProxy
source), use `AddProject` with the same extension methods:

```csharp
var authproxy = builder.AddProject<Projects.AuthProxy>("authproxy")
.WithBackend("main", apiResource)
.WithFrontend("main", webResource);
```

All `With*` methods work on any `IResourceBuilder<T> where T : IResourceWithEnvironment`,
so you can mix container and project resources freely.

---

## Services

Use `WithBackend` and `WithFrontend` to register the resources that AuthProxy should proxy:

```csharp
authproxy
.WithBackend("main", apiResource)
.WithFrontend("main", webResource);
```

Both methods accept an optional `endpointName` parameter (defaults to `"http"`) that selects
which endpoint from the target resource to forward to.

### Identity details resolution

For each service with a backend, AuthProxy calls `GET {baseUrl}/.cratis/me` after authentication
to enrich the identity cookie. This behaviour is on by default. To disable it for a specific
service, pass `resolveIdentityDetails: false` to `WithBackend`:

```csharp
authproxy
.WithBackend("reporting", reportingApi, resolveIdentityDetails: false)
.WithFrontend("reporting", reportingWeb);
```

See [Services](../configuration/services.md) for the underlying configuration model.

---

## Authentication

### OIDC providers

```csharp
authproxy.WithOidcProvider(
name: "Contoso AD",
type: OidcProviderType.Microsoft,
authority: "https://login.microsoftonline.com/<tenant-id>/v2.0",
clientId: "<client-id>",
clientSecret: "<client-secret>",
scopes: ["api://my-api/.default"]);
```

Call `WithOidcProvider` once per provider. Multiple calls produce a provider-selection page.

The `OidcProviderType` enum contains well-known provider brands:

| Value | Description |
|-------|-------------|
| `Custom` | Generic / unknown provider. |
| `Microsoft` | Microsoft Identity Platform (Azure AD / Entra ID). |
| `Google` | Google Identity. |
| `GitHub` | GitHub OAuth / OIDC. |
| `Apple` | Apple Sign-In. |

### OAuth 2.0 (non-OIDC) providers

For providers that do not expose an OIDC discovery document, use `WithOAuthProvider`:

```csharp
authproxy.WithOAuthProvider(
name: "GitHub",
type: OidcProviderType.GitHub,
authorizationEndpoint: "https://github.com/login/oauth/authorize",
tokenEndpoint: "https://github.com/login/oauth/access_token",
userInformationEndpoint: "https://api.github.com/user",
clientId: "<client-id>",
clientSecret: "<client-secret>",
scopes: ["user:email"],
claimMappings: new Dictionary<string, string>
{
["sub"] = "id",
["name"] = "login",
["email"] = "email"
});
```

See [Authentication](../configuration/authentication.md) for the full configuration reference.

---

## Tenant resolution

Add one or more resolution strategies. They run in order until a tenant is matched:

| Method | Strategy |
|--------|----------|
| `WithHostTenantResolution()` | Matches the request host against configured tenant domains. |
| `WithSubHostTenantResolution()` | Derives the tenant from the first subdomain (e.g. `acme.example.com` → `acme`). |
| `WithClaimTenantResolution(claimType?)` | Reads a claim from the authenticated user. |
| `WithRouteTenantResolution(pattern)` | Extracts a source identifier from the request path by regex. |
| `WithSpecifiedTenantResolution(tenantId)` | Pins all requests to one fixed tenant (single-tenant deployments). |
| `WithDefaultTenantResolution(tenantId)` | Fallback when no other strategy resolves a tenant. |
| `WithSelectionTenantResolution()` | Reads the tenant from the cookie set by the tenant-selection page. |

```csharp
authproxy
.WithSubHostTenantResolution()
.WithDefaultTenantResolution("lobby");
```

See [Tenancy](../configuration/tenancy.md) for detailed strategy documentation.

### Tenant verification

After resolution, AuthProxy can confirm the tenant exists by calling your back-end. You can
pass a raw URL template or reference an Aspire service resource directly:

```csharp
// Raw URL template
authproxy.WithTenantVerification("https://platform.example.com/api/tenants/{tenantId}");

// Aspire resource reference — endpoint is resolved automatically
authproxy.WithTenantVerification(platformApi, "/api/tenants/{tenantId}");
```

AuthProxy issues a `GET` to the resolved URL. A `200` response lets the request proceed; `404` or
any error serves the `tenant-not-found.html` page.

---

## Tenant selection

When users can be members of more than one tenant, the `Selection` strategy presents a
tenant-selection page after login. You can pass a raw URL or reference an Aspire service resource:

```csharp
// Raw URL
authproxy.WithSelectionTenantResolution(
tenantsEndpoint: "https://platform.example.com/api/tenants/selectable");

// Aspire resource reference — endpoint is resolved automatically
authproxy.WithSelectionTenantResolution(platformApi, "/api/tenants/selectable");
```

AuthProxy calls the endpoint after login and, if more than one tenant is returned, serves the
built-in `select-tenant.html` page. If only one tenant is returned the selection page is
skipped and the user is redirected immediately.

The endpoint must return a JSON array of `{ "id": "...", "name": "..." }` objects.

See [Tenant Selection Page](../configuration/tenant-selection.md) for details on building a
custom selection page and the full flow.

---

## Invites and lobby

### Core invite configuration

Configure the invite system with the RSA public key and exchange endpoint. You can pass a raw URL
or reference an Aspire service resource for the exchange endpoint:

```csharp
// Raw URL
authproxy.WithInvite(
publicKeyPem: File.ReadAllText("invite-public-key.pem"),
exchangeUrl: "https://studio.example.com/internal/invites/exchange",
issuer: "https://studio.example.com",
audience: "authproxy",
tenantClaim: "tenant_id",
subjectAlreadyExistsUrl: "https://app.example.com/errors/account-already-exists");

// Aspire resource reference — exchange endpoint URL is resolved automatically
authproxy.WithInvite(
publicKeyPem: File.ReadAllText("invite-public-key.pem"),
exchangeServiceResource: studioApi,
exchangeRoute: "/internal/invites/exchange",
issuer: "https://studio.example.com",
tenantClaim: "tenant_id");
```

| Parameter | Required | Description |
|-----------|----------|-------------|
| `publicKeyPem` | ✓ | PEM-encoded RSA public key to verify invite token signatures. |
| `exchangeUrl` | ✓ | Endpoint called after login to exchange the invite token. |
| `issuer` | – | Expected `iss` claim. Omit to skip issuer validation. |
| `audience` | – | Expected `aud` claim. Omit to skip audience validation. |
| `tenantClaim` | – | Claim that carries the tenant ID for tenant-issued invite detection. |
| `subjectAlreadyExistsUrl` | – | Redirect URL when the exchange endpoint returns HTTP 409. Omit to serve the built-in page. |

### Claim forwarding

To propagate invite-token claims into the principal sent to `/.cratis/me` endpoints, call
`WithInviteClaimForwarding` once per claim:

```csharp
authproxy
.WithInviteClaimForwarding("organization_id", toClaimType: "organization")
.WithInviteClaimForwarding("invited_by");
```

When `toClaimType` is omitted the original claim type is preserved.

### Lobby

The lobby is the service users are redirected to when no tenant can be resolved — typically
an onboarding application. At minimum, configure the lobby frontend:

```csharp
authproxy
.WithLobbyFrontend(lobbyResource)
.WithLobbyBackend(lobbyApiResource); // optional
```

`WithLobbyFrontend` and `WithLobbyBackend` both accept an optional `endpointName` parameter
(defaults to `"http"`).

See [Invites & Lobby](../configuration/invites.md) for the full invite flow walkthrough.

2 changes: 2 additions & 0 deletions Documentation/aspire/toc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- name: Aspire Hosting
href: index.md
2 changes: 2 additions & 0 deletions Documentation/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
href: index.md
- name: Configuration
href: configuration/toc.yml
- name: Aspire Hosting
href: aspire/toc.yml
29 changes: 29 additions & 0 deletions Source/Aspire/Aspire.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Cratis.AuthProxy.Aspire</RootNamespace>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn>

<!-- NuGet package metadata -->
<PackageId>Cratis.AuthProxy.Aspire</PackageId>
<Title>Cratis AuthProxy — Aspire Hosting</Title>
<Description>Aspire hosting integration for Cratis AuthProxy. Provides fluent extension methods to add and configure AuthProxy (authentication, tenancy, backend/frontend routing) in a .NET Aspire AppHost — either as a container resource or layered on an existing ProjectResource.</Description>
<Authors>Cratis</Authors>
<Copyright>Copyright Cratis</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/cratis/authproxy</PackageProjectUrl>
<RepositoryUrl>https://github.com/cratis/authproxy</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<PackageTags>aspire;authproxy;authentication;oidc;tenancy;cratis</PackageTags>
<IsPackable>true</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting" />
</ItemGroup>

</Project>
23 changes: 23 additions & 0 deletions Source/Aspire/AuthProxyConfigAnnotation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.AuthProxy.Aspire;

/// <summary>
/// Annotation that tracks per-resource state used when building array-based AuthProxy configuration
/// (e.g. OIDC/OAuth providers, tenant resolution strategies).
/// </summary>
sealed class AuthProxyConfigAnnotation : IResourceAnnotation
{
/// <summary>Gets or sets the number of OIDC providers that have been registered.</summary>
public int OidcProviderCount { get; set; }

/// <summary>Gets or sets the number of OAuth providers that have been registered.</summary>
public int OAuthProviderCount { get; set; }

/// <summary>Gets or sets the number of tenant resolution strategies that have been registered.</summary>
public int TenantResolutionCount { get; set; }

/// <summary>Gets or sets the number of invite claim-forwarding entries that have been registered.</summary>
public int InviteClaimForwardingCount { get; set; }
}
Loading
Loading