Implementing Dynamic Authorization for a WCF service using SQL providers

Using the ASP.NET SQL membership provider to authenticate and authorize calls to WCF services is not an uncommon scenario. But the problem is in the authorization part. Usually to authorize access to WCF service methods this is done using static hard coded attributes decorating the methods definition. So basically you would say that this method is accessible to anyone in the specific role. This is shown below.

  [PrincipalPermission(SecurityAction.Demand, Role = "Managers")]

        public string GetData(int value)

        {

            return string.Format("You entered: {0}", value);

        }

Now the more challenging scenario is when you need to configure the access rules in the runtime like from a database or configuration files rather than in the service code.

To implement this scenario you would need to implement a custom WCF service behaviour along with a custom service authorization manager.

First the behaviour goes like this.

public class SqlProviderSecurityBehavior : IServiceBehavior

{

  private ISqlProviderSecuritySettings settings;

    public SqlProviderSecurityBehavior(ISqlProviderSecuritySettings settings)

    {

        this.settings = settings;

    }

    #region IServiceBehavior Members

 

    public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)

    {

 

        if (this.settings.MembershipProvider != null)

        {

            ServiceCredentials cr = bindingParameters.Find<ServiceCredentials>();

            if (cr == null)

            {

                cr = new ServiceCredentials();

                bindingParameters.Add(cr);

            }

 

            // set membership provider

            SqlMembershipProvider sqlMembership = new SqlMembershipProvider();

            NameValueCollection config = new NameValueCollection();

 

            //if specifying the actual connection string, use reflection to set the values

            if (!string.IsNullOrEmpty(this.settings.MembershipProvider.ConnectionString))

            {

                Type t = typeof(SqlMembershipProvider);

                FieldInfo fi = t.GetField("_sqlConnectionString", BindingFlags.NonPublic | BindingFlags.Instance);

                fi.SetValue(sqlMembership, this.settings.MembershipProvider.ConnectionString);

                sqlMembership.ApplicationName = this.settings.MembershipProvider.ApplicationName;

            }

            else

            {

                //use Initialize method when specyfing connectionName... no reflection needed

                config.Add("connectionStringName", this.settings.MembershipProvider.ConnectionStringName);

                config.Add("applicationName", this.settings.MembershipProvider.ApplicationName);

                sqlMembership.Initialize("SqlMembershipProvider", config);

            }

            cr.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.MembershipProvider;

            cr.UserNameAuthentication.MembershipProvider = sqlMembership;

        }

 

        if (this.settings.RoleProvider != null)

        {

         // set role provider

            serviceHostBase.Authorization.PrincipalPermissionMode = PrincipalPermissionMode.UseAspNetRoles;

            SqlRoleProvider sqlRoleProvider = new SqlRoleProvider();

            NameValueCollection config = new NameValueCollection();

 

            //if specifying the actual connection string, use reflection to set the values

            if (!string.IsNullOrEmpty(this.settings.RoleProvider.ConnectionString))

            {

                Type t = typeof(SqlRoleProvider);

                FieldInfo fi = t.GetField("_sqlConnectionString", BindingFlags.NonPublic | BindingFlags.Instance);

                fi.SetValue(sqlRoleProvider, this.settings.RoleProvider.ConnectionString);

                sqlRoleProvider.ApplicationName = this.settings.RoleProvider.ApplicationName;

            }

            else

            {

                //use Initialize method when specyfing connectionName... no reflection needed

                config.Add("connectionStringName", this.settings.RoleProvider.ConnectionStringName);

                config.Add("applicationName", this.settings.RoleProvider.ApplicationName);

                sqlRoleProvider.Initialize("SqlRoleProvider", config);

            }

            serviceHostBase.Authorization.RoleProvider = sqlRoleProvider;

            serviceHostBase.Authorization.ServiceAuthorizationManager = new SqlAuthorizationManager(sqlRoleProvider);

 

            // store as service extension

            RoleProviderServiceHostExtension ext = new RoleProviderServiceHostExtension(sqlRoleProvider);

            serviceHostBase.Extensions.Add(ext);

        }

    }

 

    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)

    {

    }

 

    public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)

    {

    }

 

    #endregion

}

As you can see in this WCF behaviour it sets the service authorization manager to be our custom class that is implemented as follows.

public class SqlAuthorizationManager : ServiceAuthorizationManager

{

    private SqlRoleProvider sqlRoleProvider;

    public SqlAuthorizationManager(SqlRoleProvider _sqlRoleProvider) : base()

    {

        sqlRoleProvider = _sqlRoleProvider;

    }

    protected override bool CheckAccessCore(OperationContext operationContext)

    {

        bool baseResult = base.CheckAccessCore(operationContext);

        //For mex support (starting WCF service, etc.)

        //NOTE: Other than for service startup this will NOT be true because the WCF

        //configuration dictates that WindowsCredentials must be sent and Anonymous users

        //are NOT allowed.

        if (operationContext.ServiceSecurityContext.IsAnonymous) return true;

        //Extract the identity token of the current context user making the call to this service

        IIdentity Identity = operationContext.ServiceSecurityContext.PrimaryIdentity;

        //Prior to proceeding, throw an exception if the user has not been authenticated at all

        if (!Identity.IsAuthenticated)

        {

            throw new SecurityTokenValidationException("Authenticated user is required to call this service.");

        }

        string[] roles = sqlRoleProvider.GetRolesForUser(Identity.Name);

        if (roles.Length <= 0)

        {

            throw new System.ServiceModel.Security.SecurityAccessDeniedException("User is not part of the service account role.");

        }

        if (!roles.Contains("The role you need to check comes here or can be dynamic"))

        {

           throw new System.ServiceModel.Security.SecurityAccessDeniedException("User is not part of the service account role.");

        }

        //this is the custom authorization rules in a custom table of your choosing

        ASPProvidersEntities entities = new ASPProvidersEntities();

        var userrule = (from serviceAuth in entities.ServiceAuthorizations

                        where serviceAuth.ServiceContractName == operationContext.EndpointDispatcher.ContractName && serviceAuth.Username == Identity.Name

                        select serviceAuth).SingleOrDefault();

        if (userrule == null)

        {

            throw new System.ServiceModel.Security.SecurityAccessDeniedException("User is not authorized to call this service.");

        }

        return baseResult;

    }

}

Happy coding :)