Skip to content

Commit 46734f9

Browse files
committed
Feature: Client Credentials Flow Backend
1 parent 576f95b commit 46734f9

21 files changed

+1151
-253
lines changed

DEVPLAN.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ This document provides a detailed breakdown of tasks, components, features, test
398398
### Feature: Client Credentials Flow Backend
399399

400400
* **Component:** Token Endpoint (Grant Type: `client_credentials`)
401-
- [ ] Extend `POST /token` endpoint to handle `grant_type=client_credentials`.
401+
- [x] Extend `POST /token` endpoint to handle `grant_type=client_credentials`.
402402
* *Guidance:*
403403
* Accepts parameters: `grant_type`, `client_id`, `client_secret`, `scope` (optional).
404404
* *Guidance:*
@@ -412,12 +412,13 @@ This document provides a detailed breakdown of tasks, components, features, test
412412
* *Guidance:*
413413
* Return access token in the response.
414414
* **Test Case (Integration):**
415-
- [ ] `POST /token` with `grant_type=client_credentials` and valid client credentials
415+
- [x] `POST /token` with `grant_type=client_credentials` and valid client credentials
416416
returns an access token.
417-
- [ ] Token represents the client (e.g., `sub` claim is `client_id`).
418-
- [ ] Request fails if client credentials are invalid.
419-
- [ ] Request fails if requested scopes are not allowed for the client.
420-
- [ ] **Update README.md** with details on `/token` endpoint for `client_credentials` grant type.
417+
- [x] Token represents the client (e.g., `sub` claim is `client_id`).
418+
- [x] Request fails if client credentials are invalid.
419+
- [x] Request fails if requested scopes are not allowed for the client.
420+
- [x] Client authentication works with both Basic Auth header and request body parameters.
421+
- [x] **Update README.md** with details on `/token` endpoint for `client_credentials` grant type.
421422

422423
---
423424

LLMINDEX.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ things are or how they were supposed to work.
4444
* Added automatic cleanup/expiry for authorization codes (`src/CoreIdent.Storage.EntityFrameworkCore/Services/AuthorizationCodeCleanupService.cs`).
4545
* Ensured robust concurrency handling in `IAuthorizationCodeStore` implementations.
4646
* **In Progress:** Client Credentials Flow, Discovery endpoints.
47+
* **Completed:** Client Credentials Flow (`grant_type=client_credentials` added to `/token` endpoint logic).
48+
* **Next:** Discovery endpoints.
4749
* **Phase 4 (Future):** User Interaction & External Integrations (Consent, UI, MFA, Passwordless).
4850
* **Phase 5 (Future):** Advanced Features & Polish (More Flows, Extensibility, Templates).
4951
* **Reference:** `DEVPLAN.md`: Detailed breakdown of tasks for each phase.
@@ -112,6 +114,7 @@ This is the central library containing the core logic, interfaces, and models.
112114
* Maps `GET /authorize` endpoint. Includes robust concurrency handling for code generation.
113115
* Enhances `POST /token` endpoint to handle `grant_type=authorization_code` with PKCE validation.
114116
* Token refresh endpoint (`POST /token/refresh`) implements token theft detection with configurable behavior.
117+
* Added `client_credentials` grant type handling to `/token` endpoint.
115118

116119
## 6. EF Core Storage Project Details: `src\CoreIdent.Storage.EntityFrameworkCore`
117120

@@ -129,6 +132,8 @@ This is the central library containing the core logic, interfaces, and models.
129132
* `AuthorizationCodeCleanupService.cs`: Background service that automatically removes expired authorization codes.
130133
* `Extensions`: Contains DI extensions.
131134
* `CoreIdentEntityFrameworkCoreExtensions.cs`: Contains `AddCoreIdentEntityFrameworkStores` extension to register EF Core stores (Scoped) with optional token and authorization code cleanup services.
135+
* **Next Steps:** Client Credentials Flow, Discovery endpoints
136+
* **Next Steps:** Discovery endpoints
132137

133138
## 6.5 Delegated User Store Adapter Project Details: `src\CoreIdent.Adapters.DelegatedUserStore`
134139

@@ -145,7 +150,7 @@ This is the central library containing the core logic, interfaces, and models.
145150
* `AuthorizationCodeFlowTests.cs`: Tests for the Authorization Code Flow with PKCE (including happy path and negative path tests).
146151
* `RefreshTokenEndpointTests.cs`: Tests for the token refresh endpoint, including token theft detection scenarios.
147152
* **`CoreIdent.TestHost`:** A helper project providing a shared `WebApplicationFactory` for integration tests.
148-
* **Frameworks:** Uses `xUnit` as the test runner and `Shouldly` for assertions. Mocking is done using `Moq`.
153+
* **Frameworks:** Uses `xUnit` (v3 - see [What's New](https://xunit.net/docs/getting-started/v3/whats-new) and [Migration Guide](https://xunit.net/docs/getting-started/v3/migration)) as the test runner and `Shouldly` for assertions. Mocking is done using `Moq`.
149154

150155
## 8. Documentation & Root Files
151156

@@ -190,7 +195,10 @@ This is the central library containing the core logic, interfaces, and models.
190195
* Enhanced `CoreIdentOptionsValidator` to validate token security configuration, ensuring consistent security behavior.
191196
* Implemented secure hashing of refresh token handles using SHA-256 with user/client ID salting.
192197
* Added support for storing both raw (legacy) and hashed token handles during migration period.
193-
* **Next Steps:** Client Credentials Flow, Discovery endpoints
198+
* **Completed:** Client Credentials Flow (`grant_type=client_credentials` added to `/token` endpoint logic).
199+
* **Next:** Discovery endpoints.
200+
* **Phase 4 (Future):** User Interaction & External Integrations (Consent, UI, MFA, Passwordless).
201+
* **Phase 5 (Future):** Advanced Features & Polish (More Flows, Extensibility, Templates).
194202

195203
## 10. Conclusion
196204

README.md

Lines changed: 27 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Tired of wrestling with complex identity vendors or rolling your own auth from s
5353

5454
**Where CoreIdent is heading (Future Phases):**
5555

56-
* **Full OAuth 2.0 / OIDC Server:** Implementing remaining standard flows (Client Credentials, Implicit, Hybrid) for web apps, SPAs, mobile apps, and APIs.
56+
* **Full OAuth 2.0 / OIDC Server:** Implementing remaining standard flows (~~Client Credentials~~, Implicit, Hybrid) for web apps, SPAs, mobile apps, and APIs.
5757
* **OIDC Compliance:** Discovery (`/.well-known/openid-configuration`), JWKS (`/.well-known/jwks.json`), ID Tokens.
5858
* **User Interaction:** Consent screens, standard logout endpoints.
5959
* **Extensible Provider Model:**
@@ -69,6 +69,11 @@ Tired of wrestling with complex identity vendors or rolling your own auth from s
6969
* Secure token storage and management
7070
* Offline authentication support
7171
* **Tooling:** `dotnet new` templates, comprehensive documentation.
72+
* **(In Progress)** Client Credentials Flow.
73+
* **(Completed)** Client Credentials Flow (`/auth/token` grant type `client_credentials`).
74+
* **(In Progress)** OIDC Discovery & JWKS Endpoints.
75+
76+
For more details on these features, see the [Developer Training Guide](./docs/Developer_Training_Guide.md).
7277

7378
**Is this a replacement for IdentityServer?**
7479

@@ -431,8 +436,13 @@ app.UseHttpsRedirection();
431436
app.UseAuthentication(); // Must be called before UseAuthorization
432437
app.UseAuthorization();
433438

434-
// Map CoreIdent endpoints (default prefix is /auth)
435-
app.MapCoreIdentEndpoints("/auth"); // Use basePath parameter to change, e.g., app.MapCoreIdentEndpoints(basePath: "/identity");
439+
// Map CoreIdent endpoints
440+
app.MapCoreIdentEndpoints(options =>
441+
{
442+
// Example: Customize the base path or specific endpoints
443+
// options.BasePath = "/identity";
444+
// options.RegisterPath = "signup";
445+
});
436446

437447
// Map your application's endpoints/controllers
438448
app.MapGet("/", () => "Hello World!");
@@ -447,9 +457,9 @@ app.Run();
447457

448458
### 4. Core Functionality Available Now (Phase 3 In Progress)
449459

450-
With the setup above, the following CoreIdent endpoints are available (default prefix `/auth`):
460+
With the setup above, the following CoreIdent endpoints are available (default prefix `/auth`, configurable via `MapCoreIdentEndpoints`):
451461

452-
* `POST /auth/register`: Register a new user (requires non-delegated `IUserStore`, e.g., EF Core, or `CreateUserAsync` delegate).
462+
* `POST /auth/register` (or configured path): Register a new user.
453463
* **Request Body**: `{ "email": "user@example.com", "password": "YourPassword123!" }`
454464
* **Response Status Codes**: `201 Created`, `400 Bad Request`, `409 Conflict`
455465
* **Usage Example (curl)**:
@@ -459,7 +469,7 @@ With the setup above, the following CoreIdent endpoints are available (default p
459469
-d '{"email": "user@example.com", "password": "YourSecurePassword123!"}'
460470
```
461471

462-
* `POST /auth/login`: Authenticates a user with email/password and issues JWT tokens.
472+
* `POST /auth/login` (or configured path): Authenticates a user with email/password and issues JWT tokens.
463473
* **Request Body**: `{ "email": "user@example.com", "password": "YourPassword123!" }`
464474
* **Response Status Codes**: `200 OK`, `400 Bad Request`, `401 Unauthorized`, `500 Internal Server Error`
465475
* **Response Body**:
@@ -480,22 +490,26 @@ With the setup above, the following CoreIdent endpoints are available (default p
480490
-d '{"email": "user@example.com", "password": "YourSecurePassword123!"}'
481491
```
482492

483-
* `POST /auth/token/refresh`: Exchange a valid refresh token for new tokens (uses `IRefreshTokenStore`).
484-
* **Request Body**: `{ "refreshToken": "abcdef123456..." }`
485-
* **Response Body**: (Same as login)
493+
* `POST /auth/token` (grant_type=refresh_token) (or configured path): Exchange a valid refresh token for new tokens.
494+
* **Request Body (form-urlencoded)**: `grant_type=refresh_token&refresh_token=abcdef123456...`
495+
* **Response Body**: (Same as login, potentially without refresh token depending on config/flow)
486496
* **Security**: Implements refresh token rotation and **token theft detection** (family tracking & revocation) by default. You can opt-out via `CoreIdentOptions.TokenSecurity.EnableTokenFamilyTracking = false`.
487497

488498
**OAuth 2.0 / OIDC Endpoints (Phase 3):**
489499

490-
* `GET /auth/authorize`: Initiates the Authorization Code flow. Validates the request, authenticates the user, and redirects back with an authorization code.
500+
* `GET /auth/authorize` (or configured path): Initiates the Authorization Code flow.
491501
* Required parameters: `client_id`, `redirect_uri`, `response_type=code`, `scope`
492502
* Recommended parameters: `state`, `nonce`
493503
* PKCE parameters: `code_challenge`, `code_challenge_method=S256`
494504
* Example: `/auth/authorize?client_id=my-client&response_type=code&redirect_uri=https://my-app.com/callback&scope=openid%20profile&state=abc123&code_challenge=<challenge>&code_challenge_method=S256`
495-
* `POST /auth/token` (grant_type=authorization_code): Exchanges an authorization code for tokens.
505+
* `POST /auth/token` (grant_type=authorization_code) (or configured path): Exchanges an authorization code for tokens.
496506
* Required parameters (form-encoded): `grant_type=authorization_code`, `code`, `redirect_uri`, `client_id`, `code_verifier` (for PKCE)
497-
* Confidential clients also require authentication.
507+
* Confidential clients also require authentication (Basic Auth or request body).
498508
* Returns: `{ "access_token": "...", "token_type": "Bearer", "expires_in": 900, "refresh_token": "...", "id_token": "..." }`
509+
* `POST /auth/token` (grant_type=client_credentials) (or configured path): Issues an access token directly to a confidential client.
510+
* Required parameters (form-encoded): `grant_type=client_credentials`, `scope` (optional)
511+
* Requires client authentication (Basic Auth header OR `client_id`/`client_secret` in request body).
512+
* Returns: `{ "access_token": "...", "token_type": "Bearer", "expires_in": 900, "scope": "..." }` (No refresh token)
499513

500514
**Storage:**
501515
* **EF Core:** Provides persistence for users, refresh tokens, clients, scopes, **and authorization codes**. Requires `CoreIdent.Storage.EntityFrameworkCore` and DB migrations. **Expired authorization codes are cleaned up automatically by a background service.**
@@ -574,56 +588,4 @@ Contributions, feedback, and ideas are highly welcome! Please refer to the (upco
574588

575589
### Why does the DI registration order matter?
576590
**Order is critical** because:
577-
- `AddCoreIdent()` registers the core services and default (in-memory) stores.
578-
- `AddDbContext<YourDbContext>()` registers your EF Core context in the DI container.
579-
- `AddCoreIdentEntityFrameworkStores<YourDbContext>()` replaces the in-memory stores with EF Core-backed implementations, which depend on your DbContext being registered first.
580-
581-
If you call `AddCoreIdentEntityFrameworkStores` before `AddDbContext`, the EF Core stores will not be able to resolve the context and will fail at runtime.
582-
583-
### Common Issues & Solutions
584-
585-
- **Error: "No service for type 'YourDbContext' has been registered."**
586-
- **Solution:** Ensure you called `AddDbContext<YourDbContext>()` *before* `AddCoreIdentEntityFrameworkStores<YourDbContext>()`.
587-
588-
- **Error: "Table 'Users'/'RefreshTokens' does not exist" or similar database errors**
589-
- **Solution:** You likely have not run EF Core migrations. See the migration instructions below.
590-
591-
- **Error: "Cannot access a disposed object" (when using SQLite in-memory for tests)**
592-
- **Solution:** Ensure the SQLite connection remains open for the lifetime of your test host. See integration test examples for details.
593-
594-
### EF Core Migration Process (Quick Reference)
595-
596-
1. **Install EF Core CLI tools (if not already):**
597-
```bash
598-
dotnet tool install --global dotnet-ef
599-
```
600-
2. **Add a migration:**
601-
```bash
602-
dotnet ef migrations add InitialCoreIdentSchema --context YourApplicationDbContext --project src/CoreIdent.Storage.EntityFrameworkCore --startup-project src/YourWebAppProject -o Data/Migrations
603-
```
604-
- Replace `YourApplicationDbContext` with your DbContext class name.
605-
- Adjust `--project` and `--startup-project` paths as needed.
606-
3. **Apply the migration:**
607-
```bash
608-
dotnet ef database update --context YourApplicationDbContext --project src/CoreIdent.Storage.EntityFrameworkCore --startup-project src/YourWebAppProject
609-
```
610-
611-
**Official EF Core Migrations Documentation:**
612-
- [EF Core Migrations Guide (Microsoft Docs)](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli)
613-
614-
### Sample Migration Output
615-
When you run `dotnet ef migrations add InitialCoreIdentSchema`, you should see output similar to:
616-
```
617-
Build started...
618-
Build succeeded.
619-
To undo this action, use 'ef migrations remove'
620-
Done. To undo this action, use 'ef migrations remove'
621-
```
622-
And after `dotnet ef database update`:
623-
```
624-
Build started...
625-
Build succeeded.
626-
Applying migration '20250413033857_InitialCoreIdentSchema'.
627-
Done.
628-
```
629-
If you see errors, double-check your DI registration order and that your DbContext is correctly configured and referenced.
591+
- `

RELEASE_NOTES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# CoreIdent Release Notes
22

3+
## Version 0.3.4
4+
- **Client Credentials Flow:** Implemented `POST /token` (with `grant_type=client_credentials`) endpoint to support machine-to-machine authentication. Requires `IClientStore` implementation (e.g., EF Core). Validates client credentials and allowed scopes, issuing an access token representing the client.
5+
36
## Version 0.3.3
47
- Enhance `CoreIdentOptionsValidator` to include more comprehensive checks (e.g., valid Issuer URI, reasonable lifetimes).
58

nextfeature.instruction

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Review @Project_Overview.md @Technical_Plan.md @README.md @LLMINDEX.md @DEVPLAN.md
2+
3+
Then let's implement the next unimplemented feature in the DEVPLAN checklist.
4+
- If you are going to create new files, do not presume the LLMINDEX.md is up-to-date, meaning always scan the project for similar looking files or for files that contain multiple classes that might have what you might be otherwise creating.
5+
- Always create unit or integration tests for every new feature.
6+
- Always run `dotnet test` and debug the tests before calling the feature completed
7+
- Clean up any lingering build warnings
8+
- The feature isn't done until the checklist item(s) in DEVPLAN.md is checked (edit the file)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System;
2+
3+
namespace CoreIdent.Core.Configuration;
4+
5+
/// <summary>
6+
/// Configures the routes for CoreIdent endpoints.
7+
/// </summary>
8+
public class CoreIdentRouteOptions
9+
{
10+
/// <summary>
11+
/// The base path for all CoreIdent endpoints. Defaults to "/auth".
12+
/// Must start with a '/'.
13+
/// </summary>
14+
public string BasePath { get; set; } = "/auth";
15+
16+
/// <summary>
17+
/// Path for the user registration endpoint. Defaults to "register". Relative to BasePath.
18+
/// </summary>
19+
public string RegisterPath { get; set; } = "register";
20+
21+
/// <summary>
22+
/// Path for the user login endpoint. Defaults to "login". Relative to BasePath.
23+
/// </summary>
24+
public string LoginPath { get; set; } = "login";
25+
26+
/// <summary>
27+
/// Path for the token endpoint (issuance and refresh). Defaults to "token". Relative to BasePath.
28+
/// </summary>
29+
public string TokenPath { get; set; } = "token";
30+
31+
/// <summary>
32+
/// Path for the token refresh specific endpoint (legacy/alternative). Defaults to "token/refresh". Relative to BasePath.
33+
/// Note: The main /token endpoint is preferred for refresh grant_type.
34+
/// </summary>
35+
public string RefreshTokenPath { get; set; } = "token/refresh";
36+
37+
/// <summary>
38+
/// Path for the OAuth/OIDC authorization endpoint. Defaults to "authorize". Relative to BasePath.
39+
/// </summary>
40+
public string AuthorizePath { get; set; } = "authorize";
41+
42+
/// <summary>
43+
/// Path for the OIDC UserInfo endpoint. Defaults to "userinfo". Relative to BasePath.
44+
/// </summary>
45+
public string UserInfoPath { get; set; } = "userinfo"; // Not yet implemented
46+
47+
/// <summary>
48+
/// Path for the OIDC Discovery configuration endpoint. Defaults to ".well-known/openid-configuration". Relative to the root, not BasePath.
49+
/// </summary>
50+
public string DiscoveryPath { get; set; } = ".well-known/openid-configuration"; // Not yet implemented
51+
52+
/// <summary>
53+
/// Path for the OIDC JWKS endpoint. Defaults to ".well-known/jwks.json". Relative to the root, not BasePath.
54+
/// </summary>
55+
public string JwksPath { get; set; } = ".well-known/jwks.json"; // Not yet implemented
56+
57+
/// <summary>
58+
/// Path for the consent endpoint. Defaults to "consent". Relative to BasePath.
59+
/// </summary>
60+
public string ConsentPath { get; set; } = "consent"; // Not yet implemented
61+
62+
/// <summary>
63+
/// Path for the end session/logout endpoint. Defaults to "endsession". Relative to BasePath.
64+
/// </summary>
65+
public string EndSessionPath { get; set; } = "endsession"; // Not yet implemented
66+
67+
// Helper method to combine BasePath and relative path
68+
internal string Combine(string relativePath)
69+
{
70+
if (string.IsNullOrWhiteSpace(relativePath))
71+
{
72+
throw new ArgumentException("Relative path cannot be null or whitespace.", nameof(relativePath));
73+
}
74+
75+
// Handle paths that should be relative to root, not base path
76+
if (relativePath.StartsWith(".well-known/"))
77+
{
78+
// Ensure it starts with exactly one '/'
79+
return "/" + relativePath.TrimStart('/');
80+
}
81+
82+
if (string.IsNullOrWhiteSpace(BasePath) || !BasePath.StartsWith("/"))
83+
{
84+
throw new InvalidOperationException($"{nameof(BasePath)} must be configured and start with a '/'. Current value: '{BasePath}'");
85+
}
86+
87+
// Ensure BasePath ends with a single '/' and relativePath does not start with one
88+
var basePathNormalized = BasePath.TrimEnd('/') + "/";
89+
var relativePathNormalized = relativePath.TrimStart('/');
90+
91+
return basePathNormalized + relativePathNormalized;
92+
}
93+
}

0 commit comments

Comments
 (0)