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;
}
}
Related
I have a jwt after login successfully and want to get data from that token (token has encoded containing information saving with Claims).
Is there a way to get data from bearer token using HttpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme)?
I do not know how to get data from the token recieved. Here is my code.
public async Task<TokenResponse> Login(string username, string password)
{
var user = await dataContext.Users
.FirstOrDefaultAsync(x => x.Username == username && x.Password == password.Encrypt())
?? throw new BadRequestExceptions("Wrong username or password");
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Role, user.Role.ToString()),
new Claim(ClaimTypes.GivenName,user.Name),
new Claim(ClaimTypes.Upn, user.Username)
};
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(tokenConfig.Key));
var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
var tokenString = new JwtSecurityToken(tokenConfig.Issuer, tokenConfig.Audience, claims: claims, signingCredentials: signingCredentials);
tokenResponse = new TokenResponse()
{
Token = new JwtSecurityTokenHandler().WriteToken(tokenString)
};
return tokenResponse;
}
The best way to get data out of your token (provided you have multiple data to retrieve) is to extend (extension methods) ClaimsPrincipal and then you can call whatever methods you extend within that class from anywhere within your assembly.
Find a sample below:
public static class ClaimsPrincipalExtensions
{
public static string GetUsername(this ClaimsPrincipal user)
{
return user.FindFirst(ClaimTypes.Name)?.Value;
}
public static int GetUserId(this ClaimsPrincipal user)
{
return int.Parse(user.FindFirst(ClaimTypes.NameIdentifier)?.Value);
}
}
Then somewhere in your controller action, you can call User.GetUsername() and User.GetUserId()
However, if you only have to retrieve one or two records, then this would suffice:
int userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value)
Happy coding!!!
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 have this filter (below) and I want to extend the time of the token (by replacing the token and re-writing a new one for the user) ... Can anybody help me achieve this?
This is the standard filter without any custom changes or anything, I've already handled token expiry, now I want to renew the token when a request is made within token expiry time
public class JwtAuthenticationAttribute : Attribute, IAuthenticationFilter
{
public string Realm { get; set; }
public bool AllowMultiple => false;
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
var request = context.Request;
var authorization = request.Headers.Authorization;
if (authorization == null || authorization.Scheme != "Bearer")
return;
if (string.IsNullOrEmpty(authorization.Parameter))
{
context.ErrorResult = new AuthenticationFailureResult("Missing Jwt Token", request);
return;
}
var token = authorization.Parameter;
var principal = await AuthenticateJwtToken(token);
if (principal == null)
context.ErrorResult = new AuthenticationFailureResult("Invalid token", request);
else
context.Principal = principal;
// HERE SHOULD BE THE IMPLEMENTATION FOR TOKEN RENEWAL
}
private static bool ValidateToken(string token, out string username)
{
username = null;
var simplePrinciple = JwtManager.GetPrincipal(token);
var identity = simplePrinciple?.Identity as ClaimsIdentity;
if (identity == null)
return false;
if (!identity.IsAuthenticated)
return false;
var usernameClaim = identity.FindFirst(ClaimTypes.Name);
username = usernameClaim?.Value;
if (string.IsNullOrEmpty(username))
return false;
// More validate to check whether username exists in system
return true;
}
protected Task<IPrincipal> AuthenticateJwtToken(string token)
{
string username;
if (ValidateToken(token, out username))
{
// based on username to get more information from database in order to build local identity
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, username)
// Add more claims if needed: Roles, ...
};
var identity = new ClaimsIdentity(claims, "Jwt");
IPrincipal user = new ClaimsPrincipal(identity);
return Task.FromResult(user);
}
return Task.FromResult<IPrincipal>(null);
}
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
Challenge(context);
return Task.FromResult(0);
}
private void Challenge(HttpAuthenticationChallengeContext context)
{
string parameter = null;
if (!string.IsNullOrEmpty(Realm))
parameter = "realm=\"" + Realm + "\"";
context.ChallengeWith("Bearer", parameter);
}
}
I did create custom AuthorizeAttribute that should handle Jwt Bearer token and then create ClaimsIdentity. But when I send request again, anyway I can't see authorized user and must create ClaimsIdentity again and add user to CurrentPricipal again. What I'm doing wrong?
public class JwtAuthorizeAttribute : AuthorizeAttribute
{
private readonly string role;
public JwtAuthorizeAttribute()
{
}
public JwtAuthorizeAttribute(string role)
{
this.role = role;
}
protected override bool IsAuthorized(HttpActionContext actionContext)
{
var jwtToken = new JwtToken();
var ctx = actionContext.Request.GetOwinContext();
if (ctx.Authentication.User.Identity.IsAuthenticated) return true;
if (actionContext.Request.Headers.Contains("Authorization"))
{
var token = actionContext.Request.Headers.Authorization.Parameter;
try
{
IJsonSerializer serializer = new JsonNetSerializer();
IDateTimeProvider provider = new UtcDateTimeProvider();
IJwtValidator validator = new JwtValidator(serializer, provider);
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder);
var json = decoder.Decode(token, SiteGlobal.Secret, verify: true);
jwtToken = JsonConvert.DeserializeObject<JwtToken>(json);
if (jwtToken.aud != SiteGlobal.Audience || jwtToken.iss != SiteGlobal.Issuer || role != jwtToken.role)
{
return false;
}
}
catch (TokenExpiredException)
{
return false;
}
catch (SignatureVerificationException)
{
return false;
}
}
else
{
return false;
}
var identity = new ClaimsIdentity("JWT");
identity.AddClaim(new Claim(ClaimTypes.Name, jwtToken.unique_name));
identity.AddClaim(new Claim(ClaimTypes.Role, jwtToken.role));
ctx.Authentication.SignIn(new AuthenticationProperties { IsPersistent = true }, identity);
Thread.CurrentPrincipal = new ClaimsPrincipal(identity);
HttpContext.Current.User = new ClaimsPrincipal(identity);
return true;
}
}
Signin is used to create a cookie. Do you have a cookie Auth middleware to handle the signin?
I have created ASP.NET Core WebApi protected with IdentityServer4 using ROPC flow (using this example: https://github.com/robisim74/AngularSPAWebAPI).
How to manually generate access_token from the server without password?
[HttpPost("loginas/{id}")]
[Authorize(Roles = "admin")]
public async Task<IActionResult> LoginAs(int id, [FromServices] ITokenService TS,
[FromServices] IUserClaimsPrincipalFactory<ApplicationUser> principalFactory,
[FromServices] IdentityServerOptions options)
{
var Request = new TokenCreationRequest();
var User = await userManager.FindByIdAsync(id.ToString());
var IdentityPricipal = await principalFactory.CreateAsync(User);
var IdServerPrincipal = IdentityServerPrincipal.Create(User.Id.ToString(), User.UserName, IdentityPricipal.Claims.ToArray());
Request.Subject = IdServerPrincipal;
Request.IncludeAllIdentityClaims = true;
Request.ValidatedRequest = new ValidatedRequest();
Request.ValidatedRequest.Subject = Request.Subject;
Request.ValidatedRequest.SetClient(Config.GetClients().First());
Request.Resources = new Resources(Config.GetIdentityResources(), Config.GetApiResources());
Request.ValidatedRequest.Options = options;
Request.ValidatedRequest.ClientClaims = IdServerPrincipal.Claims.ToArray();
var Token = await TS.CreateAccessTokenAsync(Request);
Token.Issuer = "http://" + HttpContext.Request.Host.Value;
var TokenValue = await TS.CreateSecurityTokenAsync(Token);
return Ok(TokenValue);
}
For a newly released IdentityServer 2.0.0 the code needs some modifications:
[HttpPost("loginas/{id}")]
[Authorize(Roles = "admin")]
public async Task<IActionResult> LoginAs(int id, [FromServices] ITokenService TS,
[FromServices] IUserClaimsPrincipalFactory<ApplicationUser> principalFactory,
[FromServices] IdentityServerOptions options)
{
var Request = new TokenCreationRequest();
var User = await userManager.FindByIdAsync(id.ToString());
var IdentityPricipal = await principalFactory.CreateAsync(User);
var IdentityUser = new IdentityServerUser(User.Id.ToString());
IdentityUser.AdditionalClaims = IdentityPricipal.Claims.ToArray();
IdentityUser.DisplayName = User.UserName;
IdentityUser.AuthenticationTime = System.DateTime.UtcNow;
IdentityUser.IdentityProvider = IdentityServerConstants.LocalIdentityProvider;
Request.Subject = IdentityUser.CreatePrincipal();
Request.IncludeAllIdentityClaims = true;
Request.ValidatedRequest = new ValidatedRequest();
Request.ValidatedRequest.Subject = Request.Subject;
Request.ValidatedRequest.SetClient(Config.GetClients().First());
Request.Resources = new Resources(Config.GetIdentityResources(), Config.GetApiResources());
Request.ValidatedRequest.Options = options;
Request.ValidatedRequest.ClientClaims = IdentityUser.AdditionalClaims;
var Token = await TS.CreateAccessTokenAsync(Request);
Token.Issuer = HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value;
var TokenValue = await TS.CreateSecurityTokenAsync(Token);
return Ok(TokenValue);
}
Use this:
http://docs.identityserver.io/en/latest/topics/tools.html
Use this tool that come with identity server:
Declare it in the constructor, to receive by dependecy injection.
IdentityServer4.IdentityServerTools _identityServerTools
var issuer = "http://" + httpRequest.Host.Value;
var token = await _identityServerTools.IssueJwtAsync(
30000,
issuer,
new System.Security.Claims.Claim[1]
{
new System.Security.Claims.Claim("cpf", cpf)
}
);
Here is another way to achieve this:
first create a custom grant named loginBy
public class LoginByGrant : ICustomGrantValidator
{
private readonly ApplicationUserManager _userManager;
public string GrantType => "loginBy";
public LoginByGrant(ApplicationUserManager userManager)
{
_userManager = userManager;
}
public async Task<CustomGrantValidationResult> ValidateAsync(ValidatedTokenRequest request)
{
var userId = Guid.Parse(request.Raw.Get("user_id"));
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
return await Task.FromResult<CustomGrantValidationResult>(new CustomGrantValidationResult("user not exist"));
var userClaims = await _userManager.GetClaimsAsync(user.Id);
return
await Task.FromResult<CustomGrantValidationResult>(new CustomGrantValidationResult(user.Id.ToString(), "custom", userClaims));
}
}
then add this custom grant in identity startup class
factory.CustomGrantValidators.Add(
new Registration<ICustomGrantValidator>(resolver => new LoginByGrant(ApplicaionUserManager)));
and finally in your api
public async Task<IHttpActionResult> LoginBy(Guid userId)
{
var tokenClient = new TokenClient(Constants.TokenEndPoint, Constants.ClientId, Constants.Secret);
var payload = new { user_id = userId.ToString() };
var result = await tokenClient.RequestCustomGrantAsync("loginBy", "customScope", payload);
if (result.IsError)
return Ok(result.Json);
return Ok(new { access_token = result.AccessToken, expires_in = result.ExpiresIn});
}
Further to my comment on your original question. Implement an impersonation feature within the implicit/hybrid flow. If a user is determined to be a "super admin" then present them with an additional step after authentication that lets them enter/select the account they wish to impersonate. Once that's done simply establish the session on the identity server as the selected user (and possibly store additional claims denoting that it is an impersonated session and who is doing the impersonation). Any tokens will then be issued as if you were that user and all without having to know the password.
Additionally if you wish to create tokens yourself have a look at the ITokenCreationService provided by IdSrv4. You can inject that into your own controller/service/whatever and use CreateTokenAsync(Token token) to generate a signed JWT with any claims you like.
A little late to answer.
in my case of Generating Access Token Without Password there was another identity server as an organization sso, and our implementation already used IdentityServer, so we need to get user token from second IdentityServer (after user login and redirected to our app), extract sub, check if it is already existed(if not insert into our local IdentityServer), finally select user and use newly grant to get token for user.
your client should have this granttype as Allowed Grant types (here userexchange):
see: identity server docs, or duende docs for more information
public class TokenExchangeGrantValidator : IExtensionGrantValidator {
protected readonly UserManager<ToranjApplicationUser> _userManager;
private readonly IEventService _events;
public TokenExchangeGrantValidator(ITokenValidator validator, IHttpContextAccessor httpContextAccessor, UserManager<ToranjApplicationUser> userManager
, IEventService events) {
_userManager = userManager;
_events = events;
}
public async Task ValidateAsync(ExtensionGrantValidationContext context) {
var userName = context.Request.Raw.Get("uname");
if (string.IsNullOrEmpty(userName)) {
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
var user = await _userManager.FindByNameAsync(userName);
// or use this one, if you are sending userId
//var user = await _userManager.FindByIdAsync(userId);
if (null == user) {
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id.ToString(), user.UserName, false, context.Request.ClientId));
var customResponse = new Dictionary<string, object>
{
{OidcConstants.TokenResponse.IssuedTokenType, OidcConstants.TokenTypeIdentifiers.AccessToken}
};
context.Result = new GrantValidationResult(
subject: user.Id.ToString(),
authenticationMethod: GrantType,
customResponse: customResponse);
}
public string GrantType => "userexchange";
}
in your startup's ConfigureServices after var builder = services.AddIdentityServer(...) add your newly created class.
builder.AddExtensionGrantValidator<TokenExchangeGrantValidator>();
calling it to get token is as simple as:
POST /connect/token
grant_type=userexchange&
scope=yourapi&
uname=yourusername&
client_id=yourClientId
client_secret=secret