Let’s be friends. OpenId Connect and Yarp

Foreword

Today in this article I want to share my personal experience and the solution of a specific case. A small team worked on it, but for the sake of simplicity, I will write in the first person. Actually the case itself: there is a Windows server with a domain and an SSL certificate, on it you need to raise an authorization server with the OpenId Connect protocol and two more applications that must be authorized through the authorization server. And yes, you need to implement everything to the maximum using the built-in functionality.

“Authorization server must be taken out separately!” – you say – But in reality we twist and turn as we can. Sounds like two fingers. I’ll tell you now how it was.

General scheme

Authorization server

Everything is relatively simple here, there are ready-made examples and the described documentation. Take it and do it! The example was used as a basis damienbod . And further refined for themselves as needed.

YARP

YARP is a reverse proxy server recently released by Microsoft. There is nothing complicated in setting it up, there is the described documentation and implementation examples. Specifically, we were guided by this articles. I attach the configuration of our proxy below.

"ReverseProxy": {
    "Routes": {
      "minimumroute": {
        "ClusterId": "minimumcluster",
        "Match": {
          "Path": "{**catch-all}"
        }
      },
      "route2": {
        "ClusterId": "Server",
        "Match": {
          "Path": "/Server/{*any}"
        },
        "Transforms": [
          {
            "PathRemovePrefix": "/Server"
          }
        ]
      },
      "route3": {
        "ClusterId": "App1",
        "Match": {
          "Path": "/App1/{*any}"
        },
        "Transforms": [
          {
            "PathRemovePrefix": "/app1"
          }
        ]
      },
      "route4": {
        "ClusterId": "App2",
        "Match": {
          "Path": "/App2/{*any}"
        },
        "Transforms": [
          {
            "PathRemovePrefix": "/app2"
          }
        ]
      }
        ]
      }
    },
    "Clusters": {
      "minimumcluster": {
        "Destinations": {
          "first_destination": {
            "Address": "http://localhost:5001"
          }
        }
      },
      "Server": {
        "Destinations": {
          "first_destination": {
            "Address": "http://localhost:5001"
          }
        }
      },
      "App1": {
        "Destinations": {
          "first_destination": {
            "Address": "http://localhost:5002"
          }
        }
      },
      "App2": {
        "Destinations": {
          "first_destination": {
            "Address": "http://localhost:5003"
          }
        }
      }
    }
  }

That’s all, are you ready?

We start to run everything locally and really everything works – right? Yeah, that means deploy, and that’s bad luck – it doesn’t work. What is wrong?

The fact is that our authorization server has an address http://localhost:5001while the application http://localhost:5002. Therefore, when all this is deployed on the server, an error occurs. To solve this problem, we ourselves need to specify such a parameter as RedirectUri

options.Events.OnRedirectToIdentityProvider = ctx =>
{
  ctx.ProtocolMessage.RedirectUri = "https://domain/app1/signin-oidc";
  return Task.CompletedTask;
};
options.Events.OnRedirectToIdentityProviderForSignOut = ctx =>
{
  ctx.ProtocolMessage.PostLogoutRedirectUri = "https://domain/app1/signout-callback-oidc";
  return Task.CompletedTask;
};

Clarification: signin-oidc and signout-callback-oidc are built-in methods and are installed by default as entry and exit endpoints in the package Microsoft.AspNetCore.Authentication.OpenIdConnect.

We figured out one problem, now the redirect is correct. But we immediately meet the next one, which sounds like Corellation failed. We go into the developer panel and begin to closely study requests and responses. And we find a warning from the browser that the files cookie not set as missing attribute secure. There are two ways to solve this problem:

  1. Install self-signed SSL certificates for our applications. How to do this is described here.

  2. Or on our applications change the file policy configuration cookie. To do this, we need to write the following in program.cs:

builder.Services.Configure<CookiePolicyOptions>(options =>
{
  options.Secure = CookieSecurePolicy.Always;
});
app.UseCookiePolicy();

All done with this problem. We go, we try, and again the same thing. We study, to be listed, further. And finally we find that the files cookie then established, but on the wrong route. So now you yourself manually need to specify the installation route. This is done as follows, in the OpenIdConnect authentication settings

options.CorrelationCookie.Path = "/app1/signin-oidc";
options.NonceCookie.Path = "/app1/signin-oidc";

And finally everything works! Full file code program.cs below.

var builder = WebApplication.CreateBuilder(args);
IConfiguration configuration = builder.Configuration;
builder.Services.AddHttpClient();
builder.Services.AddOptions();
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
}).AddCookie()
        .AddOpenIdConnect(options =>
        {
            options.SignInScheme = "Cookies";
            options.Authority = "https://domain.com/server";
            options.ClientId = "ClientId";
            options.ClientSecret = "ClientSecret";
            options.ResponseType = OpenIdConnectResponseType.Code;
            options.UsePkce= true;
            options.Scope.Add("openid");
            options.Scope.Add("profile");
            options.SaveTokens = true;
            options.GetClaimsFromUserInfoEndpoint = true;
            options.CorrelationCookie.Path = "/app1/signin-oidc";
            options.NonceCookie.Path = "/app1/signin-oidc";
            options.Events.OnRedirectToIdentityProvider = ctx =>
            {
                ctx.ProtocolMessage.RedirectUri = "https://domain.com/server/signin-oidc";
                return Task.CompletedTask;
            };
            options.Events.OnRedirectToIdentityProviderForSignOut = ctx =>
            {
                ctx.ProtocolMessage.PostLogoutRedirectUri = "https://domain.com/server/signout-callback-oidc";
                return Task.CompletedTask;
            };
        });

builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.Secure = CookieSecurePolicy.Always;
});


var app = builder.Build();
IWebHostEnvironment env = app.Environment;
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseWebAssemblyDebugging();
    
}
else
{
    app.UseHsts();
}
app.UsePathBase("/App1");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("Index.html");
app.Run();

A small afterword

Perhaps the way I described to solve this case is not 100% correct, but it was developed based on the initial data that was provided to us. In the Internet, I did not find a similar solution to this case, or at least similar, so I decided to share it. If you have an option on how to improve this method or do it differently, but within the same framework, then I ask in the comments.

Similar Posts

Leave a Reply