|
| 1 | +# BasicAuth |
| 2 | + |
| 3 | +The [`SimpleW.Helper.BasicAuth`](https://www.nuget.org/packages/SimpleW.Helper.BasicAuth) package provides a lightweight HTTP Basic authentication helper for SimpleW. |
| 4 | + |
| 5 | + |
| 6 | +## Features |
| 7 | + |
| 8 | +This package is intentionally focused on the authentication engine only: |
| 9 | +- parse the `Authorization` header |
| 10 | +- validate a username/password pair |
| 11 | +- create a `HttpPrincipal` |
| 12 | +- send a `401` Basic challenge |
| 13 | + |
| 14 | +It does **not** decide which routes must be protected. |
| 15 | +That policy stays in your own custom middleware, which makes this package a good fit for: |
| 16 | +- custom auth attributes based on `IHandlerMetadata` |
| 17 | +- controller-specific authorization rules |
| 18 | +- mixed authentication strategies chosen by the application |
| 19 | + |
| 20 | + |
| 21 | +## Requirements |
| 22 | + |
| 23 | +- .NET 8.0 |
| 24 | +- SimpleW (core server) |
| 25 | + |
| 26 | + |
| 27 | +## Installation |
| 28 | + |
| 29 | +Install the package from NuGet: |
| 30 | + |
| 31 | +```sh |
| 32 | +$ dotnet add package SimpleW.Helper.BasicAuth --version 26.0.0-rc.20260415-1732 |
| 33 | +``` |
| 34 | + |
| 35 | + |
| 36 | +## Configuration options |
| 37 | + |
| 38 | +### BasicAuthHelper |
| 39 | + |
| 40 | +| Method | Description | |
| 41 | +| ------ | ----------- | |
| 42 | +| `TryAuthenticate(session, out principal)` | Parses the `Authorization` header, validates credentials, and returns a `HttpPrincipal` when authentication succeeds. | |
| 43 | +| `SendChallengeAsync(session, realm)` | Sends a `401 Unauthorized` response with the `WWW-Authenticate` header for Basic auth. | |
| 44 | + |
| 45 | +### BasicAuthOptions |
| 46 | + |
| 47 | +| Option | Default | Description | |
| 48 | +| ------ | ------- | ----------- | |
| 49 | +| `Users` | empty | Static username/password list used when no custom validator is provided. | |
| 50 | +| `CredentialValidator` | `null` | Optional callback used to validate username/password pairs yourself. | |
| 51 | +| `PrincipalFactory` | built-in | Maps a successful authentication to a `HttpPrincipal`. | |
| 52 | + |
| 53 | +### BasicAuthContext |
| 54 | + |
| 55 | +| Property | Description | |
| 56 | +| -------- | ----------- | |
| 57 | +| `Session` | Current `HttpSession`. | |
| 58 | +| `Username` | Username extracted from the `Authorization` header. | |
| 59 | +| `Password` | Password extracted from the `Authorization` header. | |
| 60 | + |
| 61 | + |
| 62 | +## Minimal Example |
| 63 | + |
| 64 | +This example shows the intended architecture: |
| 65 | +- a `BasicAuthHelper` handles the Basic auth protocol |
| 66 | +- a custom middleware reads handler metadata from `session.Metadata` |
| 67 | +- controllers decide which endpoints require authentication |
| 68 | + |
| 69 | +```csharp |
| 70 | +using System.Net; |
| 71 | +using SimpleW; |
| 72 | +using SimpleW.Helper.BasicAuth; |
| 73 | + |
| 74 | +var server = new SimpleWServer(IPAddress.Any, 2015); |
| 75 | + |
| 76 | +// configure basic helper |
| 77 | +BasicAuthHelper basic = new(options => { |
| 78 | + options.Users = [ |
| 79 | + new BasicUser("admin", "secret") |
| 80 | + ]; |
| 81 | +}); |
| 82 | + |
| 83 | +// use the basic helper in a custom auth middleware |
| 84 | +server.UseMiddleware(async (session, next) => { |
| 85 | + |
| 86 | + // fast path for anonymous |
| 87 | + if (session.Metadata.Has<AllowAnonymousAttribute>()) { |
| 88 | + await next().ConfigureAwait(false); |
| 89 | + return; |
| 90 | + } |
| 91 | + |
| 92 | + // fast path if not basic attribute |
| 93 | + BasicAuthAttribute? auth = session.Metadata.Get<BasicAuthAttribute>(); |
| 94 | + if (auth == null) { |
| 95 | + await next().ConfigureAwait(false); |
| 96 | + return; |
| 97 | + } |
| 98 | + |
| 99 | + // send challenge is not authenticate |
| 100 | + if (!basic.TryAuthenticate(session, out HttpPrincipal principal)) { |
| 101 | + await basic.SendChallengeAsync(session, auth.Realm).ConfigureAwait(false); |
| 102 | + return; |
| 103 | + } |
| 104 | + |
| 105 | + // set the Session Principal with the principal found by basic helper |
| 106 | + session.Principal = principal; |
| 107 | + |
| 108 | + await next().ConfigureAwait(false); |
| 109 | +}); |
| 110 | + |
| 111 | +server.MapController<AdminController>("/api"); |
| 112 | + |
| 113 | +await server.RunAsync(); |
| 114 | + |
| 115 | +[Route("/admin")] |
| 116 | +[BasicAuth("Admin Area")] |
| 117 | +public sealed class AdminController : Controller { |
| 118 | + |
| 119 | + [Route("GET", "/me")] |
| 120 | + public object Me() { |
| 121 | + return new { |
| 122 | + user = Principal.Name |
| 123 | + }; |
| 124 | + } |
| 125 | + |
| 126 | + [AllowAnonymous] |
| 127 | + [Route("GET", "/health")] |
| 128 | + public object Health() { |
| 129 | + return new { ok = true }; |
| 130 | + } |
| 131 | + |
| 132 | +} |
| 133 | + |
| 134 | +// definie a basic attribute |
| 135 | +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] |
| 136 | +public sealed class BasicAuthAttribute : Attribute, IHandlerMetadata { |
| 137 | + |
| 138 | + public BasicAuthAttribute(string realm = "Restricted") { |
| 139 | + Realm = realm; |
| 140 | + } |
| 141 | + |
| 142 | + public string Realm { get; } |
| 143 | + |
| 144 | +} |
| 145 | + |
| 146 | +// define a Anonymous attribute |
| 147 | +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] |
| 148 | +public sealed class AllowAnonymousAttribute : Attribute, IHandlerMetadata { |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +In this model: |
| 153 | +- the helper performs authentication |
| 154 | +- the middleware decides whether the current handler requires authentication thanks to custom attribute |
| 155 | +- the controller stays clean and only declares intent through metadata |
| 156 | + |
| 157 | + |
| 158 | +## Custom credential validation |
| 159 | + |
| 160 | +Instead of a static users list, you can validate credentials yourself: |
| 161 | + |
| 162 | +```csharp |
| 163 | +BasicAuthHelper basic = new(options => { |
| 164 | + options.CredentialValidator = (username, password) => { |
| 165 | + return username == "admin" && password == "secret"; |
| 166 | + }; |
| 167 | +}); |
| 168 | +``` |
| 169 | + |
| 170 | +This is useful when credentials come from: |
| 171 | +- a database |
| 172 | +- a configuration provider |
| 173 | +- an external service |
| 174 | + |
| 175 | + |
| 176 | +## Custom principal mapping |
| 177 | + |
| 178 | +You can fully control how an authenticated user becomes a `HttpPrincipal`: |
| 179 | + |
| 180 | +```csharp |
| 181 | +BasicAuthHelper basic = new(options => { |
| 182 | + options.Users = [ |
| 183 | + new BasicUser("admin", "secret") |
| 184 | + ]; |
| 185 | + |
| 186 | + options.PrincipalFactory = context => { |
| 187 | + return new HttpPrincipal(new HttpIdentity( |
| 188 | + isAuthenticated: true, |
| 189 | + authenticationType: "Basic", |
| 190 | + identifier: context.Username, |
| 191 | + name: context.Username, |
| 192 | + email: null, |
| 193 | + roles: [ "admin" ], |
| 194 | + properties: [ |
| 195 | + new IdentityProperty("login", context.Username), |
| 196 | + new IdentityProperty("source", "basic-auth") |
| 197 | + ] |
| 198 | + )); |
| 199 | + }; |
| 200 | +}); |
| 201 | +``` |
| 202 | + |
| 203 | + |
| 204 | +## Integration Summary |
| 205 | + |
| 206 | +| Step | Responsibility | |
| 207 | +| ---- | -------------- | |
| 208 | +| Parse Basic auth header | `BasicAuthHelper` | |
| 209 | +| Validate credentials | `BasicAuthHelper` | |
| 210 | +| Build `HttpPrincipal` | `BasicAuthHelper` | |
| 211 | +| Decide whether auth is required | your middleware | |
| 212 | +| Declare route intent | your `IHandlerMetadata` attributes | |
| 213 | + |
| 214 | + |
| 215 | +## Security Notes |
| 216 | + |
| 217 | +- HTTP Basic credentials are only base64-encoded, not encrypted |
| 218 | +- Always use HTTPS in production |
| 219 | +- Keep realms explicit so browser prompts stay understandable |
| 220 | +- Treat `CredentialValidator` and `PrincipalFactory` as trusted application code |
| 221 | + |
| 222 | + |
| 223 | +## When to use the service package instead |
| 224 | + |
| 225 | +If you only need simple prefix-based protection such as: |
| 226 | +- `/admin` |
| 227 | +- `/metrics` |
| 228 | +- `/internal` |
| 229 | + |
| 230 | +and you do not need custom handler metadata, use [`SimpleW.Service.BasicAuth`](./service-basicauth.md) instead. |
| 231 | + |
| 232 | +That package is a thin module built on top of this helper. |
0 commit comments