#29 Defining a Canonical Url
Friday, April 20, 2018
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.
Parts
- Part 30: User Database
- Part 29: Canonical Url
- Part 28: Razor Page Handlers
- Part 27: Send Screen Finished
- Part 26: Stroke Dashoffset
- Part 25: Send Screen
- Part 24: Forgot Link Behaviour
- Part 23: Animating Controls (cont.)
- Part 22: Animating Controls
- Part 21: Hiding Control Groups
- Part 20: ModelState Errors (cont.)
- Part 19: ModelState Errors
- Part 18: Animating Info Pages (cont.)
- Part 17: Animating Info Pages
- Part 16: Keyboard Navigation
- Part 15: Accessing the DOM
- Part 14: All About the Username
- Part 13: CSRF Attacks
- Part 12: Http Requests
- Part 11: HMR
- Part 10: Color Inputs And Buttons
- Part 9: Animating Sub-Actions
- Part 8: Form Validation (cont.)
- Part 7: Form Validation
- Part 6: Form Group
- Part 5: Authenticator Validators
- Part 4: Authenticator Inputs
- Part 3: First Angular Component
- Part 2: Webpack
- Part 1: Beginning
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.

The canonical url
-
WebUi
-
Extensions
- HttpContextExtensions.cs
-
Infrastructure
- CanonicalUrlMiddleware.cs
-
Extensions
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));
}
}
}
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);
}
}
}
Adding the canonical middleware to the pipleline
-
WebUi
-
Properties
- launchSettings.json
- Startup.cs
-
Properties
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/"
}
}
}
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);
...
}
}
}
Redirecting our authenticator to another url
-
WebUi
-
Pages
-
Account
- Register.cshtml.cs
- Register-Email-Sent.cshtml
-
Account
-
Source
-
components
-
authenticator
- authenticator.component.ts
- send-screen.component.ts
-
authenticator
-
components
-
Pages
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>
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")
});
}
...
}
}
Now when we make a successful registration request we should receive a url back that looks like (h).

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();
}
}
}
...
}
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."
});
});
}
...
}
Make it easy and use it everywhere
-
WebUi
-
Extensions
- UrlHelperExtensions.cs
-
Pages
-
Account
- Login.cshtml.cs
- Register.cshtml.cs
-
Account
-
Extensions
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");
}
}
}
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")
});
}
...
}
}
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")
});
}
}
}
Updating the authenticator
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.pug
- authenticator.component.scss
- authenticator.component.ts
-
authenticator
-
forms
- form.controller.ts
-
components
-
Source
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
...
}
authenticator.component.pug (1)
...
div.authenticator
div.inputs-container#inputs-container
h3 ...
form.inputs(...)
...
div.dual-input-group
+formControlGroup(..., "password", ...)
+formControlGroup(..., "password", ...)
...
form.controller.ts (1)
...
export class FormController ... {
...
public resetControl = (name: string) => {
const control = this.getControl(name);
control.reset(this._defaults[name]);
}
...
}
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:
...
}
}
...
}