brockallen / BrockAllen.MembershipReboot

MembershipReboot is a user identity management and authentication library.
Other
742 stars 239 forks source link

Question about MVC 5 and remote Web Api site. #315

Closed JellyMaster closed 10 years ago

JellyMaster commented 10 years ago

Hi Brock,

So I have implemented the latest version via nuGet and am now at a point where I am looking to do something a bit more complicated (well it is for me)

So my scenario is that I have my front end website (eg www.myfrontendsite.com) and this will then talk to a secondary web api site that has all my business logic etc (this will also be accessible to other applications within my scope. Now this will be hosted on say www.mybackendsite.com. Now I have enabled CORS to deal with the cross origin issues and this works fine without any authentication (eg allowannoyomous) but I need to secure this to prevent any random person coming along and trying to access my services.

What is the best way of being able to authenticate the current user that is logged in via my front end site to the backend site?

I used the single tenant site as my starting point with the implementation. Do I need to change this to use something like OWIN (still need to read up on this) . Obviously I would like to use Membership reboot for all the sites to ensure that there is one form of authentication rather than having different "identity" solutions for all my projects.

brockallen commented 10 years ago

You either need 1) some sort of delegation technique from the UI layer to the backend layer, or 2) the backend layer just trusts the UI layer.

For #1, AuthorizationServer supports this with the assertion flow: http://leastprivilege.com/2013/12/23/advanced-oauth2-assertion-flow-why/

For #2, you could use OAuth2 client credentials flow to authenticate the UI layer.

JellyMaster commented 10 years ago

Hi Brock,

Apologies for not coming back to you on this earlier.

I am a little confused with how I should be implementing this.

I'm not sure if I am understanding this correctly or if I am just misunderstanding something.

So let me see if I can example my scenario a little better and hopefully this will aid understanding.

User Management and Creation

So I currently have a site where I am managing and creating all my users using MR and this is uses a single MVC5 website with the logic for doing all the login, adding/removing claims, etc into a service library class.

Hopefully the attached images will help with showing how this website works.

Login Page 01 login screen

Summary of system 02 user management summary screen

Listing all users set up in the system (Admin view) 03 user management list users

Editing/Adding user screen 04 user management editing a user

List of allowed applications that the system knows about 05 user management allowed applications

Editing/Adding allowed applications 06 user management editing allowed applications

Allowed Claims setup for applications 07 user management allowed claims

Editing/Adding claims setup for applications 08 user management editing allowed claims

Applying claims to a user for allowed/denied access 09 user management detailed user with added claims for access

in addition to this I have also created a custom Authorize attribute which will be used by all sites which looks through the claims and checks to see if a user has the required access to the controller/action. see below:

using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Security.Claims; using System.Web; using System.Web.Mvc;

namespace Common.Library.DataAnnotations.Attributes { ///

/// This is the Claims Authorize attribute used for Brock Allen's MembershipReboot implementation 
/// This is designed for a single tenant based solution rather than multitenant 
/// 
/// </summary>

public class ClaimsMRSingleAuthorizeAttribute : AuthorizeAttribute
{
    private string app;

    //private string claimAction;

    private string resourceValue;

    /// <summary>
    /// This is the claims authorize attribute using the resource/action/claim values pattern
    /// </summary>
    /// <param name="resource">The Resource we are accessing</param>
    /// <remarks>There is no default being passed here.</remarks>
    /// <param name="action">The action we are checking for. </param>
    /// <remarks>The default action passed in is null </remarks>
    /// <param name="values">the claims we are looking to check against for authorization. If none present then we assume the user has access to the action </param>
    /// <remarks>If no values are pass we will assume if they have access to required resource with the appropriate action this is good enough.</remarks>
    public ClaimsMRSingleAuthorizeAttribute(string application = null, string resource = null)
    {

        app = application;
        resourceValue = resource;

    }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        bool valid = false;

        if (httpContext == null)
        {
            throw new InvalidOperationException("invalid httpContext passed into Claims Authorize Attribute");
        }

        ClaimsPrincipal user = httpContext.User as ClaimsPrincipal;

        if (user == null)
        {
            throw new InvalidCastException(string.Format("httpContext user is not a claims based principal the user principal is: {0}", httpContext.User.GetType().ToString()));
        }
        else
        {

            //do authorization logic in here. 
            //probably offload this onto a centralized authentication manager. 
            List<Claim> claimsList = new List<Claim>();
            claimsList = user.Claims.ToList();

            if (claimsList == null || claimsList.Count == 0)
            {
                throw new InvalidOperationException(string.Format("No valid claims found for the current user principal: {0}", user.Identity.Name));
            }
            else
            {

                //we have some claims so do they match the current actions requested for this user. 
                //so we need to understand if the user can access the current application. 
                //given that we are using the membership reboot solution in single tenant mode then we need to check the authorized application. 

                //first check is see if the user has the claim Developer. If they do we are effectively saying god mode is enabled 
                Claim role = claimsList.FirstOrDefault(c => c.Type == ClaimTypes.Role && c.Value == "Developer");

                //valid = true; 
                if (role != null)
                {
                    valid = true;
                }
                else
                {
                    //else we need to check if they have the appropriate claims based on the application/resource. 
                    //we need to determine if we have the application, resource and action being passed in. 

                    if (string.IsNullOrWhiteSpace(app))
                    {
                        //this will get everything before the path 
                        app = httpContext.Request.Url.GetLeftPart(UriPartial.Authority);

                    }

                    if (string.IsNullOrWhiteSpace(resourceValue))
                    {
                        resourceValue = httpContext.Request.Url.AbsolutePath;
                    }

                    //so we need to find out if we have any claims associated to this user for this application 
                    //we check the issuer attribute

                    List<Claim> appSpecificClaims = claimsList.Where(c => c.Type.ToLower().Contains(app.ToLower())).ToList();

                    if (appSpecificClaims != null && appSpecificClaims.Count > 0)
                    {
                        //so we have some claims lets see if we have one specific to access this application. 

                        string providedClaim = string.Format("{0}/claims{1}", app, resourceValue);

                        //first try to match the specific claim
                        Claim matchedClaim = appSpecificClaims.FirstOrDefault(c => c.Type.ToLower() == providedClaim.ToLower());

                        if (matchedClaim != null)
                        {
                            bool.TryParse(matchedClaim.Value, out valid);
                        }
                        else
                        {
                            //we may have a match but due to routing we may have something like id being included in the path. 
                            //remove the last item of this and see if we have a value that matches. 

                            matchedClaim = appSpecificClaims.FirstOrDefault(c => c.Type.ToLower() == providedClaim.ToLower().Substring(0,providedClaim.LastIndexOf("/")));

                            if (matchedClaim != null)
                            {
                                bool.TryParse(matchedClaim.Value, out valid);

                            }
                            else
                            {
                                //as a complete fail over try and match any item maybe at the route level. This should be the first one that gets picked. 
                                matchedClaim = appSpecificClaims.OrderBy(c => c.Type).FirstOrDefault(c => providedClaim.ToLower().Contains(c.Type.ToLower())); 

                                if(matchedClaim != null )
                                {
                                    bool.TryParse(matchedClaim.Value, out valid);

                                }
                            }

                        }

                    }

                }
            }

        }

        app = resourceValue = string.Empty;

        return valid;

    }

    protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.User.Identity.IsAuthenticated)
        {
            if (filterContext.HttpContext.Request.IsAjaxRequest())
            {
                filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                //filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(new { controller = "Login", action = "Index" }));
                filterContext.HttpContext.Response.End();
            }
        }

        base.HandleUnauthorizedRequest(filterContext);
    }

}

}

So this application used the SingleTenantSolution (The very basic one) as the basis of the site and how I have implemented MR security within this site.

Now to use this "single database" I have extracted out all the MR elements (That I require) within a class library that can be just dropped into a new site with the Login/Logout controllers and associated views.

Due to deploying this to the cloud I am also having to change the webconfig for the session provider to be:

<sessionState mode="Custom" customProvider="DefaultSessionProvider" timeout="60">
  <providers>
    <add name="DefaultSessionProvider" type="System.Web.Providers.DefaultSessionStateProvider, System.Web.Providers, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" connectionStringName="MembershipReboot" />
  </providers>
</sessionState>

In addition to this (Not sure if this is correct I have also included the following section into my webconfig

<system.identityModel> <identityConfiguration> <securityTokenHandlers> <remove type="System.IdentityModel.Tokens.SessionSecurityTokenHandler,System.IdentityModel, Version=4.0.0.0, Culture=neutral,PublicKeyToken=B77A5C561934E089" /> <add type="System.IdentityModel.Services.Tokens.MachineKeySessionSecurityTokenHandler,System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral,PublicKeyToken=B77A5C561934E089"> <sessionTokenRequirement lifetime="08:00:00" /> </add> </securityTokenHandlers> </identityConfiguration> </system.identityModel> <system.identityModel.services> <federationConfiguration> <cookieHandler requireSsl="false" persistentSessionLifetime="08:00:00" mode="Chunked" /> </federationConfiguration> </system.identityModel.services>

So I am currently having to repeat a lot/ cut and paste a lot of code across to new projects just to get the authentication solution working. In order to remove this I am now looking at the sso approach as this would be a far better solution long term for me.

Which leads me back to the original question where I need to split out the Front End and the back end layer to allow easy code reuse for additional sites that will use the central MR database.

Do I integrate the "ThinkTecture" Authorization server into my user admin site and then pass the login/logout responsibilities to this single user management site "sso style" in some federated way (WIF) or do I include the Authorization server in to each project and authenticate as I currently do in an isolated manner and then have to somehow pass either some user credentials across or some form of "token" from my front end to my back end solution to indicated that the user requesting the WebApi resource is a) Authenticated and b) Authorised to access this resource?

Sorry if this sounds a bit like security 101 but I am trying to get my head around this security business and ensure I have implemented in the correct way.

HaJoB commented 10 years ago

@JellyMaster Is it possible to get a fork or copy from your version?

JellyMaster commented 10 years ago

It difficult as I have some paid controls from telerik (kendo ui for mvc) in my project. I am also using telerik's data access as my preferred ORM for the additional applications/claims management bit.

I could provide access to the abstracted service layer and non-telerik based bits but giving you the front end would be difficult unless you are telerik licensed.

If you wanted to see a demo site I could set one up and host it (I use azure) I would just need to set you up with access.

If there are any specific questions about my implementation I will share as much as I can with you

HaJoB commented 10 years ago

The data layer is not important for me as I'm using EF, but you have a nice front end. I'm not telerik licenced. I see, it would be quite difficult.

JellyMaster commented 10 years ago

The front end could be retro coded to use the open source version of the telerik controls.

I have used a mixture of bootstrap 3, font awesome and telerik to provide all the front end elements. It is mainly the grids and the editing screen that are telerik based which could be done using similar bootstrap/ other js frameworks.

I like using the telerik mvc controls as it means I can get a front end up and running quickly and focus more on the back end/ middleware elements.

brockallen commented 10 years ago

I guess I was confused on your original requirement. So you want a user in website A to be authenticated, then the code in JS makes API calls to website B with the same security context as the user? So yes, sounds like some sort of SSO is in order and for the API site you'd need to support OAuth2 for the tokens (implicit flow it sounds like).

JellyMaster commented 10 years ago

Yes sorry. I was trying to think of the best way of explaining it. Hence the images to try and express what it is I am trying to do.

I will look at the oauth2 stuff. So should I be looking at re-engineering my solution with using owin under the hood?

Should I be looking at anything else to aid my understanding on implementing this. I have been looking at Dominic's pluralsight course but I feel I need to try and see some more of the fundamentals.

brockallen commented 10 years ago

He has a newer PS course, but it's not yet released. That would be a good resource.