I got this action:
public ActionResult Index(string username, int userID)
{
if (!HttpContext.User.Identity.IsAuthenticated)
{
var ticket = new FormsAuthenticationTicket(2, username, DateTime.Now, DateTime.Now.AddDays(7), false, string.Empty);
var encr = FormsAuthentication.Encrypt(ticket);
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encr);
Response.Cookies.Add(cookie);
}
return View("Index",null,username);
}
Later, this partial view action is invoked:
public PartialViewResult PageHeader()
{
if (HttpContext.User.Identity.IsAuthenticated)
{
string username = HttpContext.User.Identity.Name;
...
}
}
The expression HttpContext.User.Identity.IsAuthenticated is evaluated to false even though the auth cookie was set in the previous action. Only after refreshing the page does the expression evaluate to true.
So my question is: How do I tell asp.net mvc that user is already authenticated, and use HttpContext.User.Identity property?
This seems like an odd approach; you're performing authentication in the wrong layer.
The user is typically authenticated before they get to the action method (being called). This way you can prevent them from entering the controller or action if they are not authenticated.
Why not use the framework to take care of this task? It will automatically set the cookie for you in the correct context at the correct stage in the pipeline.
Subclass MembershipProvider and Override ValidateUser(string username, string password):
In this method, do your verification with username and password. If validation fails (wrong username/password, etc), return false. If it succeeds, return true, and the auth cookie will be set for you.
From this point, you can create Authorization Attributes (by subclassing AuthorizationAttribute) and decorate your controllers or actions. In these attributes, you can do things like check user roles, scopes, permissions, etc and reject the request if the user is not properly authorized to make the request. It's super simple.
You will need to do 3 things:
Create a custom MembershipProvider which validates the user (username/password check),
Create custom AuthorizeAttribute which checks the user's authenticated/authorized status. It could be as simple as verifying that they have been authenticated (just return User.Identity.IsAuthenticated. If it's false, it will send them to the login screen. Otherwise , it'll allow them to continue with the request), and add the Custom provider to the web.config so it knows to use it. You may also have to set the Login page in web.config if you haven't already done that.
This is the right(er) way to do it and will likely solve your problem while also cleaning up your project.
// The provider
// This is what gets called during login. your logic to validate the user is placed here
// Return true or false which will indicate whether or not an auth token/cookie will be set
public class MyCustomProvider : MembershipProvider
{
public override bool ValidateUser(string username, string password)
{
const string testUsername = "User1";
const string testPassword = "abcd1234";
// do whatever you need to do in order to verify this dude's identity
return username.Equals(testUsername) && password.Equals(testPassword);
}
//... bunch of other overrides. I only implement them if I actually use them otherwise just wrap them in a region and hide them.
}
// The web.config update. Tell the framework where your login page is. Typically, in an MVC project,
// The view is in Views/Account and the action Login on the Account controller calls WebSecurity.Login
// which is what runs your provider. Define both here.
<system.web>
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" />
</authentication>
<membership defaultProvider="MyCustomProvider">
<providers>
<remove name="AspNetSqlProvider" />
<add name="MyCustomProvider"
type="FullyQualifiedName.MyCustomProvider, AuthDemo, Version=1.0.0.0, Culture=neutral" />
</providers>
</membership>
<compilation debug="true" targetFramework="4.5" />
<httpRuntime targetFramework="4.5" />
</system.web>
// The authorize attribute
// This is where you can check your user's authorization. In this example, I just
// check to see that he was authenticated by the provider.
public class MyCustomAuthorizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
// do whatever you need to do here to verify that this dude is allowed to be here
return httpContext.User.Identity.IsAuthenticated;
}
}
// sample usage of the attribute
// the framework will run this attribute before it allows the user into the controller.
// You could also do this at the action level instead of the controller level
[MyCustomAuthorize]
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult About()
{
ViewBag.Message = "Your application description page.";
return View();
}
public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
}
ASP.NET only sets once the HttpContext.User.Identity.IsAuthenticated at a beginning of the request.
So setting later the authentication cookie inside a controller action does not have any effect on the HttpContext.User.Identity.IsAuthenticated because you are in the context of the same request.
The suggested "workflow" for the forms authentication is the following:
Client sends username and password to the server
The server validates the credentials and set the authentication cookie
Client client sends the authentication cookie on subsequent requests
So you need issue a new request in order to the HttpContext.User.Identity.IsAuthenticated gets updated correctly.
The standard practice on successful login is to redirect to client to the original url where it came from or in your case just redirect it to the same action:
public ActionResult Index(string username, int userID)
{
if (!HttpContext.User.Identity.IsAuthenticated)
{
var ticket = new FormsAuthenticationTicket(2, username, DateTime.Now,
DateTime.Now.AddDays(7), false, string.Empty);
var encr = FormsAuthentication.Encrypt(ticket);
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encr);
Response.Cookies.Add(cookie);
return RedirectToAction("Index", new {username, userID});
}
return View("Index", null, username);
}
Related
I have setup my ASP Net Core 2.0 project to authenticate with Azure AD (using the standard Azure AD Identity Authentication template in VS2017 which uses OIDC). Everything is working fine and the app returns to the base url (/) and runs the HomeController.Index action after authentication is successful.
However I now want to redirect to a different controller action (AccountController.CheckSignIn) after authentication so that I can check if the user already exists in my local database table and if not (ie it's a new user) create a local user record and then redirect to HomeController.Index action.
I could put this check in the HomeController.Index action itself but I want to avoid this check from running every time the user clicks on Home button.
Here are some code snippets which may help give clarity...
AAD settings in appsettings.json
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "<my-domain>.onmicrosoft.com",
"TenantId": "<my-tennant-id>",
"ClientId": "<my-client-id>",
"CallbackPath": "/signin-oidc" // I don't know where this goes but it doesn't exist anywhere in my app and authentication fails if i change it
}
I added a new action to my AccountController.CheckSignIn to handle this requirement but I cannot find a way to call it after authentication.
public class AccountController : Controller
{
// I want to call this action after authentication is successful
// GET: /Account/CheckSignIn
[HttpGet]
public IActionResult CheckSignIn()
{
var provider = OpenIdConnectDefaults.AuthenticationScheme;
var key = User.FindFirstValue(ClaimTypes.NameIdentifier);
var info = new ExternalLoginInfo(User, provider, key, User.Identity.Name);
if (info == null)
{
return BadRequest("Something went wrong");
}
var user = new ApplicationUser { UserName = User.Identity.Name };
var result = await _userManager.CreateAsync(user);
if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);
if (!result.Succeeded)
{
return BadRequest("Something else went wrong");
}
}
return RedirectToAction(nameof(HomeController.Index), "Home");
}
// This action only gets called when user clicks on Sign In link but not when user first navigates to site
// GET: /Account/SignIn
[HttpGet]
public IActionResult SignIn()
{
return Challenge(
new AuthenticationProperties { RedirectUri = "/Account/CheckSignIn" }, OpenIdConnectDefaults.AuthenticationScheme);
}
}
I have found a way to make it work by using a redirect as follows...
Inside Startup
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Account}/{action=SignIn}/{id?}");
});
Inside AccountController
// GET: /Account/CheckSignIn
[HttpGet]
[Authorize]
public IActionResult CheckSignIn()
{
//add code here to check if AzureAD identity exists in user table in local database
//if not then insert new user record into local user table
return RedirectToAction(nameof(HomeController.Index), "Home");
}
//
// GET: /Account/SignIn
[HttpGet]
public IActionResult SignIn()
{
return Challenge(
new AuthenticationProperties { RedirectUri = "/Account/CheckSignIn" }, OpenIdConnectDefaults.AuthenticationScheme);
}
Inside AzureAdServiceCollectionExtensions (.net core 2.0)
private static Task RedirectToIdentityProvider(RedirectContext context)
{
if (context.Request.Path != new PathString("/"))
{
context.Properties.RedirectUri = new PathString("/Account/CheckSignIn");
}
return Task.FromResult(0);
}
The default behavior is: user will be redirected to the original page. For example, user is not authenticated and access Index page, after authenticated, he will be redirected to Index page; user is not authenticated and access Contact page, after authenticated, he will be redirected to Contact page.
As a workaround, you can modify the default website route to redirect user to specific controller/action:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Account}/{action=CheckSignIn}/{id?}"
);
});
After your custom logic, you could redirect user to your truly default page(Home/Index).
I want to check if the user exists in my local database, not only when Sign in is selected, but also when any other link to my website is clicked which requires authentication.
After a lot of trial and error I found a solution. Not sure if it is the best solution, but it works.
Basically I use the Authorize attribute with a policy [Authorize(Policy = "HasUserId")] as described in Claims-based authorization in ASP.NET Core.
Now when the policy is not met, you can reroute to the register action.
A – very simplified – version of the AccountController would look like this (I use a LogOn action instead of SignIn to prevent conflicts with the AzureADB2C AccountController):
public class AccountController : Controller
{
public IActionResult AccessDenied([FromQuery] string returnUrl)
{
if (User.Identity.IsAuthenticated)
return RedirectToAction(nameof(Register), new { returnUrl });
return new ActionResult<string>($"Access denied: {returnUrl}").Result;
}
public IActionResult LogOn()
{
// TODO: set redirectUrl to the view you want to show when a registerd user is logged on.
var redirectUrl = Url.Action("Test");
return Challenge(
new AuthenticationProperties { RedirectUri = redirectUrl },
AzureADB2CDefaults.AuthenticationScheme);
}
// User must be authorized to register, but does not have to meet the policy:
[Authorize]
public string Register([FromQuery] string returnUrl)
{
// TODO Register user in local database and after successful registration redirect to returnUrl.
return $"This is the Account:Register action method. returnUrl={returnUrl}";
}
// Example of how to use the Authorize attribute with a policy.
// This action will only be executed whe the user is logged on AND registered.
[Authorize(Policy = "HasUserId")]
public string Test()
{
return "This is the Account:Test action method...";
}
}
In Startup.cs, in the ConfigureServices method, set the AccessDeniedPath:
services.Configure<CookieAuthenticationOptions>(AzureADB2CDefaults.CookieScheme,
options => options.AccessDeniedPath = new PathString("/Account/AccessDenied/"));
A quick-and-dirty way to implement the HasUserId policy is to add the UserId from your local database as a claim in the OnSigningIn event of the CookieAuthenticationOptions and then use RequireClaim to check for the UserId claim. But because I need my data context (with a scoped lifetime) I used an AuthorizationRequirement with an AuthorizationHandler (see Authorization Requirements):
The AuthorizationRequirement is in this case just an empty marker class:
using Microsoft.AspNetCore.Authorization;
namespace YourAppName.Authorization
{
public class HasUserIdAuthorizationRequirement : IAuthorizationRequirement
{
}
}
Implementation of the AuthorizationHandler:
public class HasUserIdAuthorizationHandler : AuthorizationHandler<HasUserIdAuthorizationRequirement>
{
// Warning: To make sure the Azure objectidentifier is present,
// make sure to select in your Sign-up or sign-in policy (user flow)
// in the Return claims section: User's Object ID.
private const string ClaimTypeAzureObjectId = "http://schemas.microsoft.com/identity/claims/objectidentifier";
private readonly IUserService _userService;
public HasUserIdAuthorizationHandler(IUserService userService)
{
_userService = userService;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, HasUserIdAuthorizationRequirement requirement)
{
// Load User Id from database:
var azureObjectId = context.User?.FindFirst(ClaimTypeAzureObjectId)?.Value;
var userId = await _userService.GetUserIdForAzureUser(azureObjectId);
if (userId == 0)
return;
context.Succeed(requirement);
}
}
_userService.GetUserIdForAzureUser searches for an existing UserId in the database, connected to the azureObjectId and returns 0 when not found or when azureObjectId is null.
In Startup.cs, in the ConfigureServices method, add the Authorization policy and the AuthorizationHandler:
services.AddAuthorization(options => options.AddPolicy("HasUserId",
policy => policy.Requirements.Add(new HasUserIdAuthorizationRequirement())));
// AddScoped used for the HasUserIdAuthorizationHandler, because it uses the
// data context with a scoped lifetime.
services.AddScoped<IAuthorizationHandler, HasUserIdAuthorizationHandler>();
// My custom service to access user data from the database:
services.AddScoped<IUserService, UserService>();
And finally, in _LoginPartial.cshtml change the SignIn action from:
<a class="nav-link text-dark" asp-area="AzureADB2C" asp-controller="Account" asp-action="SignIn">Sign in</a>
To:
<a class="nav-link text-dark" asp-controller="Account" asp-action="LogOn">Sign in</a>
Now, when the user is not logged on and clicks Sign in, or any link to an action or controller decorated with [Authorize(Policy="HasUserId")], he will first be rerouted to the AD B2C logon page. Then, after logon, when the user is already registered, he will be rerouted to the selected link. When not registered, he will be rerouted to the Account/Register action.
Remark: If using policies does not fit well for your solution, take a look at https://stackoverflow.com/a/41348219.
I am trying to implement CustomAuthorization via IsAuthorized method of AuthorizeAttribute.
Assumption here is by this time user would have been Authenticated and i can retriev userName and other Claims that were assigned to user during authentication. I am using Bearer Authentication.
But when i check for actionContext.RequestContext.Principal.Identity.IsAuthenticated it is always returning false.
How do i access who the user is and any claims that were assigned to him during authentication in this method?
Here is the code that i am referring to, this is not the full blown implementation, incomingPrincipal.Identity.IsAuthenticated is always false.
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
protected override bool IsAuthorized(HttpActionContext actionContext)
{
IPrincipal incomingPrincipal = actionContext.RequestContext.Principal;
if (!incomingPrincipal.Identity.IsAuthenticated)
return false;
else
{
return true;
}
Debug.WriteLine(string.Format("Principal is authenticated at the start of IsAuthorized in CustomAuthorizationFilterAttribute: {0}", incomingPrincipal.Identity.IsAuthenticated));
}
}
Did you write your own AuthenticationFilter? You have to set the "Principal" to the Http-Context. Otherwise you can't access the information. Could you post your code?
Have a look here.
Authentication Filters in ASP.NET Web API 2
Can anybody tell me what is going wrong with this approach for single sing on?
I have a website A with the authentication logic inside. The user can select the role to access the portal that he wants to go. The problem is how can I configure properly those websites to redirect correctly, because when I redirect, I lose the token(redirect is a GET), I never had the cookie on the portal that I want to go. Do I am missing something in my implementation? Maybe a configuration on the portal that I want to redirect? I am not using the audienceUri on the webconfig, Is this related with the problem? I am using a service that gives me a token if the user is authenticated. Then with that token, I want to redirect the page to the corresponding portal.
Said that I will show you the Login Method in AccountController
[HttpPost]
public async Task<ActionResult> Login(string ddlRoles, string ddlUrls, LoginModel user)
{
var service = new AuthenticationServiceAgent(user.Username, user.Password);
var securityService = new SecurityServiceAgent(service.GetToken());
...processing the claims
//AT THIS POINT THE USER IS AUTHENTICATED
FederatedAuthentication.WSFederationAuthenticationModule.SetPrincipalAndWriteSessionToken(token, true);
.... get the url to redirect
Response.Redirect(urlToRedirect, false);
}
My Proxy class looks like this
public class UserNameTokenServiceProxy : TokenServiceProxy
{
#region Properties
public SecurityCredential Credential { get; set; }
#endregion
#region Methods
public override SecurityToken GetToken(ISecurityCredential credential = null)
{
if (null == credential)
{
throw new ArgumentNullException("credential");
}
var factory = new WSTrustChannelFactory(
new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),
new EndpointAddress(StsEndPoint)
);
factory.TrustVersion = TrustVersion.WSTrust13;
if (null != factory.Credentials)
{
factory.Credentials.UserName.UserName = credential.UserName;
factory.Credentials.UserName.Password = credential.Password;
}
var rst = new RequestSecurityToken()
{
RequestType = RequestTypes.Issue,
KeyType = KeyTypes.Symmetric,
TokenType = TokenTypes.Saml11TokenProfile11,
***********RelyingPartyUri comes from a config file*********
AppliesTo = new EndpointReference(RelyingPartyUri)
};
try
{
var channel = factory.CreateChannel();
var sToken = channel.Issue(rst);
return sToken;
}
catch (Exception ex)
{
return null;
}
}
#endregion
}
}
And the base class of that one is the following:
public abstract class TokenServiceProxy
{
#region Fields
protected string StsEndPoint;
protected string RelyingPartyUri;
#endregion
#region Constructors
protected TokenServiceProxy()
{
StsEndPoint = ConfigurationManager.AppSettings["stsEndpoint"];
if (null == StsEndPoint)
{
throw new Exception("STSEndPoint cannot be null");
}
RelyingPartyUri = ConfigurationManager.AppSettings["relyingPartyUri"];
if (null == RelyingPartyUri)
{
throw new Exception("RelyingPartyUri cannot be null");
}
//StsEndPoint = <add key="stsEndpoint" value="https://sso.dev.MyCompany.com/idsrv/issue/wstrust/mixed/username"/>;
//RelyingPartyUri = #"value="https://dev.MyCompany.com/MyCompanyPortal"/>";
}
#endregion
#region Abstract Methods
public abstract SecurityToken GetToken(ISecurityCredential credential = null);
#endregion
}
Basically we are not using the default configuration for WS with the audienceUri and the federationConfiguration section, the equivalent would be:
<system.identityModel.services>
<federationConfiguration>
<cookieHandler mode="Default" requireSsl="false" />
<wsFederation passiveRedirectEnabled="true" issuer="https://sso.dev.MyCompany.com/idsrv/issue/wstrust/mixed/username" realm="https://dev.MyCompany.com/MyCompanyPortal" requireHttps="false" />
</federationConfiguration>
</system.identityModel.services>
I'm not quite sure what are all the components in your solution, but in any case you seem to try to do too much on your own.
SSO or not - each integrated application needs to maintain its own authentication state (with authentication cookie), and to do so it needs to get the security token from Identity Provider.
In a most common scenario for WS-Federation (Relying Party initiated SSO), Relying Party application redirects user to Identity Provider, Identity Provider checks user credentials and POSTS security token back to Relying Party application, where it is handled by WS-Federation module to create authentication cookie.
In your case I guess that you are trying to achieve "Identity Provider initiated" SSO (assuming, that your login page is part of Identity Provider).
If this is the case, you need to POST created security token to selected Relying Party (as far as I know WS-Federation does not support sending tokens in GET query string).
Of course you need to have proper WS-Federation configuration on your Relying Party side to accept this token and create authentication cookie - having that the authentication is done automatically by WS-Federation module.
I am working on an MVC site that has some pages that need authentication and others that don't. This is determined using the Authorize and AllowAnonymous attributes in a pretty standard way. If they try to access something restricted they get redirected to the login page.
I'm now wanting to add the functionality to automatically log them in using an encrypted token passed in the querystring (the link will be in emails sent out). So the workflow I want now is that if a request goes to a page that is restricted and there is a login token in the querystring I want it to use that token to log in. If it logs in successfully then I want it to run the original page requested with the new logged in context. If it fails to log in then it will redirect to a custom error page.
My question is where would I need to insert this logic into the site?
I have seen some suggestions on subclassing the Authorize attribute and overriding some of the methods but I'm not 100% sure how to go about this (eg what I would override and what I'd do in those overridden methods.
I've also had a look at putting the logic at a controller level but I am led to understand that the authorize attribute would redirect it away from the controller before any code in the controller itself was run.
It would be better to write a custom authorization attribute that will entirely replace the default functionality and check for the query string parameter and if present, decrypt it and authenticate the user. If you are using FormsAuthentication that would be to call the FormsAuthentication.SetAuthCookie method. Something along the lines of:
public class TokenAuthorizeAttribute : FilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
string token = filterContext.HttpContext.Request["token"];
IPrincipal user = this.GetUserFromToken(token);
if (user == null)
{
this.HandleUnAuthorizedRequest(filterContext);
}
else
{
FormsAuthentication.SetAuthCookie(user.Identity.Name, false);
filterContext.HttpContext.User = user;
}
}
private IPrincipal GetUserFromToken(string token)
{
// Here you could put your custom logic to decrypt the token and
// extract the associated user from it
throw new NotImplementedException();
}
private void HandleUnAuthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new ViewResult
{
ViewName = "~/Views/Shared/CustomError.cshtml",
};
}
}
and then you could decorate your action with this attribute:
[TokenAuthorize]
public ActionResult ProcessEmail(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction("Index", "Home");
}
I'm trying to implement an MVC4 web application with the following requirements:
(a) it offers its services to authenticated users only. As for authentication, I'd like to use simple membership, as it is the latest authentication technique from MVC, gives me the advantage of defining my own db tables, provides OAuth support out of the box, and is easily integrated with both MVC and WebApi.
(b) it exposes some core functions via WebApi for mobile/JS clients, which should be authenticated via basic HTTP authentication (+SSL). Typically I'll have JS clients using jQuery AJAX calls to WebApi controllers, decorated with the Authorize attribute for different user roles.
(c) ideally, in a mixed environment I would like to avoid a double authentication: i.e. if the user is already authenticated via browser, and is visiting a page implying a JS call to a WebApi controller action, the (a) mechanism should be enough.
Thus, while (a) is covered by the default MVC template, (b) requires basic HTTP authentication without the mediation of a browser. To this end, I should 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 of retrieving an IPrincipal from the received user name and password, and the WebSecurity class does not provide any method for this (except Login, but I would avoid changing the logged user just for the purpose of authorization, also because of potential "mixed" environments like (c)). So it seems my only option is giving up simple membership. Does anyone have better suggestions? Here is the relevant (slightly modified) code from the cited post:
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;
});
}
}
Here is another solution that meets all of your requirements. It uses SimpleMemberhsip with a mix of forms authentication and basic authentication in an MVC 4 application. It can also support Authorization, but it is not required by leaving the Role property null.
Thank you, this seems the best available solution 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 an MVC controller restricted action, I am redirected to the login page as expected to get authenticated.
2) when I trigger a jQuery ajax call to a WebApi controller restricted action, the call succeeds (except of course when not using SSL).
Yet, it does not work when after logging in in the website, the API call still requires authentication. Could anyone explain what's going here? In what follows I detail my procedure as I think it might be useful for starters like me.
Thank you (sorry for the formatting of what follows, but I cannot manage to let this editor mark code appropriately...)
Procedure
create a new mvc4 app (basic mvc4 app: this already comes with universal providers. All the universal providers class names start with Default...);
customize web.config for your non-local DB, e.g.:
<connectionStrings>
<add name="DefaultConnection"
providerName="System.Data.SqlClient"
connectionString="data source=(local)\SQLExpress;Initial Catalog=Test;Integrated Security=True;MultipleActiveResultSets=True" />
Also it's often useful to set a machineKey for hashing passwords, so that you can freely move this site around from server to server without having your passwords scrambled. Use a machine key generator website to define an entry like this:
<system.web>
<machineKey
validationKey="...thekey..."
decryptionKey="...thekey..."
validation="SHA1"
decryption="AES" />
if required create a new, empty database corresponding to the connection string of your web.config. Then start our good old pal WSAT (from VS Project menu) and configure security by adding users and roles as required.
if you want to, add a HomeController with an Index action, because no controller is present in this template and thus you could not test-start your web app without it.
add Thinktecture.IdentityModel.45 from NuGet and add/update all your favorite NuGet packages. Notice that at the time of writing this, jquery validation unobtrusive from MS is no more compatible with jQuery 1.9 or higher. I rather 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 bundles (App_Start/BundleConfig.cs).
modify the WebApiConfig.cs in App_Start by adding it the code after the DefaultApi route configuration:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
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, the api controllers will be placed under Controllers/Api/, so create this folder.
Add to models a LoginModel.cs:
public class LoginModel
{
[Required]
[Display(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 a StringResources.resx resource (with code generation) I usually place under an Assets folder, with the 3 strings quoted in the attributes.
Add a ClaimsTransformer.cs to your solution root, like this:
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 your app root namespace):
<configSections>
<section name="system.identityModel"
type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
...
add the other models for account controller, with their views (you can derive them from MVC3 application template, even if I prefer changing them to more localizable-friendly variants using attributes requiring string resource names rather than literals).
to test browser-based authentication, add some [Authorized] action to a controller (e.g. HomeController), and try accessing it.
to test basic HTTP authentication, insert in some view (e.g. Home/Index) a code like this (set your user name 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 jQuery plugin for encoding/decoding Base64: jquery.base64.js and its minified counterpart.
To allow SSL, follow the instructions here: http://www.hanselman.com/blog/WorkingWithSSLAtDevelopmentTimeIsEasierWithIISExpress.aspx (basically, enable SSL in the web project properties and connect to the port specified in the property value).
Maybe this helps - sounds this is like your scenario:
http://leastprivilege.com/2012/10/23/mixing-mvc-forms-authentication-and-web-api-basic-authentication/
http://leastprivilege.com/2012/10/24/extensions-to-the-web-apimvc-formsbasic-auth-sample-claims-transformation-and-ajax/