Advertisement

#29 Defining a Canonical Url

In this article we will start by enabling ssl for our application. With that done we will define the requirements for a canonical url and enforce its usage by creating a canonical url middleware that we will add to the mvc pipeline. When that is working we will add the ability to redirect a user to a different page using our authenticator component. Finally we will make a few updates to the template, styling, and behaviour of our authenticator.

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

Enabling ssl

Now that we have decided that we want to use ssl in our project we need to enable it. To do this we just need to right click on our project and choose the properties option. When we select the properties a new window will appear and we need to select the 'Debug' tag. Located inside will be a checkbox that says 'Enable SSL' (a). When we check it we will see that Visual Studio has automatically chosen a port for us. We can change this value in just a minute if we want to.

Using the debug tab within the properties window to enable ssl for our project.
(a) Using the debug tab within the properties window to enable ssl for our project.

The canonical url

  • WebUi
    • Extensions
      • HttpContextExtensions.cs
    • Infrastructure
      • CanonicalUrlMiddleware.cs

As the title suggests this section is devoted to first defining the rules that we want our urls to follow and then making sure that they are followed. Our first step is to create an extension method (b) that will allow us to apply any rules that we want to a url in order to create our canonical url. The rules that we will be applying are: https always, all lowercase, and no trailing slash. With the rules in place we just need to create a canonical url middleware that we can inject into the pipeline (c). The middleware will simple check that the current url is the same as the canonical url and if it is will just invoke the next middleware and if it is not will redirect permanently to it.

HttpContextExtensions.cs (1)

using Microsoft.AspNetCore.Http;

namespace WebUi.Extensions
{
    public static class HttpContextExtensions
    {
        public static (bool, string) CanonicalUrl(this HttpContext context, int localhostSslPort)
        {
            var host = context.Request.Host.Host;
            var port = host == "localhost"
                ? localhostSslPort
                : context.Request.Host.Port ?? 0;
            var portString = port == 80 || port == 443
                ? string.Empty
                : $":{port}";
            var pathBase = context.Request.PathBase.HasValue
                ? context.Request.PathBase.Value
                : string.Empty;

            var isCanonical = context.Request.IsHttps;

            var path = pathBase + context.Request.Path.Value;
            var pathLower = path.ToLower();
            isCanonical = isCanonical && path == pathLower;

            if (pathLower != "/" && pathLower.EndsWith('/'))
            {
                isCanonical = false;
                pathLower = pathLower.TrimEnd('/');
            }

            var query = context.Request.QueryString.HasValue
                ? $"?{context.Request.QueryString.Value}"
                : string.Empty;

            const string httpsFormat = "https://{0}{1}{2}{3}";
            return (isCanonical, string.Format(httpsFormat, host, portString, pathLower, query));
        }
    }
}
(b) Extension methods that define a canonical url and determine if the current url is canonical.

CanonicalUrlMiddleware.cs (1)

using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using WebUi.Extensions;

namespace WebUi.Infrastructure
{
    public class CanonicalUrlMiddleware
    {
        private readonly int _localhostSslPort;
        private readonly RequestDelegate _next;

        public CanonicalUrlMiddleware(RequestDelegate next, int localhostSslPort)
        {
            _next = next;
            _localhostSslPort = localhostSslPort;
        }

        public async Task Invoke(HttpContext context)
        {
            (var isCanonical, var url) = context.CanonicalUrl(_localhostSslPort);
            if (isCanonical == false)
            {
                context.Response.Redirect(url, true);
            }

            await _next(context);
        }
    }
}
(c) Middleware that can be used to enforce the use of canonical urls.

Adding the canonical middleware to the pipleline

  • WebUi
    • Properties
      • launchSettings.json
    • Startup.cs

When we first created our project the template used by Visual Studio also created a folder called 'Properties' which contains a file called 'launchSettings.json' automatically for us (d). When we checked the box to enable SSL a new entry containing the SSL port number was added to it. We will read the value from this file in order to pass it to our canonical url middleware. To add the middleware to the pipeline we just need to add it to the Startup.cs file (e). Here we are using a configuration builder to read the launchSettings.json file and extract the SSL port number and passing it into the call to the UseMiddleware method. We are also adding a couple of settings to our services so that urls are generated according to the rules that we specified previously.

launchSettings.json

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:51571/",
      "sslPort": 44357
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "WebUi": {
      "commandName": "Project",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "http://localhost:51573/"
    }
  }
}
(d) We will use the launch settings file to retrieve the value of the SSL port so that we can pass it into the canonical url middleware.

Startup.cs (1)

using System.IO;
...
namespace WebUi
{
    public class Startup
    {
        ...
        public void ConfigureServices(...)
        {
            ...
            services.AddRouting(x =>
            {
                x.LowercaseUrls = true;
                x.AppendTrailingSlash = false;
            });
        }

        public void Configure(...)
        {
            var localhostSslPort = 0;
            if (env.IsDevelopment())
            {
                ...
                localhostSslPort = new ConfigurationBuilder()
                    .SetBasePath(Directory.GetCurrentDirectory())
                    .AddJsonFile("Properties/launchSettings.json")
                    .Build()
                    .GetSection("iisSettings")
                    .GetSection("iisExpress")
                    .GetValue<int>("sslPort");
            }
            ...
            app.UseMiddleware<CanonicalUrlMiddleware>(localhostSslPort);
            ...
        }
    }
}
(e) Here we are specifying that urls generated by Asp.Net be lowercase without a trailing slash. We are also adding the canonical url middleware to the pipeline.

Redirecting our authenticator to another url

  • WebUi
    • Pages
      • Account
        • Register.cshtml.cs
        • Register-Email-Sent.cshtml
    • Source
      • components
        • authenticator
          • authenticator.component.ts
          • send-screen.component.ts

The reason that we have spent this time dealing with urls is due to the fact that when a user makes a successful request to login, register, forgot password or forgot username we want to respond with a url that our authenticator should use to redirect the page to. The first step that we will take to implement this behaviour is to create a page that we can redirect to when the user makes a successful register request (f). Now that the page is made we will modify the register post method to return the url (g).

Register-Email-Sent.cshtml (1)

@page

@{
    ViewBag.Title = "Confirmation Email Sent";
}

<div>
    <h1>
        Confirmation
    </h1>
    <p>
        Thank you for registering. Please check your email for a confirmation
        request with a link that will complete setting up your account. Once you click the
        link, your registration will be complete.
    </p>
    <p>
        If you have not received the email shortly please check your spam folder.
    </p>
</div>
(f) The view that we will show when the user makes a successful register request.

Register.cshtml.cs (1)

...
namespace WebUi.Pages.Account
{
    public class RegisterModel : PageModel
    {
        ...
        public IActionResult OnPostAsync(...)
        {
            return new OkObjectResult(new
            {
                url = Url.Page("/Account/Register-Email-Sent", null, null, "https")
            });
        }
        ...
    }
}
(g) We need to modify the post method to return the url to the page that we wish to redirect the user to.
Advertisement

Now when we make a successful registration request we should receive a url back that looks like (h).

Image showing the url that is being sent back by the server
            when a user successfully registers.
(h) Image showing the url that is being sent back by the server when a user successfully registers.

Now that the server is sending back a url on successful requests we need to do something with it. The way that we are going to handle it is by modifying the success, and even though it doesn't necessarily effect it, and the failure methods of our send screen component (i). With those changes made we need to update our authenticator component (j).

send-screen.component.ts (1)

...
interface IAnimateTimer {
    onHidden?: () => void;
    onTimerExpired?: () => void;
}

interface ISuccessOrFailure extends IAnimateTimer{
    message: string;
    url?: string;
}

export class SendScreenComponent ... {
    ...
    public failure = (failure: ISuccessOrFailure) => {
        ...
        this.setMessage(failure.message, backgroundClass);
        ...
        mainAnimation.onfinish = () => {
            ...
            supAnimation.onfinish = () => {
                this.animateTimer({
                    onHidden: this.compose(() => {
                            this.xmarkMainDOM.style.stroke = "none";
                            this.xmarkSupDOM.style.stroke = "none";

                            this._backgroundDOM.classList.remove(backgroundClass);
                            this._messageDOM.classList.remove(backgroundClass);
                        },
                        failure.onHidden),
                    onTimerExpired: failure.onTimerExpired
                });
            };
        };
    }
    ...
    public hide = (onhidden?: () => void) => {
        ...
        animation.onfinish = () => {
            ...
            if (loDashIsNil(onhidden) === false) {
                onhidden();
            }
        };
    }
    ...
    public success = (success: ISuccessOrFailure) => {
        ...
        this.setMessage(success.message, ...);
        ...
        animation.onfinish = () => {
            this.animateTimer({
                onHidden: this.compose(() => {
                        this._checkmarkDOM.style.stroke = "none";

                        this._backgroundDOM.classList.remove(backgroundClass);
                        this._messageDOM.classList.remove(backgroundClass);
                    },
                    success.onHidden),
                onTimerExpired: this.compose(() => {
                        if (loDashIsNil(success.url) === false) {
                            window.location.href = success.url;
                        }
                    },
                    success.onTimerExpired)
            });
        };
    }
    ...
    private animateTimer = (animateTimer?: IAnimateTimer) => {
        ...
        animation.onfinish = () => {
            ...
            if (loDashIsNil(animateTimer) === false) {
                if (loDashIsNil(animateTimer.onTimerExpired) === false) {
                    animateTimer.onTimerExpired();
                }
                if (loDashIsNil(animateTimer.onHidden) === false) {
                    this.hide(animateTimer.onHidden);
                }
            } else {
                this.hide();
            }
        };
    }
    ...
    private compose = (f1: () => void, f2?: () => void) => {
        return () => {
            f1();
            if (loDashIsNil(f2) === false) {
                f2();
            }
        }
    }
    ...
}
(i) Modifying the way that our send screen accepts and then processes both success and failure calls.

authenticator.component.ts (1)

...
import { Response } from "@angular/http";
...
export class AuthenticatorComponent ... {
    ...
    private forgotPassword = () => {
        this._authenticatorHttpService.forgotPassword(
            ...,
            (response: Response) => {
                this.redirectOnSuccess(response);
            },
            () => {
                ...
                this._sendScreen.failure({
                    message: "Error using the provided email."
                });
            });
    }

    private forgotUsername = () => {
        this._authenticatorHttpService.forgotUsername(
            ...,
            (response: Response) => {
                this.redirectOnSuccess(response);
            },
            () => {
                ...
                this._sendScreen.failure({
                    message: "Error using the provided email."
                });
            });
    }
    ...
    private login = () => {
        ...
        this._authenticatorHttpService.login(
            ...,
            (response: Response) => {
                this.redirectOnSuccess(response);
            },
            () => {
                ...
                this._sendScreen.failure({
                    message: "Error using the provided username and password."
                });
            });
    }
    ...

    private redirectOnSuccess = (response: Response) => {
        const json = response.json();
        this._sendScreen.success({
            message: "Request successful you will be redirected shortly.",
            url: json.url
        });
    }

    private register = () => {
        this._authenticatorHttpService.register(
            ...,
            (response: Response) => {
                this.redirectOnSuccess(response);
            },
            () => {
                ...
                this._sendScreen.failure({
                    message: "Error using the provided information."
                });
            });
    }
    ...
}
(j) We of course need to update any calls made to the send screen success and failure methods.

Make it easy and use it everywhere

  • WebUi
    • Extensions
      • UrlHelperExtensions.cs
    • Pages
      • Account
        • Login.cshtml.cs
        • Register.cshtml.cs

Now that we know that everything is working and we are also going to need to generate https urls to our razor pages it's time for another extension method (k). This method just makes it easier to generate an https url given a razor page name. With the extension method made it's time to use it in both our login (k) and register pages (l).

UrlHelperExtensions.cs (1)

using Microsoft.AspNetCore.Mvc;

namespace WebUi.Extensions
{
    public static class UrlHelperExtensions
    {
        public static string HttpsPage(this IUrlHelper url, string pageName, string pageHandler = null, object values = null)
        {
            return url.Page(pageName, pageHandler, values, "https");
        }
    }
}
(k) Small extension method that makes it easier to generate a https url for our razor pages.

Register.cshtml.cs (2)

...
using WebUi.Extensions;
...
namespace WebUi.Pages.Account
{
    public class RegisterModel : PageModel
    {
        ...
        public IActionResult OnPostAsync(...)
        {
            return new OkObjectResult(new
            {
                url = Url.HttpsPage("/Account/Register-Email-Sent")
            });
        }
        ...
    }
}
(l) Using the https extension method to generate the urls for our register page.

Login.cshtml.cs (1)

...
namespace WebUi.Pages.Account
{
    public class LoginModel : PageModel
    {
        ...
        public IActionResult OnPost(...)
        {
            ...
            return new OkObjectResult(new
            {
                url = Url.HttpsPage("/Index")
            });
        }
        
        public IActionResult OnPostForgotPassword(...)
        {
            return new OkObjectResult(new
            {
                url = Url.HttpsPage("/Account/Forgot-Password")
            });
        }

        public IActionResult OnPostForgotUsername(...)
        {
            return new OkObjectResult(new
            {
                url = Url.HttpsPage("/Account/Forgot-Username")
            });
        }
    }
}
(m) Using the https extension method to generate the urls for our login page.

Updating the authenticator

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.pug
          • authenticator.component.scss
          • authenticator.component.ts
      • forms
        • form.controller.ts

With everything so far seemingly to work the way we want it to it's time to cleanup our authenticator's files a bit. We will start by simply removing the comments preventing the overflow from being hidden (n) and change the inputs for the password and confirm password from text to password (o). Next we will add a little helper method within our form controller class for resetting a form control back to its default value (p). And finally updating the authenticator itself to once gain disable the send button if the form is invalid and update the behaviour of our controls when moving between a control and its corresponding confirm control (j).

authenticator.component.scss (1)

...
.authenticator {
    ...
    overflow: hidden; <- Remove the comment
    ...
}
(n) Once again hiding any overflow outside of the authenticator.

authenticator.component.pug (1)

...
div.authenticator
    div.inputs-container#inputs-container
        h3 ...
        form.inputs(...)
            ...
            div.dual-input-group
                +formControlGroup(..., "password", ...)
                +formControlGroup(..., "password", ...)
            ...
(o) Time to change the password and password confirm inputs to password instead of text.

form.controller.ts (1)

...
export class FormController ... {
    ...
    public resetControl = (name: string) => {
        const control = this.getControl(name);
        control.reset(this._defaults[name]);
    }
    ...
}
(p) We are going to need a way to reset an individual control to its default state.

authenticator.component.ts (1)

...
export class AuthenticatorComponent ... {
    ...
    public isButtonDisabled = (key: string) => {
        switch (key) {
            case this.buttonSendKey: <- Remove the comment
                return this.authForm.invalid; <- Remove the comment
            default:
                return false;
        }
    }
    ...
    private nextControl = (key: string) => {
        switch (key) {
            case this.controlUsernameKey:
                ...
            case this.controlPasswordKey:
                if (this.isLogin) {
                    ...
                } else if (this.isRegister) {
                    if (...) {
                        if (this.controlPasswordConfirm.value === this.controlPassword.value) {
                            if (this.controlPasswordConfirm.invalid) {
                                this.controlPasswordConfirm.updateValueAndValidity({ onlySelf: true });
                            }
                        } else {
                            this._formController.resetControl(this.controlPasswordConfirmKey);
                        }
                        ...
                    } else {
                        ...
                    }
                }
                break;
            case this.controlPasswordConfirmKey:
                ...
            case this.controlEmailKey:
                if (...) {
                    if (this.controlEmailConfirm.value === this.controlEmail.value) {
                        if (this.controlEmailConfirm.invalid) {
                            this.controlEmailConfirm.updateValueAndValidity({ onlySelf: true });
                        }
                    } else {
                        this._formController.resetControl(this.controlEmailConfirmKey);
                    }
                    ...
                } else {
                    ...
                }
                break;
            case this.controlEmailConfirmKey:
                ...
            case this.controlRememberMeKey:
                ...
        }
    }
    ...
}
(j) Once again we are disabling the send button if the form is invalid and we are updating the behaviour when the user moves between a control and its corresponding confirm control.
Exciton Interactive LLC
Advertisement