Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 79 additions & 10 deletions user-management/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,46 @@
# User Management Service
# Stickerlandia User Management Service

The User Management Service manages users. It provides one primary API:
The **Stickerlandia User Management Service** is a comprehensive, cloud-native user authentication and management system built with .NET 8. It serves as a complete OAuth 2.0 authorization server and user management platform, designed with **platform adaptability** as a core principle to run seamlessly on Azure, AWS, or any cloud-agnostic container orchestrator.

- **User Management API** (`/api/users/v1`) - Manages users and provides OAuth 2.0 endpoints for AuthN/Z
## What This Application Does

The User Management Service provides a complete identity and access management solution for the Stickerlandia ecosystem:

### 🔐 **OAuth 2.0 Authorization Server**
- Full OAuth 2.0 authorization server implementation using [OpenIddict](https://documentation.openiddict.com/)
- Authorization Code Flow with PKCE support for frontend applications (SPAs, mobile apps)
- Refresh token support for seamless user experience
- Secure user authentication and authorization
- Standard OAuth 2.0 endpoints: `/connect/authorize`, `/connect/token`, `/connect/logout`, `/connect/userinfo`

### 👥 **User Account Management**
- User registration and profile management
- ASP.NET Core Identity integration with custom user models
- Account tiers and types support
- Sticker tracking and gamification features
- Self-service account updates

### 🚀 **Event-Driven Architecture**
- Publishes user lifecycle events (registration, profile updates)
- Processes external events (sticker claiming)
- Implements outbox pattern for reliable event delivery
- Platform-specific messaging: SNS/SQS (AWS), Service Bus (Azure), Kafka (Agnostic)

### 🌐 **Multi-Platform Support**
- **Azure Native**: Azure Functions, Service Bus, PostgreSQL
- **AWS Native**: Lambda functions, SNS/SQS, PostgreSQL
- **Cloud Agnostic**: Kafka, PostgreSQL, containerized workers

### 📊 **Production-Ready Features**
- Comprehensive health monitoring and observability
- Structured logging and error handling (RFC 7807 Problem Details)
- Static code analysis and quality enforcement
- Full test coverage with platform-specific integration tests
- Infrastructure as Code for AWS (CDK) and Azure (Terraform)

The service provides one primary API:

- **User Management API** (`/api/users/v1`) - RESTful API for user operations and OAuth 2.0 endpoints

## Architecture

Expand Down Expand Up @@ -46,20 +84,51 @@ The AWS native implementation of the user management service uses a combination

You can find the full [Open API specification in the docs folder](./docs/api.yaml).

## Features

- Register users
- OIDC authorization code flow (frontend app) and client credentials flow (service->service)
- Get and update user accounts
## Key Features

### Authentication & Authorization
- **OAuth 2.0 Authorization Server** - Complete implementation with OpenIddict
- **Authorization Code Flow with PKCE** - Secure authentication for SPAs and mobile apps
- **Refresh Token Support** - Seamless token refresh for better UX
- **Multiple Scopes** - Support for `email`, `profile`, and `roles` scopes
- **ASP.NET Core Identity Integration** - Robust user management with custom models

### User Management
- **User Registration** - Self-service account creation with validation
- **Profile Management** - Update user details, preferences, and settings
- **Account Tiers & Types** - Support for different user account levels
- **Sticker Gamification** - Track and manage user sticker collections
- **Security** - JWT-based API authentication with proper access controls

### Platform Adaptability
- **Multi-Cloud Support** - Deploy on Azure, AWS, or cloud-agnostic platforms
- **Flexible Architecture** - Ports and adapters pattern for clean separation
- **Event-Driven Design** - Reliable event publishing with outbox pattern
- **Background Processing** - Platform-specific workers (Functions, Lambda, containers)

### Developer Experience
- **.NET Aspire Integration** - Local development with different platform profiles
- **Comprehensive Testing** - Unit and integration tests for all platforms
- **API Documentation** - OpenAPI and AsyncAPI specifications
- **Infrastructure as Code** - AWS CDK and Azure Terraform templates

## Events

You can find the full [Async API specification for events published and received in the docs folder](./docs/async_api.yaml)

## Authentication

All API endpoints (except `/health` and `/register`) require authentication via JWT token in the Authorization header.
Access controls ensure users can only operate on their own accounts unless they have admin privileges.
The service implements a complete OAuth 2.0 authorization server using [OpenIddict](https://documentation.openiddict.com/). For detailed authentication flows and integration guidance, see [Authentication Documentation](./docs/Auth.md).

### Quick Overview
- **Endpoints**: Authorization (`/connect/authorize`), Token (`/connect/token`), Logout (`/connect/logout`), UserInfo (`/connect/userinfo`)
- **Supported Flows**: Authorization Code with PKCE, Refresh Token
- **Scopes**: `email`, `profile`, `roles`
- **API Authentication**: JWT Bearer tokens required for all endpoints except `/health` and `/register`
- **Access Control**: Users can only operate on their own accounts unless they have admin privileges

### For Client Applications
To integrate with this OAuth 2.0 server, client applications should implement the Authorization Code Flow with PKCE. See the [Auth.md documentation](./docs/Auth.md) for sequence diagrams.

## Error Handling

Expand Down
182 changes: 182 additions & 0 deletions user-management/docs/Auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Authentication Guide

This document provides comprehensive guidance on the OAuth 2.0 authentication flows supported by the Stickerlandia User Management Service.

## Overview

The Stickerlandia User Management Service implements a complete OAuth 2.0 authorization server using [OpenIddict](https://documentation.openiddict.com/), integrated with ASP.NET Core Identity for robust user management.

### Key Components
- **Authorization Server**: OpenIddict-based OAuth 2.0 implementation
- **Identity Provider**: ASP.NET Core Identity with PostgreSQL storage
- **User Model**: Custom `PostgresUserAccount` extending `IdentityUser`
- **Security**: PKCE (Proof Key for Code Exchange) for enhanced security

## OAuth 2.0 Endpoints

The service exposes standard OAuth 2.0 endpoints:

| Endpoint | URL | Purpose |
|----------|-----|---------|
| Authorization | `/connect/authorize` | Initiate OAuth authorization flow |
| Token | `/connect/token` | Exchange authorization codes for tokens |
| UserInfo | `/connect/userinfo` | Retrieve user information using access token |
| Logout | `/connect/logout` | End user session |

## Supported Authentication Flows

### ✅ Authorization Code Flow with PKCE

**Primary flow for client applications** - Recommended for SPAs, mobile apps, and web applications.

**Features:**
- Enhanced security with PKCE (Proof Key for Code Exchange)
- Suitable for public clients (SPAs, mobile apps)
- Supports refresh tokens for seamless user experience
- Implements OpenID Connect for user identity

**Scopes Supported:**
- `email` - Access to user email address
- `profile` - Access to user profile information (name, account details)
- `roles` - Access to user roles and permissions

### ✅ Refresh Token Flow

**Token refresh capability** - Extends user sessions without re-authentication.

**Features:**
- Seamless token refresh without user interaction
- Configurable token lifetimes
- Automatic token rotation for enhanced security

### ❌ Client Credentials Flow

**Not Currently Implemented** - Service-to-service authentication can be implemented at a later date once service to service communication is required.

### ❌ Other Flows

The following OAuth 2.0 flows are **not supported** for security reasons:
- **Implicit Flow** - Deprecated due to security vulnerabilities
- **Resource Owner Password Credentials Flow** - Not recommended for modern applications

## Authentication Flow Diagrams

### Authorization Code Flow with PKCE

```mermaid
sequenceDiagram
participant U as User
participant C as Client App
participant B as Browser
participant AS as Auth Server
participant RS as Resource Server (API)

Note over C,AS: 1. Client prepares PKCE parameters
C->>C: Generate code_verifier (random string)
C->>C: Generate code_challenge = SHA256(code_verifier)

Note over U,AS: 2. Authorization Request
U->>C: Click "Login"
C->>B: Redirect to /connect/authorize?<br/>response_type=code&<br/>client_id=spa-client&<br/>redirect_uri=https://app.com/callback&<br/>scope=email profile&<br/>code_challenge=xyz&<br/>code_challenge_method=S256&<br/>state=random-state
B->>AS: GET /connect/authorize

Note over AS,B: 3. User Authentication & Consent
AS->>B: Show login page
U->>B: Enter credentials
B->>AS: POST login credentials
AS->>AS: Validate user credentials
AS->>B: Show consent page (if required)
U->>B: Grant permissions
B->>AS: POST consent approval

Note over AS,C: 4. Authorization Response
AS->>B: Redirect to callback?<br/>code=auth-code-123&<br/>state=random-state
B->>C: Follow redirect with authorization code

Note over C,AS: 5. Token Exchange
C->>AS: POST /connect/token<br/>grant_type=authorization_code&<br/>code=auth-code-123&<br/>client_id=spa-client&<br/>code_verifier=original-verifier&<br/>redirect_uri=https://app.com/callback
AS->>AS: Validate code_verifier against code_challenge
AS->>C: Return tokens:<br/>{<br/> "access_token": "...",<br/> "refresh_token": "...",<br/> "id_token": "...",<br/> "token_type": "Bearer",<br/> "expires_in": 3600<br/>}

Note over C,RS: 6. Access Protected Resources
C->>RS: GET /api/users/v1/details<br/>Authorization: Bearer access-token
RS->>RS: Validate JWT token
RS->>C: Return user data
```

### User Registration Flow

```mermaid
sequenceDiagram
participant U as User
participant C as Client App
participant AS as Auth Server
participant DB as Database
participant ES as Event System

Note over U,DB: User Registration Process
U->>C: Fill registration form
C->>AS: POST /api/users/v1/register<br/>{<br/> "email": "user@example.com",<br/> "password": "secure-password",<br/> "firstName": "John",<br/> "lastName": "Doe"<br/>}

AS->>AS: Validate input data
AS->>AS: Hash password
AS->>DB: Create user record
DB->>AS: Confirm user created

AS->>ES: Publish userRegistered.v1 event<br/>(via outbox pattern)
AS->>C: Return 201 Created<br/>{<br/> "userId": "user-123",<br/> "email": "user@example.com"<br/>}

Note over ES: Background Processing
ES->>ES: Process outbox events
ES->>ES: Send welcome email
ES->>ES: Initialize user preferences

C->>U: Show registration success
C->>U: Redirect to login
```

### Token Refresh Flow

```mermaid
sequenceDiagram
participant C as Client App
participant AS as Auth Server
participant RS as Resource Server

Note over C,AS: Token Refresh Process
C->>RS: API Request with expired token
RS->>C: 401 Unauthorized (token expired)

C->>C: Check if refresh token available
C->>AS: POST /connect/token<br/>grant_type=refresh_token&<br/>refresh_token=refresh-token-123&<br/>client_id=spa-client

AS->>AS: Validate refresh token
AS->>AS: Generate new tokens
AS->>C: Return new tokens:<br/>{<br/> "access_token": "new-access-token",<br/> "refresh_token": "new-refresh-token",<br/> "token_type": "Bearer",<br/> "expires_in": 3600<br/>}

C->>C: Store new tokens
C->>RS: Retry API request with new token<br/>Authorization: Bearer new-access-token
RS->>C: 200 OK with data
```

### Logout Flow

```mermaid
sequenceDiagram
participant U as User
participant C as Client App
participant AS as Auth Server
participant DB as Database

Note over U,DB: User Logout Process
U->>C: Click "Logout"
C->>AS: POST /connect/logout<br/>id_token_hint=user-id-token&<br/>post_logout_redirect_uri=https://app.com/logged-out

AS->>DB: Revoke user session
AS->>DB: Invalidate refresh tokens
DB->>AS: Confirm tokens revoked

AS->>C: Redirect to post_logout_redirect_uri
C->>C: Clear local tokens
C->>U: Show "Logged out successfully"
```
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ namespace Stickerlandia.UserManagement.AWS;

public static class ServiceExtensions
{
public static IServiceCollection AddAwsAdapters(this IServiceCollection services, IConfiguration configuration)
public static IServiceCollection AddAwsAdapters(this IServiceCollection services, IConfiguration configuration, bool enableDefaultUi = true)
{
ArgumentNullException.ThrowIfNull(configuration);

services.AddPostgresAuthServices(configuration);
services.AddPostgresAuthServices(configuration, enableDefaultUi: enableDefaultUi);

services.Configure<AwsConfiguration>(
configuration.GetSection("Aws"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Stickerlandia.UserManagement.Core.Observability;
using Stickerlandia.UserManagement.Core;
using Stickerlandia.UserManagement.Core.Outbox;

namespace Stickerlandia.UserManagement.Agnostic;

public class PostgresOutbox(
UserManagementDbContext dbContext,
ILogger<PostgresOutbox> logger)
: IOutbox
{
public async Task StoreEventFor(string accountId, DomainEvent domainEvent)
{
ArgumentException.ThrowIfNullOrEmpty(accountId, nameof(accountId));
ArgumentNullException.ThrowIfNull(domainEvent, nameof(domainEvent));

var outboxItem = new PostgresOutboxItem
{
Id = Guid.NewGuid().ToString(),
EventType = domainEvent.EventName,
EventData = domainEvent.ToJsonString(),
EmailAddress = accountId,
EventTime = DateTime.UtcNow,
Processed = false,
Failed = false
};

await dbContext.OutboxItems.AddAsync(outboxItem);
}

public async Task<List<OutboxItem>> GetUnprocessedItemsAsync(int maxCount = 100)
{
try
{
var items = await dbContext.OutboxItems
.Where(o => o.Processed == false && o.Failed == false)
.Take(maxCount)
.ToListAsync();

return items.Select(item => new OutboxItem
{
ItemId = item.Id,
EmailAddress = item.EmailAddress,
EventType = item.EventType,
EventData = item.EventData,
EventTime = item.EventTime,
Processed = item.Processed,
Failed = item.Failed,
FailureReason = item.FailureReason,
TraceId = item.TraceId
}).ToList();
}
catch (Exception ex)
{
Log.UnknownException(logger, ex);
throw new DatabaseFailureException("Error retrieving unprocessed outbox items", ex);
}
}

public async Task UpdateOutboxItem(OutboxItem outboxItem)
{
try
{
ArgumentNullException.ThrowIfNull(outboxItem, nameof(outboxItem));
var item = await dbContext.OutboxItems.FindAsync(outboxItem.ItemId);

if (item == null) throw new DatabaseFailureException($"Outbox item with ID {outboxItem.ItemId} not found");

item.Processed = outboxItem.Processed;
item.Failed = outboxItem.Failed;
item.FailureReason = outboxItem.FailureReason;
item.TraceId = outboxItem.TraceId;

dbContext.OutboxItems.Update(item);
await dbContext.SaveChangesAsync();
}
catch (Exception ex)
{
throw new DatabaseFailureException($"Failed to update outbox item with ID {outboxItem?.ItemId}", ex);
}
}
}
Loading
Loading