Simple Authentication Sample

 


I came across a couple of use cases where I needed remarkably simple authentication for a couple of applications. The applications would have less than 5 users. This made the prospect of standing up a new database or even using an existing one for auth using an Identity server seem like way overkill. I do want to utilize the built-in Authorization View components that Blazor has.

Use Case:

  •          Quick Access to a project for a client that is not ready to be public
  •          As an admin for a project to manage system configurations

This post describes my solution. A simple authentication system.

The base requirements for the authentication solution are :

  1.         Five or less users
  2.         No user maintenance will be required
    1.         The user may be hard coded, provided by an encrypted file, or even as app setting values
  3.         Avoid the extra cost or overhead of using a database
  4.         Authentication is valid for a single session
    1.         Once the users close the browser, they are no longer authenticated.
  5.         There is no time limit on the session
    1.         This could be added with no real overhead

    System Flow

      The steps needed to use the authentication is as follows:

  1.         If the user is not authorized send them to the login page
  2.         Have them log in
    1.         We must provide the code to validate the username/email and password
  3.         Once the user is validated, Mark them as Authenticated
    1.         This generates the claims
  4.         Store the username/email in local storage
  5.         On Each new authentication challenge
    1.         See if the session is still active
    2.         If so, continue
    3.         If not, send the user to the login page

    Implementation

      Since I wanted to leverage the built-in authorization view components, I needed an authentication state provider. I had to create this as a custom authentication state provider. The provider will be responsible for Setting the user as authenticated and generating the claims token. In addition, it must handle any authentication challenges. This results in two methods:

MarkUserAsAuthenicated : called when the user is logging in

GetAuthticatationStateAsync: called by the Authorize Route View

 

      The second class needed was a user service that the provider could interact with. It provides methods to get users.

      For the users, I am using a singleton that acts like a cache for the valid users. It contains a collection of valid users. These users can come from a file, or settings, or in the case of this demo, they are hardcoded. You can include all the information that you may need. For the demo, I needed an email, Access token, and First name. The authentication service is also responsible for evaluating the user validation during the login.

      We need to add these services to the application so we can inject them where we need them. In Program.cs you simply add them:

builder.Services.AddSingleton<WeatherForecastService>();

builder.Services.AddSingleton<AuthService>();

 

builder.Services.AddScoped<IUserService, UserService>();

builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenicationStateProvider>();


Handle User Log-In    

      Now that we have our services, we need a login page. I created a simple login page with email and password inputs. On the submit button, we will validate the user’s information.

      For validation, we first make sure an email and password are provided. Once you have an email and password, we need to see if they are on the list of users that are allowed to access the application.

      To validate the user, we call the Authenticate User method in the auth service. We will need to pass in the customer application state provider, so we can mark the user as logged. We will also need to pass in the service for local storage.    

            public bool AuthenicateUser(User user, CustomAuthenicationStateProvider authProvider, ISessionStorageService storageService)
        {
            if (string.IsNullOrEmpty(user.EmailAddress) || string.IsNullOrEmpty(user.Password))
            {
                return false;
            }
            //lookup user and password
            var validUser = validUsers.Where(x => x.EmailAddress == user.EmailAddress && x.Password == user.Password).FirstOrDefault();
            if (validUser == null)
            {
                return false;
            }
            //Add Session Token
            validUser.AccessToken = DateTime.Now.ToString();
            authProvider.MarkUserAsAuthenicated(validUser.FirstName);
            storageService.SetItemAsStringAsync("userSession", validUser.EmailAddress);
            return true;
        }


    Local Storage

      We are actually using local session storage (Blazored/SessionStorage: A library to provide access to session storage in Blazor applications (github.com)) to store the user's email.  Using this session storage library does some nice stuff for us. It will place the email in the session storage of the browser, and importantly it will remove the entry at the close of the session. This makes the cleanup of the session nice and easy. This is the way I went with session storage over just local storage.

      We now have the user log in and land on the index page. For the demo, I made a change to show, just like all the other authentication articles, who is logged in on the index page. This is where am using the first name.

    Handle an Authentication Challenge

      During the user's active session, they will go to places where the Authorization view will need to validate they are authenticated. Again, this is the GetAuthticatationStateAsync method of the Custom Application State Provider.

      In this method, we must execute the following steps to ensure the user is still authenticated:

  1.         Retrieve the user from the session storage
  2.         Get that user from the User service
  3.         Validate the user has an access token
    1.         This is the step that will keep someone from just adding an email to the session storage. 
    2.         A user will only have an access token if they have had a valid login
    3.         It is on this check that we could check to see if the user has been logged in too long.
  4.         Generate a Claims Identity

       If any of these steps fail, the user will be considered invalid and be redirected to the login screen to provide a valid email and password.


        public async override Task<AuthenticationState> GetAuthenticationStateAsync()
  {
            ClaimsIdentity identity = new();
            var emailAddress = await _localStorageService.GetItemAsync<string>("userSession");
            if (emailAddress != null)
            {
                //make sure the user is logged in
                var inUser = userService.GetUserByAccessTokenAsync(emailAddress).Result;
                if (!string.IsNullOrEmpty(inUser.AccessToken))
                {
                    identity = new ClaimsIdentity(new[]
                    {
                        new Claim(ClaimTypes.Name, inUser.FirstName),
                    }, "apiauth_type");
                }
            }
            else
            {
                identity = new ClaimsIdentity();
            }
            var user = new ClaimsPrincipal(identity);
            return await Task.FromResult(new AuthenticationState(user));
   }


       With any application solution, there are multiple paths I could have taken. Keeping the original requirements in sight, this is a workable solution.


    Wiring it all together

      To wrap this together, we need to add the CascadingAuthenticationState component around the routing in the App. razor.  We define the Authorized route and what to do if the user is not authenticated. This is what causes the user to be redirected to the login page when they fail the authentication challenge.

        <CascadingAuthenticationState>
       <Router AppAssembly="@typeof(App).Assembly" >
           <Found Context="routeData">
               <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" >
                   <NotAuthorized>
                        <Login/>
                   </NotAuthorized>
                </AuthorizeRouteView>
              <FocusOnNavigate RouteData="@routeData" Selector="h1" />
           </Found>
           <NotFound>
                <PageTitle>Not found</PageTitle>
                <LayoutView Layout="@typeof(MainLayout)">
                    <p role="alert">Sorry, there's nothing at this address.</p>
                </LayoutView>
           </NotFound>
        </Router>
    </CascadingAuthenticationState>


    Proper User

      My guess is that a security audience would not really care for this solution for lots of reasons. You must take into consideration the application domain as well. I would not use this solution on any financial applications any application that contains real personal data or any application that if compromised could cause an impact. Those types of applications justify the use of ASP.Net membership Identity Server or any other of the more complex solutions.


    How it was built

      I built this on Blazor Server, but you can apply it for web assembly as well. For web assembly, you will need to take extra care on the storage of the user information since it will be downloaded to the client.

       I used the standard Blazor Project template but removed the bootstrap styles and used TailwindCss. I found it much more enjoyable to build doing that. I even included a “Forgot Password” page for fun. On the login page, I added support to submit the form with the user on the password input and press the “enter” key. I found this a nice little feature that I wanted when I was testing.

      The validation error messages on the login form are a little weak. I may do a phase 2 that improves on the validation message.


      Summary

      I am familiar with most authentication and authorization solutions, like the ones mentioned above. I would use these in most applications, but this is a real-world solution that I will be using under the right conditions. You must also be aware of the security requirements of the application you are building.


    [source code] 

 

Comments

Popular posts from this blog

Yes, Blazor Server can scale!

Blazor new and improved Search Box

Blazor Wizard Step Component