Going real world with Episerver, Auth0 and Azure AD

Federated Security is what we all want to use for our Episerver sites right? In such a setup, having Auth0 as the identity platform has for sure put a big smile on my face.

I hope by now that most Epi devs are on top of setting up a site to use federated security. It's really easy.

A while back David Knipe posted about using Auth0 with Episerver. I've pretty much followed his basics.

The connections I use are Facebook, Google, MSA, Auth0 username-password database and Azure AD. The Auth0 docs have really good walkthroughs of how to set up the apps within each service so I'll let you find those by yourself.

My rules

Auth0 rules

In the first rule I call the Azure AD Graph API to look for the ImmutableId which according to sources should be the best thing to use. It usually survives both dir sync and users changing name or e-mail. I've added a feature request with Auth0 to include immutableId as a standard claim but anyway and for now this illustrates the power of having server side Javascript access to the auth pipeline.

I borrowed most of the code from this post about about Custom Provisioning in Auth0 docs section. In my case I do a GET request to a Graph API endpoint and look for the immutableId (look for the line context.immutable_id = fetched_user.immutableId).

function (user, context, callback) {
  var AAD_EMAIL_SUFFIX = '@netrelations.com;
  var AAD_TENAANT_ID = 'example-0000-0000-0000-000000000';
  var AAD_TENANT_NAME = 'yourname.onmicrosoft.com';
  var AAD_CLIENT_ID = 'example-0000-0000-0000-0000';
  var AAD_CLIENT_SECRET = 'example4545454=';
  var AAD_USER_CREATE_WAIT = 15000;

  if (user.tenantid === AAD_TENAANT_ID && ends_with(user.upn, AAD_EMAIL_SUFFIX)) {
    // Check if the user was already processed
    user.app_metadata = user.app_metadata || {};

    if (user.app_metadata.waad_immutable_id) {
        return continue_with_azuread_user();
    }

    // Get the token.
    get_azuread_token(function(err, token) {
      if (err) return callback(err);

      read_azuread_user(token, user, function(err) {
        if (err) return callback(err);

        console.log('Done! Storing info in user profile.');

        // Update the user.
        user.app_metadata.waad_immutable_id = context.immutable_id;
        auth0.users.updateAppMetadata(user.user_id, user.app_metadata)
        .then(function() {
          // Wait a little bit, it takes some time before the user is created.
          setTimeout(function() {
            return continue_with_azuread_user();
          }, AAD_USER_CREATE_WAIT);
        })
        .catch(function(err) {
          console.log('Error updating user profile:', err);
          return callback(err);
        });
      });
    });
  }
  else {
    return callback(null, user, context);
  }

  function ends_with(str, suffix) {
    return str.indexOf(suffix, str.length - suffix.length) !== -1;
  }

  function read_azuread_user(token, user, cb) {
    var options = {
      url: 'https://graph.windows.net/' + AAD_TENANT_NAME + '/users/' + user.upn + '?api-version=1.5',
      headers: {
        'Content-type': 'application/json',
        'Authorization': 'Bearer ' + token
      }
    };

    console.log('Reading user in Azure AD:');
    request.get(options, function(err, res, body) {
      if(err) {
        console.log('Error reading user in Azure AD.', err);
        return cb(err);
      }

      if (body.error) {
        console.log('Error reading user in Azure AD.', body.error_description);
        return cb(new Error(body.error_description));
      }

      console.log('User read!');

      var fetched_user = JSON.parse(body);
      context.immutable_id = fetched_user.immutableId;

      return cb(null);
    });
  }

  /*
   * Continue the login...
   */
  function continue_with_azuread_user() {
    user.app_metadata = user.app_metadata || {};
    user.waad_immutable_id = user.app_metadata.waad_immutable_id;
    console.log('Logging in user:', {
      upn: user.upn,
      immutable_id: user.immutable_id
    });
    return callback(null, user, context);
  }

  /*
   * Get the token for Azure AD.
   */
  function get_azuread_token(cb) {
    var options = {
      url: 'https://login.windows.net/' + AAD_TENANT_NAME + '/oauth2/token?api-version=1.5',
      headers: {
        'Content-type': 'application/json',
      },
      json: true,
      form: {
        client_id: AAD_CLIENT_ID,
        client_secret: AAD_CLIENT_SECRET,
        grant_type: 'client_credentials',
        resource: 'https://graph.windows.net'
      },
    };

    console.log('Getting token for Azure AD...');

    request.post(options, function(err, res, body) {
      if(err) {
        console.log('Error getting token for Azure AD.', err);
        return cb(err);
      }

      if (body.error) {
        console.log('Error getting token for Azure AD.', body.error_description);
        return cb(new Error(body.error_description));
      }

      console.log('Token received:', body.access_token);
      return cb(null, body.access_token);
    });
  }
}

In the second rule we do a filtering of AD roles and copy them over to a new property using a similar setup as David does in his post about roles and access. With Azure AD this means from user.groups to user.roles.

I guess most of us have seen installations with a trillion role names from the client's AD where typically only a few are actually used within Epi, this is the place to get rid of that noise. It's as you can see also very easy to give yourself some hardcoded admin role which can be nice.

Finally I reset the claim used as the identity name in my Epi config to be the most consistent and long-lived Auth0 property.

function (user, context, callback) {
  if (user.waad_immutable_id) {
    user.name = 'waad|' + user.waad_immutable_id;
  }
  else {
    user.name = user.user_id;
  }

  callback(null, user, context);
}

Signing in

Auth0 lock screen When using the standard login page you have configured your Azure AD connection in Auth0 to get activated for certain e-mail host names. When an address like this is entered the password box (if username-password connection is active) will be removed and replaced by a SSO enabled message and when moving along the user will end up on the organizations regular sign in page and be redirected back from Auth0 to the app matching the realm.

Having Auth0 in the middle like this opens up for some very useful features. One is that you can utilize their development apps for all connections. For example if you just want to check what the flow and claims of a Facebook sign in looks in your application it's just a couple of clicks away. No need to create a Facebook app for yourself just to test.

Another thing is that you can use the same connections for all your environments including your local environment (they typically all sit as an app in the same Auth0 account). Sign in with your AD/Facebook/Google/Microsoft/etc account on your localhost is a no brainer and to make things even nicer Auth0 comes with a Sign in as user in the selected app feature which I'm thinking will save huge amounts of time in future support and troubleshooting cases. In this screenshot I've only configured three of my site's enviroments but of course an app can be anything you build or buy that support any of the protocols Auth0 does.

Auth0 sign in as

Published and tagged with these categories: Episerver, ASP.NET, Development