asp.net identity CurrentPrincipal.Identity does not contain custom claim - claims-based-identity

So during login I create claims. However I seem to be losing my custom claim (UserData). Below is my code for creating the claims:
// create required claims
claims.Add(new Claim(ClaimTypes.NameIdentifier, UserContext.UserId.ToString()));
claims.Add(new Claim(ClaimTypes.Name, UserContext.Email));
// custom – my serialized UserContext object
claims.Add(new Claim(ClaimTypes.UserData, UserContext.Serialize(UserContext)));
var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent, AllowRefresh = true }, identity);
Checking the ClaimsIdentity (identity) I see:
{http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier: 12
{http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name: test#test.com}
{http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata: Something}
So while debuging I get the current principal immediately after:
var identity1 = (ClaimsPrincipal)System.Threading.Thread.CurrentPrincipal;
Now in identity1 I only see the following:
{http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier: 12}
{http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name: test#test.com}
{http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider: ASP.NET Identity}
{http://schemas.microsoft.com/ws/2008/06/identity/claims/role: Role1}
my custom UserData claim is gone.
Update:
just wanted to add that my AuthenticationManager is defined as:
HttpContext.GetOwinContext().Authentication;
Update:
I have resolved this issue. In my PasswordSignIn method I had accidentally carried over the ExtneralCookie authentication type instead of using the ApplicationCookie:
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExtenralCookie);
to
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);

you can try by reading ClaimsIdentity from current thread like,
/// <summary>
/// Gets authenticated identity
/// </summary>
private static ClaimsIdentity identity
{
get
{
return (ClaimsIdentity)Thread.CurrentPrincipal.Identity;
}
}
And read the claim by
/// <summary>
/// gets authenticated UserId
/// </summary>
public static int UserId
{
get
{
return Convert.ToInt32(identity.Claims.Where(c => c.Type == ClaimTypes.NameIdentifier).Select(c => c.Value).SingleOrDefault());
}
}
you have to get UserDetail type instance so , need to deserialize the claim value.

Related

Return "ui_locale" back to client

So I know how to make IdentityServer4 app to use culture that the challenging client has. By defining
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.UiLocales = "pl-PL";
return Task.CompletedTask;
},
}
I can make IdentityServer4 to also show me login page in "pl-PL". The trick is however, that I allow users to change the language on the login screen. How can I inform the client that culture info was changed during login?
Currently my client does not even show any page, goes directly to Login screen (thus from client app browser is redirected immediately to IdentityServer4 app, where a user can change his/her language).
It seems that this is not a functionality that IdentityServer4 offers (any contradictory comments welcome). So I ended up with using claims to pass the culture information back to my client.
So I created a class inheriting from IProfileService so I can load additional claim JwtClaimTypes.Locale to the idToken. However it seems that when it is running, it is in a different context then the user it runs for, so CultureInfo.CurrentCulture is set to a different locale than what I was expecting (for example the UI was set pl-PL but inside profile service, it was set to en-US). So I ended up with creating a InMemoryUserInfo class that is basically a wrapped ConcurrentDictionary that contains my user id and an object that contains user's selected locale. I create entry/update that dictionary, whenever user changes the preferred language or when a user language is delivered from the database. Anyway, that InMemoryUserInfo is then injected into my profile service where it is added as another claim:
public class IdentityWithAdditionalClaimsProfileService : IProfileService
{
private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory;
private readonly UserManager<ApplicationUser> _userManager;
/// <summary>
/// This services is running in a different thread then UI, so
/// when trying to obtain CultureInfo.CurrentUICulture, it not necessarily
/// is going to be correct. So whenever culture is changed,
/// it is stored in InMemoryUserInfo. Current user's culture will
/// be included in a claim.
/// </summary>
private readonly InMemoryUserInfo _userInfo;
public IdentityWithAdditionalClaimsProfileService(
IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory,
UserManager<ApplicationUser> userManager,
InMemoryUserInfo userInfo)
{
_claimsFactory = claimsFactory;
_userManager = userManager;
_userInfo = userInfo;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
var principal = await _claimsFactory.CreateAsync(user);
var claims = principal.Claims.ToList();
claims = claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();
claims.Add(new Claim(JwtClaimTypes.Locale, _userInfo.Get(user.Id).Culture ?? throw new ArgumentNullException()));
context.IssuedClaims = claims;
}
public async Task IsActiveAsync(IsActiveContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
context.IsActive = user != null;
}
}
Remember to register IProfileService with DI
services.AddTransient<IProfileService, IdentityWithAdditionalClaimsProfileService>();
Afterwards, in my client's startup, I analyse the claims in OpenIdConnectEvents and set the cookie to culture received from IdentityServer:
.AddOpenIdConnect("oidc", options =>
{
options.Events = new OpenIdConnectEvents
{
OnTicketReceived = context =>
{
//Goes through returned claims from authentication endpoint and looks for
//localization info. If found and different, then new CultureInfo is set.
string? culture = context.Principal?.FindFirstValue(JwtClaimTypes.Locale);
if (culture != null && CultureInfo.CurrentUICulture.Name != culture)
{
context.HttpContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(
new RequestCulture(culture, culture)),
new CookieOptions
{ Expires = DateTimeOffset.UtcNow.AddYears(1) }
);
}
return Task.CompletedTask;
};
}
});

ASP.NET Core Identity 2: User.IsInRole always returns false

The question: I call RoleManager.CreateAsync() and RoleManager.AddClaimAsync() to create roles and associated role claims. Then I call UserManager.AddToRoleAsync() to add users to those roles. But when the user logs in, neither the roles nor the associated claims show up in the ClaimsPrincipal (i.e. the Controller's User object). The upshot of this is that User.IsInRole() always returns false, and the collection of Claims returned by User.Claims doesn't contain the role claims, and the [Authorize(policy: xxx)] annotations don't work.
I should also add that one solution is to revert from using the new services.AddDefaultIdentity() (which is provided by the templated code) back to calling services.AddIdentity().AddSomething().AddSomethingElse(). I don't want to go there, because I've seen too many conflicting stories online about what I need to do to configure AddIdentity for various use cases. AddDefaultIdentity seems to do most things correctly without a lot of added fluent configuration.
BTW, I'm asking this question with the intention of answering it... unless someone else gives me a better answer than the one I'm prepared to post. I'm also asking this question because after several weeks of searching I have yet to find a good end-to-end example of creating and using Roles and Claims in ASP.NET Core Identity 2. Hopefully, the code example in this question might help someone else who stumbles upon it...
The setup:
I created a new ASP.NET Core Web Application, select Web Application (Model-View-Controller), and change the Authentication to Individual User Accounts. In the resultant project, I do the following:
In Package Manager Console, update the database to match the scaffolded migration:
update-database
Add an ApplicationUser class that extends IdentityUser. This involves adding the class, adding a line of code to the ApplicationDbContext and replacing every instance of <IdentityUser> with <ApplicationUser> everywhere in the project.
The new ApplicationUser class:
public class ApplicationUser : IdentityUser
{
public string FullName { get; set; }
}
The updated ApplicationDbContext class:
public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{ }
// Add this line of code
public DbSet<ApplicationUser> ApplicationUsers { get; set; }
}
In Package Manager Console, create a new migration and update the database to incorporate the ApplicationUsers entity.
add-migration m_001
update-database
Add the following line of code in Startup.cs to enable RoleManager
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>() // <-- Add this line
.AddEntityFrameworkStores<ApplicationDbContext>();
Add some code to seed roles, claims, and users. The basic concept for this sample code is that I have two claims: can_report allows the holder to create reports, and can_test allows the holder to run tests. I have two Roles, Admin and Tester. The Tester role can run tests, but can't create reports. The Admin role can do both. So, I add the claims to the roles, and create one Admin test user and one Tester test user.
First, I add a class whose sole purpose in life is to contain constants used elsewhere in this example:
// Contains constant strings used throughout this example
public class MyApp
{
// Claims
public const string CanTestClaim = "can_test";
public const string CanReportClaim = "can_report";
// Role names
public const string AdminRole = "admin";
public const string TesterRole = "tester";
// Authorization policy names
public const string CanTestPolicy = "can_test";
public const string CanReportPolicy = "can_report";
}
Next, I seed my roles, claims, and users. I put this code in the main landing page controller just for expedience; it really belongs in the "startup" Configure method, but that's an extra half-dozen lines of code...
public class HomeController : Controller
{
const string Password = "QwertyA1?";
const string AdminEmail = "admin#example.com";
const string TesterEmail = "tester#example.com";
private readonly RoleManager<IdentityRole> _roleManager;
private readonly UserManager<ApplicationUser> _userManager;
// Constructor (DI claptrap)
public HomeController(RoleManager<IdentityRole> roleManager, UserManager<ApplicationUser> userManager)
{
_roleManager = roleManager;
_userManager = userManager;
}
public async Task<IActionResult> Index()
{
// Initialize roles
if (!await _roleManager.RoleExistsAsync(MyApp.AdminRole)) {
var role = new IdentityRole(MyApp.AdminRole);
await _roleManager.CreateAsync(role);
await _roleManager.AddClaimAsync(role, new Claim(MyApp.CanTestClaim, ""));
await _roleManager.AddClaimAsync(role, new Claim(MyApp.CanReportClaim, ""));
}
if (!await _roleManager.RoleExistsAsync(MyApp.TesterRole)) {
var role = new IdentityRole(MyApp.TesterRole);
await _roleManager.CreateAsync(role);
await _roleManager.AddClaimAsync(role, new Claim(MyApp.CanTestClaim, ""));
}
// Initialize users
var qry = _userManager.Users;
IdentityResult result;
if (await qry.Where(x => x.UserName == AdminEmail).FirstOrDefaultAsync() == null) {
var user = new ApplicationUser {
UserName = AdminEmail,
Email = AdminEmail,
FullName = "Administrator"
};
result = await _userManager.CreateAsync(user, Password);
if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
result = await _userManager.AddToRoleAsync(user, MyApp.AdminRole);
if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
}
if (await qry.Where(x => x.UserName == TesterEmail).FirstOrDefaultAsync() == null) {
var user = new ApplicationUser {
UserName = TesterEmail,
Email = TesterEmail,
FullName = "Tester"
};
result = await _userManager.CreateAsync(user, Password);
if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
result = await _userManager.AddToRoleAsync(user, MyApp.TesterRole);
if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
}
// Roles and Claims are in a cookie. Don't expect to see them in
// the same request that creates them (i.e., the request that
// executes the above code to create them). You need to refresh
// the page to create a round-trip that includes the cookie.
var admin = User.IsInRole(MyApp.AdminRole);
var claims = User.Claims.ToList();
return View();
}
[Authorize(policy: MyApp.CanTestPolicy)]
public IActionResult Test()
{
return View();
}
[Authorize(policy: MyApp.CanReportPolicy)]
public IActionResult Report()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
and I register my authentication policies in the "Startup" ConfigureServices routine, just after the call to services.AddMvc
// Register authorization policies
services.AddAuthorization(options => {
options.AddPolicy(MyApp.CanTestPolicy, policy => policy.RequireClaim(MyApp.CanTestClaim));
options.AddPolicy(MyApp.CanReportPolicy, policy => policy.RequireClaim(MyApp.CanReportClaim));
});
Whew. Now, (assuming I've noted all of the applicable code I've added to the project, above), when I run the app, I notice that neither of my "built-in" test users can access either the /home/Test or /home/Report page. Moreover, if I set a breakpoint in the Index method, I see that my roles and claims do not exist in the User object. But I can look at the database and see all of the roles and claims are there.
So, to recap, the question asks why the code provided by the ASP.NET Core Web Application template doesn't load roles or role claims into the cookie when a user logs in.
After much Googling and experimenting, there appear to be two modifications that must be made to the templated code in order to get Roles and Role Claims to work:
First, you must add the following line of code in Startup.cs to enable RoleManager. (This bit of magic was mentioned in the OP.)
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>() // <-- Add this line
.AddEntityFrameworkStores<ApplicationDbContext>();
But wait, there's more! According to this discussion on GitHub, getting the roles and claims to show up in the cookie involves either reverting to the service.AddIdentity initialization code, or sticking with service.AddDefaultIdentity and adding this line of code to ConfigureServices:
// Add Role claims to the User object
// See: https://github.com/aspnet/Identity/issues/1813#issuecomment-420066501
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>>();
If you read the discussion referenced above, you'll see that Roles and Role Claims are apparently kind-of-deprecated, or at least not eagerly supported. Personally, I find it really useful to assign claims to roles, assign roles to users, and then make authorization decisions based on the claims (which are granted to the users based on their roles). This gives me an easy, declarative way to allow, for example, one function to be accessed by multiple roles (i.e. all of the roles that contain the claim used to enable that function).
But you DO want to pay attention to the amount of role and claim data being carried in the auth cookie. More data means more bytes sent to the server with each request, and I have no clue what happens when you bump up against some sort of limit to the cookie size.
Ahh, there are some changes from ASP.NET Core version 2.0 to 2.1. AddDefaultIdentity is the one.
I don't know where to start from your code, so, I will provide an example to create and get user role(s).
Let's create UserRoles first:
public enum UserRoles
{
[Display(Name = "Quản trị viên")]
Administrator = 0,
[Display(Name = "Kiểm soát viên")]
Moderator = 1,
[Display(Name = "Thành viên")]
Member = 2
}
Note: You can remove the attribute Display.
Then, we create RolesExtensions class:
public static class RolesExtensions
{
public static async Task InitializeAsync(RoleManager<IdentityRole> roleManager)
{
foreach (string roleName in Enum.GetNames(typeof(UserRoles)))
{
if (!await roleManager.RoleExistsAsync(roleName))
{
await roleManager.CreateAsync(new IdentityRole(roleName));
}
}
}
}
Next, in the Startup.cs class, we run it:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
RoleManager<IdentityRole> roleManager)
{
// other settings...
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
var task = RolesExtensions.InitializeAsync(roleManager);
task.Wait();
}
Note: Configure requires a returned type void, so we need to create a task to initialize the user roles and we call Wait method.
Do not change the returned type like this:
public async void Configure(...)
{
await RolesExtensions.InitializeAsync(roleManager);
}
Source: Async/Await - Best Practices in Asynchronous Programming
In the ConfigureServices method, these configurations would NOT work (we cannot use User.IsInRole correctly):
services.AddDefaultIdentity<ApplicationUser>()
//.AddRoles<IdentityRole>()
//.AddRoleManager<RoleManager<IdentityRole>>()
.AddEntityFrameworkStores<ApplicationDbContext>();
I don't know why but AddRoles and AddRoleManager don't support to check role for a user (User.IsInRole).
In this case, we need to register service like this:
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
By using this way, we create 3 user roles in the databse:
When register new user, we just need to call:
await _userManager.AddToRoleAsync(user, nameof(UserRoles.Administrator));
Finally, we can use [Authorize(Roles = "Administrator")] and:
if (User.IsInRole("Administrator"))
{
// authorized
}
// or
if (User.IsInRole(nameof(UserRoles.Administrator)))
{
// authorized
}
// but
if (User.IsInRole("ADMINISTRATOR"))
{
// authorized
}
P/S: There are a lot things which need to be implement to achieve this goal. So maybe I missed something in this example.
Also you can try to fix Authentication like this
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
});
If I use “Roles” instead of ClaimTypes.Role in .net6 blazor wasm , #attribute [Authorize(Roles = "admin")] not work and get this error in browser console :
RolesAuthorizationRequirement:User.IsInRole must be true for one of the following roles: (admin)”
By using of ClaimTypes.Role the problem resolved :
private async Task<List<Claim>> GetClaimsAsync(User user)
{
var claims = new List<Claim>()
{
new Claim("UserName", user.Email),
new Claim("FullName", user.FirstName+" "+user.LastName),
};
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
claims.Add(new Claim(ClaimTypes.Role, role)); // this line
return claims;
}
https://github.com/mammadkoma/Attendance/blob/master/Attendance/Server/Controllers/AccountsController.cs

AuthenticationHandler error AuthenticationScheme: Bearer was forbidden

I'm making a small service using ASP.NET Core. The first complicated thing I'm facing now is to authenticate an user into my system.
Let me introduce about my authentication flow:
+)Client -> call (api/account/authorize) -> System check whether client is valid or not -> Send token back to client as him/her is valid.
+) Client -> uses the obtained token -> requests to api/account/filter -> Service validates the token and throw the information back.
I've read some tutorials about JWT, but the response doesn't include enough information as I need. I want :
Throw 401 and a message describes that status code , i.e: "ACCOUNT_DISABLED", "ACCOUNT_PENDING", "ACCOUNT_PERMISSION_INSUFFICIENT", ... not just 401.
Therfore, I implemented my own Authenticate validator:
public class BearerAuthenticationHandler : AuthenticationHandler<BearerAuthenticationOption>
{
#region Properties
/// <summary>
/// Inject dependency service into the handler.
/// </summary>
private readonly JwtTokenSetting _encryptionSetting;
/// <summary>
/// Inject dependency service into the handler.
/// </summary>
private readonly IEncryptionService _encryptionService;
/// <summary>
/// Inject time service to handler.
/// </summary>
private readonly ITimeService _timeService;
private readonly IRepositoryAccount _repositoryAccount;
#endregion
#region Constructors
/// <summary>
/// Initialize an instance of handler with specific dependency injections.
/// </summary>
/// <param name="encryptionSetting"></param>
/// <param name="encryptionService"></param>
/// <param name="timeService"></param>
/// <param name="repositoryAccount"></param>
public BearerAuthenticationHandler(JwtTokenSetting encryptionSetting, IEncryptionService encryptionService, ITimeService timeService, IRepositoryAccount repositoryAccount)
{
_encryptionSetting = encryptionSetting;
_encryptionService = encryptionService;
_timeService = timeService;
_repositoryAccount = repositoryAccount;
}
#endregion
#region Methods
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
#region Token analyzation
// Find the authorization key in request.
var authorizationKey =
Request.Headers.Keys.FirstOrDefault(x => x.Equals("authorization", StringComparison.OrdinalIgnoreCase));
// Authorization key is not found in the request.
if (string.IsNullOrWhiteSpace(authorizationKey))
return AuthenticateResult.Fail("No authorization is found in request header.");
// Find the token in Authorization.
var authorizationValue = Request.Headers[authorizationKey].ToString();
// Authentication scheme prefix.
var authenticationScheme = $"{Options.AuthenticationScheme} ";
// No token has been specified.
if (string.IsNullOrWhiteSpace(authorizationValue) || !authorizationValue.StartsWith(authenticationScheme, StringComparison.OrdinalIgnoreCase))
return AuthenticateResult.Fail("No bearer token is found in request header.");
// Cut the string to obtain bearer token.
var accessToken = authorizationValue.Substring(authenticationScheme.Length);
#endregion
#region Token validation
// Decrypt the token.
var tokenDetailViewModel = _encryptionService.Decrypt<TokenDetailViewModel>(accessToken, _encryptionSetting.Key);
// No detail has been found.
if (tokenDetailViewModel == null)
{
InitializeHttpResponse(Response, HttpStatusCode.Unauthorized, new HttpResponseViewModel
{
Message = "TOKEN_INVALID"
});
return AuthenticateResult.Fail("Token is invalid");
}
// Find the current unix time on server.
var unixTime = _timeService.UtcToUnix(DateTime.UtcNow);
// Token is expired.
if (unixTime > tokenDetailViewModel.Expire)
{
InitializeHttpResponse(Response, HttpStatusCode.Unauthorized, new HttpResponseViewModel
{
Message = "TOKEN_EXPIRED"
});
return AuthenticateResult.Fail("Token is expired");
}
// Account filter construction.
var filterAccountViewModel = new FilterAccountViewModel
{
Email = tokenDetailViewModel.Email,
EmailComparison = TextComparision.Equal,
Password = tokenDetailViewModel.Password,
PasswordComparision = TextComparision.EqualIgnoreCase,
Statuses = new[] { AccountStatus.Active }
};
// Find the first condition statisfied account in the database.
var account = await _repositoryAccount.FindAccountAsync(filterAccountViewModel);
// Account cannot be found in the database.
if (account == null)
{
InitializeHttpResponse(Response, HttpStatusCode.Unauthorized, new HttpResponseViewModel
{
Message = "ACCOUNT_INVALID"
});
return AuthenticateResult.Fail("Account is invalid");
}
#endregion
var claimsIdentity = new ClaimsIdentity();
claimsIdentity.AddClaim(new Claim(nameof(JwtClaim.Email), account.Email));
claimsIdentity.AddClaim(new Claim(nameof(JwtClaim.Status), nameof(account.Status)));
// Update user into context.
var claimPrincipal = new ClaimsPrincipal(claimsIdentity);
// Initialize an authentication ticket.
var authenticationTicket = new AuthenticationTicket(claimPrincipal, new AuthenticationProperties
{
AllowRefresh = true,
ExpiresUtc = DateTime.UtcNow.AddMinutes(30),
IsPersistent = true,
IssuedUtc = DateTime.UtcNow
}, "Bearer");
return AuthenticateResult.Success(authenticationTicket);
}
/// <summary>
/// Initialize an application/json response.
/// </summary>
/// <param name="httpResponse"></param>
/// <param name="httpStatusCode"></param>
/// <param name="httpResponseViewModel"></param>
private void InitializeHttpResponse(HttpResponse httpResponse, HttpStatusCode httpStatusCode, HttpResponseViewModel httpResponseViewModel)
{
// Response must be always application/json.
httpResponse.ContentType = "application/json";
httpResponse.StatusCode = (int)httpStatusCode;
if (httpResponseViewModel == null)
return;
using (var streamWriter = new StreamWriter(httpResponse.Body))
{
streamWriter.AutoFlush = true;
streamWriter.WriteLineAsync(JsonConvert.SerializeObject(httpResponseViewModel));
}
}
#endregion
}
Here is my AccountController:
[Route("api/[controller]")]
public class AccountController : Controller
{
private readonly IRepositoryAccount _repositoryAccount;
private readonly IEncryptionService _encryptionService;
private readonly ITimeService _timeService;
private readonly JwtTokenSetting _jwtTokenSetting;
public AccountController(IRepositoryAccount repositoryAccount, IEncryptionService encryptionService, ITimeService timeService,
IOptions<JwtTokenSetting> jwtTokenSetting)
{
_repositoryAccount = repositoryAccount;
_encryptionService = encryptionService;
_timeService = timeService;
_jwtTokenSetting = jwtTokenSetting.Value;
}
[HttpPost("authorize")]
[AllowAnonymous]
public async Task<IActionResult> Authorize([FromBody] LoginViewModel loginViewModel)
{
// Find the encrypted password of login information.
var filterAccountViewModel = new FilterAccountViewModel();
filterAccountViewModel.Email = loginViewModel.Email;
filterAccountViewModel.EmailComparison = TextComparision.Equal;
filterAccountViewModel.Password = _encryptionService.FindEncryptPassword(loginViewModel.Password);
filterAccountViewModel.PasswordComparision = TextComparision.EqualIgnoreCase;
filterAccountViewModel.Statuses = new[] {AccountStatus.Active};
// Initialize HttpResponseViewModel.
var httpResponseViewModel = new HttpResponseViewModel();
// Find the account.
var account = await _repositoryAccount.FindAccountAsync(filterAccountViewModel);
// Account is not found.
if (account == null)
{
Response.ContentType = "application/json";
using (var streamWriter = new StreamWriter(Response.Body))
{
httpResponseViewModel.Message = "ACCOUNT_INVALID";
await streamWriter.WriteLineAsync(JsonConvert.SerializeObject(httpResponseViewModel));
}
return new UnauthorizedResult();
}
// Initialize token detail.
var tokenDetailViewModel = new TokenDetailViewModel
{
Email = loginViewModel.Email,
Password = filterAccountViewModel.Password,
Expire = _timeService.UtcToUnix(DateTime.UtcNow.AddSeconds(_jwtTokenSetting.Expire))
};
// Initialize token information and throw to client for their future use.
var tokenGeneralViewModel = new TokenGeneralViewModel
{
AccessToken = _encryptionService.Encrypt(tokenDetailViewModel, _jwtTokenSetting.Key),
Expire = _jwtTokenSetting.Expire
};
return Ok(tokenGeneralViewModel);
}
[HttpPost("filter")]
[Authorize(ActiveAuthenticationSchemes = "Bearer")]
public IEnumerable<string> FindAllAccounts()
{
Response.StatusCode = (int)HttpStatusCode.Accepted;
return new[] { "1", "2", "3", "4" };
}
}
When I use the token generated by api/account/authorize to access api/account/filter. An error was thrown to me :
AuthenticationScheme: Bearer was forbidden
Can anyone please tell me why ? Is my implementation the best approach or not ?
Thank you,
Is my implementation the best approach or not ?
I wouldn't do this as you implemented. Because(1 and 3 are just my opinions)
ACCOUNT_DISABLED, ACCOUNT_PENDING,
ACCOUNT_PERMISSION_INSUFFICIENT this statuses doesn't mean that user has to
retype its cridentials.
Even if i want to use 401 with a message, before creating my own
handler implementation, i would consider using jwt bearer events. OnChallenge event seems good to do this(See this answer how to implement).
I think your requirement is related with authorization rather than authentication. So writing a policy
would be better.
To use policy i don't know simple implementation, but here is my attempt:
Authorization Handler:
public class CheckUserRequirement : IAuthorizationRequirement
{
}
public class CheckUserAuthorizationHandler : AuthorizationHandler<CheckUserRequirement>
{
private readonly IHttpContextAccessor _accessor;
public SimpleAuthorizationHandler(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, SimpleRequirement requirement)
{
if(account.isDisabled)
{
_accessor.HttpContext.Response.Headers.Add("error_code", "ACCOUNT_DISABLED");
}
//...
context.Succeed(requirement);
}
}
ConfigureServices:
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IAuthorizationHandler, CheckUserAuthorizationHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("CheckUser", policy => { policy.AddRequirements(new CheckUserRequirement()); });
});
And use it:
[Authorize(Policy = "CheckUser")]
public class SomeController
Edit
I had suggested OnChallenge event, but i realized that it is not suitable for your case. See my another answer

Custom IsInRole() when using Cookie Middleware without ASP.NET Identity

To maintain compatibility with existing applications I was planing on using Cookie Middleware without ASP.NET Identity, as described in the documentation:
https://docs.asp.net/en/latest/security/authentication/cookie.html
This seems to work as expected as far as logging a user in, but I'm having issues with roles -- specifically when using the [Authorize(Roles = "ADMIN")].
In the code below, I can call p.IsInRole("ADMIN") and my implementation of MyClaimsPrincipal.IsInRole() is called and returns true.
What doesn't work is the [Authorize(Roles = "ADMIN")] attribute because it ends up calling ClaimsPrincipal.IsInRole (which returns False) instead of MyClaimsPrincipal.IsInRole() (which returns True).
[Authorize(Roles = "ADMIN")]
public class MyAdminController : Controller
{
public IActionResult Index()
{
var p = new MyClaimsPrincipal(ClaimsPrincipal.Current);
bool isAdmin = p.IsInRole("ADMIN");
return View();
}
}
When not using Identity and only using Cookie Middleware, can I use the [Authorize(Roles = "ADMIN")] attribute?
How? :-)
If I had to guess, I'm not implementing p.IsInRole() correctly -- currently this method loads the roles, then returns a True/False. Perhaps I have to 'load' my roles elsewhere in such a way that the ClaimsPrincipal.IsInRole is sufficient. If I was using Identity(), I assume this would be an implementation of IUserRoleStore.
My other 'if i had to guess' answer is that somewhere in startup.cs I need to replace the current ClaimsPrincipal with an instance of MyClaimsPrincipal.
Thank you!
You should add role claims when cookie is created.
In startup.cs:
app.UseCookieAuthentication(options =>
{
options.AuthenticationScheme = "MyCookieMiddlewareInstance";
options.LoginPath = new PathString("/Account/Login/");
options.AccessDeniedPath = new PathString("/Account/Forbidden/");
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
});
And login post method may be something like this(i assume that you have a custom login page):
[HttpPost]
public IActionResult Login(string userName, string password, string returnUrl)
{
var user = _userService.GetUser(userName, password);// i assume that _userService is injected
if (user == null)
{
//return Error;
}
var claims = new List<Claim>()
{
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Name, user.GetFullName() ),
};
var identity = new ClaimsIdentity(claims, "Forms");
identity.AddClaim(new Claim(ClaimTypes.Role, "ADMIN"));
var principal = new ClaimsPrincipal(identity);
HttpContext.Authentication.SignInAsync("MyCookieMiddlewareInstance", principal);
return Redirect(returnUrl);
}

Custom OAuth client in MVC4 / DotNetOpenAuth - missing access token secret

I'm currently working on implementing a Dropbox OAuth client for my application. It's been a fairly painless process until I hit the end. Once I've authorized, when I attempt to access user data I get a 401 back from Dropbox about the token being invalid. I asked on the Dropbox forums and it looks like my request is missing the access_token_secret that Dropbox returns back. I was able to use Fiddler to dig out the secret and add it to my request url and it worked fine, so that's definitely the issue. So why doesn't DotNetOpenAuth return back the access token secret when it returns the access token?
For reference, my code:
public class DropboxClient : OAuthClient
{
public static readonly ServiceProviderDescription DropboxServiceDescription = new ServiceProviderDescription
{
RequestTokenEndpoint = new MessageReceivingEndpoint("https://api.dropbox.com/1/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://www.dropbox.com/1/oauth/authorize", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
AccessTokenEndpoint = new MessageReceivingEndpoint("https://api.dropbox.com/1/oauth/access_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new PlaintextSigningBindingElement() }
};
public DropboxClient(string consumerKey, string consumerSecret) :
this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager())
{
}
public DropboxClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager) :
base("dropbox", DropboxServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
{
}
protected override DotNetOpenAuth.AspNet.AuthenticationResult VerifyAuthenticationCore(DotNetOpenAuth.OAuth.Messages.AuthorizedTokenResponse response)
{
var profileEndpoint = new MessageReceivingEndpoint("https://api.dropbox.com/1/account/info", HttpDeliveryMethods.GetRequest);
HttpWebRequest request = this.WebWorker.PrepareAuthorizedRequest(profileEndpoint, response.AccessToken);
try
{
using (WebResponse profileResponse = request.GetResponse())
{
using (Stream profileResponseStream = profileResponse.GetResponseStream())
{
using (StreamReader reader = new StreamReader(profileResponseStream))
{
string jsonText = reader.ReadToEnd();
JavaScriptSerializer jss = new JavaScriptSerializer();
dynamic jsonData = jss.DeserializeObject(jsonText);
Dictionary<string, string> extraData = new Dictionary<string, string>();
extraData.Add("displayName", jsonData.display_name ?? "Unknown");
extraData.Add("userId", jsonData.uid ?? "Unknown");
return new DotNetOpenAuth.AspNet.AuthenticationResult(true, ProviderName, extraData["userId"], extraData["displayName"], extraData);
}
}
}
}
catch (WebException ex)
{
using (Stream s = ex.Response.GetResponseStream())
{
using (StreamReader sr = new StreamReader(s))
{
string body = sr.ReadToEnd();
return new DotNetOpenAuth.AspNet.AuthenticationResult(new Exception(body, ex));
}
}
}
}
}
I found your question when I was searching for solution to a similar problem. I solved it by making 2 new classes, which you can read about in this coderwall post.
I'll also copy and paste the full post here:
DotNetOpenAuth.AspNet 401 Unauthorized Error and Persistent Access Token Secret Fix
When designing QuietThyme, our Cloud Ebook Manager, we knew that everyone hates creating new accounts just as much as we do. We started looking for OAuth and OpenId libraries that we could leverage to allow for social login. We ended up using the DotNetOpenAuth.AspNet library for user authentication, because it supports Microsoft, Twitter, Facebook, LinkedIn and Yahoo, and many others right out of the bow. While we had some issues setting it all up, in the end we only needed to do a few small customizations to get most of it working (described in a previous coderwall post). We noticed that, unlike all the others, the LinkedIn client would not authenticate, returning a 401 Unauthorized Error from DotNetOpenAuth. It quickly became apparent that this was due to a signature issue, and after looking at the source we were able to determine that the retrieved AccessToken secret is not being used with the authenticated profile info request.
It acutally makes sense, the reason that OAuthClient class doesn't include the retrieved access token secret is that it's normally not needed for authentication purposes, which is the primary purpose of the ASP.NET OAuth library.
We needed to make authenticated requests against the api, after the user has logged in, to retrieve some standard profile information, including email address and full name. We were able to solve this issue by making use of an InMemoryOAuthTokenManager temporarily.
public class LinkedInCustomClient : OAuthClient
{
private static XDocument LoadXDocumentFromStream(Stream stream)
{
var settings = new XmlReaderSettings
{
MaxCharactersInDocument = 65536L
};
return XDocument.Load(XmlReader.Create(stream, settings));
}
/// Describes the OAuth service provider endpoints for LinkedIn.
private static readonly ServiceProviderDescription LinkedInServiceDescription =
new ServiceProviderDescription
{
AccessTokenEndpoint =
new MessageReceivingEndpoint("https://api.linkedin.com/uas/oauth/accessToken",
HttpDeliveryMethods.PostRequest),
RequestTokenEndpoint =
new MessageReceivingEndpoint("https://api.linkedin.com/uas/oauth/requestToken?scope=r_basicprofile+r_emailaddress",
HttpDeliveryMethods.PostRequest),
UserAuthorizationEndpoint =
new MessageReceivingEndpoint("https://www.linkedin.com/uas/oauth/authorize",
HttpDeliveryMethods.PostRequest),
TamperProtectionElements =
new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() },
//ProtocolVersion = ProtocolVersion.V10a
};
private string ConsumerKey { get; set; }
private string ConsumerSecret { get; set; }
public LinkedInCustomClient(string consumerKey, string consumerSecret)
: this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager()) { }
public LinkedInCustomClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager)
: base("linkedIn", LinkedInServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
{
ConsumerKey = consumerKey;
ConsumerSecret = consumerSecret;
}
//public LinkedInCustomClient(string consumerKey, string consumerSecret) :
// base("linkedIn", LinkedInServiceDescription, consumerKey, consumerSecret) { }
/// Check if authentication succeeded after user is redirected back from the service provider.
/// The response token returned from service provider authentication result.
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
Justification = "We don't care if the request fails.")]
protected override AuthenticationResult VerifyAuthenticationCore(AuthorizedTokenResponse response)
{
// See here for Field Selectors API http://developer.linkedin.com/docs/DOC-1014
const string profileRequestUrl =
"https://api.linkedin.com/v1/people/~:(id,first-name,last-name,headline,industry,summary,email-address)";
string accessToken = response.AccessToken;
var profileEndpoint =
new MessageReceivingEndpoint(profileRequestUrl, HttpDeliveryMethods.GetRequest);
try
{
InMemoryOAuthTokenManager imoatm = new InMemoryOAuthTokenManager(ConsumerKey, ConsumerSecret);
imoatm.ExpireRequestTokenAndStoreNewAccessToken(String.Empty, String.Empty, accessToken, (response as ITokenSecretContainingMessage).TokenSecret);
WebConsumer w = new WebConsumer(LinkedInServiceDescription, imoatm);
HttpWebRequest request = w.PrepareAuthorizedRequest(profileEndpoint, accessToken);
using (WebResponse profileResponse = request.GetResponse())
{
using (Stream responseStream = profileResponse.GetResponseStream())
{
XDocument document = LoadXDocumentFromStream(responseStream);
string userId = document.Root.Element("id").Value;
string firstName = document.Root.Element("first-name").Value;
string lastName = document.Root.Element("last-name").Value;
string userName = firstName + " " + lastName;
string email = String.Empty;
try
{
email = document.Root.Element("email-address").Value;
}
catch(Exception)
{
}
var extraData = new Dictionary<string, string>();
extraData.Add("accesstoken", accessToken);
extraData.Add("name", userName);
extraData.AddDataIfNotEmpty(document, "headline");
extraData.AddDataIfNotEmpty(document, "summary");
extraData.AddDataIfNotEmpty(document, "industry");
if(!String.IsNullOrEmpty(email))
{
extraData.Add("email",email);
}
return new AuthenticationResult(
isSuccessful: true, provider: this.ProviderName, providerUserId: userId, userName: userName, extraData: extraData);
}
}
}
catch (Exception exception)
{
return new AuthenticationResult(exception);
}
}
}
Here's the section that has changed from the base LinkedIn client written by Microsoft.
InMemoryOAuthTokenManager imoatm = new InMemoryOAuthTokenManager(ConsumerKey, ConsumerSecret);
imoatm.ExpireRequestTokenAndStoreNewAccessToken(String.Empty, String.Empty, accessToken, (response as ITokenSecretContainingMessage).TokenSecret);
WebConsumer w = new WebConsumer(LinkedInServiceDescription, imoatm);
HttpWebRequest request = w.PrepareAuthorizedRequest(profileEndpoint, accessToken);
Unfortunately, the IOAuthTOkenManger.ReplaceRequestTokenWithAccessToken(..) method does not get executed until after the VerifyAuthentication() method returns, so we instead have to create a new TokenManager and and create a WebConsumer and HttpWebRequest using the AccessToken credentials we just retrieved.
This solves our simple 401 Unauthorized issue.
Now what happens if you would like to persist the AccessToken credentials after the authentication process? This could be useful for a DropBox client for instance, where you would like to sync files to a user's DropBox asyncronously. The issue goes back to the way the AspNet library was written, it was assumed that DotNetOpenAuth would only be used for user authethentication, not as a basis for futher OAuth api calls. Thankfully the fix was fairly simple, all I had to do was modify the base AuthetnicationOnlyCookieOAuthTokenManger so that the ReplaceRequestTokenWithAccessToken(..) method stored the new AccessToken key and secrets.
/// <summary>
/// Stores OAuth tokens in the current request's cookie
/// </summary>
public class PersistentCookieOAuthTokenManagerCustom : AuthenticationOnlyCookieOAuthTokenManager
{
/// <summary>
/// Key used for token cookie
/// </summary>
private const string TokenCookieKey = "OAuthTokenSecret";
/// <summary>
/// Primary request context.
/// </summary>
private readonly HttpContextBase primaryContext;
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationOnlyCookieOAuthTokenManager"/> class.
/// </summary>
public PersistentCookieOAuthTokenManagerCustom() : base()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AuthenticationOnlyCookieOAuthTokenManager"/> class.
/// </summary>
/// <param name="context">The current request context.</param>
public PersistentCookieOAuthTokenManagerCustom(HttpContextBase context) : base(context)
{
this.primaryContext = context;
}
/// <summary>
/// Gets the effective HttpContext object to use.
/// </summary>
private HttpContextBase Context
{
get
{
return this.primaryContext ?? new HttpContextWrapper(HttpContext.Current);
}
}
/// <summary>
/// Replaces the request token with access token.
/// </summary>
/// <param name="requestToken">The request token.</param>
/// <param name="accessToken">The access token.</param>
/// <param name="accessTokenSecret">The access token secret.</param>
public new void ReplaceRequestTokenWithAccessToken(string requestToken, string accessToken, string accessTokenSecret)
{
//remove old requestToken Cookie
//var cookie = new HttpCookie(TokenCookieKey)
//{
// Value = string.Empty,
// Expires = DateTime.UtcNow.AddDays(-5)
//};
//this.Context.Response.Cookies.Set(cookie);
//Add new AccessToken + secret Cookie
StoreRequestToken(accessToken, accessTokenSecret);
}
}
Then to use this PersistentCookieOAuthTokenManager all you need to do is modify your DropboxClient constructor, or any other client where you would like to persist the AccessToken Secret
public DropBoxCustomClient(string consumerKey, string consumerSecret)
: this(consumerKey, consumerSecret, new PersistentCookieOAuthTokenManager()) { }
public DropBoxCustomClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager)
: base("dropBox", DropBoxServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
{}
The reason that OAuthClient class doesn't include access token secret is that it's normally not needed for authentication purpose, which is the primary purpose of the ASP.NET OAuth library.
That said, if you want to retrieve the access token secret in your case, you can override the VerifyAuthentication() method, instead of VerifyAuthenticationCore() like you are doing above. Inside VerifyAuthentication(), you can call WebWorker.ProcessUserAuthorization() to validation the login and from the returned AuthorizedTokenResponse object, you have access to the token secret.
After doing some digging, I was able to solve this by changing my constructor logic as follows:
public DropboxClient(string consumerKey, string consumerSecret) :
this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager())
{
}
public DropboxClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager) :
base("dropbox", DropboxServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
{
}
becomes
public DropboxClient(string consumerKey, string consumerSecret) :
base("dropbox", DropboxServiceDescription, consumerKey, consumerSecret)
{
}
Digging through the DNOA source shows that if you construct an OAuthClient (my base class) with just the consumer key and secret, it uses the InMemoryOAuthTokenManager instead of the SimpleConsumerTokenManager. I don't know why, but now my access token secret is properly appended to my signature in the authorized request and everything works. Hopefully this helps someone else. In the meantime, I'll likely clean this up for a blog post since there is zero guidance on the net (that I can find) for doing this.
EDIT: I'm going to undo my answer since, as a colleague pointed out, this will take care of one request, but now that I'm using the in-memory manager, that will flush once I round trip fully back to the browser (I'm assuming). So I think the root issue here is that I need to get the access token secret, which I still haven't seen how to do.
As for your original question that the secret is not provided in response--the secret is right there when you get the response in the verifyAuthenticationCore function. You get both of them like this:
string token = response.AccessToken; ;
string secret = (response as ITokenSecretContainingMessage).TokenSecret;