Search code examples
asp.net-web-api.net-6.0openiddict

How to use OpenIddict AuthServer and Web API in same project


I've built an OpenIddict OAuth server using the marvelous guide by Robin van der Knaap although I've actually implemented it using Identity instead of cookies.

I'm also trying to run a Web API from the same project because the end customer only wants a single system to call.

At the moment I'm doing all my endpoint testing in postman.

This user info endpoint in the AuthorisationController works fine:

[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
[HttpGet("~/connect/userinfo")]
public async Task<IActionResult> Userinfo()
{
    var claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;

    return Ok(new
    {
        Name = claimsPrincipal.GetClaim(OpenIddictConstants.Claims.Subject),
        Occupation = "Developer",
        Age = 43
    });
}

But when I try to call this custom Web API controller endpoint (https://<domain.com>/api/Test/):

[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
    [HttpGet]
    [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
    public IActionResult Index()
    {
        return Ok("hello");
    }
}

I just get the following error:

System.InvalidOperationException: An identity cannot be extracted from this request. This generally indicates that the OpenIddict server stack was asked to validate a token for an endpoint it doesn't manage. To validate tokens received by custom API endpoints, the OpenIddict validation handler (e.g OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme or OpenIddictValidationOwinDefaults.AuthenticationType) must be used instead. at OpenIddict.Server.OpenIddictServerHandlers.ValidateAuthenticationDemand.HandleAsync(ProcessAuthenticationContext context) at OpenIddict.Server.OpenIddictServerDispatcher.DispatchAsync[TContext](TContext context) at OpenIddict.Server.OpenIddictServerDispatcher.DispatchAsync[TContext](TContext context) at OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandler.HandleAuthenticateAsync() at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.AuthenticateAsync() at Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, String scheme) at Microsoft.AspNetCore.Authorization.Policy.PolicyEvaluator.AuthenticateAsync(AuthorizationPolicy policy, HttpContext context) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

My Program.cs looks like this:

using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using OAuthServer;
using OAuthServer.Data;
using OpenIddict.Server.AspNetCore;
using OpenIddict.Validation.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
    options.UseSqlServer(connectionString);

    // Register the entity sets needed by OpenIddict.
    options.UseOpenIddict();
});

builder.Services.AddOpenIddict()
        // Register the OpenIddict core components.
        .AddCore(options =>
        {
            // Configure OpenIddict to use the EF Core stores/models.
            options.UseEntityFrameworkCore()
                .UseDbContext<ApplicationDbContext>();
        })

        // Register the OpenIddict server components.
        .AddServer(options =>
        {
            options
                .AllowClientCredentialsFlow()
                .AllowAuthorizationCodeFlow().RequireProofKeyForCodeExchange()
                .AllowRefreshTokenFlow();

            options
                .SetTokenEndpointUris("/connect/token")
                .SetAuthorizationEndpointUris("/connect/authorize")
                .SetTokenEndpointUris("/connect/token")
                .SetUserinfoEndpointUris("/connect/userinfo");

            // Encryption and signing of tokens TODO: Replace with x.509 cert
            options
                .AddEncryptionCertificate(CertificateHelper.LoadCertificateFromKeyVault(builder.Configuration["KeyVault:Name"], builder.Configuration["OAuth:EncryptionCertName"]))
                .AddSigningCertificate(CertificateHelper.LoadCertificateFromKeyVault(builder.Configuration["KeyVault:Name"], builder.Configuration["OAuth:EncryptionCertName"]))
                /*.AddEphemeralEncryptionKey()
                .AddEphemeralSigningKey()*/
                .DisableAccessTokenEncryption();

            // Register scopes (permissions)
            options.RegisterScopes("api");

            // Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
            options
                .UseAspNetCore()
                .EnableTokenEndpointPassthrough()
                .EnableAuthorizationEndpointPassthrough()
                .EnableUserinfoEndpointPassthrough();
        })
        .AddValidation();

builder.Services.AddAuthentication(options => options.DefaultScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);

builder.Services.AddHostedService<TestData>();

builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddControllers();




var app = builder.Build();


// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();


app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
    endpoints.MapRazorPages();
    endpoints.MapControllers();
});

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();

app.Run();

How can I make [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)] work on custom API endpoints?

UPDATE

I have a feeling the Web API bit is a red-herring and the issue is something more fundamental. I tried to add an MVC action in the same AuthorisationController:

[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
[HttpGet("~/connect/hello")]
public async Task<IActionResult> Hello()
{
    return Ok("hi");
}

But that gives the same error.

I think I should probably just be using [Authorize] without specifying the scheme (which is what I was originally trying to do) but that always gives unauthorised.....

I suspect this article has something in it I need, but it's for an old version of OpenIddict (I'm using 3.1.1) and I can't figure out the current behaviour.


Solution

  • Turns out I was just using the wrong scheme on the authorize attribute. I needed to change:

    [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
    

    to:

    [Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
    

    And if you want to avoid adding the scheme in the attribute then you need to add this to your service configuration:

    builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
    });
    

    NOTE: This seems to conflict with the information on The OpenIddict ASP.NET Core server cannot be used as the default scheme handler but it's what worked for me.

    Although this won't play nice with Identity sign-in (unless I fiddle with the sign-in call) so I left it out.