oidcproxydotnet / OidcProxy.Net

An extendible framework for .NET to implement the BFF Security Pattern (a.k.a. Token Handler Pattern) in Single Page Applications
GNU Lesser General Public License v3.0
101 stars 19 forks source link

BFF + API in the same Host? #257

Closed asantamariaplainconcepts closed 1 month ago

asantamariaplainconcepts commented 1 month ago

Instead of having two servers, we usually want to host BFF+ API in the same server,

is there any chance to bypass the yarp configuration?

Thanks in advance

appie2go commented 1 month ago

Yes you can, if i understand you correctly, that could look something like this:

using System.Net.Http.Headers;
using Bff;
using OidcProxy.Net;
using OidcProxy.Net.Auth0;

var builder = WebApplication.CreateBuilder(args);

var auth0Config = builder.Configuration
    .GetSection("OidcProxy")
    .Get<Auth0ProxyConfig>();

builder.Services.AddAuth0Proxy(auth0Config);

var app = builder.Build();

app.UseRouting();

app.UseAuth0Proxy();

app.Map("/api/weatherforecast", async (HttpContext context, HttpClient httpClient) =>
{
    // do things..
}).RequireAuthorization();

app.Run();

OidcProxy.Net is hooked into the asp.net authentication pipeline, so you can use asp.net like you are used to. You can use the RequireAuthorization method for minimal apis or the [Authorize] attribute for controller methods.

Hope this answers your question? If not, please describe your use case in more detail..

asantamariaplainconcepts commented 1 month ago

Thnaks for your quick response @appie2go the endpoints are called directly in the unique server (Bff + Api)

This is the server Program

var builder = WebApplication.CreateBuilder(args);

builder
    .AddApiServices()
    .AddExampleModule()
    .ConfigureAuthentication();

builder.Services
    .AddEndpointsApiExplorer()
    .AddSwaggerGen(options => { options.CustomSchemaIds(y => y.GetRequestName()); });

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseStaticFiles()
    .UseProblemDetails()
    .UseHttpsRedirection()
    .UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.MapEndpoints();

app.MapFallbackToFile("/index.html");

app.Run();

This is the ConfigureAuthentication Custom extension

 public static void ConfigureAuthentication(this WebApplicationBuilder builder)
    {
        builder.Services
            .AddControllersWithViews(options =>
                options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));

        builder.Services.AddAntiforgery(options =>
        {
            options.HeaderName = "X-XSRF-TOKEN";
            options.Cookie.Name = "__Host-X-XSRF-TOKEN";
            options.Cookie.SameSite = SameSiteMode.Strict;
            options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        });

        var scopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes");
        var initialScopes = scopes!.Split(' ');

        builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration)
            .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
            .AddMicrosoftGraph("https://graph.microsoft.com/v1.0", initialScopes)
            .AddInMemoryTokenCaches();

        builder.Services.Configure<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme,
            options => options.Events = new RejectSessionCookieWhenAccountNotInCacheEvents(initialScopes));
    }

This is one endpoint example

public class QueryExample : IFeatureModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapGet("/api/examples/{id}",
            async (string id, ISender sender, CancellationToken cancellationToken) =>
            {
                var result = await sender.Send(new Query(id), cancellationToken);
                return result.ToMinimalApiResult();
            })
            .WithName(nameof(QueryExample))
            .WithTags(nameof(Examples))
            .Produces<Response>()
            .RequireAuthorization();
    }

    public record Query(string Id) : IQuery<Result<Response>>;

    public record Response(string Name);

    public class RequestHandler(ReadOnlyProjectDbContext db) : IRequestHandler<Query, Result<Response>?>
    {
        public async Task<Result<Response>?> Handle(Query request, CancellationToken cancellationToken = default)
        {
            var data = await db.ExampleEntities
                .Where(x => x.Id == Guid.Parse(request.Id))
                .Select(x => new Response(x.Name))
                .FirstOrDefaultAsync(cancellationToken);

            return data is not null
                ? Result.Success(data)
                : Result.NotFound();
        }
    }
}

And for the login we have a challenge

 [HttpGet("Login")]
    public ActionResult Login(string returnUrl = "/")
    {
        if (hostEnvironment.IsDevelopment())
        {
            Request.Host = new HostString("localhost:5001");
        }

        var properties = new AuthenticationProperties { RedirectUri = returnUrl };

        return Challenge(properties);
    }

So we store the token in session but its coupled to AzureAd but we could use oidcproxy nuget package for auth0, entraId, etc only for ConfigureAuthentication

appie2go commented 1 month ago

Hi,

I think I understand. This project was designed for SPA use-cases and has not been designed with this scenario in mind. However, the proxy is compatible with this scenario.

To answer your question, for now the proxy requires a Yarp configuration. But you are right, technically speaking does not have to be.. It's an easy fix, I can look into it tomorrow and provide a patch for you if you want? It will probably done end of the week? Let me know if that works for you.

Cheers,

asantamariaplainconcepts commented 1 month ago

Hi,

Awesome, that works for me, thank you very much!

Cheers,

appie2go commented 1 month ago

Welcome! Stay tuned...

appie2go commented 1 month ago

Hi there,

As you may have noticed, things are taking a little longer then expected. This change will at least take another week, two weeks at max.

Still working on it, stay tuned!

Cheers

appie2go commented 1 month ago

Hi there,

You may have noticed a pull request. It's rather big, which explains the delay. Anyways, I've introduced a config property called 'Mode'.

...
"OidcProxy": {
    "Mode": "AuthenticateOnly",
    ...

By setting the mode to AuthenticateOnly, Yarp will be disabled completely. That should do the trick for you, correct?

I'll ship this change in the next release. I intend to pick up some more work this week and ship this change end of week.

Cheers,