I am using IdtentityServer4 where I have a scenario when a support personnel want to login on behalf of a user for support.
When this happens I want to know throughout my ASP.NET Core app that this support guy is acting on behalf of the user with a message on the screen that says:
"Mr Support on Behalf of Ms Username"
I am currently implementing IProfileUser for normal login. I am looking for a way to add an extra "attribute" to include the username of the support person in token without accessing the database.
I tried to pass items
await _signInManager.SignInAsync(applicationUser, new AuthenticationProperties
{
Items = {new KeyValuePair<string, string>("SupportName", "Mr Support")}
}).ConfigureAwait(false);
But I don't know how to get them from the ProfileDataRequestContext passed to GetProfileDataAsync in IProfileService implementation.
This is my IProfileService implementation:
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
ClaimsPrincipal claimsPrincipal = context.Subject;
if (claimsPrincipal == null)
{
throw new ArgumentNullException(nameof(context.Subject));
}
string id = claimsPrincipal.GetSubjectId();
ApplicationUser user = await _userManager.FindByIdAsync(id).ConfigureAwait(false);
if (user == null)
{
throw new ArgumentException("Invalid user identifier");
}
context.IssuedClaims.Add(new Claim(JwtClaimTypes.Email, user.Email));
IList<string> roles = await _userManager.GetRolesAsync(user).ConfigureAwait(false);
foreach (var role in roles)
{
context.IssuedClaims.Add(new Claim(JwtClaimTypes.Role, role));
}
context.IssuedClaims.Add(new Claim(JwtClaimTypes.Name, user.Name));
}
You could pass this data via a claim.
Modify your sign-in code to include the "SupportName" claim:
// Sign in the user and include the SupportName claim
await HttpContext.SignInAsync(userId, username, new Claim("SupportName", "Mr Support"));
and access this claim in GetProfileDataAsync:
public async Task GetProfileDataAsync(ProfileDataRequestContext context) {
// Find the "SupportName" claim added during sign-in
var supportNameClaim = context.Subject.FindFirst("SupportName");
if (supportNameClaim != null) {
Console.WriteLine($"Signed in on behalf of {supportNameClaim.Value}");
}
// Issue all the claims (if required)
context.IssuedClaims.AddRange(context.Subject.Claims);
}
Related
I have a Identity Server client set up to be able to use the password and authorization code grants, I am able to use both, but when reviewing the tokens they do not contain the same claims, is this how its suppose to work or I am missing some configuration?
If this is how it works (different claims in each grant) when using the password grant should I use the Profile service to add the other claims?
You need to implement an IResourceOwnerPasswordValidator, and return the list of claims you need. The default implementation only sends the sub claim.
See the example implementation for ASP.NET Core Identity in IdentityServer repo.
Then modify it to send additional claims. Or use ProfileService to populate it:
public virtual async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
var user = await _userManager.FindByNameAsync(context.UserName);
if (user != null)
{
var result = await _signInManager.CheckPasswordSignInAsync(user, context.Password, true);
if (result.Succeeded)
{
var sub = await _userManager.GetUserIdAsync(user);
_logger.LogInformation("Credentials validated for username: {username}", context.UserName);
// return additional claims
var claims = await _userManager.GetClaimsAsync(user);
context.Result = new GrantValidationResult(sub, AuthenticationMethods.Password, claims);
return;
}
// ... see the link above for a full implementation
}
You can also create a new ClaimsPrincipal to populate the results. See the GrantValidationResult constructor overloads for other options.
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 have an API implemented by asp.net core.
I've used OpenIddict to generate access token and refresh token for users who registered to my api by email and password.
I've added Google middleware (.UseGoogleAuthentication ... ) to my API and I can successfully log in user with Google.
My client is UWP and I use WebAuthenticationBroker to get redirected to google after sending a reuest to localhost/Account/ExternalLogin/Google.
when the users is logged In with google he is redirected to Account/ExternalLoginConfirmation which is trivial to this point now before it finishes with ExternalLoginConfirmation I Want to generate and send back an Access Token and a refresh token for the user cause if the WebAuthenticationBroker get's closed I have no other way to get tokens for this user(cause he has no password and the username will be unknown to me).
Itried this :
//
// POST: /Account/
[HttpPost("ExternalLoginConfirmation")]
[AllowAnonymous]
//[ValidateAntiForgeryToken]
public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model,
string returnUrl = null)
{
if (ModelState.IsValid)
{
// Get the information about the user from the external login provider
var info = await SignInManager.GetExternalLoginInfoAsync();
if (info == null)
return View("ExternalLoginFailure");
var user = new UserInfo { UserName = model.Email, Email = model.Email };
var result = await UserManager.CreateAsync(user);
if (result.Succeeded)
{
result = await UserManager.AddLoginAsync(user, info);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, false);
Logger.LogInformation(6, "User created an account using {Name} provider.", info.LoginProvider);
var identity = new ClaimsIdentity(
OpenIdConnectServerDefaults.AuthenticationScheme,
OpenIdConnectConstants.Claims.Name, null);
// Add a "sub" claim containing the user identifier, and attach
// the "access_token" destination to allow OpenIddict to store it
// in the access token, so it can be retrieved from your controllers.
identity.AddClaim(OpenIdConnectConstants.Claims.Subject,
user.Id,
OpenIdConnectConstants.Destinations.AccessToken);
identity.AddClaim(OpenIdConnectConstants.Claims.Name, user.UserName,
OpenIdConnectConstants.Destinations.AccessToken);
// ... add other claims, if necessary.
var principal = new ClaimsPrincipal(identity);
var authenticateInfo = await HttpContext.Authentication.GetAuthenticateInfoAsync(info.LoginProvider);
var ticket = CreateTicketAsync(principal, authenticateInfo.Properties);
// Ask OpenIddict to generate a new token and return an OAuth2 token response.
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
//return RedirectToLocal(returnUrl);
}
}
AddErrors(result);
}
//ViewData["ReturnUrl"] = returnUrl;
return BadRequest();
}
#region _helpers
private AuthenticationTicket CreateTicketAsync(ClaimsPrincipal principal,
AuthenticationProperties properties = null)
{
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(principal, properties,
OpenIdConnectServerDefaults.AuthenticationScheme);
ticket.SetScopes(new[]
{
/* openid: */ OpenIdConnectConstants.Scopes.OpenId,
/* email: */ OpenIdConnectConstants.Scopes.Email,
/* profile: */ OpenIdConnectConstants.Scopes.Profile,
/* offline_access: */ OpenIdConnectConstants.Scopes.OfflineAccess,
/* roles: */ OpenIddictConstants.Scopes.Roles
});
ticket.SetAudiences(Configuration["Authentication:OpenIddict:Audience"]);
return ticket;
}
private void AddErrors(IdentityResult result)
{
foreach (var error in result.Errors)
ModelState.AddModelError(string.Empty, error.Description);
}
private IActionResult RedirectToLocal(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
return Redirect(returnUrl);
return BadRequest();
}
#endregion
but this fails and throws exception :
an authorization or token response cannot be returned from this controller
now how do I generate these Tokens for the user?
now how do I generate these Tokens for the user?
OpenIddict deliberately prevents you from returning OIDC responses from non-OIDC endpoints (for obvious security reasons).
To make your scenario work, you must redirect your users back to the authorization endpoint with all the OpenID Connect parameters.
Concretely, you should revert all the changes added to ExternalLoginConfirmation() (that should return RedirectToLocal(returnUrl);) and move the SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme); part back to your authorization controller.
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 have searched but have not found any documentation outlining the best way to log each successful or failed attempt to get an access token and store the date/time and IP of the request. Where would I be able to do this within an application?
Ok. It's odd that there isn't any interest in answering this question.
After some trial/error and debug tracing, I found that the ApplicationOAuthProvider, located in the Providers folder in a typical ASP.NET Web API template, contains the following:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
//log the authentication attempt here
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
I put a comment in the code to show where logging could be implemented. I hope that helps.