Skip to content
Back to skills

Nano Add Authentication Jwt

BSecurity

Configure Nano's built-in JWT authentication (App:Authentication:Jwt) on a Nano.Library-based API/Web application - adds the Jwt configuration, the AuthController, the Development key setup, and (for the token-issuing app) the Staging/Production key-generation and Kubernetes secret. Use when the user asks to add login, sign-in, or JWT authentication to a Nano API or Web application - not for adding a user store by itself (that's nano-add-identity) or for API-key authentication by itself (that...

  • 5 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 4, 2026
devopsgokubernetesazuretestinggitapisecurity

Works with

  • cli
  • api

Security analysis

B80/100
  • highContains large base64-encoded strings that could be hidden payloads
  • criticalReads or references SSH private keys

Pro shows the line behind each finding and how to fix it

Scanned October 4, 2026

npx -y skills add Nano-Core/Nano.Templates --skill nano-add-authentication-jwt --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Nano Add Authentication Jwt?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Nano Add Authentication Jwt
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/nano-core-nano-add-authentication-jwt-nano-templates/badge)](https://www.skillsdirectory.com/skills/nano-core-nano-add-authentication-jwt-nano-templates)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: nano-add-authentication-jwt
description: Configure Nano's built-in JWT authentication (App:Authentication:Jwt) on a Nano.Library-based API/Web application - adds the Jwt configuration, the AuthController, the Development key setup, and (for the token-issuing app) the Staging/Production key-generation and Kubernetes secret. Use when the user asks to add login, sign-in, or JWT authentication to a Nano API or Web application - not for adding a user store by itself (that's nano-add-identity) or for API-key authentication by itself (that's nano-add-authentication-apikey, which works standalone without any of this).
---

# Nano add JWT authentication

Configures Nano's built-in JWT authentication on an existing Nano API/Web application. Read
`AGENTS.md`'s `#### Authentication` section first — it documents the full `Configuration` table,
the `AuthController`'s sub-repository table, and the persistent-vs-transient distinction in
detail; this skill does not repeat that, only how to apply it.

**This skill is JWT-specific.** API-key authentication is a genuinely independent auth mode in
Nano — it does not require `Jwt` at all, has no `AuthController`, and is `nano-add-authentication-apikey`'s
job, not this one. See step 6 below for what happens when both are configured on the same app.

**No `Program.cs` registration call.** Unlike every other add-provider skill, Authentication is
pure config plus one controller — `IAuthRepository`'s sub-repositories self-populate based on
whichever config sections exist (`Jwt.RootLogin` → root login, [Identity](nano-add-identity) →
persistent login, `Jwt.ExternalLogins` with no Identity → transient login). There's nothing to
add to `.ConfigureServices(...)`.

**Two request shapes.** A request naming this skill is one of:
1. **Persistent auth** — `Jwt` config + `AuthController` on top of already-configured Identity.
2. **Transient auth** — `Jwt` config + `AuthController`, but with `Jwt.ExternalLogins` instead of
   Identity.
Figure out which one applies before touching anything — steps 1–5 below are how. Either can be
layered with API-key auth by running `nano-add-authentication-apikey` before or after this skill — see
step 6.

These two aren't the only combination — `Jwt.ExternalLogins` can also layer on top of already-configured
Identity (e.g. "sign in with Google" but the account is still persistent, not transient). See
AGENTS.md's sub-repository table for exactly how `AuthExternalRepositoryAggregator` resolves which
repository backs a given external login in that case — not repeated here.

## Before making any change, determine

1. **Does this app issue tokens, or only validate them?** Ask if the request doesn't say.
   - **Issuer**: needs both `PublicKey` and `PrivateKey` in Staging/Production, and creates the
     `auth-jwt-secret` Kubernetes secret from real GitHub secrets.
   - **Validator-only**: needs only `PublicKey` in Staging/Production, and must **not** create or
     re-apply the secret — it references the one the issuer app already created. See the
     Kubernetes section below; getting this backwards silently corrupts the shared secret with
     unexpanded placeholder values — a real bug found and fixed this way in this codebase, so
     don't repeat it.
   - This distinction **does not apply to Development** — see below.
2. **Persistent or transient auth?** Check whether [Identity](nano-add-identity) (`Data:Identity`)
   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.
   - **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.
   - 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
   `App:Authentication:Jwt`, or an existing `AuthController`. If present, say so before changing
   anything.
4. **Application type.** The `AuthController` is API/Web only — a Console app has no HTTP surface
   to expose it on. A Console app can still have `Jwt` configured, but only for its own outbound
   Api Client authentication (`Apis:{Client}:LogInRoot`, a different, already-documented AGENTS.md
   concern) — not something this skill scaffolds a controller for.
5. **Is this a Console app whose only use of `Jwt` is outbound Api Client auth?** Skip the
   controller step below in that case, per step 4.
6. **Is API-key authentication already configured** (`Data:Identity:ApiKey:Secret` set)? Check
   the base `appsettings.json`. If so, this app was previously in pure API-key-only mode — no
   `Jwt`, no `AuthController` (see `nano-add-authentication-apikey`: that controller would fail to resolve
   `IAuthRepository` without `Jwt` configured, so it genuinely didn't exist yet). Adding `Jwt` now
   changes two things **automatically, from config alone** — nothing extra to build, but tell the
   user about both:
   - Nano's scheme selection (`AddNanoAuthentication`, based on whether `Jwt`/`ApiKeyOptions` are
     each present) switches from API-key-only to `JWT_OR_APIKEY` — existing API-key callers keep
     working unchanged, requests carrying a JWT `Authorization` header now also work.
   - The `AuthController` this skill adds will immediately expose `/auth/login/apikey` (its
     visibility is gated purely on `Data:Identity:ApiKey:Secret` being set, per
     `ConditionalActionsConvention`) — callers can now trade an API key for a JWT once instead of
     presenting the key on every request.

## appsettings.json — Jwt

Base `appsettings.json` (sibling of `App:Version`/`App:Hosting`, per AGENTS.md's `##### Configuration`
example): `Issuer`/`Audience`/`PublicKey`/`PrivateKey` all `null`, `Expiration`/`RefreshExpiration`
at their framework defaults (`01:00:00`/`72:00:00`). Leave `RootLogin`/`ExternalLogins` out
entirely unless configuring them now — they're opt-in additions, not blank placeholders.

**`appsettings.Development.json` — use the existing shared key pair, don't generate a new one.**
Every app in this codebase (issuers and validators alike) uses the exact same hardcoded RSA key
pair locally:

```json
"App": {
  "Authentication": {
    "Jwt": {
      "Issuer": "nano.development",
      "Audience": "nano.development",
      "PublicKey": "MIIBCgKCAQEAv7iVNUS5wT7Fvg/hkmlvvPnOW7Rcyh7dFStJSTtM+7f74+GGVJLl6spXasnsQ7v6rw7vlyb+uVk1UaQsUA38luSNGWfPqc3JAtkeJPWCu1kN79Yo3im7Qx6B1u4gf0AR3n86ClQGz3O5Jxo8M3+zlwveYnlf6bqhBakOVdPS5tX0Bvh/F9lXiEF53EZEcfuHjBjDLik9PUdTjqehPLCPyI1/FbfE8P1Y4S7AEfs2fIqXGxJNXDyoDRvi42qefqXcsmzBUDYtHqvwSHWcDn5DXDRY2FYkyESMvd7RRGwI6U0g8V9k3Qudd4LjQTs8LdBu5u25wvqx37Y1518BPqGQkQIDAQAB",
      "PrivateKey": "MIIEowIBAAKCAQEAv7iVNUS5wT7Fvg/hkmlvvPnOW7Rcyh7dFStJSTtM+7f74+GGVJLl6spXasnsQ7v6rw7vlyb+uVk1UaQsUA38luSNGWfPqc3JAtkeJPWCu1kN79Yo3im7Qx6B1u4gf0AR3n86ClQGz3O5Jxo8M3+zlwveYnlf6bqhBakOVdPS5tX0Bvh/F9lXiEF53EZEcfuHjBjDLik9PUdTjqehPLCPyI1/FbfE8P1Y4S7AEfs2fIqXGxJNXDyoDRvi42qefqXcsmzBUDYtHqvwSHWcDn5DXDRY2FYkyESMvd7RRGwI6U0g8V9k3Qudd4LjQTs8LdBu5u25wvqx37Y1518BPqGQkQIDAQABAoIBAEwNH3sS+RCUIwLC7/sRQhbXjSlJgalX1uFH23lmQaJ0mEIMOyofX37kpwqgcM1pqwZ4SUhPWqoRnhn1ovJaqgD9Ro92Y6T7EarEj7Wfgi1pJSMnc+y05yi32E93BIMV2kDFfTONo2n1gNPnD0xqcsYPGjc76HUh6DADoMEhFr8kHaz4J2daKV0tJjApNt2oabk8BLQEq9Uv22DsLfL+nEOHPhSMk7EmNv3QQgUNH5ugeDNfTNr+A6K8YMbVVrmDalZS/GBWSscnJ9Ma2WrHJ/x2IRQECVMf6U05vrgtKb9imPcN09ccItIzcK/8ZBbSw2v+Gzf1Je447SYT9njAOiUCgYEAzOAsty4cxCLSWt2GBTE58MoThNeiVRBvc6Gw5B1olCCnWkVxRDYwYlPnwvemqa+YsfijrjVkuS0kJmfrGJ/MkV8Wsx2XL6mRyCBXOUog0U/Nh20ANU8kcmEMkGVtxDUM8hr9QQ5qex/LmSiy8YG4c4mfD6s7KvWnRxJcviXmgUMCgYEA75AQssujQtycWx6fZ/aBQLc6+xSlGaY73k2R8XLwMSASAeq1erxCSsuPF5lPRnQ4VZyfSOV9AcOyLgeJCi4ePJEnfZZMcGkKNt2yMsZoWUlJSmHIXhEEfKqu8Qo0TRu4/vQYPKwTVXdpbZJIlDgzztPdC1gOpCg3QQH16wPyL5sCgYAQ5Ygqj14F+w04Oz7bXMT3i+LyOMqFk3Ztpe8t0RMX7F2A/2spAgMZiOv7U2tmYToJq4TsUDD/aK6rkDR+cmdvsdTwbsdSQfzo8WngKrHsMVW1DpNO0jkiSci8e/EClpF7wigS3np/rw6ekhG4A0fQF5CLvUaC84GZRfVqJTwOewKBgQC4oTKNae54oGgMvewjBtOU2eKmEcIwo3JuoSACkw/U/J+ERKz7W85HsNymVmzHotir+pq0ZtHSI03Wtc4DP4nkKgbifoyI8huCL5igE1PmxFms7vGqtbjcj/tmH/QxHVWVgPCRChmYfACQBvbS7QHYvGYW0RXvpGL5QhaSuybTUwKBgG/p/gsj6yUDAiNhEWpSsMWl/3xJeIobcnH1XQrrXWIzL1xZtX1EkcqLM6++Ojjre3UKj96ZDFRpJH4uxTilE9MDOOf+PLoL01rr1rmzaWDr5NsI3nqz2AS6ZSuofO0rs7nQlKtTnQY0vlzPGqfQp4uQ11KPzO2PB9TEGwnZy5HV",
      "Expiration": "24:00:00"
    }
  }
}
```

- **`PrivateKey` goes here even on a validator-only app**, if the app also configures
  `Jwt.RootLogin` for isolated local testing (the common case for an internal service — AGENTS.md:
  "useful in Development when testing a service in isolation"). Root login self-issues a JWT,
  which needs a private key regardless of the app's Staging/Production role. Only omit
  `PrivateKey` in Development for an app that genuinely never self-issues locally (e.g. a
  pure Public API with no isolated-testing story of its own).
- `Expiration: "24:00:00"` (vs. the base file's `01:00:00`) is the established convention for
  Development — longer-lived tokens are less annoying to work with locally. Not required, but
  match it unless the user asks otherwise.
- Add a `RootLogin` block (`Username`/`Password`) alongside `Jwt` in Development if this app
  should support isolated local testing — ask for credentials, or use a placeholder like
  `admin@domain.com` / a throwaway password if the user doesn't care.

**`appsettings.Staging.json` / `appsettings.Production.json`** — only `Issuer`/`Audience`
overrides, no keys (those come from the Kubernetes secret, never a static file):

```json
"App": { "Authentication": { "Jwt": { "Issuer": "nano.staging", "Audience": "nano.staging" } } }
```
```json
"App": { "Authentication": { "Jwt": { "Issuer": "nano.production", "Audience": "nano.production" } } }
```

## AuthController (API/Web only)

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

```csharp
public class AuthController(ILogger<AuthController> logger, IAuthRepository authRepository)
    : BaseAuthController(logger, authRepository);
```

Nothing to implement — every endpoint the current config enables (per AGENTS.md's sub-repository
table) is provided. For a non-`Guid` identity type, use `BaseAuthController<TIdentity>` and
`IAuthRepository<TIdentity>` to match (same rule as every other controller in this ecosystem).

## External Login (`Jwt.ExternalLogins`)

Only relevant if step 2 found external login in play — either the transient case, or the hybrid
persistent-plus-external-login case noted above. Two genuinely different kinds of work, not one:

**Built-in provider (Facebook / Google / Microsoft) — pure config, no code.** Add the matching
block under `Jwt.ExternalLogins` in the base `appsettings.json`, per AGENTS.md's `##### Configuration`
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.

- **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 (see
  `Nano.Lessons/Api.Auth.External.Microsoft`). 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.

**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
providers are — there's no appsettings.json entry for it at all.

1. **`TFlow`.** `ImplicitFlow` or `AuthCodeFlow` (both derive `BaseAuthFlow`) — pick whichever
   matches the provider's actual OAuth flow; ask if unclear rather than guessing. Derive a custom
   `BaseAuthFlow` subclass instead only if the provider's flow doesn't fit either built-in shape.
2. **The class**, conventionally `Auth/{Provider}ExternalRepository.cs` in the application
   project (discovery is by type, so the location isn't enforced):
   ```csharp
   public class MyExternalRepository() : BaseAuthExternalRepository<ImplicitFlow>("MyProvider")
   {
       public override async Task<ExternalAuthenticationData> AuthenticateAsync(ImplicitFlow flow, CancellationToken cancellationToken = default)
       {
           // call the external provider, map its response to ExternalAuthenticationData
           return new ExternalAuthenticationData
           {
               Id = "external-id",
               Username = "MyUser",
               EmailAddress = "user@domain.com",
               Name = "My User",
               ExternalToken = new ExternalAuthenticationToken { Name = this.ProviderName, Token = "token", RefreshToken = "refresh-token" }
           };
       }

       public override async Task<ExternalAuthenticationToken> AuthenticateRefreshAsync(string refreshToken, CancellationToken cancellationToken = default)
       {
           // refresh against the external provider
           return new ExternalAuthenticationToken { Name = this.ProviderName, Token = "token", RefreshToken = "refresh-token" };
       }
   }
   ```
   The constructor's string argument (`"MyProvider"` above) is `ProviderName` — this is what
   `AuthExternalRepositoryAggregator` resolves against, and what appears in the
   `/auth/login/external/{providerName}/...` route, so ask the user what they want it called
   rather than defaulting to the class name.
3. **Whatever credentials/endpoint the provider itself needs** (API key, base URL, etc.) — these
   are this custom repository's own concern, not `Jwt.ExternalLogins`'s. Add them as an options
   class bound from whatever config section makes sense for this provider (same pattern as any
   other custom service in this codebase), then inject it into the repository's constructor. Don't
   try to route them through `Jwt.ExternalLogins` — that section is exclusively for the three
   built-in providers.

Either way, the actual login endpoint this exposes is
`/auth/login/external/{providerName}/transient` when Identity isn't configured, or the
persistent equivalent per AGENTS.md's sub-repository table when it is — this skill doesn't scaffold
that call site, only the repository/config that backs it.

## Kubernetes / GitHub Actions (Staging/Production) — issuer app only

Only the app that **issues** tokens does this. A validator-only app does **not** create or
re-apply this secret — it only references the `auth-jwt-secret` the issuer already created (see
its `deployment.yaml` entry below). Re-applying it from an app with no real key values set pushes
unexpanded placeholder text into the shared secret, silently corrupting the real one — don't do
it for any app but the issuer.

1. **Workflow env vars**:
   ```yaml
   AUTH_JWT_PUBLIC_KEY: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_AUTH_JWT_PUBLIC_KEY || secrets.STAGING_AUTH_JWT_PUBLIC_KEY }}
   AUTH_JWT_PRIVATE_KEY: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_AUTH_JWT_PRIVATE_KEY || secrets.STAGING_AUTH_JWT_PRIVATE_KEY }}
   ```
2. **`.kubernetes/auth-jwt-secret.yaml`** (new file, issuer app only):
   ```yaml
   apiVersion: v1
   kind: Secret
   metadata:
     name: auth-jwt-secret
     namespace: %KUBERNETES_NAMESPACE%
   type: Opaque
   stringData:
     jwt-public-key: %AUTH_JWT_PUBLIC_KEY%
     jwt-private-key: %AUTH_JWT_PRIVATE_KEY%
   ```
   Apply it in the `Kubernetes Deploy` step, before `deployment.yaml`/`stateful-set.yaml`. Also
   add `.kubernetes\auth-jwt-secret.yaml = .kubernetes\auth-jwt-secret.yaml` to `{name}.sln`'s
   `.kubernetes` `SolutionItems` block (see AGENTS.md's Solution Structure note) — new files
   under `.kubernetes/` don't show up in Visual Studio's Solution Explorer otherwise.

## Kubernetes — deployment.yaml

Reference the secret in `.kubernetes/deployment.yaml`'s container `env` — **the two app types get
different entries here, not the same block with one line dropped**:

Issuer app (both keys):
```yaml
- name: App__Authentication__Jwt__PublicKey
  valueFrom:
    secretKeyRef:
      name: auth-jwt-secret
      key: jwt-public-key
- name: App__Authentication__Jwt__PrivateKey
  valueFrom:
    secretKeyRef:
      name: auth-jwt-secret
      key: jwt-private-key
```

Validator-only app (`PublicKey` only — no `PrivateKey` entry at all):
```yaml
- name: App__Authentication__Jwt__PublicKey
  valueFrom:
    secretKeyRef:
      name: auth-jwt-secret
      key: jwt-public-key
```

## API-key authentication

Not this skill's job — see `nano-add-authentication-apikey`, which works whether or not `Jwt` is
configured on this app. If the user asked for both in one request, run both skills; step 6 above
covers the one thing each needs to know about the other.

## Generating real keys (Staging/Production, or a deliberate Development change)

Never hardcode Staging/Production keys — generate a unique pair and store both halves as
GitHub secrets (`{ENVIRONMENT}_AUTH_JWT_PUBLIC_KEY`/`_PRIVATE_KEY`), consumed only via the
Kubernetes secret above. Generate with a throwaway Console app (from AGENTS.md /
`Nano.App.Api/README.md`'s `## Authentication` section):

```csharp
using System.Security.Cryptography;

using var rsa = RSA.Create();

var publicKey = rsa
    .ExportRSAPublicKeyPem()
    .Replace("-----BEGIN RSA PUBLIC KEY-----", "")
    .Replace("-----END RSA PUBLIC KEY-----", "")
    .Replace("\n", string.Empty);

var privateKey = rsa
    .ExportRSAPrivateKeyPem()
    .Replace("-----BEGIN RSA PRIVATE KEY-----", "")
    .Replace("-----END RSA PRIVATE KEY-----", "")
    .Replace("\n", string.Empty);

Console.WriteLine("PUBLIC KEY:");
Console.WriteLine(publicKey);
Console.WriteLine();
Console.WriteLine("PRIVATE KEY:");
Console.WriteLine(privateKey);

Console.Read();
```

## After making the change

- 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.
- 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
  must change **identically across every app** in the solution, or apps stop being able to
  validate each other's locally-issued tokens.
- If step 2 stopped the skill early for a missing Identity prerequisite, that's the whole
  response — don't partially wire persistent auth while waiting on it.
- If step 6 applied (API-key was already configured), restate the automatic scheme-switch and
  the newly-visible `/auth/login/apikey` endpoint one more time — it's a real behavior change on
  an app that already had callers, worth a second confirmation, not just a note in passing.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…