I have an ASP.NET 5 beta 8 application which integrates with Azure Active Directory using OpenIdConnect. I tried updating the application to RC1 and changed the openid nuget package to "Microsoft.AspNet.Authentication.OpenIdConnect": "1.0.0-rc1-final". Everything seems to be correct, but the OnAuthenticationComplete method which I used to add roles to my ClaimsIdentity is no longer in the Events object, and I can't find the alternative. How can I add claims to my Identity with the new version?
UPDATE: Changing to OnAuthenticationValidated still not works for me. I must be doing something wrong in my event's code:
OnAuthenticationValidated = async notification =>
{
var claimsIdentity = notification.AuthenticationTicket.Principal.Identity as ClaimsIdentity;
var userRepository=new UserRepository(Configuration);
var userId = claimsIdentity?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var user = await userRepository.FindAsync(userId);
if (user == null)
{
await userRepository.AddAsync(new UserDto
{
UserId = userId,
Username = claimsIdentity?.FindFirst(ClaimTypes.Name)?.Value,
DisplayName = claimsIdentity?.FindFirst(ClaimTypes.Name)?.Value
});
}
claimsIdentity?.AddClaim(new Claim(ClaimTypes.Role, "super"));
}
Also, the code I use to do the login is:
[HttpGet]
public IActionResult Login()
{
if (HttpContext.User == null || !HttpContext.User.Identity.IsAuthenticated)
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" });
return RedirectToAction("Index", "Home");
}
The AuthenticationCompleted event has been renamed to AuthenticationValidated.
You can find more information on this ticket: https://github.com/aspnet/Security/pull/442.
Related
What I have in my mind when navigating through the application, I want to save the token to the localhost along with role name and I will check if the users have access to a certain link. Is that how it works? with Authgard in Angular 8?. Can you give me some insight of navigating an application with the role from Identity(which is built in from ASP.net core 3.1).
login
// POST api/auth/login
[HttpPost("login")]
public async Task<IActionResult> Post([FromBody]CredentialsViewModel credentials)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var identity = await GetClaimsIdentity(credentials.UserName, credentials.Password);
if (identity == null)
{
//return null;
return BadRequest(Error.AddErrorToModelState("login_failure", "Invalid username or password.", ModelState));
}
var jwt = await Tokens.GenerateJwt(identity, _jwtFactory, credentials.UserName, _jwtOptions, new JsonSerializerSettings { Formatting = Formatting.Indented });
return new OkObjectResult(jwt);
}
Generate Token Method
public static async Task<string> GenerateJwt(ClaimsIdentity identity, IJwtFactory jwtFactory, string userName, JwtIssuerOptions jwtOptions, JsonSerializerSettings serializerSettings)
{
var response = new
{
id = identity.Claims.Single(c => c.Type == "id").Value,
//probably here I want to send the role too!!
auth_token = await jwtFactory.GenerateEncodedToken(userName, identity),
expires_in = (int)jwtOptions.ValidFor.TotalSeconds
};
return JsonConvert.SerializeObject(response, serializerSettings);
}
}
You need to add claims information when generating your JWT.
Here`s an example
And another one:
1 part(how to implement JWT), 2 part(about claims here)
I cannot figure out how to get an attribute from the the saml response in place of the NameID value.
My IDP team is returning the value I need in an attribute rather than in NameID(which they wont budge on).
Thanks for any help!
I am running MVC Core. I have everything setup and running for NameID from the example 'TestWebAppCore' for ITfoxtec.Identity.Saml2.
I am trying to get this value in place of NameID for the session username:
saml:AttributeStatement>
<saml:Attribute Name="valueName"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"
>
<saml:AttributeValue>IDValue</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
[Route("AssertionConsumerService")]
public async Task<IActionResult> AssertionConsumerService()
{
var binding = new Saml2PostBinding();
var saml2AuthnResponse = new Saml2AuthnResponse(config);
binding.ReadSamlResponse(Request.ToGenericHttpRequest(), saml2AuthnResponse);
if (saml2AuthnResponse.Status != Saml2StatusCodes.Success) {
throw new AuthenticationException($"SAML Response status: {saml2AuthnResponse.Status}");
}
binding.Unbind(Request.ToGenericHttpRequest(),
saml2AuthnResponse);
try {
await saml2AuthnResponse.CreateSession(HttpContext,
claimsTransform: (claimsPrincipal) =>
ClaimsTransform.Transform(claimsPrincipal));
}
catch (Exception ex) {
log.writeLog(ex.Message.ToString());
}
var relayStateQuery = binding.GetRelayStateQuery();
var returnUrl = relayStateQuery.ContainsKey(relayStateReturnUrl)
? relayStateQuery[relayStateReturnUrl] : Url.Content("~/");
return Redirect(returnUrl);
}
It is probably not possible to logout without the NameID but you can login without.
In .NET the NameID is translated into the ClaimTypes.NameIdentifier claim. The users claims is handled in the ClaimsTransform.CreateClaimsPrincipal method.
You can either translate the incoming custom claim "valueName" to a ClaimTypes.NameIdentifier claim:
private static ClaimsPrincipal CreateClaimsPrincipal(ClaimsPrincipal incomingPrincipal)
{
var claims = new List<Claim>();
claims.AddRange(GetSaml2LogoutClaims(incomingPrincipal));
claims.Add(new Claim(ClaimTypes.NameIdentifier, GetClaimValue(incomingPrincipal, "valueName")));
return new ClaimsPrincipal(new ClaimsIdentity(claims, incomingPrincipal.Identity.AuthenticationType, ClaimTypes.NameIdentifier, ClaimTypes.Role)
{
BootstrapContext = ((ClaimsIdentity)incomingPrincipal.Identity).BootstrapContext
});
}
Or change the identity claim in the ClaimsIdentity to the incoming custom claim "valueName":
private static ClaimsPrincipal CreateClaimsPrincipal(ClaimsPrincipal incomingPrincipal)
{
var claims = new List<Claim>();
// All claims
claims.AddRange(incomingPrincipal.Claims);
return new ClaimsPrincipal(new ClaimsIdentity(claims, incomingPrincipal.Identity.AuthenticationType, "valueName", ClaimTypes.Role)
{
BootstrapContext = ((ClaimsIdentity)incomingPrincipal.Identity).BootstrapContext
});
}
I have an application that uses JwtBearerAuthentication. I am trying to add my application claims to the User(ClaimsPrincipal) at the beginning of each request. I managed to do that using ClaimsTransformationOptions:
app.UseClaimsTransformation(new ClaimsTransformationOptions
{
Transformer = new ClaimsTransformer<TUser, TRole>()
});
and in my TransformAsync:
public async Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
{
var services = context.Context.RequestServices;
var userManager = services.GetRequiredService<UserManager<TUser>>();
var roleManager = services.GetRequiredService<RoleManager<TRole>>();
var userId = 1; // Get the UserId from my store, let say its 1 for now
if (userId != 0)
{
var user = await userManager.FindByIdAsync(userId);
var claimsPrincipal = await new UserClaimsPrincipalFactory<TUser, TRole>(userManager, roleManager, _optionsAccessor)
.CreateAsync(user);
context.Principal.AddIdentities(claimsPrincipal.Identities);
}
return context.Principal;
}
So far so good and the claims are being loaded from the database and added to the context.Principal. My problem is once I reach the controller, the identities are being overwritten !!
So I solved this problem by putting the app.UseClaimsTransformation after app.UseJwtBearerAuthentication which made sure that whenever JWT is going to amend the ClaimsPrincipal the ClaimsTransformation will be called afterwards to add my own claims.
I created an ASP.NET WEB API 2.2 project. I used the Windows Identity Foundation based template for individual accounts available in visual studio see it here.
The web client (written in angularJS) uses OAUTH implementation with web browser cookies to store the token and the refresh token. We benefit from the helpful UserManager and RoleManager classes for managing users and their roles.
Everything works fine with OAUTH and the web browser client.
However, for some retro-compatibility concerns with desktop based clients I also need to support Basic authentication. Ideally, I would like the [Authorize], [Authorize(Role = "administrators")] etc. attributes to work with both OAUTH and Basic authentication scheme.
Thus, following the code from LeastPrivilege I created an OWIN BasicAuthenticationMiddleware that inherits from AuthenticationMiddleware.
I came to the following implementation. For the BasicAuthenticationMiddleWare only the Handler has changed compared to the Leastprivilege's code. Actually we use ClaimsIdentity rather than a series of Claim.
class BasicAuthenticationHandler: AuthenticationHandler<BasicAuthenticationOptions>
{
private readonly string _challenge;
public BasicAuthenticationHandler(BasicAuthenticationOptions options)
{
_challenge = "Basic realm=" + options.Realm;
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
var authzValue = Request.Headers.Get("Authorization");
if (string.IsNullOrEmpty(authzValue) || !authzValue.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
{
return null;
}
var token = authzValue.Substring("Basic ".Length).Trim();
var claimsIdentity = await TryGetPrincipalFromBasicCredentials(token, Options.CredentialValidationFunction);
if (claimsIdentity == null)
{
return null;
}
else
{
Request.User = new ClaimsPrincipal(claimsIdentity);
return new AuthenticationTicket(claimsIdentity, new AuthenticationProperties());
}
}
protected override Task ApplyResponseChallengeAsync()
{
if (Response.StatusCode == 401)
{
var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
if (challenge != null)
{
Response.Headers.AppendValues("WWW-Authenticate", _challenge);
}
}
return Task.FromResult<object>(null);
}
async Task<ClaimsIdentity> TryGetPrincipalFromBasicCredentials(string credentials,
BasicAuthenticationMiddleware.CredentialValidationFunction validate)
{
string pair;
try
{
pair = Encoding.UTF8.GetString(
Convert.FromBase64String(credentials));
}
catch (FormatException)
{
return null;
}
catch (ArgumentException)
{
return null;
}
var ix = pair.IndexOf(':');
if (ix == -1)
{
return null;
}
var username = pair.Substring(0, ix);
var pw = pair.Substring(ix + 1);
return await validate(username, pw);
}
Then in Startup.Auth I declare the following delegate for validating authentication (simply checks if the user exists and if the password is right and generates the associated ClaimsIdentity)
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(DbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
Func<string, string, Task<ClaimsIdentity>> validationCallback = (string userName, string password) =>
{
using (DbContext dbContext = new DbContext())
using(UserStore<ApplicationUser> userStore = new UserStore<ApplicationUser>(dbContext))
using(ApplicationUserManager userManager = new ApplicationUserManager(userStore))
{
var user = userManager.FindByName(userName);
if (user == null)
{
return null;
}
bool ok = userManager.CheckPassword(user, password);
if (!ok)
{
return null;
}
ClaimsIdentity claimsIdentity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
return Task.FromResult(claimsIdentity);
}
};
var basicAuthOptions = new BasicAuthenticationOptions("KMailWebManager", new BasicAuthenticationMiddleware.CredentialValidationFunction(validationCallback));
app.UseBasicAuthentication(basicAuthOptions);
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
//If the AccessTokenExpireTimeSpan is changed, also change the ExpiresUtc in the RefreshTokenProvider.cs.
AccessTokenExpireTimeSpan = TimeSpan.FromHours(2),
AllowInsecureHttp = true,
RefreshTokenProvider = new RefreshTokenProvider()
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
}
However, even with settings the Request.User in Handler's AuthenticationAsyncCore method the [Authorize] attribute does not work as expected: responding with error 401 unauthorized every time I try to use the Basic Authentication scheme.
Any idea on what is going wrong?
I found out the culprit, in the WebApiConfig.cs file the 'individual user' template inserted the following lines.
//// Web API configuration and services
//// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
Thus we also have to register our BasicAuthenticationMiddleware
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
config.Filters.Add(new HostAuthenticationFilter(BasicAuthenticationOptions.BasicAuthenticationType));
where BasicAuthenticationType is the constant string "Basic" that is passed to the base constructor of BasicAuthenticationOptions
public class BasicAuthenticationOptions : AuthenticationOptions
{
public const string BasicAuthenticationType = "Basic";
public BasicAuthenticationMiddleware.CredentialValidationFunction CredentialValidationFunction { get; private set; }
public BasicAuthenticationOptions( BasicAuthenticationMiddleware.CredentialValidationFunction validationFunction)
: base(BasicAuthenticationType)
{
CredentialValidationFunction = validationFunction;
}
}
I have created an MVC 4 application which targets .NET 4.0. After deploying to my production server, it will show the login page but will not redirect to the default page. However, when I add debugging, I can see that the authentication process works but then the error I am getting is an error that says it can't find my View for my Error Page and then shows my Error Page. It just seems that it will not go to my "Home/Index" page - even when I remove the authorize attribute. Of course the application works in development. Additionally, it will not go to my register page or forgot login page.
My Login Controller looks like this:
[HttpPost]
[AllowAnonymous]
[ValidateAntiforgeryToken]
public ActionResult Login(LoginViewModel model, string returnUrl)
{
if(ModelStat.IsValid && _userService.Login(model.UserId, model.Password))
{
var user = _userService.GetUser(model.UserId);
var loggedInUser = new LoggedInUser
{
// Build the user for custom IPrincipal
};
var userData = JsonConvert.SerializeObject(loggedInUser);
var compressData = StringCompression.Compress(userData);
var authTicket = new FormsAuthenticationTicket(
1,
user.UserId,
DateTime.Now,
DateTime.Now.AddHours(1),
false,
compressData);
var encTicket = FormsAuthentication.Encrypt(authTicket);
if(encTicket != null)
{
var faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket)
{
HttpOnly = true
};
Response.Cookies.Add(faCookie);
}
user.LastActivityDate = DateTime.Now;
user.LastLoginDate = DateTime.Now;
_userService.UpdateUser(user);
_uow.Commit();
return Url.IsLocalUrl(returnUrl) ? (ActionResult)Redirect(returnUrl) : RedirectToAction("Index", "Home");
}
return View(model);
and in my Global.asax:
protected void Application_PostAuthenticateRequest(object sender, EventArgs e)
{
var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if(authCookie != null)
{
var decompressedData = StringCompression.Decompress(authTicket.UserData);
var loggedInUser = JsonConvert.DesrializeObject<LoggedInUser>(decompressedData);
var currrentUser = new CustomPrincipal(authTicket.Name)
{
// Build the CustomPrincipal from the loggedInUser
};
if(HttpContext.Current.User.Identity.IsAuthenticated)
{
HttpContext.Current.User = currentUser;
}
}
}
I hope that this is enough to give someone an idea of what I may be doing wrong. Somehow I feel that it is something small that I am missing. Thanks in advance.
~InDireStraits
Update:
After more troubleshooting, it would seem that the issue may have something to do with the fact that I am using a BaseController for specifying permissions but I am still baffled as to why the application works as intended in my development environment but not in production. To verify my IIS settings I installed the default MVC4 App to production which does not have .NET 4.5, and it runs. I am using VS 2012 so I do have 4.5. Could I somehow be introducing .NET 4.5 classes or functionality even if this targets .NET 4.0? At any rate, here is my BaseController code:
public class BaseController: Controller
{
private string _actionKey;
private const string PermisisionList = "permissionList";
private Dictionary<string, string> _requiredActionPermissions;
private static readonly IControllerActionService<ControllerAction> _actionService;
protected new CustomPrincipal User
{
get
{
return HttpContext.User as CustomPrincipal;
}
}
public BaseController(IControllerActionService<ControllerAction> actionService)
{
_actionService = actionService;
}
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Check to see if the PermissionList is loaded and load if necessary
if(!CacheLayer.Exists(PermissionList))
{
_requiredActionPermissions = _actionService.GetControllerActionDictionary();
CacheLayer.Add(_requiredActionPermissions, PermissionList);
}
else
{
_requiredActionPermission = CacheLayer.Get<Dictionary<string, string>>(PermissionList);
}
// Get the Controller/Action of the current request
_actionKey = string.Format("{0}-{1}", filterContext.ActionDescriptor.ControllerDescriptor.ControllerName, filterContext.ActionDescriptor.ActionName);
// If the user is authenticated, grab the permissions
if(filterContext.HttpContext.User.Identity.IsAuthenticated)
{
var userPermissions = User.Permissions;
if(!_requiredActionPermissions.Values.Any(a=>a.Equals(_actionKey, StringComparison.OrdinalIgnoreCase)))
{
return;
}
if(userPermissions.Contains(_requiredActionsPermissions.FirstOrDefault(x=>x.Value == _actionKey).Key))
{
return;
}
filterContext.Result = new RedirectResult("~/Error/ErrorUnauthorized");
return;
}
if(!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
if(!_requiredActionPermissions.Values.Any(a=>a.Equals(_actionKey, StringComparison.OrdinalIgnoreCase)))
{
return;
}
}
if(filterContext.HttpContext.Request.Url == null)
{
return;
}
if(filterContext.HttpContext.Request.Url.AbsolutePath == FormsAuthentication.LoginUrl)
{
return;
}
var redirectUrl = string.Format("?returnUrl={0}", filterContext.HttpContext.Request.Url.PathAndQuery);
filterContext.HttpContext.Response.Redirect(FormsAuthentication.LoginUrl + redirectUrl, true);
}
UPDATE 2: Installed .NET 4.52 on Staging Server and the application now works as intended. The problem is that I will not be able to install this on the production server. I don't understand what it is that 4.5 is fixing that 4.0 does not seem to facilitate. HELLLLLLPPP!!!!!!
The answer can be found here. To summarize, I added an extra parameter to my route config that worked in 4.5 but not in 4.0. Will follow up on the linked question. Thanks