Search code examples
asp.netwifclaims-based-identityfubumvc

Allow or deny access to entire site based on Claim


I have a FubuMvc website that uses Claims based authorization from a WIF single signon server. The Authentication happens on the SSO and the claims including roles and a set of custom claims are passed to the website for authorization.

The SSO and website work fine as does the role based authorization but what I want to do is reject access to the entire site save an error page based on the absence of a custom claim. Currently I am using a custom ClaimsAuthenticationManager which inspects the claims and checks for the existence of the required claim. If the claim is missing it throws an exception. This causes a 500 error but what I really want is the system to throw a 401 error and redirect to a Not Authorized page on the website.

The following is an example of the custom ClaimsAuthenticationManager.

public class CustomAuthenticationManager : ClaimsAuthenticationManager
{
    private readonly string expectedClaim;

    public CustomAuthenticationManager (object config)
    {
        var nodes = config as XmlNodeList;
        foreach (XmlNode node in nodes)
        {
            using (var stringReader = new StringReader(node.OuterXml))
            using (var rdr = new XmlTextReader(stringReader))
            {
                rdr.MoveToContent();
                rdr.Read();
                string claimType = rdr.GetAttribute("claimType");
                if (claimType.CompareTo(ClaimTypes.CustomClaim) != 0)
                {
                    throw new NotSupportedException("Only custom claims are supported");
                }
                expectedSystemName = rdr.GetAttribute("customClaimValue");
            }
        }
    }

    public override IClaimsPrincipal Authenticate(
        string resourceName, IClaimsPrincipal incomingPrincipal)
    {
        var authenticatedIdentities = incomingPrincipal.Identities.Where(x => x.IsAuthenticated);
        if (authenticatedIdentities.Any() &&
                authenticatedIdentities.Where(x => x.IsAuthenticated)
                                       .SelectMany(x => x.Claims)
                                       .Where(x => x.ClaimType == ClaimTypes.CustomClaim)
                                       .All(x => x.Value != expectedClaim))
        {
                throw new HttpException(
                    (int)HttpStatusCode.Unauthorized,
                    "User does not have access to the system");
        }
        return base.Authenticate(resourceName, incomingPrincipal);
    }
}

The above works and prevents access to the system but ti is not very use friendly because the user just gets a 500 error. Also because the user is essentially logged in they but have no access they have no way of logging out. I have exposed a Unauthorized error page which provides access to anonymous users but I have no way of redirecting the user yet.


Solution

  • I wonder why you have implemented your logic in the authentication manager.

    Have you instead considered to switch to a ClaimsAuthorizationManager? For this to work, you need to add the ClaimsAuthorizationModule to your pipeline:

     <system.webServer>
        <modules>
            ...
            <add name="SessionAuthenticationModule" ....
            <add name="ClaimsAuthorizationModule" type="Microsoft.IndentityModel.Web.ClaimsAuthorizationModule, Microsoft.IndentityModel.Web" />
        </modules>
     </system.webServer>
    

    then you create structure of your authorization roles:

     <claimsAuthorizationManager type="yourtype, yourassembly">
          <requiredClaim claimType="foobar" />
     </claimsAuthorizationManager>
    

    and in the manager:

    public class FederatedClaimsAuthorizationManager : ClaimsAuthorizationManager
    {
        private List<string> _requiredClaims = new List<string>();
    
        public FederatedClaimsAuthorizationManager( object config )
        {
            XmlNodeList nodes = config as XmlNodeList;
            foreach ( XmlNode node in nodes )
            {
                XmlTextReader xtr = new XmlTextReader( new StringReader( node.OuterXml ) );
                xtr.MoveToContent();
    
                _requiredClaims.Add( xtr.GetAttribute( "claimType" ) );
            }
        }
    
        public override bool CheckAccess( AuthorizationContext context )
        {
            IClaimsIdentity identity = context.Principal.Identities[0];
    
            return !identity.IsAuthenticated ||
                    identity.Claims.Any( c => _requiredClaims.Any( rq => c.ClaimType == rq ) );
        }
    }
    

    The difference between this and your approach is that failed authorization is handled by just reinvoking the authentication process - there are no exceptions, no 500s.

    For example - if you use the <wif:FederatedPassiveSignIn /> on a login page then the failed authorization just redirects users to the login page where you can check if user is authenticated and show the message that says "you were redirected to the login page which possibly means that you tried to access a resources you are not authorized to access".