Skip to content

Server-Side Authentication ​

This document covers server-side authentication components: SessionMiddleware, ServerAuthHelper, and ASP.NET Core integration.

Overview ​

Fusion's server-side authentication bridges ASP.NET Core's authentication with Fusion's auth services:

ComponentPurpose
SessionMiddlewareManages session cookies
ServerAuthHelperSyncs ASP.NET Core auth state with Fusion
AuthControllerHandles sign-in/sign-out HTTP endpoints
RpcDefaultSessionReplacerReplaces Session.Default with real sessions in RPC calls

SessionMiddleware ​

View Source

Manages session cookies and resolves the current session for each request.

How It Works ​

  1. Reads the session ID from the cookie
  2. Validates the session (checks for forced sign-out)
  3. Creates a new session if needed
  4. Sets the session on ISessionResolver
  5. Updates the cookie

Configuration ​

csharp
// In Program.cs
var fusion = services.AddFusion();
var fusionServer = fusion.AddWebServer();

fusionServer.ConfigureSessionMiddleware(_ => new SessionMiddleware.Options() {
    Cookie = new CookieBuilder() {
        Name = "FusionAuth.SessionId",
        IsEssential = true,
        HttpOnly = true,
        SameSite = SameSiteMode.Lax,
        Expiration = TimeSpan.FromDays(28),
    },
    AlwaysUpdateCookie = true,  // Refresh cookie expiration on each request
});

Options ​

OptionTypeDefaultDescription
CookieCookieBuilderSee belowCookie configuration
AlwaysUpdateCookiebooltrueRefresh cookie on every request
RequestFilterFunc<HttpContext, bool>_ => trueFilter which requests get sessions
InvalidSessionHandlerFunc<SessionMiddleware, HttpContext, Task<bool>>DefaultInvalidSessionHandlerHandles a session the validator rejected. Returns true to short-circuit the pipeline (redirect), false to continue
ReloadGuardCookieNamestring"FusionAuth.SessionReload"Cookie that breaks a redirect loop; "" disables the guard
TagProviderFunc<Session, HttpContext, Session>?nullAdd tags to sessions

The default handler signs out (using the default sign-out scheme, and only if one is configured), then redirects to the same URL. Two v14.2 fixes matter here: the session cookie is rewritten before the handler can short-circuit, so the redirected request no longer re-presents the rejected session id; and the ReloadGuardCookieName cookie stops the redirect after one attempt, falling through with a fresh session instead. Together they turn what used to be an unrecoverable redirect loop (or, with no default sign-out scheme configured, a persistent 500) into a single reload.

csharp
Cookie = new CookieBuilder() {
    Name = "FusionAuth.SessionId",
    IsEssential = true,
    HttpOnly = true,
    SameSite = SameSiteMode.Lax,
    Expiration = TimeSpan.FromDays(28),
}

App Configuration ​

csharp
// In Program.cs / Configure
app.UseFusionSession();  // Must be before UseRouting
app.UseRouting();
app.UseAuthentication();
// ...

Custom Invalid-Session Handler ​

csharp
fusionServer.ConfigureSessionMiddleware(_ => new SessionMiddleware.Options() {
    InvalidSessionHandler = async (middleware, httpContext) => {
        await httpContext.SignOutAsync();
        httpContext.Response.Redirect("/logged-out");
        return true;  // true = stop processing, false = continue to next middleware
    },
});

A custom handler that calls the parameterless SignOutAsync() throws when the app has no default sign-out scheme — pass the scheme name, or copy what SessionMiddleware.Options.DefaultInvalidSessionHandler does.

Session Tags ​

Add metadata to sessions based on the request:

csharp
fusionServer.ConfigureSessionMiddleware(_ => new SessionMiddleware.Options() {
    TagProvider = (session, httpContext) => {
        var tenant = httpContext.Request.Headers["X-Tenant"].FirstOrDefault();
        return tenant != null ? session.WithTag("tenant", tenant) : session;
    },
});

ServerAuthHelper ​

View Source

Synchronizes ASP.NET Core authentication state with Fusion's auth services.

How It Works ​

  1. Compares HttpContext.User (ASP.NET Core) with IAuth.GetUser() (Fusion)
  2. If different, calls IAuthBackend.SignIn() or Auth_SignOut to sync
  3. Updates presence information

Configuration ​

csharp
fusionServer.ConfigureServerAuthHelper(_ => new ServerAuthHelper.Options() {
    IdClaimKeys = [ClaimTypes.NameIdentifier],
    NameClaimKeys = [ClaimTypes.Name, "preferred_username"],
    CloseWindowRequestPath = "/fusion/close",
    SessionInfoUpdatePeriod = TimeSpan.FromSeconds(30),
});

Options ​

OptionTypeDefaultDescription
IdClaimKeysstring[][NameIdentifier]Claims to use for user ID
NameClaimKeysstring[][Name]Claims to use for display name
CloseWindowRequestPathstring"/fusion/close"Path for close-window flow
SessionInfoUpdatePeriodTimeSpan30sMin time between session updates
AllowSignInFunc<...>AllowAnywhereWhen to allow sign-in
AllowChangeFunc<...>AllowOnCloseWindowRequestWhen to allow user change
AllowSignOutFunc<...>AllowOnCloseWindowRequestWhen to allow sign-out

Usage in _HostPage.razor ​

razor
@code {
    [Inject] private ServerAuthHelper ServerAuthHelper { get; init; } = null!;
    [CascadingParameter] private HttpContext HttpContext { get; set; } = null!;

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (!_isInitialized) {
            _isInitialized = true;
            parameters.SetParameterProperties(this);

            // Sync ASP.NET Core auth with Fusion
            await ServerAuthHelper.UpdateAuthState(HttpContext);

            // Get available auth schemas for sign-in UI
            _authSchemas = await ServerAuthHelper.GetSchemas(HttpContext);

            // Get session ID for passing to App component
            _sessionId = ServerAuthHelper.Session.Id;
        }
        await base.SetParametersAsync(parameters);
    }
}

Key Methods ​

MethodDescription
UpdateAuthState(HttpContext)Syncs auth state from ASP.NET Core to Fusion
GetSchemas(HttpContext)Gets available authentication schemas
IsCloseWindowRequest(HttpContext)Checks if request is the close-window callback

Sign-In/Sign-Out Flow Control ​

By default, sign-in is allowed anywhere, but sign-out and user switching only happen on the "close window" request path:

csharp
// Custom flow control
fusionServer.ConfigureServerAuthHelper(_ => new ServerAuthHelper.Options() {
    // Always allow sign-in (when HttpContext.User is authenticated)
    AllowSignIn = (helper, ctx) => true,

    // Only allow sign-out on specific path
    AllowSignOut = (helper, ctx) =>
        ctx.Request.Path.StartsWithSegments("/auth"),

    // Only allow user change (switch accounts) on specific path
    AllowChange = (helper, ctx) =>
        ctx.Request.Path.StartsWithSegments("/auth"),
});

AuthController ​

Fusion provides a built-in controller for authentication endpoints.

Default Endpoints ​

EndpointMethodDescription
/signInGETRedirects to OAuth provider
/signIn/{schema}GETSign in with specific provider
/signOutGETSigns out and redirects

Configuration ​

csharp
fusionServer.ConfigureAuthEndpoint(_ => new AuthEndpoint.Options() {
    DefaultSignInScheme = MicrosoftAccountDefaults.AuthenticationScheme,
    SignInPropertiesBuilder = (_, properties) => {
        properties.IsPersistent = true;
        properties.ExpiresUtc = DateTimeOffset.UtcNow.AddDays(28);
    },
    DefaultSignOutPath = "/",
});

App Configuration ​

csharp
app.MapFusionAuthEndpoints();  // Maps /signIn, /signOut, etc.

Custom Controller ​

csharp
[Route("auth")]
public class CustomAuthController : Controller
{
    [HttpGet("login/{schema?}")]
    public async Task<IActionResult> SignIn(string? schema = null)
    {
        schema ??= "Google";
        var properties = new AuthenticationProperties {
            RedirectUri = "/",
            IsPersistent = true,
        };
        return Challenge(properties, schema);
    }

    [HttpGet("logout")]
    public async Task<IActionResult> SignOut()
    {
        await HttpContext.SignOutAsync();
        return Redirect("/");
    }
}

// Disable built-in controller
fusionServer.AddControllerFilter(t => t != typeof(AuthController));

RpcDefaultSessionReplacer ​

Automatically replaces Session.Default with the real session in RPC calls.

How It Works ​

  1. Client sends Session.Default ("~") in RPC call parameters
  2. Server middleware intercepts the call
  3. Looks up real session from ISessionResolver
  4. Replaces Session.Default with real session
  5. Processes the call

Registration ​

Automatically registered when you call fusion.AddWebServer().

Complete Server Setup ​

Program.cs ​

csharp
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;

// Add Fusion
var fusion = services.AddFusion(RpcServiceMode.Server, true);
var fusionServer = fusion.AddWebServer();

// Add database auth service
fusion.AddDbAuthService<AppDbContext, long>();

// Configure auth endpoints
fusionServer.ConfigureAuthEndpoint(_ => new AuthEndpoint.Options() {
    DefaultSignInScheme = GoogleDefaults.AuthenticationScheme,
});

// Configure session middleware
fusionServer.ConfigureSessionMiddleware(_ => new SessionMiddleware.Options() {
    Cookie = new CookieBuilder() {
        Name = "MyApp.Session",
        HttpOnly = true,
        SameSite = SameSiteMode.Lax,
        Expiration = TimeSpan.FromDays(30),
    },
});

// Configure server auth helper
fusionServer.ConfigureServerAuthHelper(_ => new ServerAuthHelper.Options() {
    NameClaimKeys = [ClaimTypes.Name, "name", "preferred_username"],
});

// Add ASP.NET Core authentication
services.AddAuthentication(options => {
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddCookie(options => {
    options.LoginPath = "/signIn";
    options.LogoutPath = "/signOut";
    options.ExpireTimeSpan = TimeSpan.FromDays(7);
    options.SlidingExpiration = true;
})
.AddGoogle(options => {
    options.ClientId = "your-client-id";
    options.ClientSecret = "your-client-secret";
});

// Blazor and other services...

var app = builder.Build();

// Middleware order matters!
app.UseWebSockets();
app.UseFusionSession();     // Session handling
app.UseRouting();
app.UseAuthentication();    // ASP.NET Core auth
app.UseAuthorization();
app.UseAntiforgery();

// Static files and Blazor
app.MapStaticAssets();
app.MapRazorComponents<_HostPage>()
    .AddInteractiveServerRenderMode()
    .AddInteractiveWebAssemblyRenderMode();

// Fusion endpoints
app.MapRpcWebSocketServer();
app.MapFusionAuthEndpoints();
app.MapFusionRenderModeEndpoints();

app.Run();

_HostPage.razor ​

razor
@using ActualLab.Fusion.Blazor
@using ActualLab.Fusion.Server.Authentication
@using ActualLab.Fusion.Server.Endpoints

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My App</title>
    <base href="/" />
    <script src="_content/ActualLab.Fusion.Blazor.Authentication/scripts/fusionAuth.js">
    </script>
    <script>
        window.FusionAuth.schemas = "@_authSchemas";
    </script>
    <HeadOutlet @rendermode="@(_renderMode?.Mode)" />
</head>
<body>
    <App @rendermode="@(_renderMode?.Mode)"
         SessionId="@_sessionId"
         RenderModeKey="@(_renderMode?.Key)"/>
    <script src="_framework/blazor.web.js"></script>
</body>
</html>

@code {
    private bool _isInitialized;
    private RenderModeDef? _renderMode;
    private string _authSchemas = "";
    private string _sessionId = "";

    [Inject] private ServerAuthHelper ServerAuthHelper { get; init; } = null!;
    [CascadingParameter] private HttpContext HttpContext { get; set; } = null!;

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        if (!_isInitialized) {
            _isInitialized = true;
            parameters.SetParameterProperties(this);
            if (HttpContext.AcceptsInteractiveRouting())
                _renderMode = RenderModeEndpoint.GetRenderMode(HttpContext);
            await ServerAuthHelper.UpdateAuthState(HttpContext);
            _authSchemas = await ServerAuthHelper.GetSchemas(HttpContext);
            _sessionId = ServerAuthHelper.Session.Id;
        }
        await base.SetParametersAsync(parameters);
    }
}

JavaScript Integration ​

fusionAuth.js ​

The fusionAuth.js script (included in ActualLab.Fusion.Blazor.Authentication) provides client-side auth functions:

javascript
window.FusionAuth = {
    schemas: "",  // Set from server
    signInPath: "/signIn",
    signOutPath: "/signOut",

    signIn: function(schema) {
        var url = this.signInPath;
        if (schema)
            url += "/" + schema;
        window.location.href = url;
    },

    signOut: function() {
        window.location.href = this.signOutPath;
    },

    getSchemas: function() {
        // Returns array of {name, displayName}
        return this.parseSchemas(this.schemas);
    }
};

Custom JavaScript ​

html
<script>
    window.FusionAuth.signInPath = "/auth/login";
    window.FusionAuth.signOutPath = "/auth/logout";

    // Override sign-in to use popup
    window.FusionAuth.signIn = function(schema) {
        var url = this.signInPath + "/" + schema;
        var popup = window.open(url, "auth", "width=500,height=600");
        // Handle popup close...
    };
</script>

Security Considerations ​

Session ID Security ​

  • Session IDs are stored in HTTP-only cookies (not accessible via JavaScript)
  • Clients use Session.Default which is replaced server-side
  • The real session ID never leaves the server

Forced Sign-Out ​

When security requires immediate session invalidation:

csharp
// Force sign-out creates a permanent invalid session state
var command = new Auth_SignOut(session, force: true);
await Commander.Call(command);

// The session is marked as "force signed out"
// Any request with this session will:
// 1. Trigger SessionMiddleware.ForcedSignOutHandler
// 2. Sign out of ASP.NET Core
// 3. Create a new session
// 4. Redirect

CORS and Cookies ​

For cross-origin scenarios:

csharp
services.AddCors(options => {
    options.AddPolicy("AllowApp", policy => {
        policy.WithOrigins("https://app.example.com")
              .AllowCredentials()  // Required for cookies
              .AllowAnyMethod()
              .AllowAnyHeader();
    });
});

fusionServer.ConfigureSessionMiddleware(_ => new SessionMiddleware.Options() {
    Cookie = new CookieBuilder() {
        SameSite = SameSiteMode.None,  // Required for cross-origin
        SecurePolicy = CookieSecurePolicy.Always,  // Required with SameSite=None
    },
});

// REQUIRED with SameSite=None: CORS does not apply to WebSocket handshakes,
// so the RPC endpoint needs its own origin allow-list.
rpc.AddWebSocketServer().Configure(_ => RpcWebSocketServerOptions.Default with {
    OriginValidator = RpcWebSocketServerOriginValidators.Allow("https://app.example.com"),
});

Cross-Site WebSocket Hijacking ​

The RPC WebSocket connection carries the session cookie, and the WebSocket handshake is exempt from CORS and from preflight — a CORS policy therefore does not protect it. Without an origin check, any page the victim visits can open wss://your-app/rpc/ws?clientId=…, have the browser attach the victim's cookie, and then speak RPC as the victim.

Two independent mechanisms can close this, and they compose:

MechanismScopeNotes
WebSocketOptions.AllowedOriginsEvery WebSocket endpoint in the appASP.NET Core only. Rejects with 403 before any endpoint runs. Empty list = allow all. Exact, case-sensitive string match
RpcWebSocketServerOptions.OriginValidatorThe RPC endpoint onlyWorks on ASP.NET Core and OWIN. Can express "same origin as this request", which a static list cannot

Both treat an absent Origin as allowed: browsers always send it on a handshake and page scripts cannot forge it (it is a forbidden header), so only non-browser clients can omit it — and they gain nothing by doing so, because the attack depends on the victim's browser attaching the victim's cookie.

Fusion's default is RpcWebSocketServerOriginValidators.AllowAll, so nothing breaks on upgrade. Pick a stricter one explicitly — see RpcWebSocketServerOptions. When connections are session-bound and the validator is still AllowAll, Fusion logs a one-time warning at startup.