|
| 1 | +# CLAUDE.md — guidance for Claude Code in this repository |
| 2 | + |
| 3 | +You are working on **`stromcom/auth-client`** — the official PHP client SDK |
| 4 | +for the `auth.stromcom.cz` SSO/OAuth 2.0 server. This file collects everything |
| 5 | +you need to keep this package consistent across future sessions. |
| 6 | + |
| 7 | +## What this package is |
| 8 | + |
| 9 | +Self-contained PHP 8.3+ client library: |
| 10 | +- OAuth 2.0 Authorization Code + PKCE flow (web app login) |
| 11 | +- OAuth 2.0 Client Credentials flow (machine-to-machine) |
| 12 | +- Refresh token grant |
| 13 | +- Local JWT verification via JWKS with TTL cache and `kid`-rotation |
| 14 | +- UserInfo (`/me`) call |
| 15 | +- Logout URL builder |
| 16 | +- OIDC discovery (`/.well-known/openid-configuration`) |
| 17 | +- RFC 9068 strict (`iss`, `token_use`, `at+jwt` required) |
| 18 | + |
| 19 | +**Production runtime dependencies:** only `lcobucci/jwt: ^5.5` (and its |
| 20 | +transitive `psr/clock`). JWT parsing, signature verification and temporal |
| 21 | +claim validation go through `lcobucci/jwt`. JWKS fetching, caching, key |
| 22 | +rotation, OAuth grant flows and PKCE are in-house. cURL is the HTTP |
| 23 | +transport. |
| 24 | + |
| 25 | +## Server coupling |
| 26 | + |
| 27 | +This SDK targets the `auth.stromcom.cz` server. The server is the source of |
| 28 | +truth for endpoints and the JWT claim contract. When adding a feature here, |
| 29 | +check whether the server already supports it. Don't invent endpoints — if |
| 30 | +the SDK needs a new server feature, change the server first. |
| 31 | + |
| 32 | +## Code style |
| 33 | + |
| 34 | +- PHP 8.3+, `declare(strict_types=1);` |
| 35 | +- 2-space indent |
| 36 | +- `final class Foo {` with same-line brace |
| 37 | +- Constructor property promotion with `public readonly` everywhere possible |
| 38 | +- camelCase method/property names |
| 39 | +- No PHPDoc unless adding type information beyond what PHP can express |
| 40 | + (array shapes, `@throws` on interfaces, examples on `Client` methods) |
| 41 | +- **No comments unless the WHY is non-obvious.** Code shouldn't explain what |
| 42 | + it does — names should. Comment only the surprising bits: a workaround for |
| 43 | + a third-party bug, a non-obvious invariant, a security-critical decision. |
| 44 | + |
| 45 | +If you ever need an example of what good prose looks like, read the existing |
| 46 | +comments in `src/TokenVerifier.php` and `src/Internal/JwkRsaKey.php`. The |
| 47 | +comment in `LeagueAccessTokenEntity.php` on the server is also a good model. |
| 48 | + |
| 49 | +## Layered structure |
| 50 | + |
| 51 | +``` |
| 52 | +src/ |
| 53 | +├── Client.php # Main facade. Per-flow methods. |
| 54 | +├── Configuration.php # Immutable config + endpoint derivation. |
| 55 | +├── TokenSet.php # The /oauth/token response, parsed. |
| 56 | +├── Claims.php # The decoded JWT payload, with helpers. |
| 57 | +├── Pkce.php # PKCE verifier + challenge generator. |
| 58 | +├── TokenVerifier.php # JWKS-based RS256 verification. |
| 59 | +├── Http/ |
| 60 | +│ ├── HttpClientInterface.php # Tiny PSR-7-ish interface (no PSR-7 dep). |
| 61 | +│ ├── CurlHttpClient.php # Default cURL transport. |
| 62 | +│ └── RawResponse.php |
| 63 | +├── Jwks/ |
| 64 | +│ ├── JwksCacheInterface.php # Cache contract. |
| 65 | +│ ├── InMemoryJwksCache.php # Per-process. Good for CLI / workers. |
| 66 | +│ ├── ApcuJwksCache.php # Shared memory. Right default for Lambda + FPM. |
| 67 | +│ └── FileJwksCache.php # Single-host fallback when APCu is unavailable. |
| 68 | +├── Internal/ |
| 69 | +│ ├── JwkRsaKey.php # JWK (n, e) → PEM via ASN.1. |
| 70 | +│ └── SystemClock.php # PSR-20 clock for lcobucci's LooseValidAt constraint. |
| 71 | +└── Exception/ |
| 72 | + ├── AuthClientException.php # Base (catch-all). |
| 73 | + ├── ConfigurationException.php |
| 74 | + ├── TransportException.php |
| 75 | + ├── OAuthServerException.php # Server returned an OAuth `error` payload. |
| 76 | + ├── TokenVerificationException.php |
| 77 | + └── AuthorizationException.php # Role/group/scope/token_use guard. |
| 78 | +``` |
| 79 | + |
| 80 | +**`Internal/` is for plumbing.** Anything under it is `@internal` and not part |
| 81 | +of the SDK's public API. Don't expose new helpers there to consumers — if a |
| 82 | +helper deserves to be public, move it up. |
| 83 | + |
| 84 | +## Architectural rules |
| 85 | + |
| 86 | +1. **Runtime dependencies are deliberately tiny.** Only `lcobucci/jwt` (and |
| 87 | + its transitive `psr/clock`). If you want to add another, justify it |
| 88 | + against the cost of every consumer pulling it in. Dev-dependencies |
| 89 | + (PHPUnit, PHPStan, php-cs-fixer) are unconstrained. |
| 90 | +2. **Don't replace `lcobucci/jwt`.** The auth server uses it transitively |
| 91 | + through `league/oauth2-server`, so both ends share the same JWT |
| 92 | + interpretation. Switching to `firebase/php-jwt` is a downgrade — it's |
| 93 | + currently flagged by `composer audit`. Switching to `web-token/jwt-framework` |
| 94 | + drags in a much larger dependency surface for the same job. |
| 95 | +3. **`Client` is the only public facade.** Don't add competing entry points. |
| 96 | + New flows are new methods on `Client`. |
| 97 | +4. **HTTP transport is injectable.** All HTTP goes through |
| 98 | + `HttpClientInterface`. Never call cURL directly anywhere else. |
| 99 | +5. **JWKS cache is injectable.** Always go through `JwksCacheInterface`. |
| 100 | +6. **Strict by default.** RFC 9068 requires `iss`, `token_use`. Don't add a |
| 101 | + "lenient mode" toggle without a strong reason — strict-in-what-you-accept |
| 102 | + is the security default and the server emits those claims. |
| 103 | +7. **`Claims` is read-only and rich.** When users ask "how do I check X", |
| 104 | + the answer should be "Claims has a method for that". Add to the API rather |
| 105 | + than telling users to dig into `$claims->all`. |
| 106 | +8. **Tests are unit + offline.** Never hit the network from unit tests. For |
| 107 | + end-to-end checks, `examples/smoke.php` exists and is run manually. |
| 108 | + |
| 109 | +## Adding features |
| 110 | + |
| 111 | +### A new grant type |
| 112 | +- Add a method on `Client` (mirror `clientCredentials()` / `exchangeCode()`). |
| 113 | +- Reuse `postToken()` — don't duplicate the request building. |
| 114 | +- Update `docs/auth-code-flow.md` or `docs/service-account.md` (or add a new doc). |
| 115 | +- Add a unit test against `Client` with a mock `HttpClientInterface`. |
| 116 | + |
| 117 | +### A new claim from the server |
| 118 | +- If it's an OIDC-standard claim → map it explicitly in `Claims::fromPayload`. |
| 119 | +- If it's a stromcom-specific claim → same, plus add convenience methods |
| 120 | + (`has*`, `require*`, `*ForProject`, etc.). |
| 121 | +- Update the "Claims — object API" section of `README.md`. |
| 122 | + |
| 123 | +### A new exception case |
| 124 | +- Always extend `AuthClientException` (so a top-level `catch` works). |
| 125 | +- Don't introduce a new exception that doesn't have at least one named |
| 126 | + factory method or constructor parameter capturing the failure context. |
| 127 | + |
| 128 | +## Server-side coupling |
| 129 | + |
| 130 | +This SDK is paired with `auth.stromcom.cz`. **Specific contract relied on:** |
| 131 | + |
| 132 | +- `GET /.well-known/jwks.json` — JWKS, RS256, `kid` from first 16 hex chars |
| 133 | + of `sha256(public_pem)`. |
| 134 | +- `GET /.well-known/openid-configuration` — OIDC discovery. |
| 135 | +- `POST /oauth/token` — grants: `authorization_code`, `refresh_token`, |
| 136 | + `client_credentials`. |
| 137 | +- `GET /oauth/authorize` — PKCE S256 supported. |
| 138 | +- `GET /me` — UserInfo with `Authorization: Bearer …`. |
| 139 | +- `GET /oauth/logout` — end-session, optional `post_logout_redirect_uri`. |
| 140 | + |
| 141 | +**JWT contract:** |
| 142 | +- Header: `{typ: "at+jwt", alg: "RS256", kid: "..."}` |
| 143 | +- Always present: `iss`, `sub`, `aud`, `iat`, `nbf`, `exp`, `jti`, `scopes`, `token_use` |
| 144 | +- Service tokens add: `client_id`, `client_name`, `roles`, `is_admin` |
| 145 | +- User tokens add (filtered by scope, OIDC Core 1.0 §5.4): |
| 146 | + `name`, `email`, `email_verified`, `picture`, `locale`, `zoneinfo`, |
| 147 | + `updated_at`, `roles`, `groups`, `is_admin` |
| 148 | + |
| 149 | +If the server changes any of this, this SDK needs corresponding updates. |
| 150 | + |
| 151 | +## Testing |
| 152 | + |
| 153 | +```bash |
| 154 | +composer install |
| 155 | +composer test # PHPUnit, no network |
| 156 | +composer phpstan # static analysis, level 8 |
| 157 | +composer ca # phpstan + test |
| 158 | +``` |
| 159 | + |
| 160 | +Unit tests live in `tests/` and mirror the `src/` layout. Use offline |
| 161 | +fixtures — JWKS sample documents, baked test keypairs (Windows PHP doesn't |
| 162 | +have `openssl.cnf` for runtime keygen). |
| 163 | + |
| 164 | +The smoke example (`examples/smoke.php`) is **the** end-to-end test. Run it |
| 165 | +manually against a running auth server with a service-account client: |
| 166 | + |
| 167 | +```bash |
| 168 | +AUTH_ISSUER=https://auth.stromcom.cz AUTH_CLIENT_ID=svc_… AUTH_CLIENT_SECRET=… \ |
| 169 | + php examples/smoke.php |
| 170 | +``` |
| 171 | + |
| 172 | +## Documentation |
| 173 | + |
| 174 | +- `README.md` — entry point, quickstart, claim API reference. |
| 175 | +- `docs/architecture.md` — internals. |
| 176 | +- `docs/auth-code-flow.md` — web app integration deep dive. |
| 177 | +- `docs/service-account.md` — M2M deep dive. |
| 178 | +- `docs/jwt-verification.md` — JWKS, claims, key rotation. |
| 179 | +- `docs/error-handling.md` — exception hierarchy + retry strategies. |
| 180 | +- `docs/security.md` — PKCE, state, secret storage, token storage. |
| 181 | +- `CHANGELOG.md` — semantic-versioned changelog. |
| 182 | +- `examples/*.php` — runnable examples, each self-contained. |
| 183 | + |
| 184 | +**Keep these in sync.** If you change a public API: |
| 185 | +1. Update the relevant `docs/*.md`. |
| 186 | +2. Update `README.md` if it appears in the quickstart or reference table. |
| 187 | +3. Add a `CHANGELOG.md` entry. |
| 188 | +4. Add or update an example demonstrating the change. |
| 189 | + |
| 190 | +## What this package is NOT |
| 191 | + |
| 192 | +- Not a generic OAuth 2.0 / OIDC library. It targets exactly one server |
| 193 | + (`auth.stromcom.cz`). Don't generalize without a clear reason. |
| 194 | +- Not an RFC 9068 reference. We aim for compliance with what auth.stromcom.cz |
| 195 | + emits; we don't aim to validate every weird edge case some other server |
| 196 | + might produce. |
| 197 | +- Not a session manager. Users persist tokens themselves (cookies, DB, |
| 198 | + whatever). The SDK only fetches and verifies. |
| 199 | +- Not a "framework". No autowiring, no DI container integration, no service |
| 200 | + provider, no PSR-7 emitter. Plain objects. |
| 201 | + |
| 202 | +## Common questions |
| 203 | + |
| 204 | +**"Should I add Guzzle support?"** No. The `HttpClientInterface` is one |
| 205 | +method wide. If a user wants Guzzle, they write an 8-line adapter. Adding a |
| 206 | +Guzzle dependency means every consumer of this SDK pulls in Guzzle. |
| 207 | + |
| 208 | +**"Should I add PSR-3 logging?"** Not yet. If you ever do, accept a |
| 209 | +`?LoggerInterface` on `Client` and log nothing by default. Don't take a hard |
| 210 | +dep on `psr/log`. |
| 211 | + |
| 212 | +**"Should I cache tokens automatically?"** No — token storage is the |
| 213 | +caller's concern (they know where their session lives). Provide a recipe in |
| 214 | +`examples/service-account-cached.php` instead. |
| 215 | + |
| 216 | +**"User asks for a feature the server doesn't support."** Fix the server |
| 217 | +first. This SDK should never paper over a missing server feature with a |
| 218 | +client-side workaround (the one exception: defensive parsing of optional |
| 219 | +fields). If you're tempted, stop and explain. |
| 220 | + |
| 221 | +## When in doubt |
| 222 | + |
| 223 | +Read the existing code. It's small (~1200 lines), idiomatic, and the |
| 224 | +conventions are consistent. New code that breaks the conventions is the |
| 225 | +problem, not the conventions. |
0 commit comments