DEV Community

Cover image for A journey to build a central identity management service in .Net with Vue.js SPA frontend
Mohammadali Forouzesh
Mohammadali Forouzesh

Posted on • Edited on

A journey to build a central identity management service in .Net with Vue.js SPA frontend

Have you ever tried implementing authentication and authorization in .Net and felt like you are stuck with the default template of Asp.Net Core framework which only uses razor pages? I mean, using framework features is great but "Never Marry The Framework". In this article, I am going to walk you through my experience of implementing an OAuth and OpenID Connect central identity service which uses .Net features and capabilities while maintaining best practices and full control of the project design.

Preface

Every tools used in this article is available with free commercial license. This article is suitable for intermediate to advance .Net programmers.

List of mentioned tools and libraries:

  • Asp.Net Core framework (of course!)
  • Openiddict library
  • Web API Empty project template
  • Vue.js frontend
  • Docker, Kubernetes and GitLab for GitOps

Feel free to comment your thoughts and correct my mistakes. Every software design may have its own pitfalls.

Disclaimer

This article is not a tutorial on how to build an authorization server. There are tons of such tutorials out there but feel free to contact me in case you had implementation questions.

The journey begins with...

An empty Asp.Net Core project template of course. One of my main goals was to decouple different layers of the application architecturally, and stick with the default provided templates as much as I can. Furthermore, I limited the implementation to my use case which for instance did not include two factor authorization (whereas TFA comes with Asp.Net Core Identity template by default).

I wanted to have two main modules in this project: OAuth and a Rest API. While I think OAuth is self explanatory, the REST API in my case is created to be responsible for user management, role and permission management, token management and etc. Next, I separated core functionalities and the domain specific classes of the project into two different class libraries. The domain project would be referenced by the Core project and the Core project will then be referenced by the Server project which serves the whole application. The final setup of the solution can be seen in the following hierarchy:

solution

  • Core
    • DbContext
    • EmailSender
    • ...
  • Domain
    • User.cs
    • Role.cs
    • UserRole.cs
    • ...
  • Server
    • Areas
      • OAuth
        • AuthenticationController.cs
        • AuthorizationController.cs
      • Rest
        • UsersController.cs
        • RolesController.cs
    • Migrations
    • Framework
    • Healthchecks
    • ...

The relation between the packages can be seen in the picture below.

components of the final application

Next stop: Implementing OAuth

To implement an OAuth server together with OpenID connect specification, I used a free and open source framework called Openiddict. You should checkout their GitHub repository at github.com/openiddict/openiddict-core. It is a nice and feature rich tool which help you get started very fast. However, comparing to alternatives like identity server, it needs more implementation rather functionalities coming out of the box.

One remark I want to mention here is that, when you come across a new framework or library or any open source GitHub page, don't stay limited to their "get started" documentation. Be sure to check out some deep parts of it like many hidden gems are not mentioned in the "get started" document.

For instance, I wanted to implement "Refresh Token OAuth Authorization Flow" but didn't quite liked the default implementation of Openiddict cause it pollutes the database so fast (of course, only if you persist tokens in database which you should). I therefore explored every server events that can be subscribed in Openiddict and manipulate the result in the way I prefer. Which then allowed me return the last old valid refresh token along with the access token instead of always generating a new one.

SPA frontend with Vue.js

As I mentioned earlier, my goal was to decouple every possible layers of this web application, mainly backend and frontend. I used Asp.Net Core Identity framework for default user, role and sign in management but as you know, the default Asp.Net Core Identity comes with UI implementation in Razor Pages which is not my preference. To me as a backend engineer, UI is a whole different topic than backend logic and if you keep these two parts of your application separated, you users are going to have the better look and feel of the software as the UI could then be enhanced by the UI expert. Microsoft tries hard to give backend developers as much UI tools and possible like Blazor, MAUI, Razor Pages, etc. so that they could also make the front of web applications with less JavaScript knowledge but trust me, that's not the professional way of doing it. If you want your software to be professionally maintainable, testable and reliable, do not opt in for server side rendered UI pages or any mix that includes server side rendered UI.

Cut to the point, together with UI experts in the team, we created the frontend using Vue 2.7 and Vuetify material design UI kit. Our UI only needed to have 4 different section: login form, logout page, forgot and reset password page. At first, the low number of UI modules might create the false perception that it's easy, BUT it is really not :) Specially when you want to add external logins using Google or Twitter for example., which was one of our main requirements.

Main Challenges

I can name two main challenges that we had to deal with which mainly cause by not having server side rendered pages and serving the UI from a different host. Those challenges were: cookies and redirect URIs. Let's go deeper into those topics.

Cookies

Cookies are one of the main part of authentication in today's web applications. They are many websites which rely on cookies for recognizing if a user has signed in on a browser. The problem is, there some restrictions in terms of sending a cookie to a client after checking his/her username/password for instance. In Asp.Net Core and Asp.Net Core Identity, cookies have some default settings which prevents them from being sent to an application which is on a different host than the server. The property that prevents this action is MinimumSameSitePolicy and to change that, you need to apply following configuration in your startup:

internal static WebApplication UseMyCookiePolicy( this WebApplication app )
{
    app.UseCookiePolicy(
        new CookiePolicyOptions
        {
            MinimumSameSitePolicy = SameSiteMode.None,
            Secure                = CookieSecurePolicy.Always,
            HttpOnly              = HttpOnlyPolicy.Always
        });

    return app;
}
Enter fullscreen mode Exit fullscreen mode

You can read about it more on Microsoft documentations at: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-6.0

Redirect URIs

When you divide your Identity service into two standalone applications that are served under different hosts, one obvious expectation is that you would have more complicated redirect Uris. Additionally, you need to have some PermanentRedirect responses from you backend server. For instance when the UI query to see if the user is logged in, in case the user is NOT logged in, the authentication middleware in backend tries to redirect the user to the login page within the backend application. But we know that our login page is being hosted elsewhere hence the permanent redirect.

Speaking of redirect Uris, we should not be afraid of nested redirect Uris. In some cases we reached up to 4 level nested redirect Uri when we attempt to login by external provider like Google. The important point here is to correctly encode those redirect Uris in the main Uri. Asp.Net Core intelligently decodes the action inputs and also encodes route values when redirecting to action. In frontend, using a promising library like qs can be an absolute time saver. For the sake of completeness, it might be worth mentioning that when encoding parameters of a Uri, you should be careful that the parameters are not already encoded. In which case, you get double encoding and of course wrong behavior. I'm sure you don't want to spend the amount of time I spent to debug that :)

Last round of fight: Kubernetes

Yes, that's right. When you finish dealing with all I mentioned above, there is one last thing we need to fight with. Of course this only applies if you want to deploy to K8s, otherwise the challenge might be different, but in my case, I wanted to deploy to K8s as it's easier to monitor and well adopted in my team.

This experience could also vary depending on you K8s cluster setup. In my case I was using not secure communication inside the cluster as the whole network is properly demilitarized behind firewalls. This could cause a serious issue as many part of OAuth and OpenID Connect specification required https always. Additionally, I had a reverse proxy in front of my cluster which will override the host of the request when forwarding the request to the pod. Nevertheless, I didn't want to give up this fight so early :)

As a solution, given all the conditions and issues I explained above, I decided to override every automatically inferred value based on host of the request. For instance the URL of the authority and token endpoint in the discovery document at /.well-known/openid-configuration. The would give me the flexibility to even change these Uris in the future easily. And it's now fully configurable for different environment such as Production, Staging or Development. Gladly, Openiddict comes with easy to use fluent API for configuring the server and you can simply set the issuer via a method call. But thing were not as easy as in Openiddict when it gets to login by Google authentication handler. :/

To override the google authentication handler you need to explore the code behind their easy to use AddGoogle(...) method. You will then find a class called GoogleHandler which is also responsible for building Uris. Gladly the two interesting methods are defined virtual so we can simple inherit from this class and override them. Not forget to mention the obvious that you need to have Microsoft.AspNetCore.Authentication.Google package installed. Here an example:

public class MyGoogleHandler : GoogleHandler
{
    private readonly IConfiguration _configuration;

    public MyGoogleHandler(
        IOptionsMonitor<GoogleOptions> options, ILoggerFactory logger,
        UrlEncoder                     encoder, ISystemClock   clock,
        IConfiguration                 configuration )
            : base(
                options, logger, encoder,
                clock)
    {
        _configuration = configuration;
    }

    protected override string BuildChallengeUrl( AuthenticationProperties properties, string redirectUri )
    {
        var newRedirectUri = "http://my-auth-server.com/loginByGoogle"; // or read from configuration
        return base.BuildChallengeUrl(properties, newRedirectUri);
    }

    protected override Task InitializeHandlerAsync()
    {
        // This ignores the fact that we have http communication inside the cluster and treat is as https
        Request.Scheme = "https";
        // Request.Host is also modifiable at this point 
        return base.InitializeHandlerAsync();
    }
}
Enter fullscreen mode Exit fullscreen mode

After this, you need to find the correct way for registering you handler instead of the default one. And there was the place where I crawled the internet and found nothing, then decide to go actually read the code behind the registration to finally find the correct approach. Luckily you don't have to spend that much time to find it :) here is the code: (don't forget the cookie configuration as explained above)

internal static void AddMyAuthentication(this IServiceCollection services, IConfiguration configuration)
{
    services.AddAuthentication()
        .AddOAuth<GoogleOptions, MyGoogleHandler>( // this is the important part
                    GoogleDefaults.AuthenticationScheme, GoogleDefaults.DisplayName, o => {
                        o.ClientId           = configuration["Authentication:Google:ClientId"];
                        o.ClientSecret       = configuration["Authentication:Google:ClientSecret"];
                        o.ReturnUrlParameter = "after";
                        o.CallbackPath       = "/loginByGoogle";
                        o.Scope.Add("https://www.googleapis.com/auth/userinfo.email");
                        o.Scope.Add("https://www.googleapis.com/auth/userinfo.profile");
                        o.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
                        o.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
                        o.CorrelationCookie.SameSite     = SameSiteMode.None;
                        o.CorrelationCookie.Name         = "coockies.CorrelationCookie";
                    })
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

Building the service with the characteristics mentioned above, was stressful yet definitely fun. There are a lot of additional elements you need to consider which are not mentioned here. Even though this article is lengthy enough by nature, it's still just the scratch of the surface of what needs to be thought of when building a central identity service. For instance things like how best to design your database, how best to tackle database migrations and cleanup, How best to take care of registering OpenID clients and keep their configuration up to date, how to make the application stateless and scalable, how to take control over access token and reference token generations and using reference tokens, how to handle authorization by JWT token and reference token with introspection, secrets and certificates, dockerising, build automation and GitOps, and many more topics are not mentioned here while being crucial parts of the server.

I hope you enjoyed following along with my journey and learn something new. Say tuned for possible and probable future parts of this article including more challenges and their solutions.

Mohammadali Forouzesh - just a passionate software engineer
01/11/2022
LinkedIn - Instagram

Top comments (0)