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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
2 changes: 1 addition & 1 deletion .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
branches:
- master
env:
VERSION: 10.0.6
VERSION: 10.0.7
jobs:
build-and-deploy:
runs-on: ubuntu-latest
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: nano-add-api-client-configuration
description: Wire an existing Nano Api Client into this application - adds the App:Apis configuration entry and injects the client into a controller/worker so it can call another Nano service. Use when the user asks to call another Nano service/API from this app, add an API client to a Public API, or compose internal services together in a Nano API, Web, or Console application.
description: Wire an existing Nano Api Client into this application - adds the App:Apis configuration entry, injects the client into a controller/worker, and nests the target service into this app's local docker-compose (with its own incremental publish step) so it's actually runnable end-to-end. Use when the user asks to call another Nano service/API from this app, add an API client to a Public API, or compose internal services together in a Nano API, Web, or Console application.
---

# Nano add API client configuration
Expand Down Expand Up @@ -129,13 +129,50 @@ public class MyController(ILogger<MyController> logger, MyApi myApi) : BaseContr
This is the step that actually makes the `App:Apis` entry take effect — without it, per the
gotcha above, nothing gets registered even though the config exists.

## docker-compose.yml (local Development) — do this automatically, every time

The target must actually run locally alongside this app, or `Host` in the config above resolves to
nothing when you `docker compose up`. Read AGENTS.md's `#### Local Development (docker-compose)`
section under Api Clients first — this is not an optional follow-up step, it's part of what
"add an Api Client configuration" means; do it in the same change as the config/injection above,
without being asked separately. **Applies to Console apps too** — a worker consuming an Api Client
needs its target runnable locally the same way an API/Web consumer does; the only difference is a
Console app's own compose service has no `ports` of its own to worry about colliding with.

1. **Is the target already nested in this app's `.docker/docker-compose.yml`?** (Check for a
service block whose `hostname`/`image` matches the target — e.g. `svc-mytarget`.) If yes,
nothing to do here.
2. **Does the target have a Data and/or Eventing provider configured?** Check the target's own
`Program.cs`/`appsettings.json` (or its own standalone `.docker/docker-compose.yml`, which
already reflects this) — determines whether the nested block gets `depends_on: [database,
eventing]` or neither. Add a shared `database`/`eventing` service to *this* app's compose file
only if not already present — one instance serves every nested dependency, never one per
dependency.
3. **Add the nested service block**, per AGENTS.md's template — `dockerfile_inline` copying from
`./bin/publish/.`, a host port that doesn't collide with this app's own or any other nested
service's port, and `depends_on` wired both onto this app's own primary service (add the new
`svc.*` key there) and, per step 2, onto `database`/`eventing` if applicable.
4. **Wire the publish step into `.docker/docker-compose.dcproj`**:
- If `publish-dependencies.ps1` doesn't exist yet in `.docker/`, create it (per AGENTS.md's
template) and add the `PublishDependentServices` MSBuild target with `Inputs`/`Outputs`
incremental-build wiring.
- If it already exists (this app already consumes at least one other Api Client), add the new
target's `.csproj` publish line to the existing script, and add a new `DependentServiceSources`
`ItemGroup` entry for the target (its main project + its `.Models` project, `.cs`/`.csproj`
globs, excluding `bin`/`obj`) — don't create a second script or a second target.
5. No `.gitignore` entry is needed for the stamp file the script writes — it lives under
`.docker/bin/`, already covered by the solution's standard `**/bin` ignore rule.

## After making the change

- Show the user every file touched in *this* app — the `.csproj` reference (if one was added),
the `appsettings.json` addition, and the injection site. Note that the client class itself
lives in the target service's `.Models` project, not here.
the `appsettings.json` addition, the injection site, and every docker-compose/dcproj/gitignore
file touched by the section above.
- Confirm the client is actually injected somewhere — if the request was just "add the client" with
no specified consumer yet, say explicitly that nothing is wired up until it's referenced.
- Confirm the target is runnable locally: nested in `docker-compose.yml`, and covered by
`publish-dependencies.ps1`/the incremental MSBuild target — don't leave `docker compose up`
producing an unreachable host for the new client.
- If `LogInRoot` was added, restate the Staging/Production secret-handling requirement — don't let
a real credential sit in the base file — and whether the target's `auth-root-login-secret` was
confirmed to actually exist or is still an open prerequisite on that other app.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,23 @@ identity store on every single request, with no token step at all.
1. **Is Identity already configured?** Check the base `appsettings.json` for `Data:Identity`, and
`Program.cs`/the project for an `.AddNanoData<...>()` + identity entity. API-key auth is an
identity-store feature (AGENTS.md), not usable without it — if missing, stop and point the
user at `nano-add-identity` first.
2. **Is API-key auth already configured?** Check for `Data:Identity:ApiKey:Secret` already set.
user at `nano-add-identity` first, which will itself check whether this app is meant to be a
Public API (Identity is an internal-service-only feature — see that skill's own warning) before
adding anything.
2. **If Identity already existed before step 1's referral would have caught it, check public
exposure directly here too — don't rely solely on the referral.** Look for
`.kubernetes/httproute-80.yaml`/`httproute-443.yaml` (the same files `nano-add-identity` and
`nano-add-public-exposure` check). Identity may have been added in an earlier session, before
this check existed, or by a path that never ran `nano-add-identity`'s gate — so its own
forward-looking check can't be assumed to have already happened. If either file is present,
**stop before touching anything** and confirm with the user this is intentional: layering
API-key auth onto an already-publicly-exposed app that also has `BaseEntityUserController`
means raw `X-Api-Key` values are now checked directly against requests from the open internet,
not just from another service that already exchanged one for a JWT (see AGENTS.md's own note
on that intended flow, under `#### Authentication`).
3. **Is API-key auth already configured?** Check for `Data:Identity:ApiKey:Secret` already set.
If so, say so and stop.
3. **Is JWT authentication already configured on this app?** Check the base `appsettings.json`
4. **Is JWT authentication already configured on this app?** Check the base `appsettings.json`
for `App:Authentication:Jwt`, or an existing `AuthController`.
- **Not configured** — this app will end up in **pure API-key mode**: no `AuthController`, no
`Jwt` config, `X-Api-Key` is the only credential, checked on every request. Don't add a
Expand All @@ -38,7 +51,7 @@ identity store on every single request, with no token step at all.
becomes reachable at `/auth/login/apikey` the moment this skill sets the config — tell the
user this new endpoint just appeared, it's a real behavior change on an app that may already
have callers, not just an implementation detail.
4. **Application type.** No controller involved either way in pure mode; in the JWT-paired case
5. **Application type.** No controller involved either way in pure mode; in the JWT-paired case
the controller already exists (added by `nano-add-authentication-jwt`). Nothing API/Web-specific
for this skill to gate on beyond that.

Expand Down Expand Up @@ -91,7 +104,10 @@ testing convenience, set one in `appsettings.Development.json` instead of the ba

- Show the user every file touched.
- State plainly which mode this app ended up in — pure API-key (no controller, no login step) or
paired with existing JWT (`/auth/login/apikey` now live) — from step 3. Don't leave this
paired with existing JWT (`/auth/login/apikey` now live) — from step 4. Don't leave this
implicit; it's the one thing genuinely worth double-checking landed as intended.
- If step 2 found this app already publicly exposed and the user confirmed proceeding anyway,
restate the specific risk one more time — raw `X-Api-Key` values now checked directly against
internet traffic — rather than letting the earlier confirmation be the only mention of it.
- If step 1 stopped the skill early for a missing Identity prerequisite, that's the whole
response.
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,20 @@ repository backs a given external login in that case — not repeated here.
is already configured.
- **Persistent** (Identity present): `AuthIdentityRepository` auto-populates and backs
`/auth/login`, `/auth/login/refresh`, `/auth/logout` — nothing further to wire beyond the
`Jwt` config and controller below.
`Jwt` config and controller below. **This combination (persistent auth + `AuthController`)
is an internal-service-only pattern** — per AGENTS.md's [Controllers § Public API vs
internal service](#public-api-vs-internal-service), a genuine Public API has no `IRepository`
of its own, so it can't have Identity configured in the first place. If this app is meant to
be a Public API, stop: it shouldn't have Identity here at all — see `nano-add-identity`'s own
warning on this, and point the user at composing through the owning internal service's Api
Client instead.
- **Transient** (no Identity): needs `Jwt.ExternalLogins` configured (built-in Facebook/
Google/Microsoft, or a custom provider — see "External Login" below) — ask which, and
whether a custom provider implementation is needed, before proceeding.
whether a custom provider implementation is needed, before proceeding. **Also ask whether
this app needs to assert its own server-computed claims/roles on top of the external login**
(e.g. an `IsAdmin` flag) — if so, see the "AuthController" section's warning below before
scaffolding a generic `AuthController`; adding one unconditionally here can open a
caller-controlled claim-injection endpoint.
- If the user wants persistent auth but Identity isn't registered yet, stop and point them at
`nano-add-identity` first.
3. **Is Authentication already configured?** Check the base `appsettings.json` for
Expand Down Expand Up @@ -127,6 +137,51 @@ overrides, no keys (those come from the Kubernetes secret, never a static file):

## AuthController (API/Web only)

**Stop and check this before scaffolding it — it's not always safe to add.** `BaseAuthController`'s
`login`/`login/external` actions bind `TransientClaims`/`TransientRoles` straight from the request
body and merge them **verbatim, with no server-side filtering,** into the minted JWT
(`AuthTransientRepository.LogInExternalAsync`/`BaseAuthIdentityRepository.LogInAsync`/
`LogInExternalAsync`) — this applies to **both persistent and transient auth**, not just transient.
Concretely: adding this controller means **any caller who can reach it can post
`{"transientClaims": {"IsAdmin": "true"}}` at login and receive back a validly-signed token carrying
that claim** — nothing here validates or restricts which claims/roles a caller may assert about
themselves. Refresh does not have this problem: `login/refresh`/the transient external-login
refresh never accept claims/roles from the caller at all — they're always recovered from a manifest
claim embedded at login, so a refresh can never grant more than the original login already did (see
`ClaimTypesExtended.TransientClaimsManifest`/the internal `TransientClaimsManifest` class in
`Nano.Data.Abstractions`). The transient refresh endpoint
(`/auth/login/external/{providerName}/transient/refresh`) also takes no request body at all - the
token being refreshed comes from the Authorization header. The risk below is specific to login, and
to whoever can reach `AuthController` at all.

- **If this app needs to compute its own claims server-side** (an `IsAdmin` flag, an internal
role, anything not meant to be caller-assignable) **at login, don't add this controller at all.**
Write a custom controller instead (derive it from this app's own base controller, *not*
`BaseAuthController`) that calls `IAuthExternalRepositoryAggregator`/`IAuthTransientRepository`/
`IAuthIdentityRepository` directly and builds the claims/roles itself from trusted data — never
from caller input. This is a real, load-bearing pattern in this codebase, not a hypothetical — see
`Api.Admin`'s `AccountsController` (deriving its own `BaseAdminController`), which implements
`login/microsoft`/`login/refresh`/`me` by hand for exactly this reason.
- Nano additionally auto-maps built-in transient external-login endpoints
(`/auth/login/external/{provider}/transient` and its `/refresh` counterpart) whenever *any*
`BaseAuthController`-derived class exists in the app **and** no Identity is configured — see
`ServiceScopeExtensions.UseNanoEndpoints`'s `!hasIdentity && hasAuthController` gate, checked by
type scan, not by whether this specific controller is the one deriving it. This is an extra
exposure specific to transient auth: it means a custom controller alone isn't enough to shield a
transient app unless `hasAuthController` also stays `false` (i.e. no `BaseAuthController`-derived
class anywhere in the app) — `Api.Admin`'s custom controller works precisely because it doesn't
derive `BaseAuthController`. The `/refresh` counterpart is auto-mapped under the same gate, but
isn't a caller-trust risk the way login is - see above.
- **This risk is sharpest on a publicly-exposed app** (anyone on the internet can reach the
endpoint), but don't treat an internal-only app as automatically safe either — anything that lets
a caller assign its own JWT claims is worth a deliberate decision, not a default. `AuthController`
is an internal-service-only pattern to begin with (see AGENTS.md's [Controllers § Public API vs
internal service](#public-api-vs-internal-service)), so "internal-only" is the floor, not a reason
to skip the decision.
- If none of the above applies — no need for server-computed claims beyond what the external
provider itself asserts, and whoever can reach this app's `AuthController` is already trusted to
assert login-time claims — the generic controller below is fine as-is.

`Controllers/AuthController.cs`, main app project:

```csharp
Expand All @@ -148,10 +203,30 @@ block under `Jwt.ExternalLogins` in the base `appsettings.json`, per AGENTS.md's
table (`Facebook.AppId`/`.AppSecret`/`.Scopes`, `Google.ClientId`/`.ClientSecret`/`.Scopes`,
`Microsoft.TenantId`/`.ClientId`/`.ClientSecret`/`.Scopes`). Treat `AppSecret`/`ClientSecret` as
real secrets, the same class of value as the JWT keys above — `null` in the base file, a real
value only where it's actually safe to have one. AGENTS.md doesn't document an established
Kubernetes-secret/GitHub-secret convention for these specifically (unlike `auth-jwt-secret`/
`auth-api-key-secret`/`auth-sql-secret`) — don't invent one; ask the user how they want it stored
for Staging/Production rather than assuming a pattern that doesn't exist yet in this codebase.
value only where it's actually safe to have one.

- **Microsoft has its own skill, `nano-add-authentication-microsoft`** — it's the one built-in
provider whose credentials can be scripted (an Entra ID app registration via the Azure CLI), so
it has an established, self-rotating Kubernetes-secret/GitHub-Actions convention. If the request
names Microsoft specifically, use that skill instead of configuring `Jwt.ExternalLogins.Microsoft`
by hand here.
- **Facebook/Google have no such convention.** Their credentials are created by hand through each
provider's own developer console — don't invent a Kubernetes/GitHub-secret pattern for them; ask
the user how they want it stored for Staging/Production rather than assuming one exists.
- **Facebook logins can never be refreshed — don't offer an `offline_access`-style option for it.**
`AuthExternalFacebookRepository.AuthenticateRefreshAsync` unconditionally throws, regardless of
config, yet `.../transient/refresh` is still auto-mapped for every registered provider and will
always 401 for Facebook. If the user asks for refresh support on a Facebook login, say plainly
that it isn't possible with the built-in provider rather than looking for a config option that
doesn't exist. Google and Microsoft, by contrast, are both refreshable — see AGENTS.md's
`#### Authentication` table.
- **`Facebook.Scopes`/`Google.Scopes` are frontend-only — setting them here does nothing server-side.**
Neither repository reads `options.Scopes` at all; scope negotiation happens in the client-side SDK
(Facebook) or the frontend's own authorize-URL redirect (Google) before Nano ever sees the
request. Still add them to config for documentation purposes if the user gives specific scopes,
but don't imply this app's config is what actually requests them — for Google specifically,
refresh support also needs the frontend's authorize request to include `access_type=offline`/
`prompt=consent`, which has nothing to do with this `Scopes` entry either.

**Custom provider — real code, no config entry.** Per AGENTS.md's `##### Custom external provider`,
this is auto-discovered by type, not registered via `Jwt.ExternalLogins` config the way built-in
Expand Down Expand Up @@ -303,6 +378,11 @@ Console.Read();
- Show the user every file touched, grouped by concern (appsettings per environment, the
controller, and — for the issuer app — Staging/Production CI + K8s), plus the external-login
repository class if one was scaffolded.
- If this is transient auth with external login and a generic `AuthController` was added, restate
explicitly that `/auth/login/external/{provider}/transient` is now live and accepts
caller-supplied `TransientClaims`/`TransientRoles` verbatim — confirm that's actually acceptable
for this app before considering the task done. If a custom controller was used instead specifically
to avoid this, say so, and confirm it does **not** derive `BaseAuthController` anywhere in the app.
- Point them at the snippet above for generating real Staging/Production keys — never the
hardcoded Development pair.
- If they want to change the Development key pair from the shared default, warn explicitly: it
Expand Down
Loading
Loading