Advertisement

#28 Multiple Razor Page Request Handlers

In this article we will create the end points for the user to be able to register and submit a request when/if they forgot the password/username. In order to do this we will be confronted with a problem when trying to post back to a razor page that is not the origin of the request. To solve this problem we add multiple get/post handlers to our razor pages and see how we can tell the server which handler we are trying to reach.

You can view a completed example of the authenticator component here.

Adding additional request methods

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.ts
          • authenticator-http.service.ts

First thing we are going to do is add the ability to make a request to the server for registering, forgot password and forgot username (a). To make it easier to take advantage of the type system we will define a few additional interfaces for the payload for these request. Now that we have the ability to make these request we need to modify our authenticator component to use them. We just need to modify our onClickSend method to make the appropriate request (b).

authenticator-http.service.ts (1)

...
export interface IForgotPayload {
    email: string;
}

interface ILoginRegisterPayload {
    password: string;
    username: string;
}

export interface ILoginPayload extends ILoginRegisterPayload {
    rememberme: boolean;
}

export interface IRegisterPayload extends ILoginRegisterPayload {
    email: string;
    emailConfirm: string;
    passwordConfirm: string;
}

export class AuthenticatorHttpService ... {
    ...
    public forgotPassword = (data: IForgotPayload, success: (response: Response) => void, error: (response: Response) => void, $finally?: (response: Response) => void) => {
        this.post(AuthenticatorHttpService.forgotPasswordUrl, data, success, error, $finally);
    }

    public forgotUsername = (data: IForgotPayload, success: (response: Response) => void, error: (response: Response) => void, $finally?: (response: Response) => void) => {
        this.post(AuthenticatorHttpService.forgotUsernameUrl, data, success, error, $finally);
    }
    ...
    public register = (data: IRegisterPayload, success: (response: Response) => void, error: (response: Response) => void, $finally?: (response: Response) => void) => {
        this.post(AuthenticatorHttpService.registerUrl, data, success, error, $finally);
    }
    ...
}
(a) Adding the ability for our http service to make register, forgot password, and forgot username requests.

authenticator.component.ts (1)

...
export class AuthenticatorComponent ... {
    ...
    private onClickSend = () => {
        ...
        if (this.isLogin) {
            if (this.isSubActionForgotPassword) {
                this.forgotPassword();
                return;
            }

            if (this.isSubActionForgotUsername) {
                this.forgotUsername();
                return;
            }

            this.login();
        } else if (this.isRegister) {
            this.register();
        }
    }
    ...
    private forgotPassword = () => {
        this._authenticatorHttpService.forgotPassword(
            {
                email: this.controlEmail.value
            },
            () => {
                this._sendScreen.success("Request successful you will be redirected shortly.");
            },
            () => {
                this.xhrFailure();
                this._sendScreen.failure("Error using the provided email.");
            });
    }

    private forgotUsername = () => {
        this._authenticatorHttpService.forgotUsername(
            {
                email: this.controlEmail.value
            },
            () => {
                this._sendScreen.success("Request successful you will be redirected shortly.");
            },
            () => {
                this.xhrFailure();
                this._sendScreen.failure("Error using the provided email.");
            });
    }
    ...
    private login = () => {
        ...
        this._authenticatorHttpService.login(this._xhrData,
            () => {
                this._sendScreen.success("Login successful you will be redirected shortly.");
            },
            ...);
    }
    ...
    private register = () => {
        this._authenticatorHttpService.register(
            {
                email: this.controlEmail.value,
                emailConfirm: this.controlEmailConfirm.value,
                password: this.controlPassword.value,
                passwordConfirm: this.controlPasswordConfirm.value,
                username: this.controlUsername.value
            },
            () => {
                this._sendScreen.success("Registration successful you will be redirected shortly.");
            },
            () => {
                this.xhrFailure();
                this._sendScreen.failure("Error using the provided information.");
            });
    }
    ...
}
(b) Modifying our authenticator component to make the appropriate request.

With those changes made we can now make a register request to the correct end point with the required payload (c), as well as, a forgot password request (d) and a forgot username request (e). You may also notice the regsiter request returns a status code of 200 and the other two return 404. This is because many articles back we created razor pages for both the login and register pages but we have not created them for the forgot requests which we will take care of next.

Image showing a register request going to the correct end point and
            containing the required payload.
(c) Image showing a register request going to the correct end point and containing the required payload.
Image showing a forgot password request going to the correct end point and
            containing the required payload.
(d) Image showing a forgot password request going to the correct end point and containing the required payload.
Image showing a forgot username request going to the correct end point and
            containing the required payload.
(e) Image showing a forgot username request going to the correct end point and containing the required payload.

Exception processing 404

  • WebUi
    • Source
      • services
        • http.service.ts

Currently if the server returns an response without a response object when our base http service attempts to process it an exception is thrown. To deal with this we are going use a try catch block within in the process error method (f).

http.service.ts (1)

...
export abstract class HttpService ... {
    ...
    private processError = (response: Response) => {
        let json: IErrorResponse;
        try {
            json = response.json() as IErrorResponse;
        } catch (e) {
            return;
        } 
        ...
    }
    ...
}
(f) Same correction that will prevent a 404 response from throwing an exception inside our base http service.

The forgot razor pages

  • WebUi
    • Pages
      • Account
        • Forgot-Password.cshtml
        • Forgot-Username.cshtml

Next thing we are going to do is create the forgot password and forgot username razor pages. For my urls I like to have a hyphen separate words to make it easier to read but if you try to create a razor page with a hyphen in it Visual Studio will either raise and error or replace the hyphen with an underscore depending on how you create the page. In order to get around this problem I create the pages without the hyphen and once it is created I rename the page itself to whatever I wanted it to be originally. For example in the present case I will create two pages, one named ForgotPassword.cshtml and the other named ForgotUsername.cshtml. Once they are created I will rename them to Forgot-Password.cshtml (g) and Forgot-Username.cshtml (h).

Forgot-Password.cshtml (1)

@page

@{
    ViewBag.Title = "Forgot Password Email Sent";
}

<div>
    <h1>@ViewBag.Title</h1>
    <p>
        Please check your email to reset your password. If you have not received the email shortly please check your spam folder.
    </p>
</div>
(i) Simple view that we will display once we have received a successful request to reset a password.

Forgot-Username.cshtml (1)

@page

@{
    ViewBag.Title = "Forgot Username Email Sent";
}

<div>
    <h1>@ViewBag.Title</h1>
    <p>
        Please check your email for your username. If you have not received the email shortly please check your spam folder.
    </p>
</div>
(j) Simple view that we will display once we have received a successful request to recover a username.

The forgot and register request models

  • WebUi
    • Models
      • Account
        • AccountForgotRequest.cs
        • AccountRegisterRequest.cs

When a user makes a register or forgot password/username request we will need to be able to bind the information that they send in to a c# class just as we have previously done for login requests. This means it is now time to create a couple of classes that we can bind forgot requests (k) and register requests (l) to. In both models we have also include some data annotations on the properties to aid in validation. We will add more latter on if needed.

AccountForgotRequest.cs (1)

using System.ComponentModel.DataAnnotations;

namespace WebUi.Models.Account
{
    public class AccountForgotRequest
    {
        [Required]
        [EmailAddress]
        public string Email { get; set; }
    }
}
(k) C# class that we can bind forgot password/username request information to.

AccountRegisterRequest.cs (1)

using System.ComponentModel.DataAnnotations;

namespace WebUi.Models.Account
{
    public class AccountRegisterRequest
    {
        [Required]
        [EmailAddress]
        public string Email { get; set; }
        [Compare("Email", ErrorMessage = "The email and confirmation email must match.")]
        public string EmailConfirm { get; set; }
        [Required]
        public string Password { get; set; }
        [Compare("Password", ErrorMessage = "The password and confirmation password must match.")]
        public string PasswordConfirm { get; set; }
        [MaxLength(100, ErrorMessage = "Maximum length is 100")]
        [MinLength(8, ErrorMessage = "Minimum length is 8")]
        public string Username { get; set; }
    }
}
(l) C# class that we can bind register request information to.

Time for the post methods

  • WebUi
    • Pages
      • Account
        • Forgot-Password.cshtml.cs
        • Forgot-Username.cshtml.cs
        • Register.cshtml.cs

With everything in place it is now time to create the post methods for sending the forgot password (m), forgot username (n) and register (o) requests to. Each of them is a simple method where we are just returning an ok response that should contain the information that was sent in so that we can test that the binding is happening properly.

Forgot-Password.cshtml.cs (1)

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using WebUi.Models.Account;

namespace WebUi.Pages.Account
{
    public class ForgotPasswordModel : PageModel
    {
        public void OnGet()
        {
        }

        public IActionResult OnPost([FromBody] AccountForgotRequest request)
        {
            return new OkObjectResult(request);
        }
    }
}
(m)

Forgot-Username.cshtml.cs (1)

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using WebUi.Models.Account;

namespace WebUi.Pages.Account
{
    public class ForgotUsernameModel : PageModel
    {
        public void OnGet()
        {
        }

        public IActionResult OnPost([FromBody] AccountForgotRequest request)
        {
            return new OkObjectResult(request);
        }
    }
}
(n)

Register.cshtml.cs (1)

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using WebUi.Models.Account;

namespace WebUi.Pages.Account
{
    public class RegisterModel : PageModel
    {
        public void OnGet()
        {
        }

        public IActionResult OnPost([FromBody] AccountRegisterRequest request)
        {
            return new OkObjectResult(request);
        }
    }
}
(o)
Advertisement

The register request and response behave just as we expect but unfortunately when a forgot request is made (p) no matter what we try to do the response is always to send back the get view (p) even though we can clearly see that we are making a post request to the correct end point.

Example of sending in a forgot username request that shows we
            are sending in the required payload to the correct end point and are making a post request.
(p) Example of sending in a forgot username request that shows we are sending in the required payload to the correct end point and are making a post request.
The response from either of the forgot end points is always to
            return the get view.
(q) The response from either of the forgot end points is always to return the get view.

Down but not out

  • WebUi
    • Controllers
      • AccountController.cs
    • Pages
      • Account
        • Forgot-Password.cshtml.cs
        • Forgot-Username.cshtml.cs
        • Login.cshtml.cs
        • Register.cshtml.cs

If I stop and think about it what I was attempting to do might actually be contrary to the intended use of the razer pages in the first place. The razor page is supposed to symbolize a physical page so we should probably be responding to every request that can be made on that page in the code behind for the page itself. To achieve this we will remove the post request method from the forgot password (r) and (s) code behinds, as well as, removing the check username action from the account controller (t).

Forgot-Password.cshtml.cs (2)

using Microsoft.AspNetCore.Mvc.RazorPages;

namespace WebUi.Pages.Account
{
    public class ForgotPasswordModel : PageModel
    {
        public void OnGet()
        {
        }
    }
}
(r) Result of removing the OnPost action from the forgot password code behind.

Forgot-Username.cshtml.cs (2)

using Microsoft.AspNetCore.Mvc.RazorPages;

namespace WebUi.Pages.Account
{
    public class ForgotUsernameModel : PageModel
    {
        public void OnGet()
        {
        }
    }
}
(s) Result of removing the OnPost action from the forgot username code behind.

AccountController.cs

using Microsoft.AspNetCore.Mvc;

namespace WebUi.Controllers
{
    [Route("account")]
    public class AccountController : Controller
    {

    }
}
(t) Result of removing the CheckUsernameAvailability action from the account controller.

Since the forgot actions are sub-actions that the user can perform when trying to login we will move them to the login code behind (u) and for the same reason we will move the check username availability action to the register code behind (v).

Login.cshtml.cs (1)

...
namespace WebUi.Pages.Account
{
    public class LoginModel : PageModel
    {
        ...
        public IActionResult OnPostForgotPassword([FromBody] AccountForgotRequest request)
        {
            return new OkObjectResult(new
            {
                message = "forgot password",
                request
            });
        }

        public IActionResult OnPostForgotUsername([FromBody] AccountForgotRequest request)
        {
            return new OkObjectResult(new
            {
                message = "forgot username",
                request
            });
        }
    }
}
(u) Moving the forgot password/username actions to the login code behind.

Register.cshtml.cs (2)

using System.Threading;
...
namespace WebUi.Pages.Account
{
    public class RegisterModel : PageModel
    {
        ...
        public IActionResult OnGetCheckUsernameAvailability(string username)
        {
            Thread.Sleep(2000);
            if (username == "aaaaaaaa")
            {
                return new BadRequestObjectResult(new
                {
                    Message = username
                });
            }
            return new OkObjectResult(new
            {
                Message = username
            });
        }
        ...
    }
}
(v) Moving the check username availability action to the register code behind.

Don't forget the anti-forgery token

  • WebUi
    • Startup.cs

We do not want to forget that we need to check for the anti-forgery token when a user performs a non-safe action such as a post request. Previously we were doing this by applying an attribute to any action that we wanted to check the token for before performing the action but there is a better way. We will apply the attribute globally in the startup class (w). Since the attribute is being applied globally we will be eliminating the risk that we forget to apply it when we need to and in addition to this it will automatically not be applied to idempotent actions such as get requests.

Startup.cs (1)

...
using Microsoft.AspNetCore.Mvc;
...
namespace WebUi
{
    public class Startup
    {
        public void ConfigureServices(...)
        {
            ...
            services.AddMvc(x =>
            {
                x.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
            });
        }
    }
}
(w) Adding the check for the anti-forgery token to non-idempotent actions globally.

Just to be sure the framework is still looking for our anti-forgery token we can temporarily remove it from our request headers and perform a login request. When we do this we see the server responds with a 400 bad request (x). Once we once again add the token to the request and resend it we receive a 200 ok response (y).

When we send in a request that does not contain the anti-forgery token
            in the header the server responds with a 400 bad request response.
(x) When we send in a request that does not contain the anti-forgery token in the header the server responds with a 400 bad request response.
When the token is add back to the headers the server responds with a
            200 ok response
(y) When the token is add back to the headers the server responds with a 200 ok response.

Razor page handler

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator-http.service.ts
      • services
        • http.service.ts

Now how do we tell the framework just what end point we are trying to reach? We do this by including a key value pair within the query string of our request. Because of this we will start by changing the get and post methods of our base http service (z). We will change the methods so that they take in an object with the required properties instead a comma delimited list of arguments, which we probably should have done originally anyway. In addition to all of the arguments that we originally had we will include an optional argument that we can use to specify the name of the handler that we want to to handle the request. If we specify the handler the base service will automatically include it within the query string of the request for both the get and post requests. With those changes we need to update our authenticator http service (aa).

http.service.ts (2)

...
import loDashIsNil = require("lodash/isNil");
...
interface IHttpRequest {
    data: Object;
    error: (response: Response) => void;
    finally?: (response: Response) => void;
    razorPageHandler?: string;
    success: (response: Response) => void;
    url: string;
}
...
export abstract class HttpService ... {
    ...
    protected get = (request: IHttpRequest) => {
        const data = request.data as any;
        if (loDashIsNil(request.razorPageHandler) === false) {
            data.handler = request.razorPageHandler;
        }
        this.doPromise(this.http.get(this.urlWithQueryString(request.url, data), this._requestOptions), request);
    }

    protected post = (request: IHttpRequest) => {
        const url = loDashIsNil(request.razorPageHandler)
            ? request.url
            : this.urlWithQueryString(request.url, { handler: request.razorPageHandler });
        this.doPromise(this.http.post(url, request.data, this._requestOptions), request);
    }
    
    private doPromise = (observable: Observable<Response>, request: IHttpRequest) => {
        observable
            .toPromise()
            .then((response: Response) => {
                request.success(response);

                if (loDashIsNil(request.finally) === false) {
                    request.finally(response);
                }
            })
            .catch((response: Response) => {
                this.processError(response);

                request.error(response);

                if (loDashIsNil(request.finally) === false) {
                    request.finally(response);
                }
            });
    }
    ...
}
(z) Modifying out base http service to allow us to more easily add arguments when needed. We are also adding a new argument that we can use to specify the name of action that we want to handle our request.

authenticator-http.service.ts (2)

...
export class AuthenticatorHttpService ... {
    private static readonly checkUsernameUrl = "/account/check-username-availability"; // <- Remove
    private static readonly forgotPasswordUrl = "/account/forgot-password"; // <- Remove
    private static readonly forgotUsernameUrl = "/account/forgot-username"; // <- Remove
    ...
    public checkUsernameAvailability = (...) => {
        this.get({
            data: { username: username },
            error: error,
            finally: $finally,
            razorPageHandler: "CheckUsernameAvailability",
            success: success,
            url: AuthenticatorHttpService.registerUrl
        });
    }

    public forgotPassword = (...) => {
        this.post({
            data: data,
            error: error,
            finally: $finally,
            razorPageHandler: "ForgotPassword",
            success: success,
            url: AuthenticatorHttpService.loginUrl
        });
    }

    public forgotUsername = (...) => {
        this.post({
            data: data,
            error: error,
            finally: $finally,
            razorPageHandler: "ForgotUsername",
            success: success,
            url: AuthenticatorHttpService.loginUrl
        });
    }

    public login = (...) => {
        this.post({
            data: data,
            error: error,
            finally: $finally,
            success: success,
            url: AuthenticatorHttpService.loginUrl
        });
    }

    public register = (...) => {
        this.post({
            data: data,
            error: error,
            finally: $finally,
            success: success,
            url: AuthenticatorHttpService.registerUrl
        });
    }
}
(aa) Updating our authenticator http service to use the changed get and post methods of our base http service.

Now that we can specify the handler when we send in a forgot password request the framework correctly binds it and returns it (bb) and does the same for a forgot username request (cc).

When we add the handler name ForgotPassword a
        request made to the login endpoint correctly gets directed to the OnPostForgotPassword action.
(bb) When we add the handler name ForgotPassword a request made to the login endpoint correctly gets directed to the OnPostForgotPassword action.
When we add the handler name ForgotUsername a
        request made to the login endpoint correctly gets directed to the OnPostForgotUsername action.
(cc) When we add the handler name ForgotUsername a request made to the login endpoint correctly gets directed to the OnPostForgotUsername action.
Exciton Interactive LLC
Advertisement