mirror of https://github.com/abpframework/abp.git
176 changed files with 2563 additions and 1740 deletions
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
@ -0,0 +1,354 @@ |
|||
# Building an API Key Management System with ABP Framework |
|||
|
|||
API keys are one of the most common authentication methods for APIs, especially for machine-to-machine communication. In this article, I'll explain what API key authentication is, when to use it, and how to implement a complete API key management system using ABP Framework. |
|||
|
|||
## What is API Key Authentication? |
|||
|
|||
An API key is a unique identifier used to authenticate requests to an API. Unlike user credentials (username/password) or OAuth tokens, API keys are designed for: |
|||
|
|||
- **Programmatic access** - Scripts, CLI tools, and automated processes |
|||
- **Service-to-service communication** - Microservices authenticating with each other |
|||
- **Third-party integrations** - External systems accessing your API |
|||
- **IoT devices** - Embedded systems with limited authentication capabilities |
|||
- **Mobile/Desktop apps** - Native applications that need persistent authentication |
|||
|
|||
## Why Use API Keys? |
|||
|
|||
While modern authentication methods like OAuth2 and JWT are excellent for user authentication, API keys offer distinct advantages in certain scenarios: |
|||
|
|||
**Simplicity**: No complex OAuth flows or token refresh mechanisms. Just include the key in your request header. |
|||
|
|||
**Long-lived**: Unlike JWT tokens that expire in minutes/hours, API keys can remain valid for months or years, making them ideal for automated systems. |
|||
|
|||
**Revocable**: You can instantly revoke a compromised key without affecting user credentials. |
|||
|
|||
**Granular Control**: Different keys for different purposes (read-only, admin, specific services). |
|||
|
|||
## Real-World Use Cases |
|||
|
|||
Here are some practical scenarios where API key authentication shines: |
|||
|
|||
### 1. Mobile Applications |
|||
Your mobile app needs to call your backend APIs. Instead of storing user credentials or managing token refresh flows, use an API key. |
|||
|
|||
```csharp |
|||
// Mobile app configuration |
|||
var apiClient = new ApiClient("https://api.yourapp.com"); |
|||
apiClient.SetApiKey("sk_mobile_prod_abc123..."); |
|||
``` |
|||
|
|||
### 2. Microservice Communication |
|||
Service A needs to call Service B's protected endpoints. |
|||
|
|||
```csharp |
|||
// Order Service calling Inventory Service |
|||
var request = new HttpRequestMessage(HttpMethod.Get, "https://inventory-service/api/products"); |
|||
request.Headers.Add("X-Api-Key", _configuration["InventoryService:ApiKey"]); |
|||
``` |
|||
|
|||
### 3. Third-Party Integrations |
|||
You're providing APIs to external partners or customers. |
|||
|
|||
```bash |
|||
# Customer's integration script |
|||
curl -H "X-Api-Key: pk_partner_xyz789..." \ |
|||
https://api.yourplatform.com/api/orders |
|||
``` |
|||
|
|||
## Implementing API Key Management in ABP Framework |
|||
|
|||
Now let's see how to build a complete API key management system using ABP Framework. I've created an open-source implementation that you can use in your projects. |
|||
|
|||
### Project Overview |
|||
|
|||
The implementation consists of: |
|||
|
|||
- **User-based API keys** - Each key belongs to a specific user |
|||
- **Permission delegation** - Keys inherit user permissions with optional restrictions |
|||
- **Secure storage** - Keys are hashed with SHA-256 |
|||
- **Prefix-based lookup** - Fast key resolution with caching |
|||
- **Web UI** - Manage keys through a user-friendly interface |
|||
- **Multi-tenancy support** - Full ABP multi-tenancy compatibility |
|||
|
|||
 |
|||
|
|||
### Architecture Overview |
|||
|
|||
The solution follows ABP's modular architecture with four main layers: |
|||
|
|||
``` |
|||
┌─────────────────────────────────────────────┐ |
|||
│ Web Layer (UI) │ |
|||
│ • Razor Pages for CRUD operations │ |
|||
│ • JavaScript for client interactions │ |
|||
└─────────────────────────────────────────────┘ |
|||
↓ |
|||
┌─────────────────────────────────────────────┐ |
|||
│ AspNetCore Layer (Middleware) │ |
|||
│ • Authentication Handler │ |
|||
│ • API Key Resolver (Header/Query) │ |
|||
└─────────────────────────────────────────────┘ |
|||
↓ |
|||
┌─────────────────────────────────────────────┐ |
|||
│ Application Layer (Business Logic) │ |
|||
│ • ApiKeyAppService (CRUD operations) │ |
|||
│ • DTO mappings and validations │ |
|||
└─────────────────────────────────────────────┘ |
|||
↓ |
|||
┌─────────────────────────────────────────────┐ |
|||
│ Domain Layer (Core Business) │ |
|||
│ • ApiKey Entity & Manager │ |
|||
│ • IApiKeyRepository │ |
|||
│ • Domain services & events │ |
|||
└─────────────────────────────────────────────┘ |
|||
``` |
|||
|
|||
### Key Components |
|||
|
|||
#### 1. Domain Layer - The Core Entity |
|||
|
|||
```csharp |
|||
public class ApiKey : FullAuditedAggregateRoot<Guid>, IMultiTenant |
|||
{ |
|||
public virtual Guid? TenantId { get; protected set; } |
|||
public virtual Guid UserId { get; protected set; } |
|||
public virtual string Name { get; protected set; } |
|||
public virtual string Prefix { get; protected set; } |
|||
public virtual string KeyHash { get; protected set; } |
|||
public virtual DateTime? ExpiresAt { get; protected set; } |
|||
public virtual bool IsActive { get; protected set; } |
|||
|
|||
// Key format: {prefix}_{key} |
|||
// Only the hash is stored, never the actual key |
|||
} |
|||
``` |
|||
|
|||
**Key Design Decisions:** |
|||
|
|||
- **Prefix-based lookup**: Keys have format `prefix_actualkey`. The prefix is indexed for fast database lookups. |
|||
- **SHA-256 hashing**: The actual key is hashed and never stored in plain text. |
|||
- **User association**: Each key belongs to a user, inheriting their permissions. |
|||
- **Soft delete**: Deleted keys are marked as deleted but not removed from database for audit purposes. |
|||
|
|||
#### 2. Authentication Flow |
|||
|
|||
Here's how authentication works when a request arrives: |
|||
|
|||
 |
|||
|
|||
```csharp |
|||
// 1. Extract API key from request |
|||
var apiKey = httpContext.Request.Headers["X-Api-Key"].FirstOrDefault(); |
|||
if (string.IsNullOrEmpty(apiKey)) return AuthenticateResult.NoResult(); |
|||
|
|||
// 2. Split prefix and key |
|||
var parts = apiKey.Split('_', 2); |
|||
var prefix = parts[0]; |
|||
var key = parts[1]; |
|||
|
|||
// 3. Find key by prefix (cached) |
|||
var apiKeyEntity = await _apiKeyRepository.FindByPrefixAsync(prefix); |
|||
if (apiKeyEntity == null) return AuthenticateResult.Fail("Invalid API key"); |
|||
|
|||
// 4. Verify hash |
|||
var keyHash = HashHelper.ComputeSha256(key); |
|||
if (apiKeyEntity.KeyHash != keyHash) |
|||
return AuthenticateResult.Fail("Invalid API key"); |
|||
|
|||
// 5. Check expiration and active status |
|||
if (apiKeyEntity.ExpiresAt < DateTime.UtcNow || !apiKeyEntity.IsActive) |
|||
return AuthenticateResult.Fail("API key expired or inactive"); |
|||
|
|||
// 6. Create claims principal with user identity |
|||
var claims = new List<Claim> |
|||
{ |
|||
new Claim(AbpClaimTypes.UserId, apiKeyEntity.UserId.ToString()), |
|||
new Claim(AbpClaimTypes.TenantId, apiKeyEntity.TenantId?.ToString() ?? ""), |
|||
new Claim("ApiKeyId", apiKeyEntity.Id.ToString()) |
|||
}; |
|||
|
|||
return AuthenticateResult.Success(ticket); |
|||
``` |
|||
|
|||
#### 3. Creating and Managing API Keys |
|||
|
|||
**Creating a new key:** |
|||
|
|||
 |
|||
|
|||
```csharp |
|||
public class ApiKeyManager : DomainService |
|||
{ |
|||
public async Task<(ApiKey, string)> CreateAsync( |
|||
Guid userId, |
|||
string name, |
|||
DateTime? expiresAt = null) |
|||
{ |
|||
// Generate unique prefix |
|||
var prefix = await GenerateUniquePrefixAsync(); |
|||
|
|||
// Generate secure random key |
|||
var key = GenerateSecureRandomString(32); |
|||
|
|||
// Hash the key for storage |
|||
var keyHash = HashHelper.ComputeSha256(key); |
|||
|
|||
var apiKey = new ApiKey( |
|||
GuidGenerator.Create(), |
|||
userId, |
|||
name, |
|||
prefix, |
|||
keyHash, |
|||
expiresAt, |
|||
CurrentTenant.Id |
|||
); |
|||
|
|||
await _apiKeyRepository.InsertAsync(apiKey); |
|||
|
|||
// Return both entity and the full key (prefix_key) |
|||
// This is the ONLY time the actual key is visible |
|||
return (apiKey, $"{prefix}_{key}"); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**Important**: The actual key is returned only once during creation. After that, only the hash is stored. |
|||
|
|||
 |
|||
|
|||
### Using API Keys in Your Application |
|||
|
|||
Once created, clients can use the API key to authenticate: |
|||
|
|||
**HTTP Header (Recommended):** |
|||
```bash |
|||
curl -H "X-Api-Key: sk_prod_abc123def456..." \ |
|||
https://api.example.com/api/products |
|||
``` |
|||
|
|||
**JavaScript:** |
|||
```javascript |
|||
const response = await fetch('https://api.example.com/api/products', { |
|||
headers: { |
|||
'X-Api-Key': 'sk_prod_abc123def456...' |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
**C# HttpClient:** |
|||
```csharp |
|||
var client = new HttpClient(); |
|||
client.DefaultRequestHeaders.Add("X-Api-Key", "sk_prod_abc123def456..."); |
|||
var response = await client.GetAsync("https://api.example.com/api/products"); |
|||
``` |
|||
|
|||
**Python:** |
|||
```python |
|||
import requests |
|||
|
|||
headers = {'X-Api-Key': 'sk_prod_abc123def456...'} |
|||
response = requests.get('https://api.example.com/api/products', headers=headers) |
|||
``` |
|||
|
|||
### Permission Management |
|||
|
|||
API keys inherit the user's permissions, but you can further restrict them: |
|||
|
|||
 |
|||
|
|||
This allows scenarios like: |
|||
- Read-only API key for reporting tools |
|||
- Limited scope keys for third-party integrations |
|||
- Service-specific keys with minimal permissions |
|||
|
|||
```csharp |
|||
// Check if current request is authenticated via API key |
|||
if (CurrentUser.FindClaim("ApiKeyId") != null) |
|||
{ |
|||
var apiKeyId = CurrentUser.FindClaim("ApiKeyId").Value; |
|||
// Additional API key specific logic |
|||
} |
|||
``` |
|||
|
|||
## Performance Considerations |
|||
|
|||
The implementation uses several optimizations: |
|||
|
|||
**1. Prefix-based indexing**: Database lookups are done by prefix (indexed column), not the full key hash. |
|||
|
|||
**2. Distributed caching**: API keys are cached after first lookup, dramatically reducing database queries. |
|||
|
|||
```csharp |
|||
// Cache configuration |
|||
Configure<AbpDistributedCacheOptions>(options => |
|||
{ |
|||
options.KeyPrefix = "ApiKey:"; |
|||
}); |
|||
``` |
|||
|
|||
**3. Cache invalidation**: When a key is modified or deleted, cache is automatically invalidated. |
|||
|
|||
**Typical Performance:** |
|||
- Cached lookup: **< 5ms** |
|||
- Database lookup: **< 50ms** |
|||
- Cache hit rate: **~95%** |
|||
|
|||
## Security Best Practices |
|||
|
|||
When implementing API key authentication, follow these guidelines: |
|||
|
|||
✅ **Always use HTTPS** - Never send API keys over unencrypted connections |
|||
|
|||
✅ **Use different keys per environment** - Separate keys for dev, staging, production |
|||
|
|||
❌ **Don't log the full key** - Only log the prefix for debugging |
|||
|
|||
## Getting Started |
|||
|
|||
The complete source code is available on GitHub: |
|||
|
|||
**Repository**: [github.com/salihozkara/AbpApikeyManagement](https://github.com/salihozkara/AbpApikeyManagement) |
|||
|
|||
To integrate it into your ABP project: |
|||
|
|||
1. Clone or download the repository |
|||
2. Add project references to your solution |
|||
3. Add module dependencies to your modules |
|||
4. Run EF Core migrations to create the database tables |
|||
5. Navigate to `/ApiKeyManagement` to start managing keys |
|||
|
|||
```csharp |
|||
// In your Web module |
|||
[DependsOn(typeof(ApiKeyManagementWebModule))] |
|||
public class YourWebModule : AbpModule |
|||
{ |
|||
// ... |
|||
} |
|||
|
|||
// In your HttpApi.Host module |
|||
[DependsOn(typeof(ApiKeyManagementHttpApiModule))] |
|||
public class YourHttpApiHostModule : AbpModule |
|||
{ |
|||
// ... |
|||
} |
|||
``` |
|||
|
|||
## Conclusion |
|||
|
|||
API key authentication remains a crucial part of modern API security, especially for machine-to-machine communication. While it shouldn't replace user authentication methods like OAuth2 for user-facing applications, it's perfect for: |
|||
|
|||
- Automated scripts and tools |
|||
- Service-to-service communication |
|||
- Third-party integrations |
|||
- Long-lived access without token refresh complexity |
|||
|
|||
The implementation shown here demonstrates how ABP Framework's modular architecture, DDD principles, and built-in features (multi-tenancy, caching, permissions) can be leveraged to build a production-ready API key management system. |
|||
|
|||
The solution is open-source and ready to be integrated into your ABP projects. Feel free to explore the code, suggest improvements, or adapt it to your specific needs. |
|||
|
|||
**Resources:** |
|||
- GitHub Repository: [salihozkara/AbpApikeyManagement](https://github.com/salihozkara/AbpApikeyManagement) |
|||
- ABP Framework: [abp.io](https://abp.io) |
|||
- ABP Documentation: [docs.abp.io](https://abp.io/docs/latest) |
|||
|
|||
Happy coding! 🚀 |
|||
@ -0,0 +1 @@ |
|||
Learn how to implement API key authentication in ABP Framework applications. This comprehensive guide covers what API keys are, when to use them over OAuth2/JWT, real-world use cases for mobile apps and microservices, and a complete implementation with user-based key management, SHA-256 hashing, permission delegation, and built-in UI. |
|||
|
After Width: | Height: | Size: 752 KiB |
@ -0,0 +1,322 @@ |
|||
# Signal-Based Forms in Angular 21: Why You’ll Never Miss Reactive Forms Again |
|||
|
|||
Angular 21 introduces one of the most exciting developments in the modern edition of Angular: **Signal-Based Forms**. Built directly on the reactive foundation of Angular signals, this new experimental API provides a cleaner, more intuitive, strongly typed, and ergonomic approach for managing form state—without the heavy boilerplate of Reactive Forms. |
|||
|
|||
> ⚠️ **Important:** Signal Forms are *experimental*. |
|||
> Their API can change. Avoid using them in critical production scenarios unless you understand the risks. |
|||
|
|||
Despite this, Signal Forms clearly represent Angular’s future direction. |
|||
--- |
|||
|
|||
## Why Signal Forms? |
|||
|
|||
Traditionally in Angular, building forms has involved several concerns: |
|||
|
|||
- Tracking values |
|||
- Managing UI interaction states (touched, dirty) |
|||
- Handling validation |
|||
- Keeping UI and model in sync |
|||
|
|||
Reactive Forms solved many challenges but introduced their own: |
|||
|
|||
- Verbosity FormBuilder API |
|||
- Required subscriptions (valueChanges) |
|||
- Manual cleaning |
|||
- Difficult nested forms |
|||
- Weak type-safety |
|||
|
|||
**Signal Forms solve these problems through:** |
|||
|
|||
1." Automatic synchronization |
|||
2." Full type safety |
|||
3." Schema-based validation |
|||
4." Fine-grained reactivity |
|||
5." Drastically reduced boilerplate |
|||
6." Natural integration with Angular Signals |
|||
|
|||
--- |
|||
|
|||
### 1. Form Models — The Core of Signal Forms |
|||
|
|||
A **form model** is simply a writable signal holding the structure of your form data. |
|||
|
|||
```ts |
|||
import { Component, signal } from '@angular/core'; |
|||
import { form, Field } from '@angular/forms/signals'; |
|||
|
|||
@Component({ |
|||
selector: 'app-login', |
|||
imports: [Field], |
|||
template: ` |
|||
<input type="email" [field]="loginForm.email" /> |
|||
<input type="password" [field]="loginForm.password" /> |
|||
`, |
|||
}) |
|||
export class LoginComponent { |
|||
loginModel = signal({ |
|||
email: '', |
|||
password: '', |
|||
}); |
|||
|
|||
loginForm = form(this.loginModel); |
|||
} |
|||
``` |
|||
|
|||
Calling `form(model)` creates a **Field Tree** that maps directly to your model. |
|||
|
|||
--- |
|||
|
|||
### 2. Achieving Full Type Safety |
|||
|
|||
Although TypeScript can infer types from object literals, defining explicit interfaces provides maximum safety and better IDE support. |
|||
|
|||
```ts |
|||
interface LoginData { |
|||
email: string; |
|||
password: string; |
|||
} |
|||
|
|||
loginModel = signal<LoginData>({ |
|||
email: '', |
|||
password: '', |
|||
}); |
|||
|
|||
loginForm = form(loginModel); |
|||
``` |
|||
|
|||
Now: |
|||
|
|||
- `loginForm.email` → `FieldTree<string>` |
|||
- Accessing invalid fields like `loginForm.username` results in compile-time errors |
|||
|
|||
This level of type safety surpasses Reactive Forms. |
|||
|
|||
--- |
|||
|
|||
### 3. Reading Form Values |
|||
|
|||
#### Read from the model (entire form): |
|||
|
|||
```ts |
|||
onSubmit() { |
|||
const data = this.loginModel(); |
|||
console.log(data.email, data.password); |
|||
} |
|||
``` |
|||
|
|||
#### Read from an individual field: |
|||
|
|||
```html |
|||
<p>Current email: {{ loginForm.email().value() }}</p> |
|||
``` |
|||
|
|||
Each field exposes: |
|||
|
|||
- `value()` |
|||
- `valid()` |
|||
- `errors()` |
|||
- `dirty()` |
|||
- `touched()` |
|||
|
|||
All as signals. |
|||
|
|||
--- |
|||
|
|||
### 4. Updating Form Models Programmatically |
|||
|
|||
Signal Forms allow three update methods. |
|||
|
|||
#### 1. Replace the entire model |
|||
|
|||
```ts |
|||
this.userModel.set({ |
|||
name: 'Alice', |
|||
email: 'alice@example.com', |
|||
}); |
|||
``` |
|||
|
|||
#### 2. Patch specific fields |
|||
|
|||
```ts |
|||
this.userModel.update(prev => ({ |
|||
...prev, |
|||
email: newEmail, |
|||
})); |
|||
``` |
|||
|
|||
#### 3. Update a single field |
|||
|
|||
```ts |
|||
this.userForm.email().value.set(''); |
|||
``` |
|||
|
|||
This eliminates the need for: |
|||
|
|||
- `patchValue()` |
|||
- `setValue()` |
|||
- `formGroup.get('field')` |
|||
|
|||
--- |
|||
|
|||
### 5. Automatic Two-Way Binding With `[field]` |
|||
|
|||
The `[field]` directive enables perfect two-way data binding: |
|||
|
|||
```html |
|||
<input [field]="userForm.name" /> |
|||
``` |
|||
|
|||
#### How it works: |
|||
|
|||
- **User input → Field state → Model** |
|||
- **Model updates → Field state → Input UI** |
|||
|
|||
No subscriptions. |
|||
No event handlers. |
|||
No boilerplate. |
|||
|
|||
Reactive Forms could never achieve this cleanly. |
|||
|
|||
--- |
|||
|
|||
### 6. Nested Models and Arrays |
|||
|
|||
Models can contain nested object structures: |
|||
|
|||
```ts |
|||
userModel = signal({ |
|||
name: '', |
|||
address: { |
|||
street: '', |
|||
city: '', |
|||
}, |
|||
}); |
|||
``` |
|||
|
|||
Access fields easily: |
|||
|
|||
```html |
|||
<input [field]="userForm.address.street" /> |
|||
``` |
|||
|
|||
Arrays are also supported: |
|||
|
|||
```ts |
|||
orderModel = signal({ |
|||
items: [ |
|||
{ product: '', quantity: 1, price: 0 } |
|||
] |
|||
}); |
|||
``` |
|||
|
|||
Field state persists even when array items move, thanks to identity tracking. |
|||
|
|||
--- |
|||
|
|||
### 7. Schema-Based Validation |
|||
|
|||
Validation is clean and centralized: |
|||
|
|||
```ts |
|||
import { required, email } from '@angular/forms/signals'; |
|||
|
|||
const model = signal({ email: '' }); |
|||
|
|||
const formRef = form(model, { |
|||
email: [required(), email()], |
|||
}); |
|||
``` |
|||
|
|||
Field validation state is reactive: |
|||
|
|||
```ts |
|||
formRef.email().valid() |
|||
formRef.email().errors() |
|||
formRef.email().touched() |
|||
``` |
|||
|
|||
Validation no longer scatters across components. |
|||
|
|||
--- |
|||
|
|||
### 8. When Should You Use Signal Forms? |
|||
|
|||
#### New Angular 21+ apps |
|||
Signal-first architecture is the new standard. |
|||
|
|||
#### Teams wanting stronger type safety |
|||
Every field is exactly typed. |
|||
|
|||
#### Devs tired of Reactive Form boilerplate |
|||
Signal Forms drastically simplify code. |
|||
|
|||
#### Complex UI with computed reactive form state |
|||
Signals integrate perfectly. |
|||
|
|||
#### ❌ Avoid if: |
|||
- You need long-term stability |
|||
- You rely on mature Reactive Forms features |
|||
- Your app must avoid experimental APIs |
|||
|
|||
--- |
|||
|
|||
### 9. Reactive Forms vs Signal Forms |
|||
|
|||
| Feature | Reactive Forms | Signal Forms | |
|||
|--------|----------------|--------------| |
|||
| Boilerplate | High | Very low | |
|||
| Type-safety | Weak | Strong | |
|||
| Two-way binding | Manual | Automatic | |
|||
| Validation | Scattered | Centralized schema | |
|||
| Nested forms | Verbose | Natural | |
|||
| Subscriptions | Required | None | |
|||
| Change detection | Zone-heavy | Fine-grained | |
|||
|
|||
Signal Forms feel like the "modern Angular mode," while Reactive Forms increasingly feel legacy. |
|||
|
|||
--- |
|||
|
|||
### 10. Full Example: Login Form |
|||
|
|||
```ts |
|||
@Component({ |
|||
selector: 'app-login', |
|||
imports: [Field], |
|||
template: ` |
|||
<form (ngSubmit)="submit()"> |
|||
<input type="email" [field]="form.email" /> |
|||
<input type="password" [field]="form.password" /> |
|||
<button>Login</button> |
|||
</form> |
|||
`, |
|||
}) |
|||
export class LoginComponent { |
|||
model = signal({ email: '', password: '' }); |
|||
form = form(this.model); |
|||
|
|||
submit() { |
|||
console.log(this.model()); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Minimal. Reactive. Completely type-safe. |
|||
|
|||
--- |
|||
|
|||
## **Conclusion** |
|||
|
|||
Signal Forms in Angular 21 represent a big step forward: |
|||
|
|||
- Cleaner API |
|||
- Stronger type safety |
|||
- Automatic two-way binding |
|||
- Centralized validation |
|||
- Fine-grained reactivity |
|||
- Dramatically better developer experience |
|||
|
|||
|
|||
Although these are experimental, they clearly show the future of Angular's form ecosystem. |
|||
Once you get into using Signal Forms, you may never want to use Reactive Forms again. |
|||
|
|||
--- |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue