Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ae38f61
Initial plan
Copilot Jan 8, 2026
b0c73e4
Phase 1-3 complete: Core OIDC module with Server and WASM implementat…
Copilot Jan 8, 2026
116b0dd
Add shared authentication abstractions for multi-provider support
Copilot Jan 8, 2026
42d2fcc
Add comprehensive authentication architecture documentation
Copilot Jan 8, 2026
203b35a
Final: Add implementation summary for PR review
Copilot Jan 8, 2026
1b44a1d
Add authentication infrastructure and modules for Elsa Studio, includ…
sfmskywalker Jan 8, 2026
0b77630
Update project references to include new Elsa Studio Authentication m…
sfmskywalker Jan 8, 2026
4001b4d
Update `RedirectToLoginUnauthorizedComponentProvider` to support fall…
sfmskywalker Jan 8, 2026
b4c8fd4
Mark `Elsa.Studio.Login` APIs as obsolete and migrate authentication …
sfmskywalker Jan 8, 2026
49fc5e1
Switch to OpenID Connect authentication, remove legacy login module, …
sfmskywalker Jan 8, 2026
2378354
Replace `Login` module with OpenID Connect, update authentication pip…
sfmskywalker Jan 8, 2026
1ce9925
Replace `BearerTokenHttpMessageHandler` with `AuthenticatingApiHttpMe…
sfmskywalker Jan 8, 2026
b07c7ce
Organize solution structure by adding new folders: authentication, lo…
sfmskywalker Jan 8, 2026
884d67a
Refactor authentication: replace legacy services, update OIDC impleme…
sfmskywalker Jan 8, 2026
2242b09
Add `Elsa.Studio.Authentication.ElsaAuth.UI` module to provide Elsa I…
sfmskywalker Jan 8, 2026
48552ab
Migrate `AUTHENTICATION_ARCHITECTURE.md` to `doc/` folder, update pro…
sfmskywalker Jan 8, 2026
b16851e
Introduce `IAnonymousBackendApiClientProvider` and refactor API clien…
sfmskywalker Jan 8, 2026
7806db9
Add token refresh mechanism for OpenID Connect and Elsa Identity auth…
sfmskywalker Jan 8, 2026
94c79e7
Add persisted token refresh for OpenID Connect in Blazor Server: impl…
sfmskywalker Jan 8, 2026
a436d2a
Remove persisted token refresh strategy and related services from Ope…
sfmskywalker Jan 9, 2026
5958644
Refactor OIDC configuration for Blazor WebAssembly: add Azure AD comp…
sfmskywalker Jan 9, 2026
12dcd61
Fix Azure AD authentication in Blazor WASM by passing explicit API sc…
Copilot Jan 9, 2026
307ecd9
Remove obsolete Azure AD compatibility patches and cleanup related Ja…
sfmskywalker Jan 9, 2026
828d003
Refactor OpenID Connect callback path handling: use null-coalescing a…
sfmskywalker Jan 9, 2026
4cedc51
Add token purposes and scoped token caching for enhanced authenticati…
sfmskywalker Jan 9, 2026
0676527
Add scoped access token capabilities and token-purpose configuration
sfmskywalker Jan 9, 2026
36f01cc
Refactor authentication modules: simplify scoped token handling, upda…
sfmskywalker Jan 9, 2026
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
538 changes: 538 additions & 0 deletions AZURE_AD_BLAZOR_WASM_AUTH_PLAN.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.22" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.22" />
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="8.0.22" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.22" />
<PackageVersion Include="Microsoft.AspNetCore.Components.CustomElements" Version="8.0.22" />
Expand All @@ -41,6 +42,7 @@
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="9.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Components.CustomElements" Version="9.0.11" />
Expand All @@ -56,6 +58,7 @@
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="10.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Components.CustomElements" Version="10.0.1" />
Expand Down
392 changes: 375 additions & 17 deletions Elsa.Studio.sln

Large diffs are not rendered by default.

335 changes: 335 additions & 0 deletions doc/AUTHENTICATION_ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
# Elsa Studio Authentication Architecture

This document provides an overview of the authentication architecture in Elsa Studio, including how different authentication providers integrate with the framework.

## Overview

Elsa Studio supports multiple authentication providers through a flexible, extensible architecture. The system is designed to:

1. Support multiple authentication mechanisms (OIDC, OAuth2, JWT, etc.)
2. Work across different Blazor hosting models (Server and WebAssembly)
3. Provide automatic token management and refresh
4. Integrate seamlessly with backend API calls and SignalR connections

## Architecture Layers

```
┌─────────────────────────────────────────────────────────────────┐
│ Elsa Studio Application │
│ (Workflows, Dashboard, etc. - uses IAuthenticationProviderManager) │
└──────────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Elsa.Studio.Core │
│ • IAuthenticationProvider - Gets tokens for the app │
│ • IAuthenticationProviderManager - Manages multiple providers │
│ • TokenNames - Standard token name constants │
└──────────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Elsa.Studio.Authentication.Abstractions │
│ • ITokenAccessor - Provider-agnostic token access │
│ • AuthenticationOptions - Base configuration │
└──────────────────────────┬──────────────────────────────────────┘
┌───────────────┴───────────────┬─────────────────┐
▼ ▼ ▼
┌──────────────────────┐ ┌──────────────────┐ ┌─────────────┐
│ OIDC Provider │ │ OAuth2 Provider │ │ Future │
│ • IOidcTokenAccessor│ │ (Future) │ │ Providers │
│ • OidcOptions │ │ │ │ (JWT, SAML) │
│ • Server & WASM │ │ │ │ │
└──────────────────────┘ └──────────────────┘ └─────────────┘
```

## Core Concepts

### 1. Token Flow

```
Application Request
IAuthenticationProviderManager.GetAuthenticationTokenAsync()
[Iterates through registered IAuthenticationProvider instances]
IAuthenticationProvider.GetAccessTokenAsync()
ITokenAccessor.GetTokenAsync()
[Provider-specific token retrieval]
Token returned to application
```

### 2. Provider Registration

Multiple authentication providers can be registered simultaneously:

```csharp
// Example: Register OIDC for API calls, JWT for specific endpoints
services.AddOidcAuthentication(options => { /* OIDC config */ });
services.AddJwtAuthentication(options => { /* JWT config */ });

// The manager will try each provider until a valid token is found
```

### 3. Hosting Model Differences

#### Blazor Server
- Uses ASP.NET Core authentication middleware
- Tokens stored server-side in authentication properties
- Accessed via `HttpContext.GetTokenAsync()`
- Cookie-based session management
- No client-side token exposure

#### Blazor WebAssembly
- Uses `Microsoft.AspNetCore.Components.WebAssembly.Authentication`
- Tokens managed by browser-based authentication framework
- Accessed via `IAccessTokenProvider`
- Automatic token refresh before expiry
- Secure token storage in browser

## Standard Interfaces

### IAuthenticationProvider (Core)

The main interface used by Elsa Studio applications.

```csharp
public interface IAuthenticationProvider
{
Task<string?> GetAccessTokenAsync(string tokenName, CancellationToken cancellationToken = default);
}
```

**Purpose**: Provides tokens to the application for API calls and SignalR connections.

### IAuthenticationProviderManager (Core)

Manages multiple authentication providers.

```csharp
public interface IAuthenticationProviderManager
{
Task<string?> GetAuthenticationTokenAsync(string? tokenName, CancellationToken cancellationToken = default);
}
```

**Purpose**: Iterates through registered providers to find a valid token.

### ITokenAccessor (Abstractions)

Provider-agnostic interface for token retrieval.

```csharp
public interface ITokenAccessor
{
Task<string?> GetTokenAsync(string tokenName, CancellationToken cancellationToken = default);
}
```

**Purpose**: Allows authentication providers to implement token access in their own way.

## Token Names

Standard token names are defined in `TokenNames` class:

- `TokenNames.AccessToken` - Access token for API authentication
- `TokenNames.IdToken` - Identity token (may not be available in all providers/hosting models)
- `TokenNames.RefreshToken` - Refresh token (may not be available in all providers/hosting models)

## Authentication Providers

### Current Providers

#### 1. Elsa.Studio.Login (Legacy)
- **Location**: `src/modules/Elsa.Studio.Login`
- **Supports**: OIDC, OAuth2, Elsa Identity
- **Status**: Maintained for backward compatibility
- **Note**: Tight coupling with general login functionality

#### 2. Elsa.Studio.Authentication.OpenIdConnect (New)
- **Location**: `src/modules/Elsa.Studio.Authentication.OpenIdConnect`
- **Supports**: OpenID Connect
- **Hosting**: Separate packages for Server and WASM
- **Features**:
- Uses Microsoft's built-in OIDC handlers
- Automatic token refresh
- PKCE support
- Cookie-based auth (Server) or framework-managed (WASM)

### Future Providers (Examples)

- `Elsa.Studio.Authentication.OAuth2` - Pure OAuth2 without OIDC
- `Elsa.Studio.Authentication.Jwt` - JWT bearer token authentication
- `Elsa.Studio.Authentication.Saml` - SAML authentication
- `Elsa.Studio.Authentication.AzureAD` - Azure AD specific optimizations
- Custom implementations for proprietary auth systems

## Integration Points

### 1. API Calls

The `AuthenticatingApiHttpMessageHandler` automatically adds authentication tokens to API requests:

```csharp
// In Elsa.Studio.Login
public class AuthenticatingApiHttpMessageHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(...)
{
var token = await jwtAccessor.ReadTokenAsync(TokenNames.AccessToken);
request.Headers.Authorization = new("Bearer", token);
// ... handle 401 with token refresh
}
}
```

### 2. SignalR Connections

The `WorkflowInstanceObserverFactory` retrieves tokens for SignalR hub connections:

```csharp
var token = await authenticationProviderManager
.GetAuthenticationTokenAsync(TokenNames.AccessToken, cancellationToken);

var connection = new HubConnectionBuilder()
.WithUrl(hubUrl, options =>
{
options.AccessTokenProvider = () => Task.FromResult(token);
})
.Build();
```

### 3. Authorization State

Blazor's `AuthenticationStateProvider` is used for UI authorization:

```razor
@attribute [Authorize]

<AuthorizeView>
<Authorized>
<!-- Show protected content -->
</Authorized>
<NotAuthorized>
<RedirectToLogin />
</NotAuthorized>
</AuthorizeView>
```

## Security Considerations

### Server Hosting
- ✅ Tokens never exposed to client browser
- ✅ Cookie-based authentication with HTTP-only cookies
- ✅ Secure server-side session management
- ✅ HTTPS-only cookies in production

### WASM Hosting
- ✅ Tokens managed by authentication framework
- ✅ Automatic token expiry and renewal
- ✅ Access tokens available, but refresh tokens hidden
- ✅ Uses standard browser security features

### General
- ✅ PKCE enabled by default for OIDC
- ✅ HTTPS required for metadata endpoints
- ✅ Token refresh on 401 responses
- ✅ Secure token storage per hosting model

## Implementation Guide

### Creating a New Authentication Provider

See `src/modules/Elsa.Studio.Authentication.Abstractions/README.md` for detailed guidance.

**Quick Steps**:

1. Create provider-specific options extending `AuthenticationOptions`
2. Implement `ITokenAccessor` for your provider
3. Implement `IAuthenticationProvider` using your token accessor
4. Create hosting-specific implementations if needed (Server vs WASM)
5. Register services in DI container

### Using an Authentication Provider

**Blazor Server**:
```csharp
builder.Services.AddOidcAuthentication(options =>
{
options.Authority = "https://identity-server.com";
options.ClientId = "elsa-studio";
options.ClientSecret = "secret";
options.Scopes = new[] { "openid", "profile", "elsa_api" };
});

app.UseAuthentication();
app.UseAuthorization();
```

**Blazor WASM**:
```csharp
builder.Services.AddOidcAuthentication(options =>
{
options.Authority = "https://identity-server.com";
options.ClientId = "elsa-studio-wasm";
options.Scopes = new[] { "openid", "profile", "elsa_api" };
});
```

## Migration Path

### From Elsa.Studio.Login to New Providers

The new authentication providers are designed to coexist with the legacy `Elsa.Studio.Login`:

1. **Phase 1**: Add new provider alongside existing Login module
2. **Phase 2**: Test new provider with your identity server
3. **Phase 3**: Switch to new provider by removing Login module registration
4. **Phase 4**: Remove Login module dependency once stable

No breaking changes to existing applications.

## Troubleshooting

### Common Issues

1. **Multiple providers registered, wrong one used**
- `IAuthenticationProviderManager` returns first valid token
- Check registration order
- Ensure only desired provider is registered

2. **Tokens not available in WASM**
- Only access tokens directly accessible
- Refresh/ID tokens managed by framework

3. **401 errors on API calls**
- Check token scopes match API requirements
- Verify `AuthenticatingApiHttpMessageHandler` is registered
- Check identity server returns correct audience

4. **SignalR connections fail**
- Ensure `offline_access` scope for refresh tokens
- Verify token provider returns valid token
- Check SignalR hub authentication configuration

## Resources

- [Elsa.Studio.Authentication.Abstractions README](../modules/Elsa.Studio.Authentication.Abstractions/README.md)
- [Elsa.Studio.Authentication.OpenIdConnect README](../modules/Elsa.Studio.Authentication.OpenIdConnect/README.md)
- [Microsoft Authentication Documentation](https://learn.microsoft.com/en-us/aspnet/core/blazor/security/)

## Future Enhancements

Potential future additions:

- Automatic provider discovery from configuration
- Multi-tenant authentication support
- Authentication caching and performance optimizations
- Enhanced token refresh strategies
- Authentication event hooks and middleware
- Support for additional identity providers (Auth0, Okta, etc.)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Diagnostics.CodeAnalysis;

namespace Elsa.Studio.Contracts;

/// <summary>
/// Provides API clients to the backend for anonymous (non-authenticated) calls.
/// </summary>
/// <remarks>
/// This provider is intended for endpoints like <c>/identity/login</c> where attaching an access token is not required
/// and can even be harmful (e.g., stale tokens, circular dependencies during sign-in).
/// </remarks>
public interface IAnonymousBackendApiClientProvider
{
/// <summary>
/// Gets the URL to the backend.
/// </summary>
Uri Url { get; }

/// <summary>
/// Gets an API client that does not attach access tokens.
/// </summary>
/// <typeparam name="T">The API client type.</typeparam>
ValueTask<T> GetApiAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(CancellationToken cancellationToken = default) where T : class;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Diagnostics.CodeAnalysis;

namespace Elsa.Studio.Contracts;

/// <summary>
Expand All @@ -15,5 +17,5 @@ public interface IBackendApiClientProvider
/// </summary>
/// <typeparam name="T">The API client type.</typeparam>
/// <returns>The API client.</returns>
ValueTask<T> GetApiAsync<T>(CancellationToken cancellationToken = default) where T : class;
ValueTask<T> GetApiAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(CancellationToken cancellationToken = default) where T : class;
}
Loading