I am developing web site with ASP.NET CORE that uses Claims for User Authentication and User Id and other infos keeps in Claims, is it security ?
ClaimsIdentity identity = new ClaimsIdentity(
new[]
{
new Claim(ClaimTypes.Name, userInfo.Name),
new Claim(ClaimTypes.Surname, userInfo.Surname),
new Claim("Image", userInfo.Image),
new Claim(ClaimTypes.NameIdentifier,result.Id.ToString()),
new Claim(ClaimTypes.IsPersistent, loginViewModel.RememberMe.ToString())
},
CookieName.User);
HttpContext.SignOutAsync(CookieName.User).Wait();
HttpContext.SignInAsync(CookieName.User, new ClaimsPrincipal(identity),
new AuthenticationProperties
{
IsPersistent = loginViewModel.RememberMe,
AllowRefresh = true
}).Wait();
Sometime i need change user infos and it uses this. is it secure way ?
//Get
int id = int.Parse(new ClaimsCookie(HttpContext).GetValue(CookieName.User, KeyName.Id));
//Set Update
new ClaimsCookie(HttpContext).SetValue(CookieName.User, new[] { KeyName.Name, KeyName.Surname }, new[] { model.Name, model.Surname });
Class :
namespace ...
{
public class ClaimsCookie
{
private readonly HttpContext _httpContext;
public ClaimsCookie(HttpContext httpContext)
{
_httpContext = httpContext;
}
public string GetValue(string cookieName, string keyName)
{
var principal = _httpContext.User;
var cp = principal.Identities.First(i => i.AuthenticationType == cookieName.ToString());
return cp.FindFirst(keyName).Value;
}
public async void SetValue(string cookieName, string[] keyName, string[] value)
{
if (keyName.Length != value.Length)
{
return;
}
if (_httpContext == null)
return;
var principal = _httpContext.User;
var cp = principal.Identities.First(i => i.AuthenticationType == cookieName.ToString());
for (int i = 0; i < keyName.Length; i++)
{
if (cp.FindFirst(keyName[i]) != null)
{
cp.RemoveClaim(cp.FindFirst(keyName[i]));
cp.AddClaim(new Claim(keyName[i], value[i]));
}
}
await _httpContext.SignOutAsync(cookieName);
await _httpContext.SignInAsync(cookieName, new ClaimsPrincipal(cp),
new AuthenticationProperties
{
IsPersistent = bool.Parse(cp.FindFirst(KeyName.IsPersistent).Value),
AllowRefresh = true
});
}
public async void SetValue(string cookieName, string keyName, string value)
{
var principal = _httpContext.User;
var cp = principal.Identities.First(i => i.AuthenticationType == cookieName.ToString());
if (cp.FindFirst(keyName) != null)
{
cp.RemoveClaim(cp.FindFirst(keyName));
cp.AddClaim(new Claim(keyName, value));
}
await _httpContext.SignOutAsync(cookieName);
await _httpContext.SignInAsync(cookieName, new ClaimsPrincipal(cp),
new AuthenticationProperties
{
IsPersistent = bool.Parse(cp.FindFirst(KeyName.IsPersistent).Value),
AllowRefresh = true
});
}
}
public static class CookieName
{
public static string Company => "CompanyUserProfilCookie";
public static string User => "UserProfilCookie";
public static string Admin => "AdminPanelCookie";
}
public static class KeyName
{
public static string Id => ClaimTypes.NameIdentifier;
public static string Name => ClaimTypes.Name;
public static string Surname => ClaimTypes.Surname;
public static string IsPersistent => ClaimTypes.IsPersistent;
public static string Image => "Image";
}
}
I am setting HttpContext to this class from any Controller. Have any way static HttpContext, i dont want set from Controller ?
One option is to inject IHttpContextAccessor from DI and access HttpContext from it.
Change ClaimsCookie constructor to reflect that:
private readonly HttpContext _httpContext;
public ClaimCookie(IHttpContextAccessor contextAccessor)
{
_httpContext = contextAccessor.HttpContext;
}
Next you need to register both IHttpContextAccessor and ClaimCookie in Startup.ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddTransient<ClaimCookie>();
...rest of code ommited...
}
Then inject Your class and use is without providing HttpContext by yourself:
public class SomeController : Controller
{
private readonly ClaimCookie _claimCookie;
public SomeController(ClaimCookie claimCookie)
{
_claimCookie = claimCookie;
}
public async Task<IActionResult> SomeAction()
{
int id = int.Parse(_claimCookie.GetValue(CookieName.User, KeyName.Id));
await _claimCookie.SetValue(CookieName.User, new[] { KeyName.Name, KeyName.Surname }, new[] { model.Name, model.Surname });
...
}
Also read msdn's best practices in asynchronous programming why you should't use async void.
And about security (im not an expert) you also shouldn't store sensitive data in cookies, if You need so then store encrypted data.
Related
I've got an identity server setup with the following 'look a like' configuration:
return new List<Client>
{
new Client
{
[...]
AllowedGrantTypes = GrantTypes.Implicit,
[....]
},
new Client
{
[...]
AllowedGrantTypes = GrantTypes.ClientCredentials,
[....]
}
};
and controlles annotated like this:
[Route("api/forms")]
[ApiController]
[Authorize(Policy = "user.api.portfolio.manager")]
[Authorize(Policy = "application.api.portfolio.manager")]
public class FormsController : ControllerBase
{
[...]
}
and a policy
private System.Action<AuthorizationOptions> AddJwtAuthorizationPolicyForRole()
{
return options => { options.AddPolicy("**POLICY_FOR_GRANT_IMPLICIT**", policy => {
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.RequireClaim(ClaimTypes.Role, "USER_ACCESSIBLE");
});
};
}
private System.Action<AuthorizationOptions> AddJwtAuthorizationPolicyForRole()
{
return options => { options.AddPolicy("**POLICY_FOR_CLIENT_CREDENTIALS**", policy => {
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
});
};
}
so I want to achieve:
Clients using the GrantType.ClientCredentials can access the controller without any further needs.
Clients using the Implicit Schema must have role USER_ACCESSIBLE
If it's configured like shown above, both policies must apply -> Both grant types are failing.
How can I achieve the described behavior using IdentityServer, that each grant-types may have an independent policy so be applied?
Thanks in advance for your help.
The most simplest solution is adding another single policy for Implicit + ClientCredential to implement logics for OR conditions .
Or you can create a custom attribute like :
MultiplePolicysAuthorizeAttribute
public class MultiplePolicysAuthorizeAttribute : TypeFilterAttribute
{
public MultiplePolicysAuthorizeAttribute(string policys, bool isAnd = false) : base(typeof(MultiplePolicysAuthorizeFilter))
{
Arguments = new object[] { policys, isAnd };
}
}
MultiplePolicysAuthorizeFilter
public class MultiplePolicysAuthorizeFilter : IAsyncAuthorizationFilter
{
private readonly IAuthorizationService _authorization;
public string Policys { get; private set; }
public bool IsAnd { get; private set; }
public MultiplePolicysAuthorizeFilter(string policys, bool isAnd, IAuthorizationService authorization)
{
Policys = policys;
IsAnd = isAnd;
_authorization = authorization;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var policys = Policys.Split(";").ToList();
if (IsAnd)
{
foreach (var policy in policys)
{
var authorized = await _authorization.AuthorizeAsync(context.HttpContext.User, policy);
if (!authorized.Succeeded)
{
context.Result = new ForbidResult();
return;
}
}
}
else
{
foreach (var policy in policys)
{
var authorized = await _authorization.AuthorizeAsync(context.HttpContext.User, policy);
if (authorized.Succeeded)
{
return;
}
}
context.Result = new ForbidResult();
return;
}
}
}
If actions require matching one of the policies(OR):
[MultiplePolicysAuthorize("POLICY_FOR_GRANT_IMPLICIT;POLICY_FOR_CLIENT_CREDENTIALS")]
If actions require matching all the policies(And) :
[MultiplePolicysAuthorize("POLICY_FOR_GRANT_IMPLICIT;POLICY_FOR_CLIENT_CREDENTIALS",true)]
Code sample reference : https://stackoverflow.com/a/52639938/5751404
The problem statement
We are developing a new enterprise level application and want to utilize Azure Active Directory for signing into the application so that we do not have to create another set of user credentials. However, our permissions model for this application is more complex than what can be handled via groups inside of AAD.
The thought
The thought was that we could use Azure Active Directory OAuth 2.0 in addition to the ASP.NET Core Identity framework to force users to authenticate through Azure Active Directory and then use the identity framework to handle authorization/permissions.
The Issues
You can create projects out of the box using Azure OpenId authentication and then you can easily add Microsoft account authentication (Not AAD) to any project using Identity framework. But there was nothing built in to add OAuth for AAD to the identity model.
After trying to hack those methods to get them to work like I needed I finally went through trying to home-brew my own solution building off of the OAuthHandler and OAuthOptions classes.
I ran into a lot of issues going down this route but managed to work through most of them. Now I am to a point where I am getting a token back from the endpoint but my ClaimsIdentity doesn't appear to be valid. Then when redirecting to the ExternalLoginCallback my SigninManager is unable to get the external login information.
There almost certainly must be something simple that I am missing but I can't seem to determine what it is.
The Code
Startup.cs
services.AddAuthentication()
.AddAzureAd(options =>
{
options.ClientId = Configuration["AzureAd:ClientId"];
options.AuthorizationEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/authorize";
options.TokenEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/token";
options.UserInformationEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/openid/userinfo";
options.Resource = Configuration["AzureAd:ClientId"];
options.ClientSecret = Configuration["AzureAd:ClientSecret"];
options.CallbackPath = Configuration["AzureAd:CallbackPath"];
});
AzureADExtensions
namespace Microsoft.AspNetCore.Authentication.AzureAD
{
public static class AzureAdExtensions
{
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder)
=> builder.AddAzureAd(_ => { });
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
{
return builder.AddOAuth<AzureAdOptions, AzureAdHandler>(AzureAdDefaults.AuthenticationScheme, AzureAdDefaults.DisplayName, configureOptions);
}
public static ChallengeResult ChallengeAzureAD(this ControllerBase controllerBase, SignInManager<ApplicationUser> signInManager, string redirectUrl)
{
return controllerBase.Challenge(signInManager.ConfigureExternalAuthenticationProperties(AzureAdDefaults.AuthenticationScheme, redirectUrl), AzureAdDefaults.AuthenticationScheme);
}
}
}
AzureADOptions & Defaults
public class AzureAdOptions : OAuthOptions
{
public string Instance { get; set; }
public string Resource { get; set; }
public string TenantId { get; set; }
public AzureAdOptions()
{
CallbackPath = new PathString("/signin-azureAd");
AuthorizationEndpoint = AzureAdDefaults.AuthorizationEndpoint;
TokenEndpoint = AzureAdDefaults.TokenEndpoint;
UserInformationEndpoint = AzureAdDefaults.UserInformationEndpoint;
Scope.Add("https://graph.windows.net/user.read");
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "unique_name");
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", "given_name");
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname", "family_name");
ClaimActions.MapJsonKey("http://schemas.microsoft.com/ws/2008/06/identity/claims/groups", "groups");
ClaimActions.MapJsonKey("http://schemas.microsoft.com/identity/claims/objectidentifier", "oid");
ClaimActions.MapJsonKey("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "roles");
}
}
public static class AzureAdDefaults
{
public static readonly string DisplayName = "AzureAD";
public static readonly string AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/authorize";
public static readonly string TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/token";
public static readonly string UserInformationEndpoint = "https://login.microsoftonline.com/common/openid/userinfo"; // "https://graph.windows.net/v1.0/me";
public const string AuthenticationScheme = "AzureAD";
}
AzureADHandler
internal class AzureAdHandler : OAuthHandler<AzureAdOptions>
{
public AzureAdHandler(IOptionsMonitor<AzureAdOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens)
{
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
HttpResponseMessage httpResponseMessage = await Backchannel.SendAsync(httpRequestMessage, Context.RequestAborted);
if (!httpResponseMessage.IsSuccessStatusCode)
throw new HttpRequestException(message: $"Failed to retrived Azure AD user information ({httpResponseMessage.StatusCode}) Please check if the authentication information is correct and the corresponding Microsoft Account API is enabled.");
JObject user = JObject.Parse(await httpResponseMessage.Content.ReadAsStringAsync());
OAuthCreatingTicketContext context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, user);
context.RunClaimActions();
await Events.CreatingTicket(context);
return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
}
protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(string code, string redirectUri)
{
Dictionary<string, string> dictionary = new Dictionary<string, string>();
dictionary.Add("grant_type", "authorization_code");
dictionary.Add("client_id", Options.ClientId);
dictionary.Add("redirect_uri", redirectUri);
dictionary.Add("client_secret", Options.ClientSecret);
dictionary.Add(nameof(code), code);
dictionary.Add("resource", Options.Resource);
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint);
httpRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpRequestMessage.Content = new FormUrlEncodedContent(dictionary);
HttpResponseMessage response = await Backchannel.SendAsync(httpRequestMessage, Context.RequestAborted);
if (response.IsSuccessStatusCode)
return OAuthTokenResponse.Success(JObject.Parse(await response.Content.ReadAsStringAsync()));
return OAuthTokenResponse.Failed(new Exception(string.Concat("OAuth token endpoint failure: ", await Display(response))));
}
protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
{
Dictionary<string, string> dictionary = new Dictionary<string, string>();
dictionary.Add("client_id", Options.ClientId);
dictionary.Add("scope", FormatScope());
dictionary.Add("response_type", "code");
dictionary.Add("redirect_uri", redirectUri);
dictionary.Add("state", Options.StateDataFormat.Protect(properties));
dictionary.Add("resource", Options.Resource);
return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, dictionary);
}
private static async Task<string> Display(HttpResponseMessage response)
{
StringBuilder output = new StringBuilder();
output.Append($"Status: { response.StatusCode };");
output.Append($"Headers: { response.Headers.ToString() };");
output.Append($"Body: { await response.Content.ReadAsStringAsync() };");
return output.ToString();
}
}
AccountController.cs
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> SignIn()
{
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account");
return this.ChallengeAzureAD(_signInManager, redirectUrl);
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
if (remoteError != null)
{
_logger.LogInformation($"Error from external provider: {remoteError}");
return RedirectToAction(nameof(SignedOut));
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null) //This always ends up true!
{
return RedirectToAction(nameof(SignedOut));
}
}
There you have it!
This is the code I have, and I'm almost sure that at this point there is something simple I am missing but am unsure of what it is. I know that my CreateTicketAsync method is problematic as well since I'm not hitting the correct user information endpoint (or hitting it correctly) but that's another problem all together as from what I understand the claims I care about should come back as part of the token.
Any assistance would be greatly appreciated!
I ended up resolving my own problem as it ended up being several issues. I was passing the wrong value in for the resource field, hadn't set my NameIdentifer mapping correctly and then had the wrong endpoint for pulling down user information. The user information piece being the biggest as that is the token I found out that the external login piece was looking for.
Updated Code
Startup.cs
services.AddAuthentication()
.AddAzureAd(options =>
{
options.ClientId = Configuration["AzureAd:ClientId"];
options.AuthorizationEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/authorize";
options.TokenEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/token";
options.ClientSecret = Configuration["AzureAd:ClientSecret"];
options.CallbackPath = Configuration["AzureAd:CallbackPath"];
});
AzureADOptions & Defaults
public class AzureAdOptions : OAuthOptions
{
public string Instance { get; set; }
public string Resource { get; set; }
public string TenantId { get; set; }
public AzureAdOptions()
{
CallbackPath = new PathString("/signin-azureAd");
AuthorizationEndpoint = AzureAdDefaults.AuthorizationEndpoint;
TokenEndpoint = AzureAdDefaults.TokenEndpoint;
UserInformationEndpoint = AzureAdDefaults.UserInformationEndpoint;
Resource = AzureAdDefaults.Resource;
Scope.Add("user.read");
ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
ClaimActions.MapJsonKey(ClaimTypes.Name, "displayName");
ClaimActions.MapJsonKey(ClaimTypes.GivenName, "givenName");
ClaimActions.MapJsonKey(ClaimTypes.Surname, "surname");
ClaimActions.MapJsonKey(ClaimTypes.MobilePhone, "mobilePhone");
ClaimActions.MapCustomJson(ClaimTypes.Email, user => user.Value<string>("mail") ?? user.Value<string>("userPrincipalName"));
}
}
public static class AzureAdDefaults
{
public static readonly string DisplayName = "AzureAD";
public static readonly string AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/authorize";
public static readonly string TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/token";
public static readonly string Resource = "https://graph.microsoft.com";
public static readonly string UserInformationEndpoint = "https://graph.microsoft.com/v1.0/me";
public const string AuthenticationScheme = "AzureAD";
}
I'm stuck on mocking the IHttpContextAccessor for some web api integration tests. My goal is to be able to mock the IHttpContextAccessor and return NameIdentifier claim and RemoteIpAddress.
Test
public class InsertUser : TestBase
{
private UserController _userController;
[OneTimeSetUp]
public void OneTimeSetUp()
{
IStringLocalizer<UserController> localizer = A.Fake<IStringLocalizer<UserController>>();
_userController = new UserController(localizer, Mapper, UserService, StatusService, IdentityService);
_userController.ControllerContext = A.Fake<ControllerContext>();
_userController.ControllerContext.HttpContext = A.Fake<DefaultHttpContext>();
var fakeClaim = A.Fake<Claim>(x => x.WithArgumentsForConstructor(() => new Claim(ClaimTypes.NameIdentifier, "1")));
var fakeIdentity = A.Fake<ClaimsPrincipal>();
A.CallTo(() => fakeIdentity.FindFirst(ClaimTypes.NameIdentifier)).Returns(fakeClaim);
A.CallTo(() => _userController.ControllerContext.HttpContext.User).Returns(fakeIdentity);
StatusTypeEntity statusType = ObjectMother.InsertStatusType(StatusTypeEnum.StatusType.User);
StatusEntity status = ObjectMother.InsertStatus(StatusEnum.Status.Active, statusType);
ObjectMother.InsertUser("FirstName", "LastName", "Email#Email.Email", "PasswordHash", "PasswordSalt", status);
}
public static IEnumerable TestCases
{
get
{
//InsertUser_Should_Insert
yield return new TestCaseData(new InsertUserModel
{
FirstName = "FirstName",
LastName = "LastName",
StatusId = 1,
Email = "Email2#Email.Email"
},
1,
2).SetName("InsertUser_Should_Insert");
//InsertUser_Should_Not_Insert_When_StatusId_Not_Exist
yield return new TestCaseData(new InsertUserModel
{
FirstName = "FirstName",
LastName = "LastName",
StatusId = int.MaxValue,
Email = "Email2#Email.Email"
},
1,
1).SetName("InsertUser_Should_Not_Insert_When_StatusId_Not_Exist");
//InsertUser_Should_Not_Insert_When_Email_Already_Exist
yield return new TestCaseData(new InsertUserModel
{
FirstName = "FirstName",
LastName = "LastName",
StatusId = 1,
Email = "Email#Email.Email"
},
1,
1).SetName("InsertUser_Should_Not_Insert_When_Email_Already_Exist");
}
}
[Test, TestCaseSource(nameof(TestCases))]
public async Task Test(InsertUserModel model, int userCountBefore, int userCountAfter)
{
//Before
int resultBefore = Database.User.Count();
resultBefore.ShouldBe(userCountBefore);
//Delete
await _userController.InsertUser(model);
//After
int resultAfter = Database.User.Count();
resultAfter.ShouldBe(userCountAfter);
}
}
Controller
[Route("api/administration/[controller]")]
[Authorize(Roles = "Administrator")]
public class UserController : Controller
{
private readonly IStringLocalizer<UserController> _localizer;
private readonly IMapper _mapper;
private readonly IUserService _userService;
private readonly IStatusService _statusService;
private readonly IIdentityService _identityService;
public UserController(IStringLocalizer<UserController> localizer,
IMapper mapper,
IUserService userService,
IStatusService statusService,
IIdentityService identityService)
{
_localizer = localizer;
_mapper = mapper;
_userService = userService;
_statusService = statusService;
_identityService = identityService;
}
[HttpPost("InsertUser")]
public async Task<IActionResult> InsertUser([FromBody] InsertUserModel model)
{
if (model == null || !ModelState.IsValid)
{
return Ok(new GenericResultModel(_localizer["An_unexpected_error_has_occurred_Please_try_again"]));
}
StatusModel status = await _statusService.GetStatus(model.StatusId, StatusTypeEnum.StatusType.User);
if (status == null)
{
return Ok(new GenericResultModel(_localizer["Could_not_find_status"]));
}
UserModel userExist = await _userService.GetUser(model.Email);
if (userExist != null)
{
return Ok(new GenericResultModel(_localizer["Email_address_is_already_in_use"]));
}
UserModel user = _mapper.Map<InsertUserModel, UserModel>(model);
var letrTryAndGetUserIdFromNameIdentifier = _identityService.GetUserId();
user.DefaultIpAddress = _identityService.GetIpAddress();
//UserModel insertedUser = await _userService.InsertUser(user, model.Password);
UserModel insertedUser = await _userService.InsertUser(user, "TODO");
if (insertedUser != null)
{
return Ok(new GenericResultModel { Id = insertedUser.Id });
}
return Ok(new GenericResultModel(_localizer["Could_not_create_user"]));
}
}
The important line here is:
var letrTryAndGetUserIdFromNameIdentifier = _identityService.GetUserId();
IdentityService
public class IdentityService : IIdentityService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public IdentityService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public int GetUserId()
{
if (_httpContextAccessor.HttpContext == null || !Authenticated())
{
throw new AuthenticationException("User is not authenticated.");
}
ClaimsPrincipal claimsPrincipal = _httpContextAccessor.HttpContext.User;
string userIdString = claimsPrincipal.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
int.TryParse(userIdString, out int userIdInt);
return userIdInt;
}
public string GetIpAddress()l
{
return _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress.ToString();
}
}
Fails here:
if (_httpContextAccessor.HttpContext == null || !Authenticated())
{
throw new AuthenticationException("User is not authenticated.");
}
Currently the _httpContextAccessor.HttpContext is always null. I'm not sure if I'm on the right path here..
For this kind of test, you probably would be better off writing an integration test that uses the TestHost type, and mocking as little as possible. It will be much simpler, and you'll be able to test filters (like routes and authorization rules), which your current approach doesn't support. You can read more in the docs here:
https://learn.microsoft.com/en-us/aspnet/core/testing/integration-testing
I have a good sample showing how to write API tests as part of my MSDN article on ASP.NET Core Filters, here:
https://msdn.microsoft.com/en-us/magazine/mt767699.aspx
Modified Test project
var userIdClaim = A.Fake<Claim>(x => x.WithArgumentsForConstructor(() => new Claim(ClaimTypes.NameIdentifier, "1")));
var httpContextAccessor = A.Fake<HttpContextAccessor>();
httpContextAccessor.HttpContext = A.Fake<HttpContext>();
httpContextAccessor.HttpContext.User = A.Fake<ClaimsPrincipal>();
IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
A.CallTo(() => httpContextAccessor.HttpContext.Connection.RemoteIpAddress).Returns(ipAddress);
A.CallTo(() => httpContextAccessor.HttpContext.User.Identity.IsAuthenticated).Returns(true);
A.CallTo(() => httpContextAccessor.HttpContext.User.Claims).Returns(new List<Claim> { userIdClaim });
var identityService = new IdentityService(httpContextAccessor);
_userController = new UserController(localizer, Mapper, UserService, StatusService, identityService);
I'm now able to do in controller:
var claims = HttpContext.User.Claims.ToList();
And identity service:
ClaimsPrincipal claimsPrincipal = _httpContextAccessor.HttpContext.User;
string userIdString = claimsPrincipal.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
int.TryParse(userIdString, out int userIdInt);
return userIdInt;
Please let me now if you think there is a better way for faking HttpContext.
I want to change the value of a claim via refresh tokens. My refresh token provider is like this:
public class MyRefreshTokenProvider : AuthenticationTokenProvider
{
public override void Create(AuthenticationTokenCreateContext context)
{
...
var claim = context.Ticket.Identity.FindFirst(ClaimTypes.UserData);
if (claim != null)
{
context.Ticket.Identity.RemoveClaim(claim);
context.Ticket.Identity.AddClaim(new Claim(ClaimTypes.UserData, "New Value"));
}
context.SetToken(context.SerializeTicket());
}
public override void Receive(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
}
}
And in the startup class:
app.UseOAuthBearerTokens(new OAuthAuthorizationServerOptions
{
...
RefreshTokenProvider = new MyRefreshTokenProvider()
});
The refresh token request completes with no error. But when I use the new access token, the claim value is still the old one.
Is my approach right? Or how can I change a claim value in the Bearer authentication?
Finally I've found the solution. I have to extend the AccessTokenProvider OAuthAuthorizationServerOptions instead of the RefreshTokenProvider:
app.UseOAuthBearerTokens(new OAuthAuthorizationServerOptions
{
...
AccessTokenProvider = new MyAccessTokenProvider(),
RefreshTokenProvider = new MyRefreshTokenProvider()
});
public class MyAccessTokenProvider : AuthenticationTokenProvider
{
public override void Create(AuthenticationTokenCreateContext context)
{
...
var claim = context.Ticket.Identity.FindFirst(ClaimTypes.UserData);
if (claim != null)
{
context.Ticket.Identity.RemoveClaim(claim);
context.Ticket.Identity.AddClaim(new Claim(ClaimTypes.UserData, "New Value"));
}
context.SetToken(context.SerializeTicket());
}
public override void Receive(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
}
}
public class MyRefreshTokenProvider : AuthenticationTokenProvider
{
public override void Create(AuthenticationTokenCreateContext context)
{
context.SetToken(context.SerializeTicket());
}
public override void Receive(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
}
}
According to the OAuthAuthorizationServerHandler class in the Microsoft.Owin.Security.OAuth the AccessTokenProvider only can update the refreshing token. For changing the claims the AccessTokenProvider should be extended:
private async Task InvokeTokenEndpointAsync()
{
...
var accessTokenContext = new AuthenticationTokenCreateContext(
Context,
Options.AccessTokenFormat,
ticket);
await Options.AccessTokenProvider.CreateAsync(accessTokenContext);
string accessToken = accessTokenContext.Token;
if (string.IsNullOrEmpty(accessToken))
{
accessToken = accessTokenContext.SerializeTicket();
}
DateTimeOffset? accessTokenExpiresUtc = ticket.Properties.ExpiresUtc;
var refreshTokenCreateContext = new AuthenticationTokenCreateContext(
Context,
Options.RefreshTokenFormat,
accessTokenContext.Ticket);
await Options.RefreshTokenProvider.CreateAsync(refreshTokenCreateContext);
string refreshToken = refreshTokenCreateContext.Token;
var memory = new MemoryStream();
byte[] body;
using (var writer = new JsonTextWriter(new StreamWriter(memory)))
{
writer.WriteStartObject();
writer.WritePropertyName(Constants.Parameters.AccessToken);
writer.WriteValue(accessToken);
writer.WritePropertyName(Constants.Parameters.TokenType);
writer.WriteValue(Constants.TokenTypes.Bearer);
if (accessTokenExpiresUtc.HasValue)
{
TimeSpan? expiresTimeSpan = accessTokenExpiresUtc - currentUtc;
var expiresIn = (long)expiresTimeSpan.Value.TotalSeconds;
if (expiresIn > 0)
{
writer.WritePropertyName(Constants.Parameters.ExpiresIn);
writer.WriteValue(expiresIn);
}
}
if (!String.IsNullOrEmpty(refreshToken))
{
writer.WritePropertyName(Constants.Parameters.RefreshToken);
writer.WriteValue(refreshToken);
}
...
I need to consume ASP.NET 5 secure Web API from a web client using local accounts. In the past there was a handler issuing bearer tokens in order to support OAuth, now bearer token issuance responsibility was removed from identity. Some people recommends to use identityServer3, which require clients registration, unlike identity2 approach. What is the simplest way to achieve authorization in ASP.NET 5 Web API? How can I avoid to pass client id and client secret in order to get an access token when using resource owner password flow? How to call api avoiding to pass scope?
I have built a simple bearer token issuer from this but using Identity password hasher. You can see full code below:
public class TokenController : Controller
{
private readonly IBearerTokenGenerator generator;
private readonly IClientsManager clientsManager;
private readonly IOptions<TokenAuthOptions> options;
public TokenController(IBearerTokenGenerator generator,
IClientsManager clientsManager,
IOptions<TokenAuthOptions> options)
{
this.generator = generator;
this.clientsManager = clientsManager;
this.options = options;
}
[HttpPost, AllowAnonymous]
public IActionResult Post(AuthenticationViewModel req)
{
return clientsManager
.Find(req.client_id, req.client_secret)
.Map(c => c.Client)
.Map(c => (IActionResult)new ObjectResult(new {
access_token = generator.Generate(c),
expires_in = options.Value.ExpirationDelay.TotalSeconds,
token_type = "Bearer"
}))
.ValueOrDefault(HttpUnauthorized);
}
}
public class BearerTokenGenerator : IBearerTokenGenerator
{
private readonly IOptions<TokenAuthOptions> tokenOptions;
public BearerTokenGenerator(IOptions<TokenAuthOptions> tokenOptions)
{
this.tokenOptions = tokenOptions;
}
public string Generate(Client client)
{
var expires = Clock.UtcNow.Add(tokenOptions.Value.ExpirationDelay);
var handler = new JwtSecurityTokenHandler();
var identity = new ClaimsIdentity(new GenericIdentity(client.Identifier, "TokenAuth"), new Claim[] {
new Claim("client_id", client.Identifier)
});
var securityToken = handler.CreateToken(
issuer: tokenOptions.Value.Issuer,
audience: tokenOptions.Value.Audience,
signingCredentials: tokenOptions.Value.SigningCredentials,
subject: identity,
expires: expires);
return handler.WriteToken(securityToken);
}
}
public class ClientsManager : IClientsManager
{
private readonly MembershipDataContext db;
private readonly ISecretHasher hasher;
public ClientsManager(MembershipDataContext db,
ISecretHasher hasher)
{
this.db = db;
this.hasher = hasher;
}
public void Create(string name, string identifier, string secret, Company company)
{
var client = new Client(name, identifier, company);
db.Clients.Add(client);
var hash = hasher.HashSecret(secret);
var apiClient = new ApiClient(client, hash);
db.ApiClients.Add(apiClient);
}
public Option<ApiClient> Find(string identifier, string secret)
{
return FindByIdentifier(identifier)
.Where(c => hasher.Verify(c.SecretHash, secret));
}
public void ChangeSecret(string identifier, string secret)
{
var client = FindByIdentifier(identifier).ValueOrDefault(() => {
throw new ArgumentException($"could not find any client with identifier { identifier }");
});
var hash = hasher.HashSecret(secret);
client.ChangeSecret(hash);
}
public string GenerateRandomSecret()
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var random = new Random();
var generated = new string(Enumerable.Repeat(chars, 12).Select(s => s[random.Next(s.Length)]).ToArray());
return Convert.ToBase64String(Encoding.UTF8.GetBytes(generated));
}
private Option<ApiClient> FindByIdentifier(string identifier)
{
return db.ApiClients
.Include(c => c.Client)
.SingleOrDefault(c => c.Client.Identifier == identifier)
.ToOptionByRef();
}
}
public class SecretHasher : ISecretHasher
{
private static Company fakeCompany = new Company("fake", "fake");
private static Client fakeClient = new Client("fake", "fake", fakeCompany);
private readonly IPasswordHasher<Client> hasher;
public SecretHasher(IPasswordHasher<Client> hasher)
{
this.hasher = hasher;
}
public string HashSecret(string secret)
{
return hasher.HashPassword(fakeClient, secret);
}
public bool Verify(string hashed, string secret)
{
return hasher.VerifyHashedPassword(fakeClient, hashed, secret) == PasswordVerificationResult.Success;
}
}
Now in Startup.cs
services.Configure<TokenAuthOptions>(options =>
{
options.Audience = "API";
options.Issuer = "Web-App";
options.SigningCredentials = new SigningCredentials(GetSigninKey(), SecurityAlgorithms.RsaSha256Signature);
options.ExpirationDelay = TimeSpan.FromDays(365);
});
services.AddAuthorization(auth =>
{
auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build());
}
app.UseJwtBearerAuthentication(options =>
{
options.TokenValidationParameters.IssuerSigningKey = GetSigninKey();
options.TokenValidationParameters.ValidAudience = "API";
options.TokenValidationParameters.ValidIssuer = "Web-App";
options.TokenValidationParameters.ValidateSignature = true;
options.TokenValidationParameters.ValidateLifetime = true;
options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(0);
});