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();
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();