DEV Community

Cover image for A Full-Stack Web App Using Blazor WebAssembly and GraphQL—Part 4
Suresh Mohan for Syncfusion, Inc.

Posted on • Originally published at syncfusion.com on

A Full-Stack Web App Using Blazor WebAssembly and GraphQL—Part 4

In the previous article of this series (part 3), we learned how to edit and delete movie data in our app. We configured the home page of the application and added the sort and filter options for the list of movies.

In this article, we will add the following features to our application:

  • User registration
  • Authentication

Let’s start with adding the user registration feature.

Add User Roles

Add a class named UserRoles.cs inside the MovieApp.Shared\Models folder. Put the following code inside it.

public static class UserRoles
{
   public const string Admin = "Admin";
   public const string User = "User";
}
Enter fullscreen mode Exit fullscreen mode

We have created a static class and defined the constants to denote the user roles allowed by our app.

Create DTO Models

DTO stands for data transfer object. A DTO is an object that helps us to transfer data between server and client.

Add a folder named Dto inside the MovieApp.Shared project. Add the class UserRegistration.cs in the Dto folder. Put the following code into it.

public class UserRegistration
{
    [Required]
    [Display(Name = "First Name")]
    public string FirstName { get; set; }

    [Required]
    [Display(Name = "Last Name")]
    public string LastName { get; set; }

    [Required]
    public string Username { get; set; }

    [Required]
    [RegularExpression(@"^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$",
        ErrorMessage = "Password should have minimum 8 characters, at least 1 uppercase letter, 1 lowercase letter and 1 number.")]
    public string Password { get; set; }

    [Required]
    [Display(Name = "Confirm Password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }

    [Required]
    public string Gender { get; set; }

    public UserRegistration()
    {
        FirstName = string.Empty;
        LastName = string.Empty;
        Gender = string.Empty;
        Username = string.Empty;
        Password = string.Empty;
        ConfirmPassword = string.Empty;
    }
}
Enter fullscreen mode Exit fullscreen mode

This UserRegistration class contains all the properties we need for the registration of a new user. All the fields are marked as required. The [Display] attribute helps us to specify the display name for the class properties. We are using a regular expression to validate the strength of the password.

Add a new class file called RegistrationResponse.cs and put the following code inside it.

public class RegistrationResponse
{
   public bool IsRegistrationSuccess { get; set; }
   public string? ErrorMessage { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

This class helps us to return the response of the registration to the client.

Note : To learn more, please refer to the article on Blazor Forms and Form Validation.

Create the IUser Interface

Add a new file called IUser.cs to the MovieApp.Server\Interfaces folder. Put the following method declarations inside it.

public interface IUser
{
   Task<bool> RegisterUser(UserMaster user);
   Task<bool> IsUserExists(int userId);
}
Enter fullscreen mode Exit fullscreen mode

Creating User Data Access Layer for the Application

Add a class named UserDataAccessLayer.cs inside the MovieApp.Server\DataAccess folder. Put the following code inside it.

public class UserDataAccessLayer : IUser
{
    readonly MovieDBContext _dbContext;

    public UserDataAccessLayer(IDbContextFactory<MovieDBContext> dbContext)
    {
       _dbContext = dbContext.CreateDbContext();
    }

    public async Task<bool> IsUserExists(int userId)
    {
       UserMaster? user = await _dbContext.UserMasters.FirstOrDefaultAsync(x => x.UserId == userId);

       if (user is not null)
       {
          return true;
       }
       else
       {
          return false;
       }
    }
}
Enter fullscreen mode Exit fullscreen mode

We have implemented the IUser interface and added the definition for the IsUserExists method.

Now, add the RegisterUser and CheckUserNameAvailability methods to the UserDataAccessLayer class.

public async Task<bool> RegisterUser(UserMaster userData)
{
    bool isUserNameAvailable = CheckUserNameAvailability(userData.Username);

    try
    {
        if (isUserNameAvailable)
        {
            await _dbContext.UserMasters.AddAsync(userData);
            await _dbContext.SaveChangesAsync();
            return true;
        }
        else
        {
            return false;
        }
    }
    catch
    {
       throw;
    }
}

bool CheckUserNameAvailability(string userName)
{
    string? user = _dbContext.UserMasters.FirstOrDefault(x => x.Username == userName)?.ToString();

    if (user is not null)
    {
       return false;
    }
    else
    {
       return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

The CheckUserNameAvailability method accepts the username as a parameter and checks the availability of the user in the UserMaster table.

The RegisterUser method accepts a parameter of type UserMaster. Before registering the user, we will check if the username is available. If the username is available, then we will add the new userData to the UserMaster table, or else the method will return false. We will not allow any duplicate username registration for our app.

Add a GraphQL Mutation Resolver for Authentication

Add a class named AuthMutationResolver.cs inside the MovieApp.Server/GraphQL folder. Put the following code inside it.

[ExtendObjectType(typeof(MovieMutationResolver))]
public class AuthMutationResolver
{
   readonly IUser _userService;
   readonly IConfiguration _config;

   public AuthMutationResolver(IConfiguration config, IUser userService)
   {
      _config = config;
      _userService = userService;
   }

   [GraphQLDescription("Register a new user.")]
   public async Task<RegistrationResponse> UserRegistration(UserRegistration registrationData)
   {
       UserMaster user = new()
       {
           FirstName = registrationData.FirstName,
           LastName = registrationData.LastName,
           Username = registrationData.Username,
           Password = registrationData.Password,
           Gender = registrationData.Gender,
           UserTypeName = UserRoles.User
       };

       bool userRegistrationStatus = await _userService.RegisterUser(user);

       if (userRegistrationStatus)
       {
          return new RegistrationResponse { IsRegistrationSuccess = true };
       }
       else
       {
          return new RegistrationResponse { IsRegistrationSuccess = false, ErrorMessage = "This User Name is not available." };
       }
   }
}
Enter fullscreen mode Exit fullscreen mode

Note : GraphQL does not allow us to create more than one mutation type.

The ExtendObjectType attribute helps us to split the type into multiple files. When the GraphQL server schema is generated, these two files will be merged into a single type.

In the UserRegistration method, we will create an object of the type UserMaster. We will set the UserTypeName property to the role of type User. We will allow registration for the role User via the UI. To add a user with the role Admin , we will update the value directly to the database.

If the user registration fails, we will return the RegistrationResponse object with the error that the username is not available for registration.

Register the Mutation Resolver

Since we have added a new mutation resolver, we need to register it in our middleware.

Update the Program.cs file like in the following code example.

builder.Services.AddGraphQLServer()
       .AddDefaultTransactionScopeHandler()
       .AddQueryType<MovieQueryResolver>()
       .AddMutationType<MovieMutationResolver>()
       .AddTypeExtension<AuthMutationResolver>()
       .AddFiltering()
       .AddSorting();
Enter fullscreen mode Exit fullscreen mode

The AddDefaultTransactionScopeHandler extension method helps us to define the transaction scope when we have multiple mutations executed in the same request.

We can add only one mutation type using the AddMutationType extension method. Therefore, we use the AddTypeExtension method to register the new mutation resolver of the type MovieMutationResolver.

We then register the scoped lifetime of the IUser service using the following code.

builder.Services.AddScoped<IUser, UserDataAccessLayer>();
Enter fullscreen mode Exit fullscreen mode

We are done with the server configuration. Let’s move to the client-side of the app.

Add GraphQL client queries

Since we have added a new method on the server for user registration, we need to regenerate the GraphQL client using the process discussed in part 2 of this series.

Add a file named RegisterUser.graphql inside the MovieApp.Client\GraphQLAPIClient folder. Add the GraphQL mutation to register a new user like in the following code example.

mutation RegisterUser($userData:UserRegistrationInput!){
   userRegistration(registrationData:$userData){
      isRegistrationSuccess,
      errorMessage
   }
}
Enter fullscreen mode Exit fullscreen mode

Use the Visual Studio shortcut Ctrl+Shift+B to build the project. It will regenerate the Strawberry Shake client class.

Add the Custom Validator for Registration

Create a class file named CustomValidator.cs inside the MovieApp.Client\Shared folder. Add the following code to it.

public class CustomValidator : ComponentBase
{
    private ValidationMessageStore messageStore = default!;

    [CascadingParameter]
    private EditContext CurrentEditContext { get; set; } = default!;

    protected override void OnInitialized()
    {
        if (CurrentEditContext == null)
        {
            throw new InvalidOperationException(
               $"{nameof(CustomValidator)} requires a cascading parameter of type {nameof(EditContext)}");
        }

        messageStore = new ValidationMessageStore(CurrentEditContext);

        CurrentEditContext.OnValidationRequested += (s, e) =>
            messageStore.Clear();
        CurrentEditContext.OnFieldChanged += (s, e) =>
            messageStore.Clear(e.FieldIdentifier);
   }

   public void DisplayErrors(string formField, string error)
   {
        messageStore.Add(CurrentEditContext.Field(formField), error);

        CurrentEditContext.NotifyValidationStateChanged();
   }

   public void ClearErrors()
   {
        messageStore.Clear();
        CurrentEditContext.NotifyValidationStateChanged();
   }
}
Enter fullscreen mode Exit fullscreen mode

The custom validator component will allow us to handle the server-side validations. The EditContext class helps us to hold the metadata related to a data editing process. The variable of type ValidationMessageStore maintains the current list of form errors.

The DisplayErrors method accepts the name of the form field and the error message as parameters. We will then attach the error to the form field of the CurrentEditContext.

Create the Register Component

Create a new component named Registration.razor under the pages folder. Add a base class for the component named Registration.razor.cs.

Add the following code to the base class.

public class RegistrationBase : ComponentBase
{
   [Inject]
   public NavigationManager NavigationManager { get; set; } = default!;

   [Inject]
   MovieClient MovieClient { get; set; } = default!;

   [Inject]
   ILogger<RegistrationBase> Logger { get; set; } = default!;

   protected UserRegistration registration = new();

   protected CustomValidator registerValidator = default!;

   protected async Task RegisterUser()
   {
      registerValidator.ClearErrors();

      try
      {
          UserRegistrationInput registrationData = new()
          {
              FirstName = registration.FirstName,
              LastName = registration.LastName,
              Username = registration.Username,
              Password = registration.Password,
              ConfirmPassword = registration.ConfirmPassword,
              Gender = registration.Gender,
          };

          var response = await MovieClient.RegisterUser.ExecuteAsync(registrationData);

          if (response.Data is not null)
          {
              RegistrationResponse RegistrationStatus = new()
              {
                  IsRegistrationSuccess = response.Data.UserRegistration.IsRegistrationSuccess,
                  ErrorMessage = response.Data.UserRegistration.ErrorMessage
              };

              if (!RegistrationStatus.IsRegistrationSuccess)
              {
                  registerValidator.DisplayErrors(nameof(registration.Username), RegistrationStatus.ErrorMessage);
                  throw new HttpRequestException($"User registration failed. Status Code: 403 Forbidden");
              }
              else
              {
                  NavigationManager.NavigateTo("/login");
              }
         }
     }
     catch (Exception ex)
     {
         Logger.LogError(ex.Message);
     }
  }
}
Enter fullscreen mode Exit fullscreen mode

In the RegisterUser method, we invoked the ClearFormErrors method of our custom validators to clear out any existing form errors. We then created an object of type UserRegistrationInput and invoked the RegisterUser method of the MovieClient.

If the registration fails, we attach the error to the Username field. If the registration succeeds, we would redirect the user to the Login component.

The catch block handles the exception, and the logger logs the error on the console.

Add the following code to the Registration.razor file.

@page "/register"
@inherits RegistrationBase

<div class="row justify-content-center">
  <div class="col-md-6">
     <div class="card mt-3 mb-3">
         <div class="card-header">
             <div class="d-flex justify-content-between">
                  <h2>User Registration</h2>
                  <div class="d-flex align-items-center">
                     <strong>Already Registered? </strong>
                     <a href='login' class="nav-link">Login</a>
                  </div>
             </div>
         </div>
         <div class="card-body">
            <EditForm Model="@registration" OnValidSubmit="RegisterUser">
                <DataAnnotationsValidator />
                <CustomValidator @ref="registerValidator" />

                <div class="mb-3">
                   <label class="control-label col-md-12">First name</label>
                   <div class="col">
                      <InputText class="form-control" @bind-Value="registration.FirstName" />
                      <ValidationMessage For="@(() => registration.FirstName)" />
                   </div>
                </div>

                <div class="mb-3">
                   <label class="control-label col-md-12">Last name</label>
                   <div class="col">
                      <InputText class="form-control" @bind-Value="registration.LastName" />
                       <ValidationMessage For="@(() => registration.LastName)" />
                   </div>
                </div>

                <div class="mb-3">
                   <label for="Username" class="form-label">User name</label>
                   <div class="col">
                       <InputText class="form-control" @bind-Value="registration.Username" />
                       <ValidationMessage For="@(() => registration.Username)" />
                   </div>
                </div>

                <div class="mb-3">
                  <label for="Password" class="form-label">Password</label>
                  <div class="col">
                       <InputText type="password" class="form-control" @bind-Value="registration.Password" />
                       <ValidationMessage For="@(() => registration.Password)" />
                  </div>
                </div>

                <div class="mb-3">
                   <label for="ConfirmPassword" class="form-label">Confirm password</label>
                   <div class="col">
                       <InputText type="password" class="form-control" @bind-Value="registration.ConfirmPassword" />
                       <ValidationMessage For="@(() => registration.ConfirmPassword)" />
                   </div>
                </div>

                <div class="mb-3">
                   <label for="Gender" class="form-label">Gender</label>
                   <div class="col">
                       <InputSelect class="form-control" @bind-Value="registration.Gender">
                          <option value="-- Select City --">-- Select Gender --</option>
                          <option value="Male">Male</option>
                          <option value="Female">Female</option>
                       </InputSelect>
                       <ValidationMessage For="@(() => registration.Gender)" />
                   </div>
                </div>

                <div class="form-group" align="right">
                   <button type="submit" class="btn btn-success">Register</button>
                </div>
             </EditForm>
          </div>
       </div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

The EditForm component helps us to create the registration form. To use the custom validator component in the form, use the validator name as the tag name and provide the reference for the local variable to the @ref attribute. The RegisterUser method will be invoked on the valid form submission.

Note : The login component will be created in the later part of this article. However, here we have added a navigation link to the login page for consistency.

This completes the registration feature for the app. Now, we are going to add the authentication mechanism.

How Will We Secure the App?

Let’s understand how we can implement the authentication and authorization mechanism in our app.

The user will log in via a form on the UI. The user credentials will be validated on the server. If the authentication is successful on the server, we will create a JWT with user claims and return it to the client.

The JWT is stored in the browser’s local storage. It will be sent back to the server as a Bearer token in the header of each subsequent API request.

We will implement policy-based authorization and restrict access to the app resource based on the policy.

Note : For more details, please refer to JSON Web Tokens.

Add the Dto Classes

Create a class file called UserLogin.cs inside the MovieApp.Shared\Dto folder. Add the following code inside it.

public class UserLogin
{
   [Required]
   public string Username { get; set; }

   [Required]
   public string Password { get; set; }

   public UserLogin()
   {
      Username = string.Empty;
      Password = string.Empty;
   }
}
Enter fullscreen mode Exit fullscreen mode

The UserLogin class will contain the properties required by a user to login into the app.

Create another class file named AuthResponse.cs in the Dto folder and add the following code inside it.

public class AuthResponse
{
   public string? ErrorMessage { get; set; }

   public string? Token { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

This class helps us to return the response of the registration to the client.

Add a New Class

Create a class file named AuthenticatedUser.cs inside the MovieApp.Server\Models. Add the following code inside it.

public class AuthenticatedUser
{
   public int UserId { get; set; }

   public string Username { get; set; }

   public string UserTypeName { get; set; }

   public AuthenticatedUser()
   {
      Username = string.Empty;
      UserTypeName = string.Empty;
   }
}
Enter fullscreen mode Exit fullscreen mode

This class helps us to hold the user data once the user is authenticated.

Update the IUser Interface

Update the IUser.cs file by adding the method declaration like in the following code.

public interface IUser
{
   // other methods

   AuthenticatedUser AuthenticateUser(UserLogin loginCredentials);
}
Enter fullscreen mode Exit fullscreen mode

Update the UserDataAccessLayer Class

Update the UserDataAccessLayer class by implementing the AuthenticateUser method like in the following code.

public AuthenticatedUser AuthenticateUser(UserLogin loginCredentials)
{
    AuthenticatedUser authenticatedUser = new();

    var userDetails = _dbContext.UserMasters
          .FirstOrDefault(u =>
          u.Username == loginCredentials.Username &&
          u.Password == loginCredentials.Password);

    if (userDetails != null)
    {
       authenticatedUser = new AuthenticatedUser
      {
          Username = userDetails.Username,
          UserId = userDetails.UserId,
          UserTypeName = userDetails.UserTypeName
      };
    }
    return authenticatedUser;
}
Enter fullscreen mode Exit fullscreen mode

The AuthenticateUser method accepts an object of the type UserLogin as the parameter. We will validate the user login information from the UserMasters table. If the user is successfully authenticated, we will return an object of the type AuthenticatedUser containing the information such as Username, UserId, and UserTypeName. We will send this information back to the UI.

Configure the JWT Secret Key

We will add the secret key, issuer, and audience details for the JSON web token (JWT) in the appsettings.json file like in the following code.

"Jwt": {
   "SecretKey": "EnSJ3YxydKxrKwg7",
   "Issuer": "https://localhost:7104",
   "Audience": "https://localhost:7104"
},
Enter fullscreen mode Exit fullscreen mode

Note : We will use HmacSha256 as our preferred encryption algorithm for JWT. This algorithm accepts a key size of 128 bits. The secret key you use must satisfy this criterion. Otherwise, it will throw a runtime error.

Add the GraphQL Server Mutation to Authenticate the User

Add the GenerateJWT method to generate the JWT data in the AuthMutationResolver class. Refer to the following code example.

string GenerateJWT(AuthenticatedUser userInfo)
{
    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:SecretKey"]));
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

    List<Claim> userClaims = new()
    {
       new Claim(ClaimTypes.Name, userInfo.Username),
       new Claim("userId", userInfo.UserId.ToString()),
       new Claim(ClaimTypes.Role, userInfo.UserTypeName),
    };

    var token = new JwtSecurityToken(
       issuer: _config["Jwt:Issuer"],
       audience: _config["Jwt:Audience"],
       claims: userClaims,
       expires: DateTime.Now.AddHours(24),
       signingCredentials: credentials
    );

    return new JwtSecurityTokenHandler().WriteToken(token);
}
Enter fullscreen mode Exit fullscreen mode

The GenerateJWT method will accept an object of type AuthenticatedUser and return the JWT as a string. We will then create the claims for the user information we want to send as the payload of JWT.

We will set the token parameters such as issuer, audience, claims, expiry time, and signing credentials. The expiry time of the token is set to 24 hours from the time of creation.

Add the mutation for validating the user login information.

[GraphQLDescription("Authenticate the user.")]
public AuthResponse UserLogin(UserLogin userDetails)
{
    AuthenticatedUser authenticatedUser = _userService.AuthenticateUser(userDetails);

    if (!string.IsNullOrEmpty(authenticatedUser.Username))
    {
       string tokenString = GenerateJWT(authenticatedUser);

       return new AuthResponse { Token = tokenString };
    }

    else
    {
       return new AuthResponse { ErrorMessage = "Username or Password is incorrect." };
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, we authenticate the user by invoking the AuthenticateUser method of the userService. If the user is authenticated successfully, we will create a new JWT and send it back to the UI via the AuthResponse object. If the authentication fails, we will set the ErrorMessage property of the AuthResponse object and send it back to the UI.

Resource

Refer to the complete source code for Creating a full-stack web app using Blazor WebAssembly and GraphQL on GitHub.

Summary

Thanks for reading! In this article, we have added the feature to register a new user to our app. We then implemented authentication using JWT.

In our next article of this series, we will configure policy-based authorization to restrict access to the app resources based on the policies defined for the user.

Syncfusion’s Blazor component suite offers over 70 UI components that work with both server-side and client-side (WebAssembly) hosting models seamlessly. Use them to build marvelous apps!

If you have any questions or comments, you can contact us through our support forums, support portal, or feedback portal. We are always happy to assist you!

Top comments (0)