Context
I have creds auth flow outside of the OpenIddict configuration with SignIn form in SPA React App and AspNetCore.Identity used on backend part.
The actual flow I expect.
- User tries to reach the protected route and get redirected to SignIn form
- User successfully passed the creds flow and react-oidc-context starts the OIDC flow
- OpenIddict sees the user is already authenticated with Identity Cookie and issue the authorization code on /connect/authorize request.
For MVP and simplicity all the responsibilities are implemented in one client app in a row with one backend service (Identity/OIDC/Protected resources)
OIDC configuration
const oidcConfig = {
authority: "https://localhost:5001",
client_id: "web-client",
redirect_uri: "https://localhost:8080",
response_type: "code",
scope: "openid offline_access email profile",
post_logout_redirect_uri: "https://localhost:8080",
automaticSilentRenew: true,
withCredentials: true
};
Back-end part
Entrance point
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Configuration.SetBasePath(AppContext.BaseDirectory);
builder.Logging.ClearProviders();
builder.Services.AddCors(options =>
{
options.AddPolicy("default", policy =>
{
policy.WithOrigins("https://localhost:8080")
.AllowCredentials()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
builder.Services.AddControllers().AddNewtonsoftJson(options =>
{
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
builder
.AddEnglishResidence()
.AddIdentity();
WebApplication app = builder.Build();
ILogger logger = app.Services.GetRequiredService<IRootLogger>();
await IdentityBuilderExtensions.AddOidcWebClient(app.Services);
try
{
app.UseRouting();
app.UseCors("default");
app.UseMiddleware<RequestLoggingMiddleware>();
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
app.UseEnglishResidenceScriptProviders(app.Services);
app.MapGet("/", () => "Hello World");
app.MapControllers();
app.MapUserEndpoints();
logger.Log("Application is successfully started up...",
null, LogLevel.Information, LogTag.Startup);
}
catch (Exception exception)
{
logger.Log("Failed to start up.", exception, LogLevel.Fatal, LogTag.Startup);
}
await app.RunAsync();
Identity and OpenIddict configuration
public static class IdentityBuilderExtensions
{
public static IHostApplicationBuilder AddIdentity(this IHostApplicationBuilder builder)
{
builder.Configuration
.AddJsonFile("sr-infrastructure-identity.settings.json", false, true)
.AddJsonFile("sr-data-identity.settings.json", false, true);
builder.Services
.AddHttpContextAccessor()
.AddScoped<SignInManager<ApplicationUser>>()
.AddIdentityCore<ApplicationUser>(options =>
{
//options.SignIn.RequireConfirmedEmail = true;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<IdentityDbContext>()
.AddDefaultTokenProviders();
builder.Services
.AddScoped<SignInManager<ApplicationUser>>()
.Configure<DataProtectionTokenProviderOptions>(options =>
{
options.TokenLifespan = TimeSpan.FromMinutes(30);
});
builder.Services
.AddSrDataIdentity(builder.Configuration)
.AddSrDomainIdentity()
.AddSrInfrastructureIdentity();
builder.Services
.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<IdentityDbContext>();
})
.AddServer(options =>
{
options
.SetTokenEndpointUris("/connect/token")
.SetAuthorizationEndpointUris("/connect/authorize")
.SetEndSessionEndpointUris("/connect/logout")
.AllowAuthorizationCodeFlow()
.AllowRefreshTokenFlow()
.RequireProofKeyForCodeExchange();
if (builder.Environment.IsDevelopment())
{
options.AddEphemeralEncryptionKey().AddEphemeralSigningKey();
}
else
{
options.AddDevelopmentEncryptionCertificate().AddDevelopmentSigningCertificate();
}
options.RegisterScopes("email", "profile");
options.UseAspNetCore(options =>
{
if (builder.Environment.IsDevelopment())
{
options.DisableTransportSecurityRequirement();
}
options
.EnableTokenEndpointPassthrough()
.EnableAuthorizationEndpointPassthrough()
.EnableEndSessionEndpointPassthrough();
});
})
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = "sr-identity",
ValidAudience = "user",
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(
builder.Configuration["Identity:Key"]
?? throw new ApplicationException("Identity configuration is missing"))),
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true
};
})
.AddCookie(IdentityConstants.ApplicationScheme, options =>
{
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
}
);
builder.Services.AddAuthorization();
return builder;
}
public static async Task AddOidcWebClient(IServiceProvider serviceProvider)
{
using IServiceScope scope = serviceProvider.CreateScope();
IOpenIddictApplicationManager applicationManager =
scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
object? webClient = await applicationManager.FindByClientIdAsync("web-client");
OpenIddictApplicationDescriptor descriptor = new OpenIddictApplicationDescriptor
{
ClientId = "web-client",
ClientType = OpenIddictConstants.ClientTypes.Public,
DisplayName = "Web Client",
RedirectUris = { new Uri("https://localhost:8080") },
PostLogoutRedirectUris = { new Uri("https://localhost:8080") },
Permissions =
{
// Endpoints the client is allowed to access
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.Endpoints.EndSession,
// Grant types allowed for this client
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken,
// Response type
OpenIddictConstants.Permissions.ResponseTypes.Code,
// Scopes the client can request
"scp:openid",
"scp:offline_access",
"scp:profile",
"scp:email"
//"scp:api"
},
Requirements =
{
// Enforce Proof Key for Code Exchange (PKCE)
OpenIddictConstants.Requirements.Features.ProofKeyForCodeExchange
}
};
if (webClient is null)
{
await applicationManager.CreateAsync(descriptor);
}
else
{
await applicationManager.UpdateAsync(webClient, descriptor);
}
}
}
Question
When creds flow is finished on success I'm starting the OIDC flow on client:
const handleLogin = form.handleSubmit(async (data) => {
try {
await trigger(data).unwrap();
await auth.signinRedirect();
}
...
});
The subsequent requests are made:
- https://localhost:5001/.well-known/openid-configuration
- https://localhost:5001/connect/authorize?client_id=web-client&redirect_uri=https%3A%2F%2Flocalhost%3A8080&response_type=code&scope=openid+offline_access+email+profile&state=32e9703a065a4cab80c1c4c4307588e9&code_challenge=aJxeLanPhzJeaL9pVu6VuMGIdaVUbQ_-Szqlr1iezvM&code_challenge_method=S256
I can see that Identity cookie is attached to the /connect/authorize request but the request is finished with 404 result. I'm sure that the request reaches its handler and managed by OpenIddict because when I'm manipulating by scopes it produces 400 result with according error messages. Here I expect that OpenIddict will determine the user is authenticated and issue the authorization code but it doesn't. Is it issue related to the cookie that isn't recognized by OpenIddct or anything else I'm not realizing yet?
Notes
- I don't have any login routes on backend part as it pure api service and I don't want to have it as I have creds flow outside of the OpenIddict flow. I just want to bypass this flow as I expect user already to be authenticated to the moment of the /connect/authrize request.