Piranha.core: Add Security module for Azure AD

Created on 1 May 2018  路  12Comments  路  Source: PiranhaCMS/piranha.core

Create security module for Azure AD so that the CMS can be easily integrated into an existing user base

enhancement help wanted module

Most helpful comment

i've also been trying to authenticate with azure ad b2c and i have this working.

by working i mean

  • anonymous get /manager redirects to /microsoftidentity/account/signin
  • azure ad b2c challenge is displayed, with identity provided by social. logins and azure ad
  • people who aren't in a specifically configured set of azure ad tenants cannot authenticate to /manager - this means the social logins don't get to authenticate at /manager even though if they type /manager they will get an auth challenge
  • on. login from an approved azure ad tenant the manager interface is displayed
  • identitywithseed is being used, to hack around dependency injection issues if there is no identity service registered. this means that in certain circumstances you do see the identitywithseed challange despite my efforts to date. but it's only visible if you're logged in to an approved azure ad tenant. the problem with the identity interface as i see it is the username/password on the interface definition. the impetus. for doing this is to keep passwords out of the database

how i did it

  • if anyone responds here i will tell you. other than that i will publish a blog and update this with the link later. i expect you know i will see you hitting /manager and in the azure risky behaviour analytics
  • essentially i forwarded challenges, loginurls for cookie auth to the oidc scheme registered by microsoftidentity
  • also i wrote an asp.net core iclaimstransformer that supplies the full set of admin claims to approved users. therefore this solution is incomplete, in that there is no mapping mechanism between azure rbac (https://docs.microsoft.com/en-us/azure/role-based-access-control/overview) (b2c groups are a special case https://mrochon.azurewebsites.net/2019/05/06/using-groups-in-azure-ad-b2c/) and piranha rbac- for that matter, i hardcoded the approved. domains, so there. obviously. a production attempt would read such things from asp.net core configuration

a proposed first cut of a solution architecture that requires no code changes to piranha

IMG_0346

the full gist for the claims transformer is here https://gist.github.com/vigouredelaruse/f462a35c3ff5b8028868fd56ea5f9f3d

    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        if (principal.Identity.IsAuthenticated)
        {
            var p = principal.Identity as ClaimsIdentity;

            var matches = p.Claims.Where(w => w.Value.ToLower().Contains("wizardcontroller.com")).Count();
            var claimCount = p.Claims.Where(c => c.Value.ToLower().Contains("PiranhaAdmin")).Count();

            if (matches > 0 && claimCount <= 1)
            {
                EnsureAdminClaimsForAdminPrincipal(principal);
            }
        }
        return Task.FromResult(principal);
    }

and you register it in startup.cs like this

        services.AddSingleton<IClaimsTransformation, ClaimsTransformer>();

as you can see my proof of concept has the claims mapping logic hardcoded to my specific situation but it's not quite generalized enough for me to deploy. the pieces in the architecture diagram aren't all there, and i'm not quite sure yet the diagram has all the required components. but it's a first cut

additionally i currently have

        // Middleware setup
        app.UsePiranha(options => {
            options.UseManager();
            options.UseTinyMCE();
            options.UseIdentity();

        });

and this bogus rewrite setup that's not actually doing anything useful yet

        var options = new RewriteOptions()
                    //
                    .AddRedirect("'.*login.*'", "/")
                    .AddRewrite(@".*login", "/",
                        skipRemainingRules: true);

for additional service configuration we have

        services.AddAuthentication(options =>
        {
            options.DefaultScheme = "OpenIdConnect";
            options.DefaultChallengeScheme = "OpenIdConnect";
            options.DefaultAuthenticateScheme = "OpenIdConnect";
            options.DefaultSignInScheme = "OpenIdConnect";
        });

        services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAdB2C");

and swinging the hammer to kill the fly we have

        services.Configure<CookieAuthenticationOptions>(
            AzureADDefaults.CookieScheme, options =>
            {
                options.LoginPath = "/microsoftidentity/account/signin";
                options.LogoutPath = "/microsoftidentity/account/signout";
                options.ForwardAuthenticate = "OpenIdConnect";
                options.ForwardChallenge = "OpenIdConnect";
                options.ForwardSignIn = "OpenIdConnect";
                options.ForwardSignOut = "OpenIdConnect";
            });

you'll need sauce that smells similar to this bogus code

        services.Configure<JwtBearerOptions>(
        AzureADDefaults.JwtBearerAuthenticationScheme, options =>
        {

            options.TokenValidationParameters.ValidateIssuer = true;
            options.TokenValidationParameters.ValidIssuers = new[]
            {
                $"https://login.microsoftonline.com/tenant/v2.0"
            };

        });

and your version of this

       // as per https://www.josephguadagno.net/2020/06/26/connecting-to-an-api-protected-by-microsoft-identity-platform
        services.AddControllersWithViews(options =>
        {

            //var policy = new AuthorizationPolicyBuilder()
            //    .RequireAuthenticatedUser()
            //    .Build();
            //options.Filters.Add(new AuthorizeFilter(policy));

        }).AddMicrosoftIdentityUI();

        // services.AddRazorPages();

        // as per https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-sign-user-app-configuration?tabs=aspnetcore
        services.AddRazorPages().AddMvcOptions(options =>
        {

            //var policy = new AuthorizationPolicyBuilder()
            //              .RequireAuthenticatedUser()
            //              .Build();
            //options.Filters.Add(new AuthorizeFilter(policy));
        }).AddMicrosoftIdentityUI();

and the piece duh resistance

      // Service setup
        services.AddPiranha(options =>
        {
            options.AddRazorRuntimeCompilation = true;

            options.UseBlobStorage(Configuration.GetConnectionString("blobstorage"));
            options.UseImageSharp();
            options.UseManager();
            options.UseTinyMCE();
            options.UseMemoryCache();
            options.UseEF<SQLServerDb>(db =>
                db.UseSqlServer(Configuration.GetConnectionString("piranha")));

            options.UseIdentityWithSeed<IdentitySQLServerDb>(db =>
                db.UseSqlServer(Configuration.GetConnectionString("piranha")),
                identityOpts =>
                {

                },
                cookieOpts =>
                {
                    cookieOpts.LogoutPath = "/microsoftidentity/account/signout";
                    cookieOpts.LoginPath = "/microsoftidentity/account/signin";
                    cookieOpts.ForwardAuthenticate = "OpenIdConnect";
                    cookieOpts.ForwardChallenge = "OpenIdConnect";
                    cookieOpts.ForwardSignIn = "OpenIdConnect";
                    cookieOpts.ForwardSignOut = "OpenIdConnect";
                    cookieOpts.ForwardDefault = "OpenIdConnect";
                });

            options.LoginUrl = "/microsoftidentity/Account/Signin";

for me code complete would approach a solution that doesn't use identitywithseed, and has zero users in the database. for this proof of concept i only have that first admin user created during piranha install, but i won't go to market like that

obviously for piranha with tens of thousands of downloads you might want to expand. the reach to https://auth0.com/

so hopefully you're able to duplicate and better my results for all to consume because the current issue with this setup is

  • i've been unable to hijack the identitywithseed challenge after you logout from the manager (the solution may lie with Policy Schemes as per https://docs.microsoft.com/en-us/aspnet/core/security/authentication/policyschemes?view=aspnetcore-3.1)
  • if you don't use identity with seed then there are signoutmodel dependency injection errors when you log out of the manager
  • because you likely don't want users bruteforcing /manager because of this https://www.zdnet.com/article/wordpress-deploys-forced-security-update-for-dangerous-bug-in-popular-plugin/. you thus have to hide it behind the azure auth challenge, less than ideal. but i have other code to work on today

All 12 comments

Is this enhancement for Azure AD + Piranha Identity?

I haven't had the time to investigate it yet, but the idea is to.

  1. Implement the ISecurity to authenticate users against Azure AD
  2. If possible, implement admin views to view available users

This isn't really my area of expertise so I need to do some investigation on how it's done. Also the users from the AD needs to be connected with the claims they need for the Piranha Manager, not really sure if this kind of thing can be administered from the Azure AD or if you actually need a local Identity user handling these things and you only use the AD for login.

Do you have any experience in integrating Azure AD with .NET Core?

Yes, I've integrated Piranha with Azure AD as well. Since you have the manager set up with claims it's relatively straightforward if you augment the claims within startup code. Mapping custom claims in Azure AD is a pain so I would avoid it. Ideally those claims would be augmented by the roles assigned in the manager. I haven't gotten that far yet but would love to help.

Well I haven't got that much experience with any of this :) How does it work, are you using the Identity module but with a custom ISecurity implementation that authenticates with Azure AD. Then if the user is authenticated you map it to a local Identity user, and if it doesn't exist you create it? Are you using the standard login view for the manager or are you redirecting to a single-sing-on view from Azure AD?

Regards

I followed these steps to setup WSFederation using Azure AD. https://docs.microsoft.com/en-us/aspnet/core/security/authentication/ws-federation?view=aspnetcore-2.1#use-ws-federation-without-aspnet-core-identity. So the basics it really had nothing to do with Piranha. I'll fork your examples repo and submit a PR for you to review as a basic example.

Hi again @lestersconyers Do you have some code regarding this that you might be willing to share? Got a question regarding this from another user 馃檪

Hey I鈥檓 evaluating using piranha CMS as our new code base for a public website, but it is kind of a requirement to use azure ad for the manager area, if you could share some example code I would pick it from there.
Thanks

I need support for IdentityServer. Both Azure AD and IdentityServer are OAuth2, so there shouldn't be much of a difference. I'll try it out in my fork and share if I get it up and running. (Also need multi-tenancy BTW, as mentioned in #1132 and related, so I'll see how far I get using Piranha.)

@eloekset We really don鈥檛 have full multi tenancy planned as this is currently not in the roadmap.

Best regards

Yes, I saw that. I will have to live in a fork and keep it updated from your master branch. My first goal is OpenID/OAuth2 with IdSrv/Azure AD, and then it shouldn't be too hard to extend all storage with an AccountID or TenantID.

I've finally had some time to work on the Azure AD module again, but I'm struggling with an issue that is difficult to figure out, so I created a discussion over in the aspnetcore repo and hope to get some help by someone: https://github.com/dotnet/aspnetcore/discussions/24690

i've also been trying to authenticate with azure ad b2c and i have this working.

by working i mean

  • anonymous get /manager redirects to /microsoftidentity/account/signin
  • azure ad b2c challenge is displayed, with identity provided by social. logins and azure ad
  • people who aren't in a specifically configured set of azure ad tenants cannot authenticate to /manager - this means the social logins don't get to authenticate at /manager even though if they type /manager they will get an auth challenge
  • on. login from an approved azure ad tenant the manager interface is displayed
  • identitywithseed is being used, to hack around dependency injection issues if there is no identity service registered. this means that in certain circumstances you do see the identitywithseed challange despite my efforts to date. but it's only visible if you're logged in to an approved azure ad tenant. the problem with the identity interface as i see it is the username/password on the interface definition. the impetus. for doing this is to keep passwords out of the database

how i did it

  • if anyone responds here i will tell you. other than that i will publish a blog and update this with the link later. i expect you know i will see you hitting /manager and in the azure risky behaviour analytics
  • essentially i forwarded challenges, loginurls for cookie auth to the oidc scheme registered by microsoftidentity
  • also i wrote an asp.net core iclaimstransformer that supplies the full set of admin claims to approved users. therefore this solution is incomplete, in that there is no mapping mechanism between azure rbac (https://docs.microsoft.com/en-us/azure/role-based-access-control/overview) (b2c groups are a special case https://mrochon.azurewebsites.net/2019/05/06/using-groups-in-azure-ad-b2c/) and piranha rbac- for that matter, i hardcoded the approved. domains, so there. obviously. a production attempt would read such things from asp.net core configuration

a proposed first cut of a solution architecture that requires no code changes to piranha

IMG_0346

the full gist for the claims transformer is here https://gist.github.com/vigouredelaruse/f462a35c3ff5b8028868fd56ea5f9f3d

    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        if (principal.Identity.IsAuthenticated)
        {
            var p = principal.Identity as ClaimsIdentity;

            var matches = p.Claims.Where(w => w.Value.ToLower().Contains("wizardcontroller.com")).Count();
            var claimCount = p.Claims.Where(c => c.Value.ToLower().Contains("PiranhaAdmin")).Count();

            if (matches > 0 && claimCount <= 1)
            {
                EnsureAdminClaimsForAdminPrincipal(principal);
            }
        }
        return Task.FromResult(principal);
    }

and you register it in startup.cs like this

        services.AddSingleton<IClaimsTransformation, ClaimsTransformer>();

as you can see my proof of concept has the claims mapping logic hardcoded to my specific situation but it's not quite generalized enough for me to deploy. the pieces in the architecture diagram aren't all there, and i'm not quite sure yet the diagram has all the required components. but it's a first cut

additionally i currently have

        // Middleware setup
        app.UsePiranha(options => {
            options.UseManager();
            options.UseTinyMCE();
            options.UseIdentity();

        });

and this bogus rewrite setup that's not actually doing anything useful yet

        var options = new RewriteOptions()
                    //
                    .AddRedirect("'.*login.*'", "/")
                    .AddRewrite(@".*login", "/",
                        skipRemainingRules: true);

for additional service configuration we have

        services.AddAuthentication(options =>
        {
            options.DefaultScheme = "OpenIdConnect";
            options.DefaultChallengeScheme = "OpenIdConnect";
            options.DefaultAuthenticateScheme = "OpenIdConnect";
            options.DefaultSignInScheme = "OpenIdConnect";
        });

        services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAdB2C");

and swinging the hammer to kill the fly we have

        services.Configure<CookieAuthenticationOptions>(
            AzureADDefaults.CookieScheme, options =>
            {
                options.LoginPath = "/microsoftidentity/account/signin";
                options.LogoutPath = "/microsoftidentity/account/signout";
                options.ForwardAuthenticate = "OpenIdConnect";
                options.ForwardChallenge = "OpenIdConnect";
                options.ForwardSignIn = "OpenIdConnect";
                options.ForwardSignOut = "OpenIdConnect";
            });

you'll need sauce that smells similar to this bogus code

        services.Configure<JwtBearerOptions>(
        AzureADDefaults.JwtBearerAuthenticationScheme, options =>
        {

            options.TokenValidationParameters.ValidateIssuer = true;
            options.TokenValidationParameters.ValidIssuers = new[]
            {
                $"https://login.microsoftonline.com/tenant/v2.0"
            };

        });

and your version of this

       // as per https://www.josephguadagno.net/2020/06/26/connecting-to-an-api-protected-by-microsoft-identity-platform
        services.AddControllersWithViews(options =>
        {

            //var policy = new AuthorizationPolicyBuilder()
            //    .RequireAuthenticatedUser()
            //    .Build();
            //options.Filters.Add(new AuthorizeFilter(policy));

        }).AddMicrosoftIdentityUI();

        // services.AddRazorPages();

        // as per https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-sign-user-app-configuration?tabs=aspnetcore
        services.AddRazorPages().AddMvcOptions(options =>
        {

            //var policy = new AuthorizationPolicyBuilder()
            //              .RequireAuthenticatedUser()
            //              .Build();
            //options.Filters.Add(new AuthorizeFilter(policy));
        }).AddMicrosoftIdentityUI();

and the piece duh resistance

      // Service setup
        services.AddPiranha(options =>
        {
            options.AddRazorRuntimeCompilation = true;

            options.UseBlobStorage(Configuration.GetConnectionString("blobstorage"));
            options.UseImageSharp();
            options.UseManager();
            options.UseTinyMCE();
            options.UseMemoryCache();
            options.UseEF<SQLServerDb>(db =>
                db.UseSqlServer(Configuration.GetConnectionString("piranha")));

            options.UseIdentityWithSeed<IdentitySQLServerDb>(db =>
                db.UseSqlServer(Configuration.GetConnectionString("piranha")),
                identityOpts =>
                {

                },
                cookieOpts =>
                {
                    cookieOpts.LogoutPath = "/microsoftidentity/account/signout";
                    cookieOpts.LoginPath = "/microsoftidentity/account/signin";
                    cookieOpts.ForwardAuthenticate = "OpenIdConnect";
                    cookieOpts.ForwardChallenge = "OpenIdConnect";
                    cookieOpts.ForwardSignIn = "OpenIdConnect";
                    cookieOpts.ForwardSignOut = "OpenIdConnect";
                    cookieOpts.ForwardDefault = "OpenIdConnect";
                });

            options.LoginUrl = "/microsoftidentity/Account/Signin";

for me code complete would approach a solution that doesn't use identitywithseed, and has zero users in the database. for this proof of concept i only have that first admin user created during piranha install, but i won't go to market like that

obviously for piranha with tens of thousands of downloads you might want to expand. the reach to https://auth0.com/

so hopefully you're able to duplicate and better my results for all to consume because the current issue with this setup is

  • i've been unable to hijack the identitywithseed challenge after you logout from the manager (the solution may lie with Policy Schemes as per https://docs.microsoft.com/en-us/aspnet/core/security/authentication/policyschemes?view=aspnetcore-3.1)
  • if you don't use identity with seed then there are signoutmodel dependency injection errors when you log out of the manager
  • because you likely don't want users bruteforcing /manager because of this https://www.zdnet.com/article/wordpress-deploys-forced-security-update-for-dangerous-bug-in-popular-plugin/. you thus have to hide it behind the azure auth challenge, less than ideal. but i have other code to work on today
Was this page helpful?
0 / 5 - 0 ratings

Related issues

aneff-official picture aneff-official  路  3Comments

stefanolsenn picture stefanolsenn  路  4Comments

i-love-code picture i-love-code  路  3Comments

aneff-official picture aneff-official  路  6Comments

aneff-official picture aneff-official  路  4Comments