Log in to sitecore 9 editor using Okta provider

Sitecore 9 Okta provider

How to implement federated authentication on sitecore 9 to allow content editors log in to sitecore using their okta accounts.

Versions used: Sitecore Experience Platform 9.0 rev. 171219 (9.0 Update-1).

Update/Warning:
Okta has now introduced “OIDC Compliant” which does a GET redirect when user clicks on the Okta app (okta dashboard) instead of POST as it happens when user clicks on the login with okta button from sitecore.`.

This post is a continuation of my previous post. Instead of logging visitors in using federated authentication, in this post I’ll show how to implement Okta authentication to log in to sitecore editor. It’s an udpated version of my sitecore 8 okta post. Source code here.

The good news is things have changed and the implementation is easier and shorter than before. I won’t be going into details on how to configure the application in Okta because it hasn’t changed much since last time. Click here for previous configuration.
2 key settings:

Please see my previous post or download here to enable required settings and add default services.

Okta middleware/provider implementation

The following code is based on the sample provided by Okta.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class OktaIdentityProvider : IdentityProvidersProcessor
{
protected override string IdentityProviderName => "Okta";
private const string ClientId = "your clientid here";
private const string ClientSecret = "your ClientSecret here";
private const string Authority = "your okta site URL here";
private const string OauthTokenEndpoint = "/oauth2/v1/token";
private const string OauthUserInfoEndpoint = "/oauth2/v1/userinfo";
private const string OAuthRedirectUri = "http://sc9xp0.sc/identity/externallogincallback";
private const string OpenIdScope = OpenIdConnectScope.OpenIdProfile + " email";

protected IdentityProvider IdentityProvider { get; set; }

public OktaIdentityProvider(FederatedAuthenticationConfiguration federatedAuthenticationConfiguration)
: base(federatedAuthenticationConfiguration)
{
}

protected override void ProcessCore(IdentityProvidersArgs args)
{
Assert.ArgumentNotNull(args, "args");
IdentityProvider = this.GetIdentityProvider();
var options = new OpenIdConnectAuthenticationOptions
{
ClientId = ClientId,
ClientSecret = ClientSecret,
Authority = Authority,
RedirectUri = OAuthRedirectUri,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
Scope = OpenIdScope,
AuthenticationType = IdentityProvider.Name,
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name"
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = ProcessAuthorizationCodeReceived,
RedirectToIdentityProvider = n =>
{
// If signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType == Microsoft.IdentityModel.Protocols.OpenIdConnectRequestType.LogoutRequest )
{
var idTokenClaim = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenClaim != null)
n.ProtocolMessage.IdTokenHint = idTokenClaim.Value;
}
return Task.CompletedTask;
}
}
};
args.App.UseOpenIdConnectAuthentication(options);
}

private async Task ProcessAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
// Exchange code for access and ID tokens
var tokenClient = new TokenClient(Authority + OauthTokenEndpoint, ClientId, ClientSecret);
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(notification.Code, notification.RedirectUri);
if (tokenResponse.IsError)
throw new Exception(tokenResponse.Error);

var userInfoClient = new UserInfoClient(Authority + OauthUserInfoEndpoint);
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
var claims = new List<Claim>();
claims.AddRange(userInfoResponse.Claims);
claims.Add(new Claim("id_token", tokenResponse.IdentityToken));
claims.Add(new Claim("access_token", tokenResponse.AccessToken));

if (!string.IsNullOrEmpty(tokenResponse.RefreshToken))
claims.Add(new Claim("refresh_token", tokenResponse.RefreshToken));

notification.AuthenticationTicket.Identity.AddClaims(claims);
notification.AuthenticationTicket.Identity.ApplyClaimsTransformations(new TransformationContext(this.FederatedAuthenticationConfiguration, IdentityProvider));
}
}

We first create our OpenIdConnectAuthenticationOptions object and set the settings required by the middleware to know who to communicate to and Okta’s settings so it allows requests from our application.

AuthenticationType is very important because, after authentication is triggered, a 401 status code will be set to the response and each owin middleware will inspect and decide if it should action the request by matching its AuthenticationType.

Remember Scope defines the claims we are requesting from Okta. We are setting it to the enum OpenIdConnectScope.OpenIdProfile plus email. Sitecore requires email by default. See OpenId specification for more info on scope values. ResponseType determines the authorization processing flow to be used. See OpenId specification for more info on scope authentication request.

/identity/externallogincallback is the callback URL sitecore creates to process external logins after they have been authenticated on the providers.

Our AuthorizationCodeReceived task ProcessAuthorizationCodeReceived is making sure the token is valid and retrieving claims to be added to the context. It uses sitecore’s ApplyClaimsTransformations helper to map claims according to configuration set in config file.

Okta provider configuration

The configuration is pretty much the same as the previous post so I won’t go into any details. You can get it here. Do not forget to include main configuration file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
<sitecore role:require="Standalone or ContentDelivery or ContentManagement">
<pipelines>
<owin.identityProviders>
<processor type="Sitecore9SSO.Pipelines.IdentityProviders.OktaIdentityProvider, Sitecore9SSO" resolve="true" />
</owin.identityProviders>
</pipelines>

<federatedAuthentication type="Sitecore.Owin.Authentication.Configuration.FederatedAuthenticationConfiguration, Sitecore.Owin.Authentication">

<identityProvidersPerSites hint="list:AddIdentityProvidersPerSites">
<mapEntry name="editors" type="Sitecore.Owin.Authentication.Collections.IdentityProvidersPerSitesMapEntry, Sitecore.Owin.Authentication">
<sites hint="list">
<site>shell</site>
<site>admin</site>
</sites>

<identityProviders hint="list:AddIdentityProvider">
<identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='Okta']" />
</identityProviders>

<externalUserBuilder type="Sitecore.Owin.Authentication.Services.DefaultExternalUserBuilder, Sitecore.Owin.Authentication">
<param desc="isPersistentUser">false</param>
</externalUserBuilder>
</mapEntry>
</identityProvidersPerSites>

<identityProviders hint="list:AddIdentityProvider">
<identityProvider id="Okta" type="Sitecore.Owin.Authentication.Configuration.DefaultIdentityProvider, Sitecore.Owin.Authentication">
<param desc="name">$(id)</param>
<param desc="domainManager" type="Sitecore.Abstractions.BaseDomainManager" resolve="true" />
<caption>Log in with Okta</caption>
<icon>/assets/okta.png</icon>
<domain>sitecore</domain>
<transformations hint="list:AddTransformation">

<transformation name="map role to idp" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
<sources hint="raw:AddSource">
<claim name="idp" value="Okta" />
</sources>
<targets hint="raw:AddTarget">
<claim name="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" value="sitecore\Developer" />
</targets>
<keepSource>true</keepSource>
</transformation>

<transformation name="fullname" type="Sitecore.Owin.Authentication.Services.DefaultTransformation,Sitecore.Owin.Authentication">
<sources hint="raw:AddSource">
<claim name="name" />
</sources>
<targets hint="raw:AddTarget">
<claim name="FullName" />
</targets>
</transformation>

</transformations>
</identityProvider>
</identityProviders>

</federatedAuthentication>
</sitecore>
</configuration>

====================
References:
https://coding.abel.nu/2014/06/understanding-the-owin-external-authentication-pipeline/
https://dzone.com/articles/understanding-owin-external
http://openid.net/specs/openid-connect-core-1_0.html
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-openid-connect-code


Please let me know what you think and/or if you can spot any errors.
/eom

Share