SimpleMembership in MVC4 + WebApi Application Using Basic HTTP Authentication

I am trying to implement an MVC4 web application with the following requirements:

(a) he offers his services only to authenticated users. Regarding authentication, I would like to use simple membership, as this is the latest authentication technology from MVC, gives me an advantage in defining my own db tables, provides OAuth support out of the box and integrates seamlessly with MVC and WebAPI.

(b) it provides some basic functions through WebApi for mobile / JS clients, which must be authenticated using basic HTTP authentication (+ SSL). Usually I will have JS clients using jQuery AJAX calls for WebApi controllers, decorated with the Authorize attribute for different user roles.

(c) ideally, in a mixed environment, I would like to avoid double authentication: that is, if the user has already authenticated through a browser and visits a page that implies a JS call for the action of the WebApi controller, (a) the mechanism should be enough.

Thus, although (a) is covered by default MVC pattern, (b) basic HTTP authentication without browser mediation is required. To this end, I have to create a DelegatingHandler, like the one I found in this post: http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-message-handlers . The problem is that its implementation requires some way to get the IPrincipal from the received username and password, and the WebSecurity class does not provide any method for this (except Login, but I would avoid changing the registered user just for authorization, also because of potential "mixed" environments, such as (c)). Therefore, it seems that my only option is to give up simple membership. Does anyone have any better suggestions? Here is the corresponding (slightly modified) code from the quoted message:

public interface IPrincipalProvider { IPrincipal GetPrincipal(string username, string password); } public sealed class Credentials { public string Username { get; set; } public string Password { get; set; } } public class BasicAuthMessageHandler : DelegatingHandler { private const string BasicAuthResponseHeader = "WWW-Authenticate"; private const string BasicAuthResponseHeaderValue = "Basic"; public IPrincipalProvider PrincipalProvider { get; private set; } public BasicAuthMessageHandler(IPrincipalProvider provider) { if (provider == null) throw new ArgumentNullException("provider"); PrincipalProvider = provider; } private static Credentials ParseAuthorizationHeader(string sHeader) { string[] credentials = Encoding.ASCII.GetString( Convert.FromBase64String(sHeader)).Split(new[] { ':' }); if (credentials.Length != 2 || string.IsNullOrEmpty(credentials[0]) || String.IsNullOrEmpty(credentials[1])) return null; return new Credentials { Username = credentials[0], Password = credentials[1], }; } protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { AuthenticationHeaderValue authValue = request.Headers.Authorization; if (authValue != null && !String.IsNullOrWhiteSpace(authValue.Parameter)) { Credentials parsedCredentials = ParseAuthorizationHeader(authValue.Parameter); if (parsedCredentials != null) { Thread.CurrentPrincipal = PrincipalProvider .GetPrincipal(parsedCredentials.Username, parsedCredentials.Password); } } return base.SendAsync(request, cancellationToken) .ContinueWith(task => { var response = task.Result; if (response.StatusCode == HttpStatusCode.Unauthorized && !response.Headers.Contains(BasicAuthResponseHeader)) { response.Headers.Add(BasicAuthResponseHeader, BasicAuthResponseHeaderValue); } return response; }); } } 
+7
source share
3 answers

Here is another solution that meets all your requirements . It uses SimpleMemberhsip with a combination of form authentication and basic authentication in the MVC 4 application. It can also support authorization, but this is not required, leaving the Role null property.

+3
source

Thank you, this seems like the best solution available at this time! I managed to create a dummy solution from scratch (find it here: http://sdrv.ms/YpkRcf ), and it seems to work in the following cases:

1) when I try to access the limited action of the MVC controller, I am redirected to the login page, as expected, to authenticate.

2) when I launch a jQuery ajax call for the limited action of the WebApi controller, the call succeeds (except, of course, if SSL is not used).

However, it does not work when, after entering the website, the API call still requires authentication. Can someone explain what is going on here? In the future, I describe my procedure in detail, because, in my opinion, this can be useful for beginners such as myself.

Thanks (sorry for formatting what follows, but I can't let this editor mark the code correctly ...)


Procedure

  • create a new mvc4 application (the basic mvc4 application: these are already universal providers. All class names of universal providers begin with the default value ...);

  • configure web.config for your non-local database, for example:

      <connectionStrings> <add name="DefaultConnection" providerName="System.Data.SqlClient" connectionString="data source=(local)\SQLExpress;Initial Catalog=Test;Integrated Security=True;MultipleActiveResultSets=True" /> 

It is also often useful to set machineKey for password hashing so that you can freely move this site from server to server without using your passwords. Use the key generator website to determine the following entry:

  <system.web> <machineKey validationKey="...thekey..." decryptionKey="...thekey..." validation="SHA1" decryption="AES" /> 
  • if necessary, create a new, empty database corresponding to the connection string of your web.config. Then run our good old WSAT (from the VS Project menu) and configure security by adding users and roles as needed.

  • if you want, add a HomeController with an index action, because there is no controller in this template, and you cannot run its web application without it.

  • add Thinktecture.IdentityModel.45 from NuGet and add / update all your favorite NuGet packages. Please note that at the time of this writing, jquery validation, unobtrusive from MS, is no longer compatible with jQuery 1.9 or higher. I prefer to use http://plugins.jquery.com/winf.unobtrusive-ajax/ . So, remove jquery.unobtrusive * and add this library (which consists of winf.unobtrusive-ajax and additional methods) in your packages (App_Start / BundleConfig.cs).

  • change WebApiConfig.cs in App_Start, adding it after configuring the DefaultApi route:

    public static class WebApiConfig {public static void Register (HttpConfiguration) {config.Routes.MapHttpRoute (name: "DefaultApi", routeTemplate: "api / {controller} / {id}", defaults: new {id = RouteParameter.Optional} );

      // added for Thinktecture var authConfig = new AuthenticationConfiguration { InheritHostClientIdentity = true, ClaimsAuthenticationManager = FederatedAuthentication.FederationConfiguration.IdentityConfiguration.ClaimsAuthenticationManager }; // setup authentication against membership authConfig.AddBasicAuthentication((userName, password) => Membership.ValidateUser(userName, password)); config.MessageHandlers.Add(new AuthenticationHandler(authConfig)); } 

    }

To be cleaner, api controllers will be placed under Controllers / Api /, so create this folder.

  • Add to LoginModel.cs models:

    public class LoginModel {[Required] [Show (Name = "UserName", ResourceType = typeof (StringResources))] public string UserName {get; set; }

     [Required] [DataType(DataType.Password)] [Display(Name = "Password", ResourceType = typeof(StringResources))] public string Password { get; set; } [Display(Name = "RememberMe", ResourceType = typeof(StringResources))] public bool RememberMe { get; set; } 

    }

This model requires the StringResources.resx resource (with code generation), which I usually place in the Assets folder with three lines specified in the attributes.

  • Add ClaimsTransformer.cs to your solution root, for example:

    public class ClaimsTransformer: ClaimsAuthenticationManager {public override ClaimsPrincipal Authenticate (string resourceName, ClaimsPrincipal incomingPrincipal) {if (! incomingPrincipal.Identity.IsAuthenticated) {return base.Authenticate (resourceName, incomingPrincipal); }

      var name = incomingPrincipal.Identity.Name; return Principal.Create( "Custom", new Claim(ClaimTypes.Name, name + " (transformed)")); } 

    }

  • Add Application_PostAuthenticateRequest to Global.asax.cs:

    public class MvcApplication: HttpApplication {... protected void Application_PostAuthenticateRequest () {if (ClaimsPrincipal.Current.Identity.IsAuthenticated) {var transformer = FederatedAuthentication.FederationConfiguration.IdentityConfiguration.ClaimsAuthenticationManager; var newPrincipal = transformer.Authenticate (string.Empty, ClaimsPrincipal.Current);

      Thread.CurrentPrincipal = newPrincipal; HttpContext.Current.User = newPrincipal; } } 

    }

  • web.config (replace YourAppNamespace with the application namespace):

    <configSections> <section name = "system.identityModel" type = "System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = B77A5C561934E089" /> ...

  • add other models for the account controller with their representations (you can get them from the MVC3 application template, even if I prefer to change them to more accessible options for localization, using attributes that require line names rather than literals).

  • to test browser-based authentication, add some [Authorized] actions to the controller (for example, HomeController) and try to access it.

  • to test basic HTTP authentication, insert the following code in some form (for example, Home / Index) (specify your username and password in the token variable):

    ...

    <p> Test call

    $ (function () {$ ("# test"). click (function () {var token = "USERNAME: PASSWORD"; var hash = $ .base64.encode (token); var header = "Basic" + hash; console.log (header);
      $.ajax({ url: "/api/dummy", dataType: "json", beforeSend: function(xhr) { xhr.setRequestHeader("Authorization", header); }, success: function(data) { alert(data); }, error: function(jqXHR, textStatus, errorThrown) { alert(errorThrown); } }); }); }); 

This requires the Base64 jQuery encoding / decoding plugin: jquery.base64.js and its miniature instance.

To enable SSL, follow the instructions here: http://www.hanselman.com/blog/WorkingWithSSLAtDevelopmentTimeIsEasierWithIISExpress.aspx (basically, enable SSL in the properties of the web project and connect to the specified port in the property value).

+3
source

All Articles