I'm trying to create an authorize with jwt bearer, but I cant do it on my application, when I do with postman it happening what I want, but on my app don't...
Service.ts (Angular 8)
getAll(): Observable<...[]> {
return this.httpClient.get<...[]>(environment.url + "api",
{ headers: {'Authorization' : 'Bearer ' + token });
}
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
var symetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"]));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = symetricSecurityKey
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
AuthController.cs
[HttpPost]
[Route("login")]
public async Task<AccountModel> Login([FromBody] AccountModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser();
var signInResultado = new Microsoft.AspNetCore.Identity.SignInResult();
Task.Run(async () =>
{
user = await _userManager.FindByEmailAsync(model.Email);
}).Wait();
Task.Run(async () =>
{
await _userManager.CheckPasswordAsync(user, model.Password);
}).Wait();
Task.Run(async () =>
{
signInResultado = await _signInManager.PasswordSignInAsync(
user.UserName,
model.Password,
isPersistent: false,
lockoutOnFailure: false);
}).Wait();
if (signInResultado.Succeeded)
{
var appUser = _userManager.Users.FirstOrDefault(u => u.Id == user.Id);
var claims = await GetValidClaims(appUser);
var accountModel = new AccountModel(user, _roleManager);
accountModel.Token = GenerateJwtToken(appUser, claims);
return accountModel;
}
}
return model;
}
private string GenerateJwtToken(ApplicationUser user, List<Claim> claims)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_configuration["JwtKey"]);
var tokens = new JwtSecurityToken(
claims: claims,
expires: DateTime.UtcNow.AddDays(1),
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
);
return new JwtSecurityTokenHandler().WriteToken(tokens);
}
private async Task<List<Claim>> GetValidClaims(ApplicationUser user)
{
IdentityOptions _options = new IdentityOptions();
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(_options.ClaimsIdentity.UserIdClaimType, user.Id.ToString()),
new Claim(_options.ClaimsIdentity.UserNameClaimType, user.UserName)
};
var userClaims = await _userManager.GetClaimsAsync(user);
var userRoles = await _userManager.GetRolesAsync(user);
claims.AddRange(userClaims);
foreach (var userRole in userRoles)
{
claims.Add(new Claim(ClaimTypes.Role, userRole));
var role = await _roleManager.FindByNameAsync(userRole);
if (role != null)
{
var roleClaims = await _roleManager.GetClaimsAsync(role);
foreach (Claim roleClaim in roleClaims)
{
claims.Add(roleClaim);
}
}
}
return claims;
}
Any method with Role = Admin
[HttpGet]
[Authorize(Roles = "Admin")]
public ActionResult<IEnumerable<RoleModel>> Get()
Postman - Login (Create Token)
Postman - Get any method with Role = Admin
When I did on my application, it redirected to Identity/Account/Login
Related
I am trying to implement Okta integration with Existing application.
Current Application - it has login screen and user gets authenticated and signed in using SignInManager.PasswordSignInAsync method.
NEW - I am using Okta login page. My StartUp file has below code :
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Ssl3;
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext(() => DependencyResolver.Current.GetService<ApplicationUserManager>());
app.CreatePerOwinContext(() => DependencyResolver.Current.GetService<ApplicationRoleManager>());
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
// app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseOktaMvc(new OktaMvcOptions()
{
OktaDomain = ConfigurationManager.AppSettings["okta:OktaDomain"],
ClientId = ConfigurationManager.AppSettings["okta:ClientId"],
ClientSecret = ConfigurationManager.AppSettings["okta:ClientSecret"],
RedirectUri = ConfigurationManager.AppSettings["okta:RedirectUri"],
PostLogoutRedirectUri = ConfigurationManager.AppSettings["okta:PostLogoutRedirectUri"],
GetClaimsFromUserInfoEndpoint = true,
Scope = new List<string> { "openid", "profile", "email", "offline_access" },
AuthorizationServerId = string.Empty
});
}
}
My Redirect Uri is : https://localhost:port/Account/Login
Account Controller Code:
public class AccountController : Controller
{
private readonly ApplicationSignInManager _signInManager;
private readonly ApplicationUserManager _userManager;
public AccountController(ApplicationUserManager userManager, ApplicationSignInManager signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
[AllowAnonymous]
public async Task<ActionResult> Login()
{
if (!HttpContext.User.Identity.IsAuthenticated)
{
var properties = new AuthenticationProperties();
properties.RedirectUri = "/Account/Login";
HttpContext.GetOwinContext().Authentication.Challenge(properties, OktaDefaults.MvcAuthenticationType);
return new HttpUnauthorizedResult();
}
var userClaims = HttpContext.GetOwinContext().Authentication.User.Claims;
//Fetching values before Sign IN as they are getting lost, adding as custom claim when receive success status
var accesstoken = userClaims.FirstOrDefault(x => x.Type == "access_token");
var idtoken = userClaims.FirstOrDefault(c => c.Type == "id_token");
var refreshtoken = userClaims.FirstOrDefault(c => c.Type == "refresh_token");
var expiresat = userClaims.FirstOrDefault(c => c.Type == "exp");
var issuedat = userClaims.FirstOrDefault(c => c.Type == "iat");
//SignInManager
//Not getting below information after using ExternalCookie so commenting
//ExternalLoginInfo loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
//Get Custom ExternalloginInfo
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
ExternalLoginInfo externalInfo = new ExternalLoginInfo();
externalInfo.DefaultUserName = externalLogin.DefaultUserName;
externalInfo.Email = externalLogin.Email;
externalInfo.Login = externalLogin.LoginInfo;
externalInfo.ExternalIdentity = externalLogin.ExternalIdentity;
var result = await _signInManager.ExternalSignInAsync(externalInfo, isPersistent: false);
switch (result)
{
When I put a debug point to Switch, User.Identity does not have Okta Claims, it has only AspNet.Identity Claims with UserId,Email, SecurityStamp values.
When I get SigninStatus as Success, I manually add accesstoken, idtoken,etc claims in AspNetUserClaims Table and then Signs user in again to get updated Identity.
And When it gives Failure status then I CreateExternal User and Use AddLoginAsync method to map Sub with AspNetUsers table UserId value.
Is this the right way? Also I am not getting ExternalLoginInfo even after adding ExternalCookie, so manually making the ExternalLoginInfo object.
After Changes
[AllowAnonymous]
public ActionResult Login(string returnUrl)
{
if (!HttpContext.User.Identity.IsAuthenticated)
{
return new ChallengeResult("OpenIdConnect", Url.Action("ExtLoginCallback", "Account", new { ReturnUrl = returnUrl }));
}
return RedirectToAction(returnUrl?? "{Controller}/{Action}"); //verify this
}
[AllowAnonymous]
public async Task<ActionResult> ExtLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
if (loginInfo.Email == null)//Email is coming null from loginInfo
{
loginInfo.Email = loginInfo.ExternalIdentity.Claims.FirstOrDefault(c => c.Type == "email").Value;
}
var user = await this._userManager.FindAsync(loginInfo.Login);
if (user == null)
{
//Create User
}
//Add Okta provided Claims here
var idTokenClaim = user.Claims.FirstOrDefault(c => c.ClaimType == "id_token");
if (idTokenClaim != null)
{
_userManager.RemoveClaim(user.Id, new Claim(idTokenClaim.ClaimType, idTokenClaim.ClaimValue));
_userManager.AddClaim(user.Id, new Claim("id_token", loginInfo.ExternalIdentity.Claims.FirstOrDefault(c => c.Type == "id_token").Value));
}
else
{
_userManager.AddClaim(user.Id, new Claim("id_token", loginInfo.ExternalIdentity.Claims.FirstOrDefault(c => c.Type == "id_token").Value));
}
var accessTokenClaim = user.Claims.FirstOrDefault(c => c.ClaimType == "access_token");
if (accessTokenClaim != null)
{
_userManager.RemoveClaim(user.Id, new Claim(accessTokenClaim.ClaimType, accessTokenClaim.ClaimValue));
_userManager.AddClaim(user.Id, new Claim("access_token", loginInfo.ExternalIdentity.Claims.FirstOrDefault(c => c.Type == "access_token").Value));
}
else
{
_userManager.AddClaim(user.Id, new Claim("access_token", loginInfo.ExternalIdentity.Claims.FirstOrDefault(c => c.Type == "access_token").Value));
}
var refreshTokenClaim = user.Claims.FirstOrDefault(c => c.ClaimType == "refresh_token");
if (refreshTokenClaim != null)
{
_userManager.RemoveClaim(user.Id, new Claim(refreshTokenClaim.ClaimType, refreshTokenClaim.ClaimValue));
_userManager.AddClaim(user.Id, new Claim("refresh_token", loginInfo.ExternalIdentity.Claims.FirstOrDefault(c => c.Type == "refresh_token").Value));
}
else
{
_userManager.AddClaim(user.Id, new Claim("refresh_token", loginInfo.ExternalIdentity.Claims.FirstOrDefault(c => c.Type == "refresh_token").Value));
}
var expClaim = user.Claims.FirstOrDefault(c => c.ClaimType == "exp");
if (expClaim != null)
{
_userManager.RemoveClaim(user.Id, new Claim(expClaim.ClaimType, expClaim.ClaimValue));
_userManager.AddClaim(user.Id, new Claim("exp", GetClaimsDateTime(Convert.ToInt64(loginInfo.ExternalIdentity.Claims.FirstOrDefault(c => c.Type == "exp").Value))));
}
else
{
_userManager.AddClaim(user.Id, new Claim("exp", GetClaimsDateTime(Convert.ToInt64(loginInfo.ExternalIdentity.Claims.FirstOrDefault(c => c.Type == "exp").Value))));
}
var iatClaim = user.Claims.FirstOrDefault(c => c.ClaimType == "iat");
if (iatClaim != null)
{
_userManager.RemoveClaim(user.Id, new Claim(iatClaim.ClaimType, iatClaim.ClaimValue));
_userManager.AddClaim(user.Id, new Claim("iat", GetClaimsDateTime(Convert.ToInt64(loginInfo.ExternalIdentity.Claims.FirstOrDefault(c => c.Type == "iat").Value))));
}
else
{
_userManager.AddClaim(user.Id, new Claim("iat", GetClaimsDateTime(Convert.ToInt64(loginInfo.ExternalIdentity.Claims.FirstOrDefault(c => c.Type == "iat").Value))));
}
//Sign the User with additional claims
result = await _signInManager.ExternalSignInAsync(loginInfo, isPersistent: true);
if(result == SignInStatus.Success){
return RedirectToAction(returnUrl ?? "{Controller}/{Action}");
}
//Authorize Filter used to get Active AccessToken from Okta
//Created Custom Filter to use as Authorize filter on all controllers
public class CustomAuthAttribute : AuthorizeAttribute
{
public override void OnAuthorization(System.Web.Mvc.AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
//Get New Accesstoken before it expires
if (filterContext.HttpContext.User.Identity.IsAuthenticated)
{
var _oktaDomain = ConfigurationManager.AppSettings["okta:OktaDomain"];
var _redirectUri = ConfigurationManager.AppSettings["okta:RedirectUri"];
var _clientId = ConfigurationManager.AppSettings["okta:ClientId"];
var _clientSecret = ConfigurationManager.AppSettings["okta:ClientSecret"];
var _refreshToken = HttpContext.Current.User.Identity.GetUserRefreshToken();
var _issuedat = HttpContext.Current.User.Identity.GetUserIat();
var _expiresat = HttpContext.Current.User.Identity.GetUserExp();
var expire = 3600;
if (DateTime.Now.Subtract(Convert.ToDateTime(_issuedat)).TotalSeconds >= (expire - 3540))//Testing for 1 min
{
var client = new RestClient(_oktaDomain + "/oauth2/v1/token");
client.Timeout = -1;
var request = new RestRequest(Method.POST);
request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
request.AddParameter("client_id", _clientId);
request.AddParameter("client_secret", _clientSecret);
request.AddParameter("grant_type", "refresh_token");
request.AddParameter("redirect_uri", _redirectUri);
request.AddParameter("scope", "openid profile email offline_access");
request.AddParameter("refresh_token", _refreshToken);
IRestResponse response = client.Execute(request);
//Console.WriteLine(response.Content);
dynamic jsonResponse = null;
if (response.StatusCode == HttpStatusCode.OK) {
jsonResponse = JObject.Parse(response.Content);
}
if (jsonResponse.error == null)
{
//find common place to keep below
ClaimsPrincipal cp = HttpContext.Current.GetOwinContext().Authentication.User;
foreach (var uidentity in cp.Identities)
{
var idTokenClaim = uidentity.FindFirst("id_token");
if (idTokenClaim != null)
{
uidentity.RemoveClaim(idTokenClaim);
uidentity.AddClaim(new Claim("id_token", jsonResponse.id_token.Value));
}
else
{
uidentity.AddClaim(new Claim("id_token", jsonResponse.id_token.Value));
}
var accessTokenClaim = uidentity.FindFirst("access_token");
if (accessTokenClaim != null)
{
uidentity.RemoveClaim(accessTokenClaim);
uidentity.AddClaim(new Claim("access_token", jsonResponse.access_token.Value));
}
else
{
uidentity.AddClaim(new Claim("access_token", jsonResponse.access_token.Value));
}
var refreshTokenClaim = uidentity.FindFirst("refresh_token");
if (refreshTokenClaim != null)
{
uidentity.RemoveClaim(refreshTokenClaim);
uidentity.AddClaim(new Claim("refresh_token", jsonResponse.refresh_token.Value));
}
else
{
uidentity.AddClaim(new Claim("refresh_token", jsonResponse.refresh_token.Value));
}
var expClaim = uidentity.FindFirst("exp");
if (expClaim != null)
{
uidentity.RemoveClaim(expClaim);
uidentity.AddClaim(new Claim("exp", DateTime.UtcNow.AddSeconds(Convert.ToDouble(jsonResponse.expires_in)).ToLocalTime().ToString(CultureInfo.InvariantCulture)));
}
else
{
uidentity.AddClaim(new Claim("exp", DateTime.UtcNow.AddSeconds(Convert.ToDouble(jsonResponse.expires_in)).ToLocalTime().ToString(CultureInfo.InvariantCulture)));
}
var iatClaim = uidentity.FindFirst("iat");
if (iatClaim != null)
{
uidentity.RemoveClaim(iatClaim);
uidentity.AddClaim(new Claim("iat", DateTime.UtcNow.ToLocalTime().ToString()));
}
else
{
uidentity.AddClaim(new Claim("iat", DateTime.UtcNow.ToString()));
}
//This will only add claims to Identity, Find a way to save in DB as well
HttpContext.Current.GetOwinContext().Authentication.SignIn(uidentity);
}
}
}
LogOut: On Logout button click, Logout is called, and PostLogoutRedirectUri is set to Account/LogOff. But after setting RedirectUri to Authorization-code/callback, logout is becoming infinite loop of Logout, Authorize Endpoint, Callback. What I am doing wrong here?
public void Logout()
{
if (HttpContext.User.Identity.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.SignOut(
DefaultAuthenticationTypes.ExternalCookie,
CookieAuthenticationDefaults.AuthenticationType,
OktaDefaults.MvcAuthenticationType);
}
}
public ActionResult LogOff()
{
//Login action is called after authorization-code/callback as it is set to default rout
return RedirectToAction("Login", "Account");
}
In your startup.cs
app.UseOktaMvc(new OktaMvcOptions()
{
OktaDomain = ConfigurationManager.AppSettings["okta:OktaDomain"],
ClientId = ConfigurationManager.AppSettings["okta:ClientId"],
ClientSecret = ConfigurationManager.AppSettings["okta:ClientSecret"],
RedirectUri = ConfigurationManager.AppSettings["okta:RedirectUri"],
PostLogoutRedirectUri = ConfigurationManager.AppSettings["okta:PostLogoutRedirectUri"],
Scope = new List<string> { "openid", "profile", "email", "offline_access" },
});
In your web.config make sure you have redirect uri as below
<add key="okta:RedirectUri" value="https://localhost:{port}/authorization-code/callback" />
You should not write controller for this route. This is provided by the Okta.AspNet package already. The sign in process is handled by this route itself unless you want to do something out of the box.
Your default route's action should look something like:
if (!HttpContext.User.Identity.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge(OpenIdConnectAuthenticationDefaults.AuthenticationType);
return new HttpUnauthorizedResult();
}
return RedirectToAction("Index", "Home");
Now, you should be able to read all the userclaims in your application.
var claims = HttpContext.GetOwinContext().Authentication.User.Claims.ToList();
You can also decorate your controllers/action by [Authorize] attribute to secure them from unauthenticated access.
I want to update the user's claims using HttpContext.User instance, but after updating the claims they only stay within the scope of the current request. I need to make it persist for the upcoming requests as well, please help me out with this.
Please find my code below. In the POST method I update the claim and next time when the GET method is hit, I am trying to get the updated value but I get the old value.
[Route("login")]
public class LoginController : Controller
{
private readonly IList<User> users = new List<User>
{
new User { UserName = "admin", Password = "1234", Role="Administrator"},
new User { UserName = "user", Password ="1234", Role="User"}
};
private IConfiguration _config;
public LoginController(IConfiguration config)
{
this._config = config;
}
[HttpGet("Enter")]
public IActionResult Login([FromQuery]string username, [FromQuery]string password)
{
User login = new User();
login.UserName = username;
login.Password = password;
IActionResult response = Unauthorized();
var user = AuthenticateUser(login);
if(user != null)
{
var tokenStr = GenerateJSONWebToken(user);
response = Ok(new { token = tokenStr });
}
return response;
}
private string GenerateJSONWebToken(User userinfo)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim("username", userinfo.UserName),
new Claim(ClaimTypes.Role, userinfo.Role),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Issuer"],
claims,
expires: DateTime.Now.AddMinutes(120),
signingCredentials: credentials
);
var encodettoken = new JwtSecurityTokenHandler().WriteToken(token);
return encodettoken;
}
[Authorize(Roles = "Administrator")]
[Authorize]
[HttpPost("Post")]
public string Post()
{
var identity = HttpContext.User.Identity as ClaimsIdentity;
IList<Claim> claim = identity.Claims.ToList();
var username = claim[0].Value;
return "Welcome To " + username;
// i update claim here
var identityClaims = (ClaimsIdentity)User.Identity;
var username = identityClaims.FindFirst("username");
if (username != null)
identityClaims.RemoveClaim(username);
identityClaims.AddClaim(new Claim("username", "sample username"));
}
[Authorize(Roles = "Administrator, User")]
[HttpGet("GetValue")]
public ActionResult<IEnumerable<string>> Get()
{
// in the next get request i try to access the claim, but it does not have the updated value
// instead it has the old value
// here i have to persist the value
var identityClaims = (ClaimsIdentity)User.Identity;
var username = identityClaims.FindFirst("username");
return new string[] { "Value1", "Value2", "Value3" };
}
private User AuthenticateUser(User login)
{
User entity = null;
if (users.Where(x=>x.UserName == login.UserName && x.Password == login.Password).ToList().Count() > 0)
{
entity = users.Where(x => x.UserName == login.UserName && x.Password == login.Password).FirstOrDefault();
}
return entity;
}
}
I am currently trying to authenticate users for a web app with our domain AD and so far I have been able to successfully login.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
var authService = new ADAuthenticationService(AuthenticationManager);
var authenticationResult = authService.SignIn(model.Email, model.Password);
if (authenticationResult.IsSuccess)
{
// To home/index since no returnUrl is actually being captured for now
return RedirectToLocal(returnUrl);
}
ModelState.AddModelError("", authenticationResult.ErrorMessage);
return View(model);
}
My auth service creates a claim identity and signs the user in. Next, I want to secure my web api endpoints to allow only logged in clients to send requests to it. Following an example I have the following:
[HttpPost]
public ActionResult Authorize()
{
var claims = new ClaimsPrincipal(User).Claims.ToArray();
var identity = new ClaimsIdentity(claims, "Bearer");
AuthenticationManager.SignIn(identity);
return new EmptyResult();
}
However, when I actually try to go to the endpoint it tells me I am unauthorized. In the request headers I have "Authorization: Bearer token here".
EDIT:
In Startup.Auth.cs I have auth configured as such
static Startup()
{
PublicClientId = "web";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
AuthorizeEndpointPath = new PathString("/Account/Authorize"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
AllowInsecureHttp = true
};
}
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = ADAuthentication.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(validateInterval: TimeSpan.FromMinutes(20),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
},
CookieName = "MyAppAuthenticationType",
CookieHttpOnly = true,
ExpireTimeSpan = TimeSpan.FromHours(1),
});
app.UseOAuthBearerTokens(OAuthOptions);
}
EDIT 2:
ADAuthenticationService.cs
public ADAuthenticationService(IAuthenticationManager authenticationManager)
{
this.authenticationManager = authenticationManager;
}
public AuthenticationResult SignIn(String username, String password)
{
PrincipalContext principalContext = new PrincipalContext(authenticationType);
bool isAuthenticated = false;
UserPrincipal userPrincipal = null;
try
{
userPrincipal = UserPrincipal.FindByIdentity(principalContext, username);
if (userPrincipal != null)
{
isAuthenticated = principalContext.ValidateCredentials(username, password, ContextOptions.Negotiate);
}
}
catch (Exception exception)
{
System.Diagnostics.Debug.WriteLine(exception);
return new AuthenticationResult("Username or Password is not correct");
}
/* some other invalid login results here */
var identity = CreateIdentity(userPrincipal);
authenticationManager.SignOut(ADAuthentication.ApplicationCookie);
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = true }, identity);
return new AuthenticationResult();
}
private ClaimsIdentity CreateIdentity(UserPrincipal userPrincipal)
{
var identity = new ClaimsIdentity(ADAuthentication.ApplicationCookie, ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);
identity.AddClaim(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "Active Directory"));
identity.AddClaim(new Claim(ClaimTypes.Name, userPrincipal.SamAccountName));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userPrincipal.SamAccountName));
if (!String.IsNullOrEmpty(userPrincipal.EmailAddress))
{
identity.AddClaim(new Claim(ClaimTypes.Email, userPrincipal.EmailAddress));
}
var groups = userPrincipal.GetAuthorizationGroups();
foreach (var #group in groups)
{
identity.AddClaim(new Claim(ClaimTypes.Role, #group.Name));
}
return identity;
}
ASP.NET Core combined with Identity already provide a simple way to check roles once after login, but I would like to query the database for the current roles for the current user before every controller action.
I've read up on Role-based, Policy-based, and Claims-based authorization from Microsoft. (https://learn.microsoft.com/en-us/aspnet/core/security/authorization/introduction)
None of these solutions seem to check roles on every action. Here is my latest attempt to implement the desired outcome, in the form of some policy-based authorization:
In Startup.cs:
DatabaseContext context = new DatabaseContext();
services.AddAuthorization(options =>
{
options.AddPolicy("IsManager",
policy => policy.Requirements.Add(new IsManagerRequirement(context)));
options.AddPolicy("IsAdmin",
policy => policy.Requirements.Add(new IsAdminRequirement(context)));
});
In my requirements file:
public class IsAdminRequirement : IAuthorizationRequirement
{
public IsAdminRequirement(DatabaseContext context)
{
_context = context;
}
public DatabaseContext _context { get; set; }
}
public class IsAdminHandler : AuthorizationHandler<IsAdminRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IsAdminRequirement requirement)
{
// Enumerate all current users roles
int userId = Int32.Parse(context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value);
Roles adminRoles = requirement._context.Roles.FirstOrDefault(r => r.Name == "Administrator" && r.IsActive == true);
bool hasRole = requirement._context.UserRoles.Any(ur => ur.UserId == userId && adminRoles.Id == ur.RoleId && ur.IsActive == true);
// Check for the correct role
if (hasRole)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
and in the controller:
[HttpGet]
[Authorize(Policy = "IsAdmin")]
public async Task<IActionResult> Location()
{
// do action here
}
With this code, the requirement middleware is somehow never called, and therefore the database is never checked.
How would I correctly query the database to check for the current user's roles before carrying out each controller action?
I solved this problem in my application (SignalR + JwtBearer) by handling the OnTokenValidated event. I just check the roles from the claims with the one in my database. If they're not valid anymore, i set the TokenValidatedContext to failed.
Here's an extract of my ASP.NET Core Startup.cs:
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserRoleStore<User>>();
var username = context.Principal.Identity.Name;
var user = await userService.FindByNameAsync(username, CancellationToken.None);
if (user == null)
{
// return unauthorized if user no longer exists
context.Fail("Unauthorized");
}
else
{
// Check if the roles are still valid.
var roles = await userService.GetRolesAsync(user, CancellationToken.None);
foreach (var roleClaim in context.Principal.Claims.Where(p => p.Type == ClaimTypes.Role))
{
if (roles.All(p => p != roleClaim.Value))
{
context.Fail("Unauthorized");
return;
}
}
context.Success();
}
},
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
I am stumped by this behavior. Basically, when a refresh of a token is requested, WebAPI goes through the logic of removing the old refresh_token from the data, and I don't see any errors in the debugger. However, the API returns a HTTP 400 with "invalid_grant" as the error.
Startup.cs
// OAuth Server configuration
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = false,
TokenEndpointPath = new PathString("/oauth2/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(1),
AccessTokenFormat = new AccessTokenJwtFormat(issuer),
RefreshTokenProvider = new SimpleRefreshTokenProvider(),
RefreshTokenFormat = new RefreshTokenJwtFormat(issuer),
Provider = new CustomOAuthProvider()
};
// OAuth 2.0 Bearer Access Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
SimpleRefreshTokenProvider.cs
public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
{
public async Task CreateAsync(AuthenticationTokenCreateContext context)
{
var clientid = context.Ticket.Properties.Dictionary["as:client_id"];
if (string.IsNullOrEmpty(clientid))
{
return;
}
var refreshTokenId = Guid.NewGuid().ToString("n");
var refreshTokenLifeTime = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTime");
var _repo = ClientDbProvider.GetInstance();
var token = new RefreshToken()
{
Id = ClientHelper.GetHash(refreshTokenId),
ClientId = clientid,
Subject = context.Ticket.Identity.Name,
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.AddMinutes(Convert.ToDouble(refreshTokenLifeTime))
};
context.Ticket.Properties.IssuedUtc = new DateTimeOffset(token.IssuedUtc);
context.Ticket.Properties.ExpiresUtc = new DateTimeOffset(token.ExpiresUtc);
context.Ticket.Properties.Dictionary.Add("refreshTokenId", refreshTokenId);
context.Ticket.Identity.AddClaim(new Claim(ClaimTypes.Role, "refreshToken"));
token.ProtectedTicket = context.SerializeTicket();
var result = await _repo.AddRefreshToken(token);
if (result)
{
context.SetToken(token.ProtectedTicket);
}
return;
}
public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
var _repo = ClientDbProvider.GetInstance();
string hashedTokenId = ClientHelper.GetHash(context.Token);
var refreshToken = await _repo.FindRefreshToken(hashedTokenId);
if (refreshToken != null)
{
//Get protectedTicket from refreshToken class
context.DeserializeTicket(refreshToken.ProtectedTicket);
var result = await _repo.RemoveRefreshToken(hashedTokenId);
}
}
public void Create(AuthenticationTokenCreateContext context)
{
throw new NotImplementedException();
}
public void Receive(AuthenticationTokenReceiveContext context)
{
throw new NotImplementedException();
}
}
RefreshTokenJwtFormat.cs
public class RefreshTokenJwtFormat : ISecureDataFormat<AuthenticationTicket>
{
private const string AudiencePropertyKey = "audience";
private readonly string _issuer = string.Empty;
public RefreshTokenJwtFormat(string issuer)
{
_issuer = issuer;
audStore = new AudiencesStore();
}
public string Protect(AuthenticationTicket data)
{
if (data == null)
{
throw new ArgumentNullException("data");
}
string audienceId = data.Properties.Dictionary.ContainsKey(AudiencePropertyKey) ? data.Properties.Dictionary[AudiencePropertyKey] : null;
if (string.IsNullOrWhiteSpace(audienceId)) throw new InvalidOperationException("AuthenticationTicket.Properties does not include audience");
var audience = GetAudience(audienceId);
string symmetricKeyAsBase64 = audience.Base64Secret;
var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
var signingCredentials = new SigningCredentials(
new InMemorySymmetricSecurityKey(keyByteArray),
SecurityAlgorithms.HmacSha256Signature,
SecurityAlgorithms.Sha256Digest);
var issued = data.Properties.IssuedUtc;
var expires = data.Properties.ExpiresUtc;
var payload = new JwtPayload(_issuer, audienceId, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime);
if (data.Properties.Dictionary.ContainsKey("refreshTokenId"))
{
payload.Add("refreshTokenId", data.Properties.Dictionary["refreshTokenId"]);
}
var header = new JwtHeader(signingCredentials);
var token = new JwtSecurityToken(header, payload);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.WriteToken(token);
return jwt;
}
public AuthenticationTicket Unprotect(string protectedText)
{
var handler = new JwtSecurityTokenHandler();
SecurityToken securityToken = handler.ReadToken(protectedText);
var audienceId = ((JwtSecurityToken)securityToken).Claims.First(x => x.Type == "aud").Value;
var audience = GetAudience(audienceId);
string symmetricKeyAsBase64 = audience.Base64Secret;
var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
var securityKey = new InMemorySymmetricSecurityKey(keyByteArray);
var validationParameters = new TokenValidationParameters()
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = securityKey,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
ValidateAudience = true,
ValidAudience = audienceId,
ValidateIssuer = true,
ValidIssuer = _issuer
};
SecurityToken validatedToken;
ClaimsPrincipal principal = null;
try
{
principal = handler.ValidateToken(protectedText, validationParameters, out validatedToken);
}
catch(Exception ex)
{
return null;
}
return new AuthenticationTicket(principal.Identities.First(), new AuthenticationProperties());
}
private Models.Audience GetAudience(string audienceId)
{
var findAudTask = Task.Run(() => audStore.FindAudienceAsync(audienceId));
findAudTask.Wait();
var audience = findAudTask.Result;
return audience;
}
private AudiencesStore audStore;
}
First you need
implement GrantRefreshToken(OAuthGrantRefreshTokenContext context) in your CustomOAuthProvider
something like
public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
{
// Change auth ticket for refresh token requests
var newIdentity = new ClaimsIdentity(context.Ticket.Identity);
//newIdentity.AddClaim(new Claim("newClaim", "newValue"));
var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties);
context.Validated(newTicket);
return Task.FromResult<object>(null);
}
Second you need to change your method Unprotect in RefreshTokenJwtFormat.
replace
return new AuthenticationTicket(principal.Identities.First(), new AuthenticationProperties());
with
return new AuthenticationTicket(principal.Identities.First(), new AuthenticationProperties
{
IssuedUtc = validatedToken.ValidFrom,
ExpiresUtc = validatedToken.ValidTo
});
i have similar (working) code. I'm not a specialist on this but i compared my code a bit and noticed:
in SimpleRefreshToken.cs:
in receiveAsync, my code ends with:
context.SetTicket(context.Ticket);
return Task.FromResult<object>(null);
Which sets the ticket and return a value.
The same for createAsync, my code ends with:
return Task.FromResult<object>(null);
Allthough, no idea if this will help