question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Improve Single Sign-On Compatibility

See original GitHub issue

I’m in charge of a web application with the a microservices-style architecture with an authentication service implemented on IdentityServer4 and an Angular 2+ UI. We have need to add new features which require content management, and I selected Piranha CMS for the job based on it’s clean API, large selection of example implementations (Angular!), and apparent development activity compared to competitors.

My goal is to package the management UI and content API into a single ASP.NET Core MVC service with the identity provider (IdentityServer4) providing identity and authentication for both the manager and the API. It’s common in a microservices architecture to keep one service with one database per data domain and force all data interaction for that domain to pass through the one service.

I have it mostly working at this point, but the one sticking point in the framework at this point is ISecurity. It appears that the manager is designed to expect users to enter their credentials directly into the built-in login page instead of allowing the possibility of redirecting to an external IDp, and this is manifested in the ISecurity interface’s SignIn method. At first I simply avoided implementing ISecurity, but I soon discovered that it’s an important component of logging out the user when they click the logout button. Since I want that to work, I’ll have to implement ISecurity. Now I’m considering implementing the SignOut method and simply throwing a NotSupportedException in the SignIn method. I’m not 100% sure what the solution would be, but I’m proposing that the Piranha framework should be changed a little bit to accommodate external authentication better by loosening or eliminating the assumption that a username and password will be supplied directly within Piranha’s login page. I imagine that this change would also help with #429 .

If it’s decided that this is worth implementing, I would be happy to help do the work and create a PR, but at this moment, I’m unsure of the best solution to the problem, and I would like input.

For anyone who stumbles across this later, I would like to share my solution for getting Piranha working with IdentityServer4, bypassing Piranha manager’s login screen:

I created two authorization policies in the Piranha project: one to control the manager and one to control the APIs. This is likely what you’ll want, because you likely have different requirements for content managers versus content consumers (your users). Then I added a convention to select the right policy based on which controller was being accessed.

Adding authorization policies In Startup.ConfigureServices:

services.AddAuthorization(options =>
{
    options.AddPolicy("managerpolicy", b =>
    {
        b.RequireAuthenticatedUser();
    });

    //if you require your content consumers to be logged in to view content,
    //add a policy for the API
    //options.AddPolicy("apipolicy", b =>
    //{
    //    b.RequireAuthenticatedUser();
    //    b.AuthenticationSchemes = new List<string> { JwtBearerDefaults.AuthenticationScheme };
    //});
});

Creating a new convention class to apply the correct policy based on whether the bound controller is in the “manager” area or not:

public class AddAuthorizeFiltersControllerConvention : IControllerModelConvention
{
    public void Apply(ControllerModel controller)
    {
        if (controller.RouteValues.Any()
            && controller.RouteValues.TryGetValue("area", out string area)
            && area.Equals("manager", StringComparison.OrdinalIgnoreCase))
        {
            controller.Filters.Add(new AuthorizeFilter("managerpolicy"));
        }
        //if you require your content consumers to be logged in to view content,
        //add a policy for the API
        //else
        //{
        //    controller.Filters.Add(new AuthorizeFilter("apipolicy"));
        //}
    }
}

Applying the convention in Startup.ConfigureServices:

services.AddMvc(config =>
{
    config.Conventions.Add(new AddAuthorizeFiltersControllerConvention());

    config.ModelBinderProviders.Insert(0, new Piranha.Manager.Binders.AbstractModelBinderProvider());
});

You’ll also need to provide authentication configuration for both policies to use. Right now, I’m still working on having the MVC (cookies) and API (bearer) schemes working correctly side-by-side, so I’ll just give some example code for cookies authentication only:

services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = "Cookies";
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie("Cookies")
    .AddOpenIdConnect("oidc", options =>
    {
        options.Authority = "http://localhost:49508/";
        options.RequireHttpsMetadata = false;

        options.ClientId = "cmsmanager";
        options.SaveTokens = true;
    });

The other thing to consider is permissions and user claims. I’m choosing to keep permissions information within the services where they are relevant, so when my managers log in, the IdentityServer doesn’t know anything about Piranha claims, and therefore they are not automatically included on the ClaimsPrinciple on log in. To load user claims from the local database after receiving the identity info from the IdentityServer, you’ll have to create a new class that inherits from ClaimAction:

public class RetrieveLocalClaimsAction : ClaimAction, IDisposable
{
    public RetrieveLocalClaimsAction() : base(null, null)
    {
        var options = new DbContextOptionsBuilder<CmsDbContext>();
        _dbContext = new CmsDbContext(options.UseSqlServer(GetConnectionString()).Options);
    }

    private readonly CmsDbContext _dbContext;

    public override void Run(JObject userData, ClaimsIdentity identity, string issuer)
    {
        string userId = identity.FindFirst("sub").Value;

        IEnumerable<Claim> claims = _dbContext.UserRoles
            .Where(ur => ur.UserId == userId)
            .Join(_dbContext.Roles, ur => ur.RoleId, r => r.Id, (ur, r) => r)
            .Join(_dbContext.RoleClaims, r => r.Id, rc => rc.RoleId, (r, rc) => rc)
            .ToArray()
            .Select(rc => rc.ToClaim());

        identity.AddClaims(claims);
    }

    public void Dispose()
    {
        _dbContext?.Dispose();
    }

    private static string GetConnectionString()
    {
        IConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json");

        IConfigurationRoot config = configurationBuilder.Build();

        return config.GetConnectionString("CmsDbContext");
    }
}

And then you’ll need to add it to your OpenIdConnect options so it runs: options.ClaimActions.Add(new RetrieveLocalClaimsAction());

Together, the Startup.ConfigureServices method should look something like this:

services.AddMvc(config =>
{
    config.Conventions.Add(new AddAuthorizeFiltersControllerConvention());

    config.ModelBinderProviders.Insert(0, new Piranha.Manager.Binders.AbstractModelBinderProvider());
});

services.AddAuthorization(options =>
{
    options.AddPolicy("managerpolicy", b =>
    {
        b.RequireAuthenticatedUser();
    });

    //if you require your content consumers to be logged in to view content,
    //add a policy for the API
    //options.AddPolicy("apipolicy", b =>
    //{
    //    b.RequireAuthenticatedUser();
    //    b.AuthenticationSchemes = new List<string> { JwtBearerDefaults.AuthenticationScheme };
    //});
});

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

services
.AddAuthentication(options =>
{
    options.DefaultScheme = "Cookies";
    options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
    options.Authority = "http://localhost:49508/";
    options.RequireHttpsMetadata = false;

    options.ClientId = "cmsmanager";
    options.SaveTokens = true;
    options.ClaimActions.Add(new RetrieveLocalClaimsAction());
});

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:3
  • Comments:17 (8 by maintainers)

github_iconTop GitHub Comments

4reactions
mattgwagnercommented, Nov 22, 2019

@x3haloed I tweaked your solution a bit and here’s an abridged version of what I got working on my side.

Essentially, when we get the callback from OIDC (Auth0 in my case), I grab their email, check if the user exists, and add one if not. Then I can retrieve their claims via associated roles. There’s some refinement that could be done, but it meets my case.

Thanks for the notes!

options.Events = new OpenIdConnectEvents
                    {
                        OnTicketReceived = async (context) =>
                        {
                            if (context.Principal.Identity is ClaimsIdentity identity)
                            {
                                var db =
                                    context
                                    .HttpContext
                                    .RequestServices
                                    .GetService<IdentitySQLiteDb>();

                                var email =
                                    identity
                                    .FindFirst("email")?
                                    .Value;

                                var user =
                                    await db
                                    .Users
                                    .Where(u => u.Email == email)
                                    .SingleOrDefaultAsync();

                                if (user == null)
                                {
                                    user = new Piranha.AspNetCore.Identity.Data.User
                                    {
                                        UserName = email,
                                        Email = email,
                                        NormalizedEmail = email,
                                        NormalizedUserName = email,
                                        EmailConfirmed = true
                                    };

                                    db.Users.Add(user);

                                    await db.SaveChangesAsync();
                                }

                                IEnumerable<Claim> claims =
                                   db
                                   .UserRoles
                                   .Where(ur => ur.UserId == user.Id)
                                   .Join(db.Roles, ur => ur.RoleId, r => r.Id, (ur, r) => r)
                                   .Join(db.RoleClaims, r => r.Id, rc => rc.RoleId, (r, rc) => rc)
                                   .ToArray()
                                   .Select(rc => rc.ToClaim());

                                identity.AddClaims(claims);
                            }
                        }
                    };

2reactions
x3haloedcommented, Jun 8, 2019

I would be happy to write! I’ll have to check with my company to see if they’re alright with it.

I belive that just a couple of tweaks could improve SSO compatibility, probably without making Piranha any more complicated. I’ll give it a shot.

I’m new to contributing on GitHub and to Piranha in General. I’ve looked at the contribution guide. If Master is the branch with the latest code, it looks like you’re moving to Razor pages. Is that right? Is there any documention or discussion about the themes and initiatives for 7.0? I just want to understand the broader context.

Read more comments on GitHub >

github_iconTop Results From Across the Web

How do you resolve SSO conflicts and compatibility issues?
How do you resolve SSO conflicts and compatibility issues? · Check your SSO configuration · Test your network connectivity · Review your SSO...
Read more >
The Top 10 Single Sign-On Solutions For Business
Discover the top ten best Single Sign (SSO) solutions. Explore features such as identity management, app integrations, multi-factor ...
Read more >
Top Benefits of SSO and Why It's Important for Your Business
Single sign-on has many benefits for organizations, ... SSO works based on a trust relationship established between the party that holds the ...
Read more >
Best Single Sign-On (SSO) Solutions
Single sign-on (SSO) solutions are authentication tools that allows users to sign into multiple applications or databases with a single set of credentials....
Read more >
What Is Single Sign-On and How Does It Increase Security?
Implementing SSO​​ An ideal solution should have full compatibility with the suite of apps used by employees, facilitate an easy transition to the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found