United Kingdom: +44 (0)208 088 8978

SAFE Authentication with Azure Active Directory – Part 2

In part one, Ryan showed how we can secure our API using Active Directory. Now in part two he explains how to secure the application itself, and redirect unauthenticated users to a login flow.

In the first part of this blog, we looked at setting up authentication with Azure Active Directory and integrating it into a simple SAFE application.

We covered setting up the Active Directory app registration and securing our API endpoints, so that if a user isn't logged in they return a 401 Unauthorised response.

This time we will go a step further, and secure access to our entire Elmish application. If a user is not logged in, they will be redirected to a login page.

It will require us to make a few changes to the out-of-the-box SAFE webpack and FAKE scripts, which is a great opportunity to learn a bit more about how they work.

Routing

The first step we want to take is to route all requests from the webpack dev server to our application.

If you haven't heard of webpack before, it can do quite a few things, such as

  • bundle and minify your app's web resources (styles, scripts, plugins etc)
  • build your client application
  • during development, it can serve your client side app. This allows it to perform hot reloading when you make changes to the source code.

You will find the webpack config file at the root of your SAFE app solution, named webpack.config.js.

By default, the webpack dev server is configured to pass all api routes to the app server, but to serve the app page itself.

However, want the server to receive the app request so that it can pass it through the authentication middleware.

We need to change

devServerProxy: {
    // redirect requests that start with /api/ to the server on port 8085
    '/api/**': {

to

devServerProxy: {
    // redirect all requests to the server on port 8085
    '**': {

Here's where we hit a small issue that could catch you out (it certainly did me!). Even though we are now forwarding all requests to the app server, webpack treats index as a special case, and still serves up the application.

I tried a number of workarounds for this, and the only solution I found was to rename all index references to app.

In the webpack file, we need to change

var CONFIG = {
    appHtmlTemplate: './src/Client/index.html',
//...

to

var CONFIG = {
    appHtmlTemplate: './src/Client/app.html',
//...

and

var commonPlugins = [
    new HtmlWebpackPlugin({
        filename: 'index.html',
//...

to

var commonPlugins = [
    new HtmlWebpackPlugin({
        filename: 'app.html',
//...

Next, you will need to open the Client project in your solution explorer and rename index.html to app.html.

Finally, we can open the Server project and update our routes to handle the request.

I am assuming at this point that you have followed through the first part of this blog and so are already working with the Server code I showed there, including having installed Microsoft.Identity.Web and enabled auth etc. I will repeat one thing though - due to restrictions on cross-site cookies requiring secure transport and the way SAFE runs with the webpack dev server, authentication during local development has issues on Chromium-based browsers. It has no issues when actually deployed to Azure however. As a workaround you can either use Firefox locally or selectively disable auth when in a dev environment as I have shown below.

let authScheme = "AzureAD"

let isDevelopment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") = Environments.Development;

let noAuthenticationRequired nxt ctx = task { return! nxt ctx }

let authChallenge : HttpFunc -> HttpContext -> HttpFuncResult =
    if isDevelopment then
       noAuthenticationRequired
    else
        requiresAuthentication (Auth.challenge authScheme)

let routes =
    choose [
        route "/" >=> authChallenge >=>  htmlFile "public/app.html"
    ]

Content

Ok, so we have routed the app requests to the .NET app server. The trouble is, during development the webpack dev server is usually serving the pages, so it doesn't copy the app to the .NET server's public folder after it builds it.

To change this, we need to make another edit to webpack.config.js, updating the build output directory to point at the Server's public folder during development.

Move the line

// If we're running the webpack-dev-server, assume we're in development mode
var isProduction = !process.argv.find(v => v.indexOf('webpack-dev-server') !== -1);

above the CONFIG block and then change

var CONFIG = {
    // ... other webpack config settings
    outputDir: './deploy/public',
    // ...

to

var CONFIG = {
    // ... other webpack config settings
    outputDir: isProduction ? './deploy/public' : './src/Server/public',
    // ...

We also need to add writeToDisk to the dev server config:

    devServer: {
        // ...other dev server config settings
        writeToDisk: true
    },

Assets

Nearly there! The webpack build above won't copy static assets to the server's public folder.

To achieve this, we need to make a small edit to our FAKE build script.

At the top, add the following paths:

let serverPublicPath = Path.getFullName "./src/Server/public"
let clientPublicPath = Path.getFullName "./src/Client/public"

Next, update the Clean target like so:

Target.create "Clean" (fun _ ->
    Shell.cleanDir deployDir
    Shell.cleanDir serverPublicPath)

Finally, add the following command to the Run target:

Target.create "Run" (fun _ ->
    Shell.copyDir serverPublicPath clientPublicPath FileFilter.allFiles
    //... other commands

Testing it out

Providing you have correctly set everything up, you should now be able to test the login process out.

Remember, you will need to use a non-Chrome browser if you are working locally, such as Firefox, due to the cookie issue mentioned earlier.

It is a good idea to open a private browsing window, to make sure that you don't already have a logged in account etc.

If you are having issues, check that you have

  • properly set up your app's Active Directory registration in Azure
  • added the required AD configuration to your Server in appsettings.json, including the login / logout callback urls you set in the AD registration.

With my app settings like this:

{
    "AzureAd": {
        "Instance": "https://login.microsoftonline.com/",
        "TenantId": "3EF504DF-AABF-41D7-A516-0B969D76A5F4",
        "ClientId": "1DF152E0-79A0-4295-8D8C-27D49EF21627",
        "CallbackPath": "/api/login-callback",
        "SignedOutCallbackPath": "/api/logout-callback"
    }
}

my AD app reg looks like this:

AD reg

Conclusion

I hope this has been an interesting insight into some of the parts that make up a SAFE stack application, and helped you get up and running with authentication.

It is quite possible that you may think of a better way to handle some of the challenges I faced getting it all running, such as the Chrome cookie origin issue and webpack grabbing the index route automatically.

If so then let us know on Twitter and I will update this post!

There is a sample app to go with this blog post, which you can find here. You will of course need to update the Tenant and Client Ids etc. to match your own app registration.