Skip to content

Commit eedf02c

Browse files
CopilotsfmskywalkerCopilot
authored
Add standalone OpenID Connect authentication module with multi-provider abstractions (#721)
* Initial plan * Phase 1-3 complete: Core OIDC module with Server and WASM implementations Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Add shared authentication abstractions for multi-provider support Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Add comprehensive authentication architecture documentation Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Final: Add implementation summary for PR review Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Add authentication infrastructure and modules for Elsa Studio, including ElsaAuth and OpenID Connect integration. * Update project references to include new Elsa Studio Authentication modules. * Update `RedirectToLoginUnauthorizedComponentProvider` to support fallback to default Unauthorized component when `IAuthorizationService` is unavailable. * Mark `Elsa.Studio.Login` APIs as obsolete and migrate authentication to `Elsa.Studio.Authentication.ElsaAuth`. Simplify dependencies and refactor JWT parsing for BlazorServer and BlazorWasm modules. * Switch to OpenID Connect authentication, remove legacy login module, and update service registration methods. * Replace `Login` module with OpenID Connect, update authentication pipeline, and revise default `GetClaimsFromUserInfoEndpoint`. * Replace `BearerTokenHttpMessageHandler` with `AuthenticatingApiHttpMessageHandler` and remove obsolete references * Organize solution structure by adding new folders: authentication, localization, workflows, deprecated, samples, and dashboard. Remove obsolete project references. * Refactor authentication: replace legacy services, update OIDC implementation, and restructure PKCE flow * Add `Elsa.Studio.Authentication.ElsaAuth.UI` module to provide Elsa Identity authentication with a login UI and unauthorized redirect behavior. * Migrate `AUTHENTICATION_ARCHITECTURE.md` to `doc/` folder, update project references, and refine namespace imports for authentication module. * Introduce `IAnonymousBackendApiClientProvider` and refactor API client creation to support non-authenticated backend calls. * Add token refresh mechanism for OpenID Connect and Elsa Identity authentication modules. Introduce token refresh coordinators, configuration providers, and support for silent token refresh. Update related services and integrate advanced options for customization. * Add persisted token refresh for OpenID Connect in Blazor Server: implement browser-side pings, background services, and configurable strategies. * Remove persisted token refresh strategy and related services from OpenID Connect configuration. * Refactor OIDC configuration for Blazor WebAssembly: add Azure AD compatibility patches, improve URI handling, and modularize features. * Fix Azure AD authentication in Blazor WASM by passing explicit API scopes during token exchange (#722) * Initial plan * Fix Azure AD authentication by passing explicit API scopes during token requests - Updated WasmOidcTokenAccessor to request access tokens with explicit resource scopes - Filter out standard OIDC scopes (openid, profile, email, offline_access) and pass only API scopes - Register OidcOptions in DI container so WasmOidcTokenAccessor can access configured scopes - This ensures Azure AD receives scope parameter during token exchange, fixing AADSTS errors Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Add Azure AD configuration documentation for Blazor WASM - Document Azure AD app registration setup and requirements - Explain single-resource scope limitation (no mixing Graph + custom API scopes) - Add troubleshooting guide for common Azure AD errors (AADSTS28000, AADSTS28003) - Update example to use AddElsaOidcAuthentication instead of AddOidcAuthentication - Document that standard OIDC scopes are automatically filtered Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Address code review feedback - Register OidcOptions as singleton instance instead of using Configure<T> - Remove IOptions<T> dependency from WasmOidcTokenAccessor - Add null check for Scopes array to prevent NullReferenceException - Simplify DI registration pattern Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> * Update src/modules/Elsa.Studio.Authentication.OpenIdConnect.BlazorWasm/Services/WasmOidcTokenAccessor.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/Elsa.Studio.Authentication.OpenIdConnect.BlazorWasm/Services/WasmOidcTokenAccessor.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove obsolete Azure AD compatibility patches and cleanup related JavaScript and Razor components. * Refactor OpenID Connect callback path handling: use null-coalescing assignments and remove default path values from `OidcOptions`. * Add token purposes and scoped token caching for enhanced authentication configuration * Add scoped access token capabilities and token-purpose configuration Introduce `IScopedAccessTokenProvider`, `IOidcTokenAccessorWithScopes`, and associated models to enable scope-aware token acquisition based on token purposes. Update handlers to support backend API scopes and implement scoped token caching for multi-audience token scenarios. * Refactor authentication modules: simplify scoped token handling, update OIDC providers, and enhance incremental consent support. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> Co-authored-by: Sipke Schoorstra <sipke.schoorstra@nexxbiz.io> Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 401fd83 commit eedf02c

File tree

116 files changed

+4976
-211
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

116 files changed

+4976
-211
lines changed

AZURE_AD_BLAZOR_WASM_AUTH_PLAN.md

Lines changed: 538 additions & 0 deletions
Large diffs are not rendered by default.

Directory.Packages.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
</ItemGroup>
2727
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
2828
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.22" />
29+
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.22" />
2930
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="8.0.22" />
3031
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.22" />
3132
<PackageVersion Include="Microsoft.AspNetCore.Components.CustomElements" Version="8.0.22" />
@@ -41,6 +42,7 @@
4142
</ItemGroup>
4243
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
4344
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.11" />
45+
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.11" />
4446
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="9.0.11" />
4547
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.11" />
4648
<PackageVersion Include="Microsoft.AspNetCore.Components.CustomElements" Version="9.0.11" />
@@ -56,6 +58,7 @@
5658
</ItemGroup>
5759
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
5860
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
61+
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1" />
5962
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="10.0.1" />
6063
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1" />
6164
<PackageVersion Include="Microsoft.AspNetCore.Components.CustomElements" Version="10.0.1" />

Elsa.Studio.sln

Lines changed: 375 additions & 17 deletions
Large diffs are not rendered by default.

doc/AUTHENTICATION_ARCHITECTURE.md

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
# Elsa Studio Authentication Architecture
2+
3+
This document provides an overview of the authentication architecture in Elsa Studio, including how different authentication providers integrate with the framework.
4+
5+
## Overview
6+
7+
Elsa Studio supports multiple authentication providers through a flexible, extensible architecture. The system is designed to:
8+
9+
1. Support multiple authentication mechanisms (OIDC, OAuth2, JWT, etc.)
10+
2. Work across different Blazor hosting models (Server and WebAssembly)
11+
3. Provide automatic token management and refresh
12+
4. Integrate seamlessly with backend API calls and SignalR connections
13+
14+
## Architecture Layers
15+
16+
```
17+
┌─────────────────────────────────────────────────────────────────┐
18+
│ Elsa Studio Application │
19+
│ (Workflows, Dashboard, etc. - uses IAuthenticationProviderManager) │
20+
└──────────────────────────┬──────────────────────────────────────┘
21+
22+
23+
┌─────────────────────────────────────────────────────────────────┐
24+
│ Elsa.Studio.Core │
25+
│ • IAuthenticationProvider - Gets tokens for the app │
26+
│ • IAuthenticationProviderManager - Manages multiple providers │
27+
│ • TokenNames - Standard token name constants │
28+
└──────────────────────────┬──────────────────────────────────────┘
29+
30+
31+
┌─────────────────────────────────────────────────────────────────┐
32+
│ Elsa.Studio.Authentication.Abstractions │
33+
│ • ITokenAccessor - Provider-agnostic token access │
34+
│ • AuthenticationOptions - Base configuration │
35+
└──────────────────────────┬──────────────────────────────────────┘
36+
37+
┌───────────────┴───────────────┬─────────────────┐
38+
▼ ▼ ▼
39+
┌──────────────────────┐ ┌──────────────────┐ ┌─────────────┐
40+
│ OIDC Provider │ │ OAuth2 Provider │ │ Future │
41+
│ • IOidcTokenAccessor│ │ (Future) │ │ Providers │
42+
│ • OidcOptions │ │ │ │ (JWT, SAML) │
43+
│ • Server & WASM │ │ │ │ │
44+
└──────────────────────┘ └──────────────────┘ └─────────────┘
45+
```
46+
47+
## Core Concepts
48+
49+
### 1. Token Flow
50+
51+
```
52+
Application Request
53+
54+
IAuthenticationProviderManager.GetAuthenticationTokenAsync()
55+
56+
[Iterates through registered IAuthenticationProvider instances]
57+
58+
IAuthenticationProvider.GetAccessTokenAsync()
59+
60+
ITokenAccessor.GetTokenAsync()
61+
62+
[Provider-specific token retrieval]
63+
64+
Token returned to application
65+
```
66+
67+
### 2. Provider Registration
68+
69+
Multiple authentication providers can be registered simultaneously:
70+
71+
```csharp
72+
// Example: Register OIDC for API calls, JWT for specific endpoints
73+
services.AddOidcAuthentication(options => { /* OIDC config */ });
74+
services.AddJwtAuthentication(options => { /* JWT config */ });
75+
76+
// The manager will try each provider until a valid token is found
77+
```
78+
79+
### 3. Hosting Model Differences
80+
81+
#### Blazor Server
82+
- Uses ASP.NET Core authentication middleware
83+
- Tokens stored server-side in authentication properties
84+
- Accessed via `HttpContext.GetTokenAsync()`
85+
- Cookie-based session management
86+
- No client-side token exposure
87+
88+
#### Blazor WebAssembly
89+
- Uses `Microsoft.AspNetCore.Components.WebAssembly.Authentication`
90+
- Tokens managed by browser-based authentication framework
91+
- Accessed via `IAccessTokenProvider`
92+
- Automatic token refresh before expiry
93+
- Secure token storage in browser
94+
95+
## Standard Interfaces
96+
97+
### IAuthenticationProvider (Core)
98+
99+
The main interface used by Elsa Studio applications.
100+
101+
```csharp
102+
public interface IAuthenticationProvider
103+
{
104+
Task<string?> GetAccessTokenAsync(string tokenName, CancellationToken cancellationToken = default);
105+
}
106+
```
107+
108+
**Purpose**: Provides tokens to the application for API calls and SignalR connections.
109+
110+
### IAuthenticationProviderManager (Core)
111+
112+
Manages multiple authentication providers.
113+
114+
```csharp
115+
public interface IAuthenticationProviderManager
116+
{
117+
Task<string?> GetAuthenticationTokenAsync(string? tokenName, CancellationToken cancellationToken = default);
118+
}
119+
```
120+
121+
**Purpose**: Iterates through registered providers to find a valid token.
122+
123+
### ITokenAccessor (Abstractions)
124+
125+
Provider-agnostic interface for token retrieval.
126+
127+
```csharp
128+
public interface ITokenAccessor
129+
{
130+
Task<string?> GetTokenAsync(string tokenName, CancellationToken cancellationToken = default);
131+
}
132+
```
133+
134+
**Purpose**: Allows authentication providers to implement token access in their own way.
135+
136+
## Token Names
137+
138+
Standard token names are defined in `TokenNames` class:
139+
140+
- `TokenNames.AccessToken` - Access token for API authentication
141+
- `TokenNames.IdToken` - Identity token (may not be available in all providers/hosting models)
142+
- `TokenNames.RefreshToken` - Refresh token (may not be available in all providers/hosting models)
143+
144+
## Authentication Providers
145+
146+
### Current Providers
147+
148+
#### 1. Elsa.Studio.Login (Legacy)
149+
- **Location**: `src/modules/Elsa.Studio.Login`
150+
- **Supports**: OIDC, OAuth2, Elsa Identity
151+
- **Status**: Maintained for backward compatibility
152+
- **Note**: Tight coupling with general login functionality
153+
154+
#### 2. Elsa.Studio.Authentication.OpenIdConnect (New)
155+
- **Location**: `src/modules/Elsa.Studio.Authentication.OpenIdConnect`
156+
- **Supports**: OpenID Connect
157+
- **Hosting**: Separate packages for Server and WASM
158+
- **Features**:
159+
- Uses Microsoft's built-in OIDC handlers
160+
- Automatic token refresh
161+
- PKCE support
162+
- Cookie-based auth (Server) or framework-managed (WASM)
163+
164+
### Future Providers (Examples)
165+
166+
- `Elsa.Studio.Authentication.OAuth2` - Pure OAuth2 without OIDC
167+
- `Elsa.Studio.Authentication.Jwt` - JWT bearer token authentication
168+
- `Elsa.Studio.Authentication.Saml` - SAML authentication
169+
- `Elsa.Studio.Authentication.AzureAD` - Azure AD specific optimizations
170+
- Custom implementations for proprietary auth systems
171+
172+
## Integration Points
173+
174+
### 1. API Calls
175+
176+
The `AuthenticatingApiHttpMessageHandler` automatically adds authentication tokens to API requests:
177+
178+
```csharp
179+
// In Elsa.Studio.Login
180+
public class AuthenticatingApiHttpMessageHandler : DelegatingHandler
181+
{
182+
protected override async Task<HttpResponseMessage> SendAsync(...)
183+
{
184+
var token = await jwtAccessor.ReadTokenAsync(TokenNames.AccessToken);
185+
request.Headers.Authorization = new("Bearer", token);
186+
// ... handle 401 with token refresh
187+
}
188+
}
189+
```
190+
191+
### 2. SignalR Connections
192+
193+
The `WorkflowInstanceObserverFactory` retrieves tokens for SignalR hub connections:
194+
195+
```csharp
196+
var token = await authenticationProviderManager
197+
.GetAuthenticationTokenAsync(TokenNames.AccessToken, cancellationToken);
198+
199+
var connection = new HubConnectionBuilder()
200+
.WithUrl(hubUrl, options =>
201+
{
202+
options.AccessTokenProvider = () => Task.FromResult(token);
203+
})
204+
.Build();
205+
```
206+
207+
### 3. Authorization State
208+
209+
Blazor's `AuthenticationStateProvider` is used for UI authorization:
210+
211+
```razor
212+
@attribute [Authorize]
213+
214+
<AuthorizeView>
215+
<Authorized>
216+
<!-- Show protected content -->
217+
</Authorized>
218+
<NotAuthorized>
219+
<RedirectToLogin />
220+
</NotAuthorized>
221+
</AuthorizeView>
222+
```
223+
224+
## Security Considerations
225+
226+
### Server Hosting
227+
- ✅ Tokens never exposed to client browser
228+
- ✅ Cookie-based authentication with HTTP-only cookies
229+
- ✅ Secure server-side session management
230+
- ✅ HTTPS-only cookies in production
231+
232+
### WASM Hosting
233+
- ✅ Tokens managed by authentication framework
234+
- ✅ Automatic token expiry and renewal
235+
- ✅ Access tokens available, but refresh tokens hidden
236+
- ✅ Uses standard browser security features
237+
238+
### General
239+
- ✅ PKCE enabled by default for OIDC
240+
- ✅ HTTPS required for metadata endpoints
241+
- ✅ Token refresh on 401 responses
242+
- ✅ Secure token storage per hosting model
243+
244+
## Implementation Guide
245+
246+
### Creating a New Authentication Provider
247+
248+
See `src/modules/Elsa.Studio.Authentication.Abstractions/README.md` for detailed guidance.
249+
250+
**Quick Steps**:
251+
252+
1. Create provider-specific options extending `AuthenticationOptions`
253+
2. Implement `ITokenAccessor` for your provider
254+
3. Implement `IAuthenticationProvider` using your token accessor
255+
4. Create hosting-specific implementations if needed (Server vs WASM)
256+
5. Register services in DI container
257+
258+
### Using an Authentication Provider
259+
260+
**Blazor Server**:
261+
```csharp
262+
builder.Services.AddOidcAuthentication(options =>
263+
{
264+
options.Authority = "https://identity-server.com";
265+
options.ClientId = "elsa-studio";
266+
options.ClientSecret = "secret";
267+
options.Scopes = new[] { "openid", "profile", "elsa_api" };
268+
});
269+
270+
app.UseAuthentication();
271+
app.UseAuthorization();
272+
```
273+
274+
**Blazor WASM**:
275+
```csharp
276+
builder.Services.AddOidcAuthentication(options =>
277+
{
278+
options.Authority = "https://identity-server.com";
279+
options.ClientId = "elsa-studio-wasm";
280+
options.Scopes = new[] { "openid", "profile", "elsa_api" };
281+
});
282+
```
283+
284+
## Migration Path
285+
286+
### From Elsa.Studio.Login to New Providers
287+
288+
The new authentication providers are designed to coexist with the legacy `Elsa.Studio.Login`:
289+
290+
1. **Phase 1**: Add new provider alongside existing Login module
291+
2. **Phase 2**: Test new provider with your identity server
292+
3. **Phase 3**: Switch to new provider by removing Login module registration
293+
4. **Phase 4**: Remove Login module dependency once stable
294+
295+
No breaking changes to existing applications.
296+
297+
## Troubleshooting
298+
299+
### Common Issues
300+
301+
1. **Multiple providers registered, wrong one used**
302+
- `IAuthenticationProviderManager` returns first valid token
303+
- Check registration order
304+
- Ensure only desired provider is registered
305+
306+
2. **Tokens not available in WASM**
307+
- Only access tokens directly accessible
308+
- Refresh/ID tokens managed by framework
309+
310+
3. **401 errors on API calls**
311+
- Check token scopes match API requirements
312+
- Verify `AuthenticatingApiHttpMessageHandler` is registered
313+
- Check identity server returns correct audience
314+
315+
4. **SignalR connections fail**
316+
- Ensure `offline_access` scope for refresh tokens
317+
- Verify token provider returns valid token
318+
- Check SignalR hub authentication configuration
319+
320+
## Resources
321+
322+
- [Elsa.Studio.Authentication.Abstractions README](../modules/Elsa.Studio.Authentication.Abstractions/README.md)
323+
- [Elsa.Studio.Authentication.OpenIdConnect README](../modules/Elsa.Studio.Authentication.OpenIdConnect/README.md)
324+
- [Microsoft Authentication Documentation](https://learn.microsoft.com/en-us/aspnet/core/blazor/security/)
325+
326+
## Future Enhancements
327+
328+
Potential future additions:
329+
330+
- Automatic provider discovery from configuration
331+
- Multi-tenant authentication support
332+
- Authentication caching and performance optimizations
333+
- Enhanced token refresh strategies
334+
- Authentication event hooks and middleware
335+
- Support for additional identity providers (Auth0, Okta, etc.)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace Elsa.Studio.Contracts;
4+
5+
/// <summary>
6+
/// Provides API clients to the backend for anonymous (non-authenticated) calls.
7+
/// </summary>
8+
/// <remarks>
9+
/// This provider is intended for endpoints like <c>/identity/login</c> where attaching an access token is not required
10+
/// and can even be harmful (e.g., stale tokens, circular dependencies during sign-in).
11+
/// </remarks>
12+
public interface IAnonymousBackendApiClientProvider
13+
{
14+
/// <summary>
15+
/// Gets the URL to the backend.
16+
/// </summary>
17+
Uri Url { get; }
18+
19+
/// <summary>
20+
/// Gets an API client that does not attach access tokens.
21+
/// </summary>
22+
/// <typeparam name="T">The API client type.</typeparam>
23+
ValueTask<T> GetApiAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(CancellationToken cancellationToken = default) where T : class;
24+
}

src/framework/Elsa.Studio.Core/Contracts/IBackendApiClientProvider.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
13
namespace Elsa.Studio.Contracts;
24

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

0 commit comments

Comments
 (0)