Search code examples
asp.net-corecorsblazoropenid-connectopeniddict

How can I configure OpenIddict to work with CORS-middleware on the well-known endpoint?


I am setting up a test project to learn about OpenID Connect generally and OpenIddict specifically. I have set up 3 project in ASP.NET Core 8, a ResourceAPI, a AuthServer and a Client (Blazor). They all run locally but on different ports.

I want to setup the Authorization Code Flow. I have made the appropriate configuration in the server and the client, based on the Balosar example. When run the Client the first thing that happens is a call to the .well-known/openid-configuration endpoint, as I expected. I get a response but it is blocked by my browser (Brave) because of missing CORS-headers:

Access to XMLHttpRequest at 'https://localhost:7120/.well-known/openid-configuration' from origin 'https://localhost:7098' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. There is indeed no CORS-headers and the error makes sense to me since the applications are on different ports.

I have added CORS configuration in the AuthServer. Digging in to the OpenIddict handling of middleware I noticed that unless I enable endpoint-passtrough in the configuration no other middleware will be run, include the CORS middleware.

Since there is no configuration that lets me enable passthrough on the .well-known/openid-configuration endpoint (and I do not wish to implement it myself even if it did) and don't know how to continue from this.

I found this answer from mr Chavlet (OpenIddict author) on a similar question which makes me think that I am doing something wrong, but I don't know what

The fact you have to use CORS for the authorization endpoint makes me think you're doing something wrong .

Is there some configuration detail I am missing that allows CORS for OpenIddict or am I missunderstanding the flow?

This is my configuration for the AuthServer. Note that the Authorization Code flow is not fully implemented yet, I want to get past this CORS-obstacle before I continue with it. Also note that I am running in degraded mode, since this is what I will use once I set this up in our real code base.

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddCors(opt => opt.AddPolicy("AllowThePlaygroundClient", policy => policy
        .WithOrigins("https://localhost:7098")
        .AllowAnyHeader()
        .AllowAnyMethod()
        .AllowCredentials())
    )
    .AddOpenIddict()
    .AddServer(opt =>
    {
        opt.EnableDegradedMode();
        opt.AddEphemeralEncryptionKey();
        opt.AddEphemeralSigningKey();
        opt.DisableAccessTokenEncryption();

        opt.AllowPasswordFlow();
        opt.AllowAuthorizationCodeFlow();
        opt.SetTokenEndpointUris("connect/token");

        opt.AddEventHandler<ValidateTokenRequestContext>(builder => builder.UseInlineHandler(context =>
        {
            // TODO: Add validation logic
            return default;
        }));
        opt.UseAspNetCore()
            .EnableTokenEndpointPassthrough();
    });

var app = builder.Build();

app.UseCors("AllowThePlaygroundClient");

app.MapPost("connect/token", (Delegate)((HttpContext context) =>
{
    var request = context.GetOpenIddictServerRequest() ?? throw new InvalidOperationException("Not a valid OIDC request");

    if (request.IsPasswordGrantType())
    {
        // In a reald world scenario we should never tell why the authentication has failed
        if (request.Username != _user.email || request.Password != _user.password)
        {
            return Task.FromResult(Results.NotFound("Invalid credentials!"));
        }

        var identity = new ClaimsIdentity(authenticationType: "jivete")
            .SetClaim(Claims.Subject, _user.email)
            .SetClaim(Claims.Name, _user.name)
            .SetClaim(Claims.Email, _user.email);

        identity.SetAudiences(new[] { "resourceApi" });
        return Task.FromResult(Results.SignIn(new ClaimsPrincipal(identity), authenticationScheme: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme));
    }

    throw new InvalidOperationException("The specified grant type is not supported.");
}));

app.Run();

This is the configuration for the client:

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddOidcAuthentication(opt =>
{
    opt.ProviderOptions.Authority = "https://localhost:7120";
    opt.ProviderOptions.ClientId = "myClientId";
    opt.ProviderOptions.ResponseType = "code";
    opt.ProviderOptions.ResponseMode = "query";

    opt.ProviderOptions.DefaultScopes.Add("roles");
    opt.UserOptions.RoleClaim = "role";
});

await builder.Build().RunAsync();

Solution

  • For this scenario to work, the CORS middleware MUST be invoked before the authentication middleware (that is responsible for invoking OpenIddict, which is implemented as an IAuthenticationRequestHandler) kicks in.

    When using the minimal web host (i.e WebApplication.CreateBuilder(args)), the authentication middleware is registered for you at a place chosen by the ASP.NET team, that doesn't necessarily work depending on the scenarios.

    To prevent that, register the authentication and authorization middleware manually:

    var app = builder.Build();
    
    app.UseCors("AllowThePlaygroundClient");
    
    app.UseAuthentication();
    app.UseAuthorization();