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 :
- Five or less users
- No user maintenance will be required
- The user may be hard coded, provided by an encrypted file, or even as app setting values
- Avoid the extra cost or overhead of using a database
- Authentication is valid for a single session
- Once the users close the browser, they are no longer authenticated.
- There is no time limit on the session
- This could be added with no real overhead
System Flow
The steps needed to use the authentication is as follows:
- If the user is not authorized send them to the login page
- Have them log in
- We must provide the code to validate the username/email and password
- Once the user is validated, Mark them as Authenticated
- This generates the claims
- Store the username/email in local storage
- On Each new authentication challenge
- See if the session is still active
- If so, continue
- 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.
{
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:
- Retrieve the user from the session storage
- Get that user from the User service
- Validate the user has an access token
- This is the step that will keep someone from just adding an email to the session storage.
- A user will only have an access token if they have had a valid login
- It is on this check that we could check to see if the user has been logged in too long.
- 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.
{
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.
<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.
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.
Comments
Post a Comment