dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.61k stars 10.07k forks source link

Exception during HTTP request using Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler #55881

Closed abylikhsanov closed 6 months ago

abylikhsanov commented 6 months ago

Is there an existing issue for this?

Describe the bug

I have configured my app to use OpenID authentication:

builder.Services.AddAuthentication(options => {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options => {

        if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production")
        {
            options.ClientId = Environment.GetEnvironmentVariable("CRIIPTO_AUTH_CLIENT_ID");
            options.ClientSecret = Environment.GetEnvironmentVariable("CRIIPTO_AUTH_CLIENT_SECRET");
            options.Authority = $"https://{Environment.GetEnvironmentVariable("CRIIPTO_AUTH_DOMAIN")}/";
        }
        else
        {
            options.ClientId = builder.Configuration["CriiptoAuth:ClientId"];
            options.ClientSecret = builder.Configuration["CriiptoAuth:Secret"];
            options.Authority = $"https://{builder.Configuration["CriiptoAuth:Domain"]}/";
        }
        options.ResponseType = "code";
        options.SkipUnrecognizedRequests = true;

        // The next to settings must match the Callback URLs in Criipto Verify
        options.CallbackPath = new PathString("/api/Auth/success"); 
        options.SignedOutCallbackPath = new PathString("/api/auth/signout");

    });

However when I try to access the GET endpoint that returns the challenge so the user could authenticate:

[HttpGet("bankid")]
    public IActionResult Authenticate()
    {
        return Challenge(new AuthenticationProperties
        {
            RedirectUri = "/api/Auth/success"
        }, OpenIdConnectDefaults.AuthenticationScheme);
    }

I get an exception:

fail: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[17]
      Exception occurred while processing message.
      System.InvalidOperationException: An invalid request URI was provided. Either the request URI must be an absolute URI or BaseAddress must be set.
         at System.Net.Http.HttpClient.PrepareRequestMessage(HttpRequestMessage request)
         at System.Net.Http.HttpClient.SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
         at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.RedeemAuthorizationCodeAsync(OpenIdConnectMessage tokenEndpointRequest)
         at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.HandleRemoteAuthenticateAsync()
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
      An unhandled exception has occurred while executing the request.
      Microsoft.AspNetCore.Authentication.AuthenticationFailureException: An error was encountered while handling the remote login.
       ---> System.InvalidOperationException: An invalid request URI was provided. Either the request URI must be an absolute URI or BaseAddress must be set.
         at System.Net.Http.HttpClient.PrepareRequestMessage(HttpRequestMessage request)
         at System.Net.Http.HttpClient.SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
         at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.RedeemAuthorizationCodeAsync(OpenIdConnectMessage tokenEndpointRequest)
         at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.HandleRemoteAuthenticateAsync()
         --- End of inner exception stack trace ---
         at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
         at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

I can confirm that token_endpoint is being received as an absolute URI and you can check my session I have recorded using the Fiddler here: https://we.tl/t-57vWwyE0P3

So I think there is an issue with the MS middleware or could be something I messed up?

My full Program.cs:

using Microsoft.EntityFrameworkCore;
using System.Text.RegularExpressions;
using Criipto.Signatures;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.OpenApi.Models;
using FastEndpoints;
using Microsoft.AspNetCore.Authentication.Cookies;
using ztlme.Data;
using ztlme.Services;
using Environment = System.Environment;

var builder = WebApplication.CreateBuilder(args);

builder.Logging.AddConsole(options => {
    options.LogToStandardErrorThreshold = LogLevel.Trace;
});
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddControllers();
builder.Services.AddHttpContextAccessor();
builder.Services.AddLogging();
builder.Services.AddDbContext<DataContext>(options =>
{
    if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production")
    {
        var m = Regex.Match(Environment.GetEnvironmentVariable("DATABASE_URL")!, @"postgres://(.*):(.*)@(.*):(.*)/(.*)");
        options.UseNpgsql($"Server={m.Groups[3]};Port={m.Groups[4]};User Id={m.Groups[1]};Password={m.Groups[2]};Database={m.Groups[5]};sslmode=Prefer;Trust Server Certificate=true");
    }
    else
    {
        options.UseNpgsql(builder.Configuration.GetConnectionString("ztlme_db"));
    }

});
builder.Services.AddCors(options =>
{
    string? frontendUrl = builder.Configuration["Frontend:RootURI"];
    if (frontendUrl == null)
    {
        frontendUrl = "http://localhost:3000";
    }
    if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production")
    {
        frontendUrl = Environment.GetEnvironmentVariable("FRONTEND_URI")!;
    }

    options.AddPolicy("MyCorsPolicy", builder =>
        builder.WithOrigins(frontendUrl)
            .AllowAnyMethod()
            .AllowAnyHeader()
            .AllowCredentials()); // Important for cookies to be allowed
});

// Add FastEndpoints
//builder.Services.AddFastEndpoints();

//Criipto Auth
builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.CheckConsentNeeded = context => true;
    options.MinimumSameSitePolicy = SameSiteMode.None;
});

builder.Services.AddAuthentication(options => {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options => {

        if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production")
        {
            options.ClientId = Environment.GetEnvironmentVariable("CRIIPTO_AUTH_CLIENT_ID");
            options.ClientSecret = Environment.GetEnvironmentVariable("CRIIPTO_AUTH_CLIENT_SECRET");
            options.Authority = $"https://{Environment.GetEnvironmentVariable("CRIIPTO_AUTH_DOMAIN")}/";
        }
        else
        {
            options.ClientId = builder.Configuration["CriiptoAuth:ClientId"];
            options.ClientSecret = builder.Configuration["CriiptoAuth:Secret"];
            options.Authority = $"https://{builder.Configuration["CriiptoAuth:Domain"]}/";
        }
        options.ResponseType = "code";
        options.SkipUnrecognizedRequests = true;

        // The next to settings must match the Callback URLs in Criipto Verify
        options.CallbackPath = new PathString("/api/Auth/success"); 
        options.SignedOutCallbackPath = new PathString("/api/auth/signout");

    });

// Criipto signature
builder.Services.AddSingleton<CriiptoSignaturesClient>(serviceProvider =>
{
    var configuration = serviceProvider.GetRequiredService<IConfiguration>();
    if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production")
    {
        return new CriiptoSignaturesClient(Environment.GetEnvironmentVariable("CRIIPTO_CLIENT_ID")!,
            Environment.GetEnvironmentVariable("CRIIPTO_CLIENT_SECRET")!);
    }
    else
    {
        string clientId = builder.Configuration["Criipto:ClientId"]!;
        string clientSecret = builder.Configuration["Criipto:ClientSecret"]!;
        return new CriiptoSignaturesClient(clientId, clientSecret);
    }
});

// Register services
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<ISignatureService, SignatureService>();

// Swagger
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "API", Version = "v1" });
    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "JWT Authorization header using the Bearer scheme (Example: 'Bearer 12345abcdef')",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer"
    });
    c.AddSecurityRequirement(new OpenApiSecurityRequirement()
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                },
                Scheme = "oauth2",
                Name = "Bearer",
                In = ParameterLocation.Header,
            },
            new List<string>()
        }
    });
});

var app = builder.Build();

// Configure the HTTP request pipeline.
/*if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production")
{
    app.UseForwardedHeaders(new ForwardedHeadersOptions
    {
        ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedFor | Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto
    });
    app.UseHttpsRedirection();
}

app.UseCors("MyCorsPolicy");*/
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
//app.UseFastEndpoints();
app.MapControllers();
app.UseHttpsRedirection();

app.Run();

public partial class Program { }

Expected Behavior

No exception and the next callback specified in the authority should be called

Steps To Reproduce

Repo: https://github.com/abylikhsanov/ztlme Run dotnet run, go to the browser and type http://localhost:5272/api/Auth/bankid, choose the first option and enter 30070721151 for the personal number, click next, enter otp for the one time password click next and type qwer1234 for the password and keep clicking next after which you will see the exception

Exceptions (if any)

No response

.NET Version

8.0.0

Anything else?

No response

easyslav commented 6 months ago

I have the same issue

javiercn commented 6 months ago

@abylikhsanov thanks for contacting us.

Does your .well-known/openid-configuration document return a fully qualified URL?

abylikhsanov commented 6 months ago

@javiercn @halter73 Yes it does

halter73 commented 6 months ago

This appears to basically be a duplicate of #55194, #55355 and #55774. And the problem is misaligned Microsoft.IdentityModel NuGet package versions. There's an indirect reference to the 7.1.2 Microsoft.IdentityModel.Protocols.OpenIdConnect package which is incompatible with the newer 7.5.1 Microsoft.IdentityModel.Tokens package that's referenced in directly.

Out of sync NuGet package versions ought not to cause issues like this as long as breaking changes are avoided, but it turns out there were some breaking changes behavioral in more recent Microsoft.IdentityModel.Tokens packages that can lead to these hard-to-diagnose errors. We'll try harder to avoid this in the future. https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/2513#issuecomment-2099109337 is tracking fixing this issue on the Microsoft.IdentityModel side.

But in general, packages with aligned versions are more thoroughly tested together, so it's a good idea to line everything up if you can. I submitted a PR to the repro project at https://github.com/abylikhsanov/ztlme/pull/1 which fixes the issue.