Custom Sign-In Redirects in BFF Architectures: A Guide Using OidcProxy.Net
Tags: webdev, dotnet, bff, authentication
Published: 2024-01-24 15:17:08

Implementing authentication in ASP.NET Core is usually straightforward in most cases. Installing a couple of NuGet packages and configuring them typically does the trick, unless there are complex requirements. In such cases, implementing authentication can be quite daunting.

Consider the following scenario: it isn't uncommon for web applications to have an admin console. Both administrators and end-users use the same login page to authenticate. However, after signing in, the admin sees the admin page, while the end-user does not.

Assume the following requirements:
- Admins see the /admin.aspx page after signing in.
- Users see the /welcome.aspx page after signing in.
- Users see the /birthday.aspx page after signing in on their birthday.

This article covers:
- How to bootstrap an Angular Spa + BFF project
- How to implement custom sign-in requirements using OidcProxy.Net
- Security considerations when implementing custom-redirection

## Context
The Backend For Frontend architectural pattern has been widely adopted. However, it is advisable to move the authentication process out of Single-Page applications that run in the browser, to the server-side.

In a .NET 8 ecosystem, this is easily accomplished by installing a package called OidcProxy.Net. By installing this package in a solution, the web application becomes an Identity-Aware proxy. It is meant to receive requests from a front-end and forward it to downstream services. This is illustrated in the following context diagram:

![Figure 1.) A back-end for front-end architecture.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7wnezs837alv27k2ij0s.png)

## Creating the Single-Page Application and the BFF.
Adhering to the latest best-practices in web-authentication, token-exchange takes place at the server side. This requires hosing the Single-Page Application and the BFF on the same domain. 

Implementing that requires runtime on the server-side and wwwroot to serve the dist folder of the Single-Page Application. To implement that, scaffold an Angular App + BFF using the OidcProxy.Net Template-Pack:

```bash
# Download and install the template pack first
dotnet new install OidcProxy.Net.Templates

# Scaffold the proxy
dotnet new OidcProxy.Net.Angular --backend "https://backend-1.myapp.com"
    --idp "https://idp.myapp.com"
    --clientId xyz
    --clientSecret abc

# Run it
dotnet run
```

This will generate the following project:

![figure 2.) A boilerplate Bff project with Angular](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ekrkmq8d4vq96v3vc566.png)

_Sidenote: This boilerplate project is only compatible with OpenID Connect implementations that are fully compliant such as IdentityServer or KeyCloak, for example. For products that deviate from the OpenID Connect specification, like Auth0, install the `OidcProxy.Net.Auth0` package. For Microsoft Entra Id, install the `OidcProxy.Net.EntraId` package._

Run the project by typing dotnet run or by clicking the play button in Visual Studio. This action will launch the website, which features a sign-in button in the upper left corner. You can use this button to test the sign-in functionality. When clicked, it will navigate the user to the /.auth/login endpoint. Subsequently, it will redirect the user to the identity provider for signing in, and eventually bring them back to the website.

## Configuring the default landing-page
The scaffolded project contains an appsettings.json file, which includes a section named OidcProxy. In this section, the configuration for the identity provider's address (idp), client_id, and client_secret is specified.

Configuring the destination after a user signs in is also a matter of configuration, with a setting called `OidcProxy__Landingpage`:

```json
{
   # ...
   "OidcProxy": {
      # Your other settings..
      #...
      "LandingPage": "/welcome.aspx"
      # ...
   }
}
```

Configuring this would be sufficient for most cases. However, in light of the requirements outlined in the first part of the article, this doesn't address the need for the administrator to land on the /admin endpoint, and users aren't redirected to the birthday.aspx page on their birthday.

## Specifying a landing-page on the fly
Most websites have fewer admins than customers who sign in. And usually, websites have a special link for admins to sign in.

With OidcProxy.Net it is possible to specify the landingpage an end-user should be redirected to after signing in. Assume administrators need to be redirect to /admin.aspxafter signing in, use the following link: `/.auth/login?landingpage=/admin.aspx`

For security reasons, this link will not work out of the box. You need to whitelist the landing pages that may be specified in the ?landingpage= query string parameter in the config. To enable this functionality, add the following configuration:

```json
{
   ...
   "LandingPage": "/welcome.aspx",    
   "AllowedLandingPages": [
      "/admin.aspx"
    ],
   ...
}
```

## Implementing complex business-rules to determine the landing-page
Then there's still the issue of the birthday celebrants. They need to be redirected to /birthday.aspx on their birthday. To tackle complex scenario's you'll need to write some custom middleware.

The OidcProxy.Net processes every sign-in using a class called DefaultAuthenticationCallbackHandler. This class is extendable, which allows implementing the following code:

```csharp
public class BirthdayAuthenticationCallbackHandler : DefaultAuthenticationCallbackHandler
{
    public BirthdayAuthenticationCallbackHandler(ILogger<DefaultAuthenticationCallbackHandler> logger) : base(logger)
    {
    }

    public override Task<IResult> OnAuthenticated(HttpContext context, 
        JwtPayload? payload,
        string defaultLandingPage, 
        string? userPreferredLandingPage)
    {
        var birthday = payload
            .Where(claim => claim.Key == "birthday")
            .Select(value =>
            {
                DateTime.TryParse(value.ToString(), out var birthday);
                return birthday;
            })
            .FirstOrDefault();

        IResult landingPage;
        if (birthday.Day == DateTime.Now.Day && birthday.Month == DateTime.Now.Month)
        {
            landingPage = Results.Redirect("birthday.aspx");
        }
        else
        {
            landingPage = Results.Redirect("welcome.aspx");
        }

        return Task.FromResult(landingPage);
    }
}
```

The BirthdayAuthenticationCallbackHandler can be configured by adding the following code program.cs:

```csharp
builder.Services.AddOidcProxy(config, o =>
{
     o.AddAuthenticationCallbackHandler<BirthdayAuthenticationCallbackHandler>();
});
```

Implementing all of the above fulfills the requirements mentioned earlier in the article.

## Why landing-page != redirect_uri
OidcProxy.Net makes a distinct difference between the concept redirect_url and their landingpage-concept. That's because they serve a different purpose. 

- The landingpage is used to redirect a user to a page after signing in successfully.
- The redirect_url is used for token exchange and is an explicit part of the OAuth2 protocol.

OidcProxy.Net uses a special endpoint, the `/.auth/login/callback` endpoint to receive information from the Identity Provider. This endpoint needs to be whitelisted on the Identity Provider. 

Landingpages should not be whitelisted on the Identity Provider.

## Summary
Adhering to the latest OAuth-best-practices means token-exchange should take place on the server-side. However, this poses challenges in case of complex post-authentication logic.
These challenges can be overcome by utilising a NuGet package called OidcProxy.Net in your ASP.NET Core webapp. It has three main features which make complex redirection possible:
- The `landingpage` setting inappsettings.json
- Specifying the `landingpage` when invoking the login endpoint (`/.auth/login?landingpage=/admin.aspx`)
- Configuring a custom implementation of the `DefaultAuthenticationCallbackHandler` class.