Fermer

décembre 7, 2022

Mettre à jour ClaimsIdentity d’Okta vers Optimizely

Mettre à jour ClaimsIdentity d’Okta vers Optimizely


Nous avions un projet pour un nouveau site Optimizely CMS 12, qui nécessitait une intégration SSO avec Okta. Je n’avais aucune expérience de la configuration de SSO avec CMS 12, j’ai donc commencé à lire de la documentation, des blogs et des messages de forum. La plupart de ce que j’ai trouvé concernait des versions antérieures du CMS et n’était plus directement applicable. Je n’ai pas trouvé grand-chose en ligne sur l’utilisation d’Okta pour l’authentification sur CMS 12.

J’ai finalement trouvé un message sur le forum qui contenait quelque chose qui m’a aidé à surmonter l’obstacle initial pour faire fonctionner l’authentification. (Merci à Ludovic Royer pour avoir posé la question et fourni un exemple de code aussi utile.

https://world.optimizely.com/forum/developer-forum/CMS/Thread-Container/2022/4/mixed-mode-okta-owin-authentication–optimizely-regular-login-for-cms-editadmin-section/

Je ne sais pas si toutes les réponses d’authentification Okta sont configurées de la même manière ou si elles peuvent être configurées (je soupçonnais ce dernier). Pourtant, pour mon implémentation, Okta renvoyait le nom commun (prénom et nom) en tant que valeur « Name » dans l’objet ClaimsIdentity.

Valeurs Claimsidentity de Chrome

Ce n’est généralement pas un problème. Cependant, lors de l’utilisation de ISynchonizingUserService pour synchroniser ClaimsIdentity, le CMS a utilisé la valeur « Name » comme nom d’utilisateur dans Optimizely. C’était un gros problème. Dans une grande organisation, il y a probablement plus d’un utilisateur avec le même prénom et le même nom.

Toute la documentation que j’ai trouvée indiquait que je pouvais remplacer cela en définissant la propriété « preferred_username » dans l’objet ClaimsIdentity. Mais, pour ma vie, je ne pouvais pas comprendre comment définir cette propriété dans mon objet ClaimsIdentity. La propriété était en lecture seule et non quelque chose que je pouvais mettre à jour.

La solution

Comme la plupart des bonnes solutions, cependant, celle-ci s’est avérée plutôt simple. J’ai proposé cette solution pour créer un nouvel objet ClaimsIdentity, définir les propriétés nécessaires et transmettre ce nouvel objet à la fonction SynchronizeAsync. Ces nouvelles valeurs sont ensuite entrées dans les tables tblSynchedUsers du CMS.

// Set name values into Identity claims for sync into Optimizely
private static ClaimsIdentity AddUserInfoClaims(ClaimsPrincipal claimsPrincipal)
{
    var authClaimsIdentity = new ClaimsIdentity(claimsPrincipal.Identity.AuthenticationType, CustomClaimNames.EpiUsername, ClaimTypes.Role);
   
    var name = claimsPrincipal.Claims.FirstOrDefault(x => x.Type == "name").Value;
    var email = claimsPrincipal.Claims.FirstOrDefault(x => x.Type == "email").Value;
    var preferredUsername = claimsPrincipal.Claims.FirstOrDefault(x => x.Type == "preferred_username").Value ?? email;

    var nameAry = name.Split(" ", StringSplitOptions.RemoveEmptyEntries);
    var givenName = nameAry[0];
    var surName = string.Empty;
    if (nameAry.Length == 2)
    {
        surName = nameAry[1];
    }
    else
    {
        surName = nameAry[2];
    }

    authClaimsIdentity.AddClaim(new Claim(ClaimTypes.Name, preferredUsername));
    authClaimsIdentity.AddClaim(new Claim(CustomClaimNames.EpiUsername, preferredUsername));
    authClaimsIdentity.AddClaim(new Claim(ClaimTypes.Email, email));
    authClaimsIdentity.AddClaim(new Claim(ClaimTypes.GivenName, givenName));
    authClaimsIdentity.AddClaim(new Claim(ClaimTypes.Surname, surName));

    return authClaimsIdentity;
}

Cette section suivante du code est le cheval de bataille de l’authentification, où j’ai lutté jusqu’à trouver le post du forum de Ludovic Royer. Je suis très reconnaissant que d’autres membres de la communauté partagent leur travail acharné.

public static IServiceCollection AddOktaAuthentication(this IServiceCollection services, IConfiguration configuration)
{
    ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
    Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = configuration.GetValue<bool>("Okta:ShowPII");

    services.ConfigureApplicationCookie(options =>
    {
        options.Cookie.HttpOnly = true;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    })
    .AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOktaMvc(new OktaMvcOptions()
    {
        OktaDomain = configuration.GetValue<string>("Okta:OktaDomain"),
        AuthorizationServerId = configuration.GetValue<string>("Okta:AuthorizationServerId"),
        ClientId = configuration.GetValue<string>("Okta:ClientId"),
        ClientSecret = configuration.GetValue<string>("Okta:ClientSecret"),
        PostLogoutRedirectUri = configuration.GetValue<string>("Okta:PostLogoutRedirectUrl"),
        GetClaimsFromUserInfoEndpoint = true,
        Scope = configuration.GetValue<string>("Okta:Scope").Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries).ToList(),
        OpenIdConnectEvents = new OpenIdConnectEvents
        {
            OnAuthenticationFailed = context =>
            {
                context.HandleResponse();
                context.Response.BodyWriter.WriteAsync(Encoding.ASCII.GetBytes(context.Exception.Message));
                return Task.FromResult(0);
            },
            OnTokenValidated = (ctx) =>
            {
                var redirectUri = new Uri(ctx.Properties.RedirectUri, UriKind.RelativeOrAbsolute);
                if (redirectUri.IsAbsoluteUri)
                {
                    ctx.Properties.RedirectUri = redirectUri.PathAndQuery;
                }

                // Update user Identity with minimal claim properties and roles
                var authClaimsIdentity = AddUserInfoClaims(ctx.Principal);
                authClaimsIdentity = AddRoleClaims(authClaimsIdentity, ctx.Principal);

                // Sync user and the roles to Optimizely CMS in the background
                ServiceLocator.Current.GetInstance<ISynchronizingUserService>().SynchronizeAsync(authClaimsIdentity);

                // replace the current Principal object with our new identity
                ctx.Principal = new ClaimsPrincipal(authClaimsIdentity);

                return Task.FromResult(0);
            },
            OnRedirectToIdentityProvider = context =>
            {
                // To avoid a redirect loop in the federation server, send 403 
                // when user is authenticated but does not have access
                if (context.Response.StatusCode == 401 && context.HttpContext.User.Identity.IsAuthenticated)
                {
                    context.Response.StatusCode = 403;
                    context.HandleResponse();
                }

                // XHR requests cannot handle redirects to a login screen, return 401
                if (context.Response.StatusCode == 401 && IsXhrRequest(context.Request))
                {
                    context.HandleResponse();
                }

                return Task.CompletedTask;
            },
        }
    });
}

Est-ce la solution la plus élégante ou la meilleure ? Probablement pas, et s’il y a une meilleure façon de le faire, s’il vous plaît partagez-la avec moi. Cependant, cette solution fonctionne rapidement, efficacement, sans aucun problème, et le client est satisfait. C’était le but premier.






Source link

décembre 7, 2022