JSS and OKTA: Blog 2 – Login via embedded OKTA Form (August RELEASE)

This blog post is based on the OKTA samples @ https://developer.okta.com/quickstart-fragments/angular/default-example/ and the OKTA sign in widget that can be integrated into the page.

The branch mentioned above contains a working example that I will now run through in the rest of this blog. Once up and running your should get a login embedded into our JSS application that allows us to login via OKTA.


Overview

Firstly lets have a quick run through of the changes we made to the first blog in order to integrate the widget.

  1. OKTA configuration settings
    • These are stored in a settings file in the root of the JSS application: jss-okta-config.json
    • All settings to do with our OKTA instance can be added in here.
  2. Create the placeholder component to contain the widget
    • In JSS we scaffold up a new component
    • src\app\components\okta-sign-in\okta-sign-in.component.ts
    • Have a look at https://github.com/TomTyack/jssokta/blob/feature/okta-sign-in-widget/JSS/src/app/components/okta-sign-in/okta-sign-in.component.ts
      1. ngOnInit()
        1. Note how the code dynamically imports the OKTA sign in widget.
        • import('@okta/okta-signin-widget')
        • This is necessary as the library contains a reference to window. Which will break our Server Side code if imported normaly.
        • So in order to workaround this flaw we dynamically import the library only after verifying that this code is running client side.
      2. detectTranslationLoading()
        • Wait for the dictionary service to be loaded so that we can inject the Dictionary into the form.
      3. bootupSignin()
        • Initialise the OKTA widget configuration, inject labels and URLS
      4. injectWidgetPhase()
        • Render the widget
    • This contains the widget code and the has a matching from end HTML template in Angular.
      • The HTML file contains the following important tag
      •  <div id=”okta-signin-container”></div>
  3. Add the component to the /login route
  4. Adjust the navigation to include a new button for the widget
  5. Dictionary additions

Demo Video

To show you a video of this JSS example in action take a quick gander at the following video:


Demo Installation

  1. Clone the Repository
  2. Deploy the application. Follow the same instructions from the first blog. <<< UPDATE LINK
  3. Run the application. Follow the same instructions from the first blog. <<< UPDATE LINK
  1. Click on the “Login Embedded” link in the navigation
  2. Login to OKTA (if not already) using the details you registered with.
  3. You should arrive back on the profile page
  4. undefined
  5. Success!! hopefully ­čÖé
  6. Inside the OKTA Dashboard is a handy log that shows all login activity. This is a great way to see what is going on. Screenshot shown at the bottom.
    • undefined

Summary

That concludes the run through of how to integrate the OKTA Angular SDK and Embedded Widget into Sitecore JSS. OKTA is a leader in user authentication management and having the ability to integrate into our JSS applications is an exciting prospect. I hope this example is of use to you and your teams if your considering the same technology mix.


JSS and OKTA: Blog 1 – Login via External OKTA Form

This blog post is based on the OKTA samples @ https://developer.okta.com/quickstart-fragments/angular/default-example/

The OKTA samples makes use of the OKTA Angular SDK and allows you to set up a development OKTA cloud instance for testing the code.

For this blog I have integrated the OKTA example into the Angular JSS starter repository to for a new repository to accompany this blog. https://github.com/TomTyack/jssokta

This repository contains a working example that I will now run through in the rest of this blog. All you need to do is sign up for your own developer sandbox (OKTA) instance using a dummy user and test it out.


Overview

Firstly lets have a quick run through of the changes we made to the original examples in order to integrate them with the angular JSS application.

  1. OKTA configuration settings
  2. Provide the Login and Logout Buttons
  3. Create the Callback Handler
    • The callback handles integration in JSS takes place inside: src\app\routing\routing.module.ts
    • JSS Example: https://github.com/TomTyack/jssokta/blob/master/JSS/src/app/routing/routing.module.ts
    • In this case I made the route: /implicitcallback
    • Importantly the route must be added at the top of the list so that JSS routing doesn’t hijack the route before our OKTA module gets a chance. I spent a little while scratching my head over this one when I originally added it the bottom of the route config.
  4. Update your NgModule
    • This requires a little bit of adjustment from the original example.
    • Module integration is done via: src\app\app.module.ts
  5. Use the Access Token

Demo Video

To show you a video of this JSS example in action take a quick gander at the following video:

Note: In the video I started off the demo by running in Disconnected mode with localhost. Surprisingly this worked and it redirected back to the connected app domain. In reality a better test would have been to start on the same domain in integrated mode. I’m not sure that you could run this test end to end in disconnected.

Demo Installation

  1. Clone the Repository
  2. Sign up for an OKTA developer account
    • https://developer.okta.com/signup/
    • When it asks you what sort of application you want just click “Do this later”.
    • Confirm your email address and fill out the security questions and change your temporary password.
  3. In the top navigation withing the OKTA Dashboard
  4. Back in the JSS OKTA Repository (open it in VS Code or your editor of choice)
    • From the command line run: npm install
    • Open the file: jss-okta-config.json (in the root of the JSS Application)
      • issuer: Swtich out “INSERT-OKTA-ID” with the relevant ID from the same domain your viewing the OKTA portal in.
      • redirectUri: Replace ‘jssokta’ with ‘jss.okta.portal’ or whichever domain you set.
      • clientId: Found on the application > General tab. About 20 characters long.
  5. IIS
    • OKTA requires us to be running a domain with https, as such it makes it difficult to test this out in Disconnected mode.
    • Setup a Sitecore instance and make sure JSS is installed.
    • Add your new domain to the local hosts file and make sure its mapped to 127.0.0.1
    • Add the new domain to your Sitecore instance in IIS and make sure it capable of HTTPS. Use the developer certificate as a default.
  6. Back in the JSS OKTA Repository
    • Got to sitecore\config\JSSOkta.config
      • hostName: jss.okta.portal
    • From the command line run: JSS Deploy app
      • run through the setup as you would any JSS application so that it would connect with your Sitecore instance.
      • Sample config from my tests: See scjssconfig.json at the bottom of this blog
    • From the command line run: JSS Deploy config
    • From the command line run (again): JSS Deploy app -c -d
  7. Test out the deployed application:
    • Navigate to your domain: example: https://jss.okta.portal/ (must have SSL)
    • If prompted about the SSL security warning proceed and ignore. “Proceed to jss.okta.portal (unsafe)”
    • undefined
    • Click on the “Login” link in the navigation
    • Login to OKTA (if not already) using the details you registered with.
    • You should arrive back on the profile page
    • undefined
    • Success!! hopefully ­čÖé
    • Inside the OKTA Dashboard is a handy log that shows all login activity. This is a great way to see what is going on. Screenshot shown at the bottom.
      • undefined

Summary

That concludes the run through of how to integrating the OKTA Angular SDK in Sitecore JSS. OKTA is a leader in user authentication management and having the ability to integrate into our JSS applications is an exciting prospect. I hope this example is of use to you and your teams if your considering the same technology mix.


OKTA Setup Screenshots and Sample Config

I have included screenshots of all my OKTA settings below. As I know this can be difficult to diagnose at times.


scjssconfig.json

{
  "sitecore": {
    "instancePath": "C:\\inetpub\\wwwroot\\test.dev.local",
    "apiKey": "{FB95B118-E04F-4D5B-9465-01AE804A2F5A}",
    "deploySecret": "r6bnvuhv13beudhix1lxnej2ci38u0i6kxhnpy22i6ps",
    "deployUrl": "http://jss.okta.portal/sitecore/api/jss/import",
    "layoutServiceHost": "https://jss.okta.portal"
  }
}

Sitecore membership guide

join1Below is a core list of commonly used authentication and authorization methods to act like a quick reference guide for people building membership based Sitecore solutions.

This reference is intended to get you on the right track immediately from the one source.

If anyone has anything they would like me to add to the list please comment below and I’ll update it accordingly.

Using

These are the required references for the following examples:

using System;
using System.Collections.Generic;
using System.Linq;
using Sitecore;
using Sitecore.Security;
using Sitecore.Security.Accounts;
using System.Web.Security;
using Sitecore.Security.Authentication;

Retrieving Users

//Get all users regardless of domain, Sitecore, extranet, custom etc:
public IEnumerable<User> GetUsers()
{
return UserManager.GetUsers();
}

//Get all users for a particular domain, in most situations this will be of more use then getting all users:
public IEnumerable<User> GetUsers(string domainName)
{
return UserManager.GetUsers().Where(user => user.Domain != null && user.Domain.Name == domainName);
}

//Get a user from a custom property. This can be useful to find a user by some unique value.
public User GetUserFromCustomField(string fieldName, string fieldValue)
{
return UserManager.GetUsers().FirstOrDefault(user => user.Profile.GetCustomProperty(fieldName) == fieldValue);
}

//Get a collection of users from a custom property. An example might be to get all male users.
public IEnumerable<User> GetUsersFromCustomField(string fieldName, string fieldValue)
{
return UserManager.GetUsers().Where(user => user.Profile.GetCustomProperty(fieldName) == fieldValue);
}

//Again with queries like this it is most likely that filtering by domain name will be more useful then searching all users.
public User GetUserFromCustomField(string fieldName, string fieldValue, string domainName)
{
return UserManager.GetUsers().FirstOrDefault(user => user.Profile.GetCustomProperty(fieldName) == fieldValue && user.Domain.Name == domainName);
}

public IEnumerable<User> GetUsersFromCustomField(string fieldName, string fieldValue, string domainName)
{
return UserManager.GetUsers().Where(user => user.Profile.GetCustomProperty(fieldName) == fieldValue && user.Domain.Name == domainName);
}

//Get user by user name, it must include the domain.
public User GetUserByUserName(string domainName, string userName)
{
return User.FromName(String.Format(@"{0}\{1}", domainName, userName), false);
}

//Get user by email, first get the username for the email then get user by username.
public User GetUserByEmail(string domainName, string email)
{
var userName = Membership.GetUserNameByEmail(email);
return !String.IsNullOrEmpty(userName) ? GetUserByUserName(userName, domainName) : null;
}

//Check if a user exists
public bool DoesUserExist(string domainName, string userName)
{
return User.Exists(String.Format(@"{0}\{1}", domainName, userName));
}

//Get the current context user. This will by default be extranet\Anonymous if no one is logged in.
public User GetCurrentUser()
{
return Context.User;
}

Users Roles

public  IEnumerable<Role> GetUsersRoles(UserProfile profile)
{
return profile.ProfileUser.Roles;
}

public void AddRoleToUser(UserProfile profile, string roleName, string domainName)
{
profile.ProfileUser.Roles.Add(RoleHelper.GetRole(domainName, roleName));
profile.Save();
}

public void RemoveRoleFromUser(UserProfile profile, string roleName, string domainName)
{
profile.ProfileUser.Roles.Remove(RoleHelper.GetRole(domainName, roleName));
profile.Save();
}

public bool IsUserInRole(User user, string role)
{
return user != null && user.IsInRole(role);
}

User CRUD

public void AddUser(string domainName, string userName, string password, string email)
{
Membership.CreateUser(String.Format(@"{0}\{1}", domainName, userName), password, email);
}

public string GetUsersCutomProperty(User user, string fieldName)
{
return user.Profile.GetCustomProperty(fieldName);
}

//Update the Users default sitecore and custom fields
public void UpdateUser(UserProfile profile, string email, string fullName, string customFieldName, string customFieldValue)
{
profile.Email = email;
profile.FullName = fullName;
profile.SetCustomProperty(customFieldName, customFieldValue);
profile.Save();
}

public void DeleteUser(User user)
{
user.Delete();
}

Is authenticated, is administrator

//Method not required but here for completeness
public bool IsUserAuthenticated(User user)
{
return user.IsAuthenticated;
}

//Method not required but here for completeness
public bool IsUserAdministrator(User user)
{
return user.IsAdministrator;
}

Change and reset users password

public void ChangeUsersPassword(string userName, string currentPassword, string newPassword)
{
Membership.Provider.ChangePassword(userName, currentPassword, newPassword);
}

public static string ResetUsersPassword(string userName)
{
var membershipUser = Membership.GetUser(userName);
return membershipUser != null ? membershipUser.ResetPassword() : string.Empty;
}

Authentication

//login without password
public void LoginUser(string domainName, string userName)
{
AuthenticationManager.Login(string.Format(@"{0}\{1}", domainName, userName), false);
}

//default login
public void LoginUser(string domainName, string userName, string password)
{
AuthenticationManager.Login(string.Format(@"{0}\{1}", domainName, userName), password, false);
}

public void LogOutUser()
{
AuthenticationManager.Logout();
}

Roles

//Roles are seperate per domain
public Role GetRole(string domainName, string roleName)
{
return Role.FromName(string.Format(@"{0}\{1}", domainName, roleName));
}

public IEnumerable<string> GetRoles()
{
return Roles.GetAllRoles();
}

public IEnumerable<string> GetRoles(string domainName)
{
return Roles.GetAllRoles().Where(r => r.Contains(domainName));
}