A critical vulnerability was discovered in React Server Components (Next.js). Our systems remain protected but we advise to update packages to newest version. Learn More

ASP.NET Core + Okta: Authentication Cookie Not Persisting on /signin-oidc Callback

Vote:
 

Hi,

I'm running into an issue with Okta authentication in my ASP.NET Core application. Here's the scenario:

When a user accesses a direct URL (e.g., an image, PDF, or a specific page), the authentication flow correctly redirects them to Okta for login.
After successful authentication, Okta redirects back to the /signin-oidc endpoint.
However, at this point, the ASP.NET Core authentication cookies that are normally created during a standard login flow are not being properly created or persisted.
This issue only occurs when accessing direct URLs. The normal login flow works fine and sets the cookies correctly.

Interestingly, this works perfectly in my development environment—even unauthenticated users can access the files and the cookies are set as expected. But in the deployed environment, the cookies are missing after the callback.

Here’s a snippet of my Okta extension class that I’m calling from Startup.cs:

using EPiServer.Security;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.IdentityModel.Tokens;
using Okta.AspNetCore;
using System.Security.Claims;
using System.Text;

namespace XXX.EpiServer.Okta;

/// <summary>
/// Extension methods for configuring Okta authentication in an ASP.NET Core application.
/// </summary>
public static class OktaExtensions
{
    // Constants for better maintainability
    private const string OktaAuthScheme = "okta.auth";
    private const string CorrelationCookieName = ".AspNetCore.Correlation.OpenIdConnect";
    private const string SessionCorrelationKey = "OktaCorrelation";
    private const string XsrfCorrelationKey = ".xsrf";
    private const string CorrelationIdParamName = "correlation_id";
    
    /// <summary>
    /// Configures Okta Authentication.
    /// </summary>
    /// <param name="services">The service collection.</param>
    /// <param name="configuration">The application configuration.</param>
    /// <param name="forceHttpsRedirect">Indicates whether HTTPS redirection is enforced.</param>
    public static void ConfigureOkta(this IServiceCollection services, IConfiguration configuration, bool forceHttpsRedirect)
    {
        // Configure core Okta services
        ConfigureDataProtection(services);
        
        // Parse scopes from configuration
        var scopes = ParseScopes(configuration);
        
        // Configure authentication
        services.AddAuthentication(ConfigureAuthenticationOptions)
                .AddCookie(OktaAuthScheme, ConfigureCookieOptions)
                .AddOktaMvc(CreateOktaMvcOptions(configuration, scopes, forceHttpsRedirect));
                
        // Configure OpenID Connect options
        services.PostConfigureAll<OpenIdConnectOptions>(ConfigureOpenIdConnectOptions);
    }
    
    #region Configuration Methods
    
    /// <summary>
    /// Configures data protection for authentication cookies
    /// </summary>
    private static void ConfigureDataProtection(IServiceCollection services)
    {
        services.AddDataProtection()
                .SetApplicationName("XXX.EpiServer");
    }
    
    /// <summary>
    /// Configures authentication options
    /// </summary>
    private static void ConfigureAuthenticationOptions(AuthenticationOptions options)
    {
        options.DefaultScheme = OktaAuthScheme;
        options.DefaultAuthenticateScheme = OktaAuthScheme;
        options.DefaultSignInScheme = OktaAuthScheme;
        options.DefaultSignOutScheme = OktaAuthScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    }
    
    /// <summary>
    /// Configures cookie authentication options
    /// </summary>
    private static void ConfigureCookieOptions(CookieAuthenticationOptions options)
    {
        // Configure cookie security settings
        ConfigureCookieSecurity(options.Cookie);
        
        // Configure expiration settings
        options.ExpireTimeSpan = TimeSpan.FromHours(12);
        options.SlidingExpiration = true;
        
        // Configure event handlers
        options.Events = CreateCookieAuthenticationEvents();
    }
    
    /// <summary>
    /// Creates and configures cookie authentication events
    /// </summary>
    private static CookieAuthenticationEvents CreateCookieAuthenticationEvents()
    {
        return new CookieAuthenticationEvents
        {
            OnSignedIn = SynchronizeUserOnSignIn,
            OnValidatePrincipal = _ => Task.CompletedTask
        };
    }
    
    /// <summary>
    /// Synchronizes the user with EPiServer on sign-in
    /// </summary>
    private static async Task SynchronizeUserOnSignIn(CookieSignedInContext ctx)
    {
        if (ctx.Principal?.Identity is ClaimsIdentity claimsIdentity)
        {
            var synchronizingUserService = ctx.HttpContext.RequestServices.GetRequiredService<ISynchronizingUserService>();
            await synchronizingUserService.SynchronizeAsync(claimsIdentity);
        }
    }
    
    /// <summary>
    /// Configures cookie security settings
    /// </summary>
    private static void ConfigureCookieSecurity(CookieBuilder cookieBuilder)
    {
        cookieBuilder.Name = "XXX.EpiServer.Auth";
        cookieBuilder.HttpOnly = true;
        cookieBuilder.SameSite = SameSiteMode.None;
        cookieBuilder.SecurePolicy = CookieSecurePolicy.Always;
        cookieBuilder.IsEssential = true;
        cookieBuilder.Path = "/";
    }
    
    /// <summary>
    /// Creates OktaMvc options from configuration
    /// </summary>
    private static OktaMvcOptions CreateOktaMvcOptions(IConfiguration configuration, IList<string> scopes, bool forceHttpsRedirect)
    {
        return new OktaMvcOptions
        {
            OktaDomain = configuration["OktaDomain"],
            ClientId = configuration["OktaClientId"],
            ClientSecret = configuration["OktaClientSecret"],
            AuthorizationServerId = configuration["OktaAuthorizationServerId"],
            Scope = scopes,
            CallbackPath = "/signin-oidc",
            GetClaimsFromUserInfoEndpoint = true,
            PostLogoutRedirectUri = "/",
            OpenIdConnectEvents = CreateOpenIdConnectEvents(forceHttpsRedirect)
        };
    }
    
    /// <summary>
    /// Creates OpenIdConnect event handlers
    /// </summary>
    private static OpenIdConnectEvents CreateOpenIdConnectEvents(bool forceHttpsRedirect)
    {
        return new OpenIdConnectEvents
        {
            OnAuthenticationFailed = HandleAuthenticationFailure,
            OnTokenValidated = HandleTokenValidation,
            OnRedirectToIdentityProvider = context => HandleRedirectToIdentityProvider(context, forceHttpsRedirect),
            OnMessageReceived = HandleMessageReceived,
            OnSignedOutCallbackRedirect = HandleSignedOutCallback
        };
    }
    
    /// <summary>
    /// Configures OpenIdConnect options
    /// </summary>
    private static void ConfigureOpenIdConnectOptions(OpenIdConnectOptions options)
    {
        // Configure token validation
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            NameClaimType = "name",
            RoleClaimType = "groups",
            ValidateAudience = false
        };
        
        // Configure cookie settings
        ConfigureOpenIdConnectCookies(options);
        
        // Configure security features
        options.UsePkce = true;
        options.SaveTokens = true;
        options.ResponseType = "code";
    }
    
    /// <summary>
    /// Configures OpenIdConnect cookie settings
    /// </summary>
    private static void ConfigureOpenIdConnectCookies(OpenIdConnectOptions options)
    {
        // Configure nonce cookie
        options.NonceCookie.SecurePolicy = CookieSecurePolicy.Always;
        options.NonceCookie.SameSite = SameSiteMode.None;
        options.NonceCookie.HttpOnly = true;
        options.NonceCookie.IsEssential = true;
        options.NonceCookie.Path = "/";
        
        // Configure correlation cookie
        options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
        options.CorrelationCookie.SameSite = SameSiteMode.None;
        options.CorrelationCookie.HttpOnly = true;
        options.CorrelationCookie.IsEssential = true;
        options.CorrelationCookie.Path = "/";
    }
    
    #endregion
    
    #region Event Handlers
    
    /// <summary>
    /// Handles authentication failures
    /// </summary>
    private static async Task HandleAuthenticationFailure(AuthenticationFailedContext context)
    {
        context.HandleResponse();
        await context.Response.BodyWriter.WriteAsync(Encoding.ASCII.GetBytes(context.Exception.Message));
    }
    
    /// <summary>
    /// Handles token validation
    /// </summary>
    private static async Task HandleTokenValidation(TokenValidatedContext ctx)
    {
        // Synchronize user with EPiServer
        if (ctx?.Principal?.Identity is ClaimsIdentity claimsIdentity)
        {
            var synchronizingUserService = ctx.HttpContext.RequestServices.GetRequiredService<ISynchronizingUserService>();
            await synchronizingUserService.SynchronizeAsync(claimsIdentity);
        }

        // Fix redirect URI if it's absolute
        if (!string.IsNullOrEmpty(ctx?.Properties?.RedirectUri))
        {
            var redirectUri = new Uri(ctx.Properties.RedirectUri, UriKind.RelativeOrAbsolute);
            if (redirectUri.IsAbsoluteUri)
            {
                ctx.Properties.RedirectUri = redirectUri.PathAndQuery;
            }
        }
    }
    
    /// <summary>
    /// Handles redirect to identity provider
    /// </summary>
    private static Task HandleRedirectToIdentityProvider(RedirectContext context, bool forceHttpsRedirect)
    {
        // Force HTTPS redirect if needed
        if (forceHttpsRedirect)
        {
            context.ProtocolMessage.RedirectUri = context.ProtocolMessage.RedirectUri.Replace("http:", "https:");
        }

        // Handle 401/403 responses
        if (context.HttpContext.User.Identity?.IsAuthenticated == true && context.Response.StatusCode == 401)
        {
            context.Response.StatusCode = 403;
            context.HandleResponse();
        }
        else if (context.Response.StatusCode == 401)
        {
            context.HandleResponse();
        }
        
        // Handle correlation ID
        StoreCorrelationId(context);

        return Task.CompletedTask;
    }
    
    /// <summary>
    /// Handles message received event
    /// </summary>
    private static Task HandleMessageReceived(MessageReceivedContext context)
    {
        // Restore correlation ID if needed
        RestoreCorrelationId(context);
        return Task.CompletedTask;
    }
    
    /// <summary>
    /// Handles signed out callback
    /// </summary>
    private static Task HandleSignedOutCallback(RemoteSignOutContext context)
    {
        context.Response.Redirect("/");
        context.HandleResponse();
        return Task.CompletedTask;
    }
    
    #endregion
    
    #region Helper Methods
    
    /// <summary>
    /// Parses scopes from configuration
    /// </summary>
    private static IList<string> ParseScopes(IConfiguration configuration)
    {
        return configuration["OktaScopes"]?
            .Split(' ', StringSplitOptions.RemoveEmptyEntries)
            .Select(scope => scope.Trim())
            .ToList() ?? new List<string>();
    }
    
    /// <summary>
    /// Stores correlation ID in URL parameter and session
    /// </summary>
    private static void StoreCorrelationId(RedirectContext context)
    {
        if (context.Properties.Items.TryGetValue(XsrfCorrelationKey, out string correlationId) && 
            !string.IsNullOrEmpty(correlationId))
        {
            // Set as URL parameter
            context.ProtocolMessage.SetParameter(CorrelationIdParamName, correlationId);

            // Store in session for backup
            context.HttpContext.Session?.SetString(SessionCorrelationKey, correlationId);
        }
    }
    
    /// <summary>
    /// Restores correlation ID from session if needed
    /// </summary>
    private static void RestoreCorrelationId(MessageReceivedContext context)
    {
        if (context.ProtocolMessage.State != null && 
            context.HttpContext.Session != null &&
            !context.Request.Cookies.ContainsKey(CorrelationCookieName))
        {
            var sessionCorrelation = context.HttpContext.Session.GetString(SessionCorrelationKey);
            if (!string.IsNullOrEmpty(sessionCorrelation))
            {
                // Add correlation cookie back manually if it's missing
                context.HttpContext.Response.Cookies.Append(
                    CorrelationCookieName,
                    sessionCorrelation,
                    new CookieOptions
                    {
                        HttpOnly = true,
                        Secure = true,
                        SameSite = SameSiteMode.None,
                        IsEssential = true,
                        Path = "/"
                    });
            }
        }
    }
    
    #endregion
}


using EPiServer;
using EPiServer.Core;
using EPiServer.Data;
using EPiServer.Framework;
using EPiServer.LinkAnalyzer;
using EPiServer.Scheduler;
using EPiServer.ServiceLocation;
using EPiServer.Web.Routing;
using Geta.NotFoundHandler.Infrastructure.Initialization;
using Geta.NotFoundHandler.Optimizely.Infrastructure.Initialization;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.CookiePolicy;
using Microsoft.AspNetCore.Rewrite;
using XXX.EpiServer.BlockServices;
using XXX.EpiServer.BlockServices.Interfaces;
using XXX.EpiServer.Business;
using XXX.EpiServer.Business.CdnSupport;
using XXX.EpiServer.Business.Extensions;
using XXX.EpiServer.Business.Messaging;
using XXX.EpiServer.Business.Middlewares;
using XXX.EpiServer.Models.Pages;
using XXX.EpiServer.Okta;

namespace XXX.EpiServer;

public class Startup
{
    private readonly IWebHostEnvironment _webHostingEnvironment;
    private readonly IConfiguration _configuration;

    public Startup(IWebHostEnvironment webHostingEnvironment, IConfiguration configuration)
    {
        _webHostingEnvironment = webHostingEnvironment;
        _configuration = configuration;
    }

    /// <summary>
    /// Configures services for the application
    /// </summary>
    public void ConfigureServices(IServiceCollection services)
    {
        // Core infrastructure services
        ConfigureInfrastructureServices(services);
        
        // Optimizely CMS services
        ConfigureOptimizelyServices(services);
        
        // Authentication and authorization
        ConfigureAuthenticationServices(services);
        
        // Application services
        ConfigureApplicationServices(services);
    }

    /// <summary>
    /// Configures the HTTP request pipeline
    /// </summary>
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // Error handling and diagnostics
        ConfigureErrorHandling(app, env);
        
        // Basic middleware
        ConfigureBasicMiddleware(app);
        
        // Authentication and authorization
        ConfigureAuthMiddleware(app);
        
        // Configure endpoints
        ConfigureEndpoints(app);
        
        // Configure legacy routes
        ConfigureLegacyRoutes(app);
    }
    
    #region Service Configuration Methods
    
    private void ConfigureInfrastructureServices(IServiceCollection services)
    {
        // Telemetry
        services.AddApplicationInsightsTelemetry();
        
        // Environment-specific settings
        if (_webHostingEnvironment.IsDevelopment())
        {
            AppDomain.CurrentDomain.SetData("DataDirectory",
                Path.Combine(_webHostingEnvironment.ContentRootPath, "App_Data"));
            services.Configure<DataAccessOptions>(options => options.UpdateDatabaseSchema = true);
            services.Configure<SchedulerOptions>(options => options.Enabled = false);
        }
        else
        {
            services.AddCmsCloudPlatformSupport(_configuration)
                   .AddAzureBlobProvider(options => 
                        options.ContainerName = _configuration["AzureBlobContainerName"]);
        }
        
        // Caching and HTTP services
        services.AddMemoryCache()
               .AddDistributedMemoryCache()
               .AddResponseCaching()
               .AddHttpContextAccessor()
               .AddHttpClient();
               
        // Session configuration
        services.AddSession(options =>
        {
            options.IdleTimeout = TimeSpan.FromMinutes(30);
            options.Cookie.HttpOnly = true;
            options.Cookie.IsEssential = true;
            options.Cookie.SameSite = SameSiteMode.None;
            options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
            options.Cookie.Path = "/";  
        });
        
        // SMTP configuration
        services.Configure<SmtpOptions>(options =>
        {
            options.DeliveryMethod = DeliveryMethod.Network;
            options.Network = new Network
            {
                UserName = _configuration["SendGridUserName"],
                Password = _configuration["SendGridPassword"],
                UseSsl = false,
                Port = Convert.ToInt16(_configuration["SendGridPort"]),
                Host = _configuration["SendGridHost"]
            };
        });
        
        // Link validator
        services.Configure<LinkValidatorOptions>(options => 
            options.ExcludePatterns.Add("^(?!http(s)?:\\/\\/|~).*"));
            
        // CORS configuration
        services.AddCors(options =>
        {
            options.AddPolicy("AllowSpecificOrigin", policy =>
            {
                policy.WithOrigins(_configuration["AllowedOrigins"]?.Split(',') ?? Array.Empty<string>())
                      .AllowAnyHeader()
                      .AllowAnyMethod()
                      .AllowCredentials();
            });
        });
        
        // Device detection
        services.AddDetection();
    }
    
    private void ConfigureOptimizelyServices(IServiceCollection services)
    {
        services.AddCms()
               .AddAlloy(_configuration)
               .AddFind()
               .AddEmbeddedLocalization<Startup>();
    }
    
    private void ConfigureAuthenticationServices(IServiceCollection services)
    {
        // Okta authentication
        services.ConfigureOkta(_configuration, false);
        
        // Cookie policy
        services.Configure<CookiePolicyOptions>(options =>
        {
            options.MinimumSameSitePolicy = SameSiteMode.None;
            options.Secure = CookieSecurePolicy.Always;
            options.HttpOnly = HttpOnlyPolicy.Always;
            options.OnAppendCookie = cookieContext =>
            {
                if (IsAuthCookie(cookieContext.CookieName))
                {
                    cookieContext.CookieOptions.SameSite = SameSiteMode.None;
                    cookieContext.CookieOptions.Secure = true;
                    cookieContext.CookieOptions.Path = "/";
                }
            };
        });

        // Application cookie settings
        services.ConfigureApplicationCookie(options =>
        {
            options.Cookie.SameSite = SameSiteMode.None;
            options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
            options.Cookie.Path = "/";  
            options.ExpireTimeSpan = TimeSpan.FromHours(12);
            options.SlidingExpiration = true;
        });
    }
    
    private void ConfigureApplicationServices(IServiceCollection services)
    {
        // Register application services
        services.AddTransient<ContentLocator>()
               .AddTransient<IOktaServices, OktaServices>()
               .AddTransient<IMessageSender, SmtpSender>();
    }
    
    #endregion
    
    #region Middleware Configuration Methods
    
    private void ConfigureErrorHandling(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        
        app.UseStatusCodePagesWithReExecute("/error/{0}")
           .UseNotFoundHandler()
           .UseOptimizelyNotFoundHandler();
    }
    
    private void ConfigureBasicMiddleware(IApplicationBuilder app)
    {
        app.UseMiddleware<RobotsMiddleware>()
           .UseDetection()
           .UseHttpsRedirection()
           .UseSession()
           .UseCdnSupport();
           
       
        app.UseStaticFiles()
           .UseRouting();
    }
    
    private void ConfigureAuthMiddleware(IApplicationBuilder app)
    {
        app.UseCookiePolicy()
           .UseCors("AllowSpecificOrigin")
           .UseResponseCaching()
           .UseAuthentication()
           .UseAuthorization();
    }
    
    private void ConfigureEndpoints(IApplicationBuilder app)
    {
        app.UseEndpoints(endpoints =>
        {
            // Map Razor Pages first for better routing efficiency
            endpoints.MapRazorPages();
            
            // Map API endpoints
            endpoints.MapControllerRoute(
                name: "private-api",
                pattern: "api/private/{controller}/{action}"
            );
            
            // Map controller endpoints
            endpoints.MapControllerRoute(
                name: "Default", 
                pattern: "{controller}/{action}/{id?}"
            );
            
            endpoints.MapControllers();
            
            // Optimizely content routing
            endpoints.MapContent();
        });
    }
    
    private void ConfigureLegacyRoutes(IApplicationBuilder app)
    {
        app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/util/logout.aspx"), appBuilder =>
        {
            appBuilder.Run(async context =>
            {
                await context.SignOutAsync();

                if (ContentReference.StartPage.ID == 0)
                {
                    context.Response.Redirect("/", false);
                    return;
                }

                var contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
                var urlResolver = ServiceLocator.Current.GetInstance<UrlResolver>();
                var startPage = contentLoader.Get<StartPage>(ContentReference.StartPage);

                context.Response.Redirect(
                    $"{context.Request.Scheme}://{context.Request.Host}{urlResolver.GetUrl(startPage)}");
            });
        });
    }
    
    #endregion
    
    #region Helper Methods
    
    private static bool IsAuthCookie(string cookieName)
    {
        return cookieName.Contains("Okta") ||
               cookieName.Contains("oidc") ||
               cookieName.Contains("Auth") ||
               cookieName.Contains(".AspNetCore");
    }
    
    #endregion
}

Questions:

1- Is there something missing in the Okta authentication setup that would prevent cookies from being created on the callback?
2- Could this be related to SameSite cookie settings, HTTPS enforcement, or something environment-specific?
3- Any suggestions on how to debug or log the cookie creation process during the callback?

Would appreciate any inputs on this.

Regards.

#339807
Jul 25, 2025 22:35
Vote:
 

I think you have too much code in your authentication configuration. The official sample code is much leaner. Maybe some of your customizations are affecting the post-authentication flow.

The default cookie options usually work fine without customizations. And I have never stored or restored correlation IDs in session. Those IDs usually work fine in cookies and parameters.

Besides, the sample code for OpenID handles the synchronization of users in the OnSignedIn event, not in the OnTokenValidated.

I suggest you try replacing your integration with the sample code, even though the sample code is angled at Entra ID. Then just make Okta-related adjustments through configuration (by setting properties).

#339808
Jul 27, 2025 13:36
Farhin - Jul 28, 2025 12:19
Thanks, Stefan, for the response.
I understand it's a lot of code, especially around the correlation ID logic — I added that to handle a multi-tab scenario where duplicating edit mode tabs was causing correlation ID errors. That’s why I had to include that part. I’ll definitely revisit my code again.
However, I have a question regarding direct file access — for example, accessing a restricted file like https://sample.com/contentassets/.... When I enter the URL directly in the address bar, I get redirected to the Okta login page as expected. But after entering credentials, I get redirected to the signin-oidc endpoint, and it doesn’t proceed further.
This makes me think there might be an issue with cookie generation when accessing a direct URL versus the normal login flow.
Any thoughts on this? I’d really appreciate your input.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.