Using the [Authorize] attribute on an ASP.Net Web API method causes a "401 Unauthorized" response.
I have an Http Module that handles the context.AuthenticateRequest event in which I examine the Authorization header (Basic authorization) of the request, and, if valid, set the System.Threading.Thread.CurrentPrincipal to a new GenericPrincipal containing a new GenericIdentity based on the info in the Authorization header. I also set the HttpContext.Current.User to the same instance of GenericPrincipal.
At this point, the IsAuthenticated property of the IIdentity is true. However, by the time the action method in the controller is invoked, System.Threading.Thread.CurrentPrincipal has been set to a System.Security.Claims.ClaimsPrincipal containing a System.Security.Claims.ClaimsIdentity with IsAuthenticated = false.
So... somewhere in the pipeline between the point where I set the CurrentPrincipal and when it reaches the action method, the CurrentPrincipal and the Identity is getting replaced.
Some of the methods of the API access ASP.Net Identity users (for a related website, the API itself does not use ASP.Net Identity for authentication/authorization), so the API project is set up with all the relevant ASP.Net Identity NuGet packages, etc.
I've used the same Http Module in other API projects that DON'T have all the ASP.Net Identity NuGet packages, etc. and it works like a champ.
I suspect that the ASP.Net Identity configuration is causing my Basic Authentication System.Security.Claims.ClaimsPrincipal to be replaced.
Any help would be much appreciated!
Here's my code:
Http Module - at the end of this method, System.Threading.Thread.CurrentPrincipal and HttpContext.Current.User are correctly set.
public class FsApiHttpAuthentication : IHttpModule, IDisposable {
public void Init( HttpApplication context ) {
context.AuthenticateRequest += AuthenticateRequests;
context.EndRequest += TriggerCredentials;
}
private static void AuthenticateRequests( object sender, EventArgs e ) {
string authHeader = HttpContext.Current.Request.Headers["Authorization"];
if ( authHeader != null ) {
System.Net.Http.Headers.AuthenticationHeaderValue authHeaderVal = System.Net.Http.Headers.AuthenticationHeaderValue.Parse(authHeader);
if ( authHeaderVal.Parameter != null ) {
byte[] unencoded = Convert.FromBase64String(authHeaderVal.Parameter);
string userpw = System.Text.Encoding.GetEncoding("iso-8859-1").GetString(unencoded);
string[] creds = userpw.Split(':');
CredentialCache.Credential cred = CredentialCache.GetCredential(creds[0], creds[1]);
if ( cred != null ) {
System.Security.Principal.GenericIdentity identity = new System.Security.Principal.GenericIdentity
(cred.Username);System.Threading.Thread.CurrentPrincipal = new System.Security.Principal.GenericPrincipal(identity, roles);
if ( string.IsNullOrWhiteSpace(cred.RolesList) ) {
System.Threading.Thread.CurrentPrincipal = new System.Security.Principal.GenericPrincipal(identity, null);
} else {
System.Threading.Thread.CurrentPrincipal = new System.Security.Principal.GenericPrincipal(identity, cred.RolesList.Split(','));
}
HttpContext.Current.User = System.Threading.Thread.CurrentPrincipal;
}
}
}
}
Api Controller - when the Post action in this controller is reached, System.Threading.Thread.CurrentPrincipal and HttpContext.Current.User have been set to a System.Security.Claims.ClaimsPrincipal containing a System.Security.Claims.ClaimsIdentity with IsAuthenticated = false.
public class ConsumerAccountController : ApiController {
private ApplicationUserManager _userManager;
private ApplicationUserManager UserManager {
get {
return _userManager ?? Request.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
set {
_userManager = value;
}
}
public async System.Threading.Tasks.Task<IHttpActionResult> Post( API.FinancialSamaritan.com.ViewModels.UserCredentials creds ) {
API.FinancialSamaritan.com.ViewModels.CreateUserResult cccur = null;
try {
string username = creds.Username;
string password = creds.Password;
var user = new API.FinancialSamaritan.com.Models.ApplicationUser {
UserName = username,
Email = username,
SecurityQuestion = creds.SecurityQuestion,
SecurityAnswer = UserManager.PasswordHasher.HashPassword(creds.SecurityAnswer),
IsPasswordChangeRequired = true,
EmailConfirmed = true
};
IdentityResult userResult = await UserManager.CreateAsync(user, password);
...
The [Authorize] attribute was deriving from System.Web.Http.AuthorizeAttribute instead of System.Web.Mvc.AuthorizeAttribute. I had to fully qualify the namespace of the attribute so that MVC version of [Authorize] would be used. Thanks to #RonBrogan for pointing me in the right direction!
Related
I am new to ASP.NET core itself. However, I am creating WebAPIs in ASP.NET Core 2.0. I have configured JWT Bearer Token based authentication. Below is my Controller which return token.
[AllowAnonymous]
[Route("api/[controller]")]
public class TokenController : Controller
{
private readonly UserManager<UserEntity> userManager;
private readonly SignInManager<UserEntity> signInManager;
public TokenController(UserManager<UserEntity> userManager, SignInManager<UserEntity> signInManager)
{
this.userManager = userManager;
this.signInManager = signInManager;
}
// GET: api/values
[HttpGet]
public async Task<IActionResult> Get(string username, string password, string grant_type)
{
{
var user = await userManager.FindByEmailAsync(username);
if (user != null)
{
var result =await signInManager.CheckPasswordSignInAsync(user, password, false);
if (result.Succeeded)
{
var claims = new[]
{
new Claim( JwtRegisteredClaimNames.Sub, username),
new Claim( JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim( JwtRegisteredClaimNames.GivenName, "SomeUserID")
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("secretesecretesecretesecretesecretesecrete"));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken( issuer: "test",
audience: "test",
claims: claims,
expires: DateTime.Now.AddDays(15),
signingCredentials: creds);
return Ok(new { access_token = new JwtSecurityTokenHandler().WriteToken(token), expires_on=DateTime.Now.AddDays(15) });
}
}
}
return BadRequest("Could not create token");
}
}
But when calling ValuesController API which is decorated with [Authorize] attributes. I am getting User.Identity.Name is empty. I am not getting any information about user. I am not sure, My token controller is correctly written. As long as it is protecting my ValuesController, I assume, it is correct. However, I might be missing something. Please help.
Note: I am developing using Visual Studio 2017 with Mac Community
addition
Yes, you need to specify the claim for the unique name which is translated into the user.identity.name:
new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName)
I've also been having this problem with ASP.Net Core 2, and I'm really surprised no one's discovered the other cause of this problem.
When my webapp is deployed to IIS, "User.Identity.Name" always returns null. The IIS site has anonymous access disabled, and windows authentication is enabled.
BUT.
I didn't realise that my ASP.Net Core 2 has a "launchSettings.json" file, quietly hidden under the Properties folder, and in there, there's also some iisSettings, and in here "windowsAuthentication" was, strangely, set as false by default.
Changing "windowsAuthentication" to true, and "anonymousAuthentication" to false solved the problem for me.
After doing this, "User.Identity.Name" did finally contain the correct username.
But what the heck is this setting ? Why would this get priority over the actual settings we've setup in IIS Manager ?!
Had this problem too (Core 3.1) using the "DefaultIdentity" (Individual User Accounts).
User.Identity.Name is null, User.Identity.IsAuthenticated = true.
By using httpContextAccessor you can get the userId an with that id you can find the user and the UserName.
In your controller add
using System.Security.Claims;
...
private readonly IHttpContextAccessor _httpContextAccessor;
public MyController(MyContext context, IHttpContextAccessor httpContextAccessor)
{
_context = context;
_httpContextAccessor = httpContextAccessor;
}
// Any method username needed
[HttpGet("{id}")]
public async Task<ActionResult<MyInfo>> GetMyInfo(int id)
{
var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
var user = _context.AspNetUsers.Find(userId);
var userName = user.UserName;
...
}
In the Startup.cs add the following line:
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
For Azure OAuth v2, use preferred_username instead of unique_name (see this and this).
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
serviceCollection.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.RoleClaimType = "roles";
options.TokenValidationParameters.NameClaimType = "preferred_username";
//options.TokenValidationParameters.NameClaimType = "email"; // or if you want to use user's email for User.Identity.Name
//below lines of code can be removed. just there if you want some code to be executed right after user is validated.
options.Events.OnTokenValidated = async context =>
{
var personFirstName = context.Principal.FindFirstValue("given_name") ?? string.Empty;
var personLastName = context.Principal.FindFirstValue("family_name") ?? string.Empty;
var personEmail = context.Principal.FindFirstValue("email")?.ToLower();
var personName = context.Principal.Identity.Name;
};
});
Then in your controllers, you will get username from User.Identity.Name
I've been trying to setup a new IdentityServer3 with AspNetIdentity for a few days now. I'm able to login using my existing Identity DB and that's all good but I can never get the User.Identity.Name to contain data.
I've tried multiple attempts at adding custom claims & scopes and adding scopes to clients.
Finally, I loaded up the IdentityServer3 Sample repository and tested it out with the webforms client project since it already used the User.Identity.Name in it's About page.
Using WebForms sample client + AspNetIdentity sample server = User.Identity.Name is always null
Using WebForms sample client + SelfHost with Seq sample server = User.Identity.Name with data
I've tried other sample host projects that all populate the User.Identity.Name value just fine.
Now, on the client side I've written a workaround to pull the 'preferred_username' claim value and set the 'name' claim with it.
var id = new claimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaims(userInfoResponse.GetClaimsIdentity().Claims);
//set the User.Identity.Name value
var name = id.Claims.Where(x => x.Type == "name").Select(x => x.Value).FirstOrDefault() ??
id.Claims.Where(x => x.Type == "preferred_username").Select(x => x.Value).FirstOrDefault();
id.AddClaim(new Claim("name", name));
My questions are:
Why doesn't the AspNetIdentity package fill this by default?
And what do I need to change on the server side so that I don't need to change the client?
public static IEnumerable<ApiResource> GetApis()
{
return new ApiResource[]
{
new ApiResource("MyApi", "My Admin API")
{
UserClaims = { JwtClaimTypes.Name, JwtClaimTypes.Email }
}
};
}
In Identityserver4 you can add the UserClaims to your resource. Fixed it for me.
On IdentityServer4 you can implement IProfileService on server and add the Claim in GetProfileDataAsync
public class AspNetIdentityProfileService : IProfileService
{
protected UserManager<ApplicationUser> _userManager;
public AspNetIdentityProfileService(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
//Processing
var user = _userManager.GetUserAsync(context.Subject).Result;
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName),
};
context.IssuedClaims.AddRange(claims);
//Return
return Task.FromResult(0);
}
public Task IsActiveAsync(IsActiveContext context)
{
//Processing
var user = _userManager.GetUserAsync(context.Subject).Result;
context.IsActive = (user != null) && ((!user.LockoutEnd.HasValue) || (user.LockoutEnd.Value <= DateTime.Now));
//Return
return Task.FromResult(0);
}
}
Then add "AddProfileService()" to your ConfigureServices method.
services.AddIdentityServer(...)
...
.AddProfileService<AspNetIdentityProfileService>();
Server
SignalR hub within MVC 5 WebApi 2,
Security: Bearer token
Client
C# class using HttpWebRequest to retrieve bearer token from WebApi controller /Token endpoint
I used the pattern described here and here to deliver the bearer token to my AuthorizeAttribute sub-class.
When the code within the AuthorizeHubConnection method executes the ticket delivered by the call to "secureDataFormat.Unprotect(token)" is always null. I have confirmed the token is identical on both ends of the communication.
Here is the override method:
public override bool AuthorizeHubConnection(AspNet.SignalR.Hubs.HubDescriptor hubDescriptor, IRequest request)
{
var dataProtectionProvider = new DpapiDataProtectionProvider();
var secureDataFormat = new TicketDataFormat(dataProtectionProvider.Create());
var token = request.QueryString.Get("Bearer");
var ticket = secureDataFormat.Unprotect(token);
if (ticket != null && ticket.Identity != null && ticket.Identity.IsAuthenticated)
{
// set the authenticated user principal into environment so that it can be used in the future
request.Environment["server.User"] = new ClaimsPrincipal(ticket.Identity);
return true;
}
return false;
}
When I run hub without the authorize attribute and set a breakpoint within the "OnConnected" override, the Context.User property is also null.
Any assistance would be greatly appreciated.
Rich
Finally figured this out, I was using the wrong library to decrypt the token. DpapiDataProtectionProvider is used in self-host scenarios, we are hosted in IIS. Here is the functioning code.
public override bool AuthorizeHubConnection(Microsoft.AspNet.SignalR.Hubs.HubDescriptor hubDescriptor, IRequest request)
{
var token = request.QueryString.Get("Bearer");
var ticket = Startup.OAuthOptions.AccessTokenFormat.Unprotect(token);
if (ticket != null && ticket.Identity != null && ticket.Identity.IsAuthenticated)
{
// set the authenticated user principal into environment so that it can be used in the future
request.Environment["server.User"] = new ClaimsPrincipal(ticket.Identity);
return true;
}
return false;
}
Here is my solution, WORK on Azure and local. AngularJS, Web API and SignalR
request.Environment["server.User"] this code doesn't work on Azure.
First i create my Custom Filter Class.
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public class QueryStringBearerAuthorizeAttribute : AuthorizeAttribute
{
public override bool AuthorizeHubConnection(Microsoft.AspNet.SignalR.Hubs.HubDescriptor hubDescriptor, IRequest request)
{
var _Authorization = request.QueryString.Get("Bearer");
if (!string.IsNullOrEmpty(_Authorization))
{
var ticket = Startup.OAuthOptions.AccessTokenFormat.Unprotect(_Authorization);
if (ticket != null && ticket.Identity != null && ticket.Identity.IsAuthenticated)
{
request.Environment["server.User"] = new ClaimsPrincipal(ticket.Identity);
return true;
}
}
return false;
}
public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
{
var connectionId = hubIncomingInvokerContext.Hub.Context.ConnectionId;
var request=hubIncomingInvokerContext.Hub.Context.Request;
var _Authorization = request.QueryString.Get("Bearer");
if (!string.IsNullOrEmpty(_Authorization))
{
//var token = _Authorization.Replace("Bearer ", "");
var ticket = Startup.OAuthOptions.AccessTokenFormat.Unprotect(_Authorization);
if (ticket != null && ticket.Identity != null && ticket.Identity.IsAuthenticated)
{
Dictionary<string, object> _DCI = new Dictionary<string, object>();
_DCI.Add("server.User", new ClaimsPrincipal(ticket.Identity));
hubIncomingInvokerContext.Hub.Context = new HubCallerContext(new ServerRequest(_DCI), connectionId);
return true;
}
}
return false;
}
}
Then in all my connection from SignalR i put
connection.qs = { Bearer:
localStorageService.get('authorizationData').token };
My Startup Class
public void Configuration(IAppBuilder app)
{
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration
{
EnableDetailedErrors = true
};
var authorizer = new QueryStringBearerAuthorizeAttribute();
var module = new AuthorizeModule(authorizer, authorizer);
GlobalHost.HubPipeline.AddModule(module);
map.RunSignalR(hubConfiguration);
});
GlobalHost.HubPipeline.AddModule(new LoggingPipelineModule());
ConfigureAuth(app);
}
It works perfect for me, i'm not sure if sending my token for quesry string instead from header is a security issue. Thats my solution using angularjs, asp.net web api, signal r for autenticate SignalR hubs with a beared token.
In your Hub you can Access to User variable in this way
public ClaimsPrincipal _User { get { return Context.Request.Environment["server.User"] as ClaimsPrincipal; } }
I have written a couple of ms lightswitch applications with forms authentication -> this creates aspnet_* tables in sql server.
How can I use the defined users, passwords, maybe even memberships, roles and application rights in a servicestack - application?
I have not tested this but I think it should get you started. Gladly stand corrected on any of my steps.
Things I think you will need to do..
In order to Authenticate against both 'systems' you'll need to set the Forms cookie and save your ServiceStack session.
Instead of calling FormsAuthentication.Authentiate() do something like below. This won't work until you complete all the steps.
var apiAuthService = AppHostBase.Resolve<AuthService>();
apiAuthService.RequestContext = System.Web.HttpContext.Current.ToRequestContext();
var apiResponse = apiAuthService.Authenticate(new Auth
{
UserName = model.UserName,
Password = model.Password,
RememberMe = false
});
Create a subclass of IUserAuthRepository (for retrieving membership/user/roles from aspnet_* tables and filling ServiceStack AuthUser).
CustomAuthRepository.cs (incomplete, but should get you started)
public class CustomAuthRepository : IUserAuthRepository
{
private readonly MembershipProvider _membershipProvider;
private readonly RoleProvider _roleProvider;
public CustomAuthRepository()
{
_membershipProvider = Membership.Provider;
_roleProvider = Roles.Provider;
}
public UserAuth GetUserAuthByUserName(string userNameOrEmail)
{
var user = _membershipProvider.GetUser(userNameOrEmail, true);
return new UserAuth {FirstName = user.UserName, Roles = _roleProvider.GetRolesForUser(userNameOrEmail).ToList() //FILL IN REST OF PROPERTIES};
}
public bool TryAuthenticate(string userName, string password, out UserAuth userAuth)
{
//userId = null;
userAuth = GetUserAuthByUserName(userName);
if (userAuth == null) return false;
if (FormsAuthentication.Authenticate(userName, password))
{
FormsAuthentication.SetAuthCookie(userName, false);
return true;
}
userAuth = null;
return false;
}
//MORE METHODS TO IMPLEMENT...
}
Wire Authentication up for ServiceStack in AppHost configure method.
var userRep = new CustomAuthRepository();
container.Register<IUserAuthRepository>(userRep);
Plugins.Add(
new AuthFeature(() => new AuthUserSession(),
new IAuthProvider[] {
new CredentialsAuthProvider()
}
));
I changed the following line:
private readonly IPrincipal _user =
new GenericPrincipal(new GenericIdentity("someUser"), null /* roles */);
to
private readonly IPrincipal _user =
new GenericPrincipal(new GenericIdentity("realUser"), null /* roles */);
and the following test broke, but I am not sure why and I am not sure how to trace it. I am new to TDD
[TestMethod]
public void ChangePassword_Post_ReturnsRedirectOnSuccess()
{
// Arrange
AccountController controller = GetAccountController();
ChangePasswordModel model = new ChangePasswordModel()
{
OldPassword = "goodOldPassword",
NewPassword = "goodNewPassword",
ConfirmPassword = "goodNewPassword"
};
// Act
ActionResult result = controller.ChangePassword(model);
// Assert
Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));
RedirectToRouteResult redirectResult = (RedirectToRouteResult)result;
Assert.AreEqual("ChangePasswordSuccess", redirectResult.RouteValues["action"]);
}
An explanation of how this works and why changing someUser to realUser would break the above test would be very helpful.
This suggests the login failed. You can confirm this by debugging the test and tracing through controller.ChangePassword(model);. Testing any action involving authentication requires mocking or replacing the membership provider.