Clean way to create user and issue jwt token? - asp.net-core

Currently i have an AuthenticationController which listens on post registration.
This registration method, stores the user and create a jwt token.
[HttpPost("registration")]
public async Task<IActionResult> Registration([FromBody] RegistrationViewModel model)
{
if (!ModelState.IsValid)
return BadRequest(new { code = "ModelNotValid", description = "Model is not valid." });
if (_userService.UserNameTaken(model.UserName))
return BadRequest(new { code = "UserNameTaken", description = $"The User Name
{model.UserName} is taken." });
var identityResult = await _userService.CreateUserAsync(model);
if (!identityResult.Succeeded)
return BadRequest(identityResult.Errors);
var user = _userService.GetUserByUserName(model.UserName);
int validityDurationInHours = 3;
string token = _jwtService.GenerateJwtToken(user, await _userService.GetUserRolesAsync(user),
validityDurationInHours);
return Ok(new { code = "RegistrationSuccess", auth_token = token });
}
But now i would like to refactor the code, so there would be a UsersController which saves the user and i am not sure if i should generate the jwt token in the same method as it is done now.
So i what i see as a way is when the user registers on client side it sends the data to the backend, if it was correct than the client will be notifed, which than redirects user to the login page. But i feel unnecesarry the redirection, the user data was correct so it should get the jwt token straigth away.
But i am unsure that the issueing the token should be done by the userscontroller default post method, which saves the user.
The other way i see is when the user is saved the backend notifies the client and than the client create a new request to the backend to issue the token. But then the request feels unnecessary.

Related

Custom Authorizationhandler for token evaluation that is done externally

When the user submits his credentials to my api, I call an external api to authenticate the user. After that, a token gets generated on the external api and will be sent to me. For that I implemented the HandleAuthenticateAsync function from the AuthenticationHandler:
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//before this: make call to external api to get the access token
var claims = new[] {
new Claim(ClaimTypes.Name, submittedToken),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
I have implemented a custom AuthorizationHandler which I want to check for the access token that you got when you successfully authenticate. Note that the actual authentication and authorization is done by an external api which is a custom implementation. Here is the function:
public class IsAuthorizedRequirement : AuthorizationHandler<IsAuthorizedRequirement>, IAuthorizationRequirement
{
public AuthenticateHandlerHelperFunctions AuthenticateHandlerHelper;
public IsAuthorizedRequirement()
{
AuthenticateHandlerHelper = new AuthenticateHandlerHelperFunctions();
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, IsAuthorizedRequirement requirement)
{
if(!context.User.HasClaim(c => c.Type == ClaimTypes.Name))
{
context.Fail();
return;
}
var token = context.User.FindFirst(c => c.Type == ClaimTypes.Name).Value;
if (!string.IsNullOrEmpty(token))
{
context.Fail();
return;
}
var checkedToken = await AuthenticateHandlerHelper.CheckAccessToken(token);
if (checkedToken == null)
{
context.Fail();
return;
}
context.Succeed(requirement);
}
}
The CheckAccessToken function makes a simple HTTP Post Request to the external Api where I get back if the token is still valid or not. Is this a valid implementation especially when multiple users are using this? Especially the claims that I use: Are they created for each user or will the content inside ClaimsType.Name be overwritten each time a user makes a request? Currently I have no way to test this so I just wanted to know if I am on the right track for this. Thanks
Is this a valid implementation especially when multiple users are using this?
I strongly stand against this approach. Implementation like this mean you would call external API for validate and generate token(or cookie or any form of authenticated certificate) on external server for each and any of your request(which require authentication).
It's could be consider acceptable if we have some special cases on just some endpoints. But for the whole API/Web server. Please don't use this approach.
Especially the claims that I use: Are they created for each user or will the content inside ClaimsType.Name be overwritten each time a user makes a request?
They'll create for each request. As I can see in the code there are no part for generate cookie or some form of retaining user information for the client to attach next request afterward.

Doubts on Authentication Techniques - Passport

So I was trying out the authentication techniques with passport and passport-jwt with the express server. Here is the code I've been working with
const JwtStrategy = require("passport-jwt").Strategy;
const ExtractJwt = require("passport-jwt").ExtractJwt;
const User = require("../models/user");
const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = "secret";
module.exports = passport => {
passport.use(
new JwtStrategy(opts, (jwt_payload, done) => {
User.findById(jwt_payload.id,(err,user)=>{
if(err){
return done(err,false);
}
if(user){
done(null,user);
}
else{
done(null,false);
}
})
})
)
};
So the all point of using this passport authorization is to minimize the number of times the database is accessed, right?
But in this code after extracting the token, the database is accessed through the findById method to find whether the user is in the database, so what's the point in all of this if the database is accessed during each authentication request?
I'm pretty sure I'm saying something wrong, some help in clarifying this matter is deeply appreciated.
The question is, why would you need to do User.findById on the middleware?
You don't have to access the database on the middleware to find whether user exists or not from the JWT payload. When the user is getting the jwt through the /login endpoint, you should've already checked whether the user exists or not
// just a logic example on the login enpoint
const user = User.findUserByEmail(req.body.email);
if (!user) res.sendStatus(401); //returns 401 if user not found
else {
if (verifyPassword(req.body.password, password)) {
res.send(generatedJwtWithUserIdOnThePayload)
} else {
res.sendStatus(401); //returns 401 if password invalid
}
}
The jwt that's passed when logging in to the client already had valid user id in it, therefore you dont need to get User document from User.findById everytime client sending a request to your other endpoint.
Since user id is already inside the payload, unless you need other data beside user id from User document, you don't really need to do User.findById on the middleware

How to invalidate a JWT token of a user after password is changed by someone

I created an MVC page used JWT authentication. Once a user logged in successfully, the app returns a JWT token to the user that's stored in the request header. But then a problem occurred. Another person also signs in with the same user account and changes the password. So the first logged in session should be terminated because of security issues. The solution I thought is invalidating the JWT token of that user. But I have to define when was the user's password changed. The JWT token doesn't contain the password information so I couldn't request to the backend server to determinate the password was changed every time the user (with old password) request to the server, either. I need some ideas, suggestions.
For this feature you should add new property like SerialNumber as string on Users table
public class User { public string SerialNumber { get; set; } }
when you want to create new token for user add user SerialNumber to Claims like this
new Claim(ClaimTypes.SerialNumber, user.SerialNumber, ClaimValueTypes.String, issuer),
and when changed user password or username or status or every important property you should update serial number. when serial changed on token validator method after first http request will raise error code 401 (that means Unauthorized)
public async Task ValidateAsync(TokenValidatedContext context)
{
var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
{
context.Fail("This is not our issued token. It has no claims.");
return;
}
var serialNumberClaim = claimsIdentity.FindFirst(ClaimTypes.SerialNumber);
if (serialNumberClaim == null)
{
context.Fail("This is not our issued token. It has no serial.");
return;
}
var userIdString = claimsIdentity.FindFirst(ClaimTypes.UserData).Value;
if (!int.TryParse(userIdString, out int userId))
{
context.Fail("This is not our issued token. It has no user-id.");
return;
}
var user = await _signInService.GetUserAsync(userId);
if (user == null)
{
context.Fail("User deleted!");
return;
}
if (user.SerialNumber != serialNumberClaim.Value || !user.Status)
{
context.Fail("This token is expired. Please login again.");
return;
}
}
on JWT Token configuration
OnTokenValidated = context =>
{
var tokenValidatorService = context.HttpContext.RequestServices.GetRequiredService<ITokenFactoryService>();
return tokenValidatorService.ValidateAsync(context);
},
Using the password directly in the token is a security risk, since an attacker could retrieve it from the user's computer. Better to either:
include a unique token ID in the token, and maintain a list of revoked tokens (or of allowed tokens; whichever is a better fit). or
include a "version number" in the user list, change it whenever the password is changed, and include the version number when the token is issued. That way, all old tokens can be rejected. #Mohammad's answer has an example of something similar.
None of those pieces of information means anything by themselves.
Use password as claim in jwt?
It will be checked in every request, so after password is changed it will return 401

Not able to SignOut using Saml2 from Sustainsys

This should be redirecting my app to my AdFs signOut Page, and then redirect me back to my app.
However, it simply redirects me to my route "/logout".
Watching the log on my ADFS server nothing happens.
[AllowAnonymous]
[HttpGet]
[Route("api/logout")]
public async Task<IActionResult> Logout()
{
return SignOut(new AuthenticationProperties()
{
RedirectUri = "/logout"
},
Saml2Defaults.Scheme);
}
SignIn works fine. I even tried this same approach, but does not work. Here, the ReturnUrl method gets the location from HttpContext.Response.Header. When I try this for the logout, the location is always null.
[AllowAnonymous]
[HttpGet]
[Route("api/login")]
public async Task<string> LoginAdfs()
{
string redirectUri = _appSettings.Saml.SpEntityId;
await HttpContext.ChallengeAsync(new AuthenticationProperties
{
RedirectUri = string.Concat(redirectUri, "/autenticado")
});
return ReturnUrl();
}
Any idea what could be happening?
UPDATE 21/11/2019
Turns out the Saml2Handler is simply not trying to send the request to the server. I'm getting these messages on my output window:
Sustainsys.Saml2.AspNetCore2.Saml2Handler: Debug: Initiating logout, checking requirements for federated logout
Issuer of LogoutNameIdentifier claim (should be Idp entity id):
Issuer is a known Idp: False
Session index claim (should have a value):
Idp has SingleLogoutServiceUrl:
There is a signingCertificate in SPOptions: True
Idp configured to DisableOutboundLogoutRequests (should be false):
Sustainsys.Saml2.AspNetCore2.Saml2Handler: Information: Federated logout not possible, redirecting to post-logout
Here is my StartUp Configuration, I don't get what is wrong here:
ServiceCertificate se = new ServiceCertificate()
{
Certificate = new X509Certificate2(SpCert, "",X509KeyStorageFlags.MachineKeySet),
Use = CertificateUse.Signing
};
SPOptions sp = new SPOptions
{
AuthenticateRequestSigningBehavior = SigningBehavior.Never,
EntityId = new EntityId(SpEntityId),
ReturnUrl = new Uri("/login"),
NameIdPolicy = new Sustainsys.Saml2.Saml2P.Saml2NameIdPolicy(null, Sustainsys.Saml2.Saml2P.NameIdFormat.Unspecified),
};
sp.ServiceCertificates.Add(se);
IdentityProvider idp = new IdentityProvider(new EntityId(appSettings.Saml.EntityId), sp);
idp.Binding = Saml2BindingType.HttpPost;
idp.AllowUnsolicitedAuthnResponse = true;
//idp.WantAuthnRequestsSigned = true;
idp.SingleSignOnServiceUrl = new Uri("/login");
//idp.LoadMetadata = true;
idp.SigningKeys.AddConfiguredKey(new X509Certificate2(IdpCert));
idp.MetadataLocation = theMetadata;
idp.DisableOutboundLogoutRequests = true;
For the logout to work, two special claims "LogoutNameIdentifier" and "SessionIndex" (full names are http://Sustainsys.se/Saml2/LogoutNameIdentifier and http://Sustainsys.se/Saml2/SessionIndex need to be present on the user. Those carries information about the current session that the Saml2 library needs to be able to do a logout.
Now I don't see your entire Startup, so I cannot understand your application's flow. But those claims should be present in the identity returned by the library - possibly stored in an External cookie (if you are using asp.net identity). When your application then sets the application cookie those two claims must be carried over to the session identity.
Also you have actually disabled outbound logout with DisableOutboundLogoutRequests. But that's not the main problem here as your logs indicates that the required claims are not present.
From my own experience, the two claims, as mentioned by Anders Abel, should be present on the user. I had not seen these claims until I passed all of the claims along with the sign-in request. ASP.NET Core recreates the principal on SignInAsync and needs claims to be passed in with the request.
With the following, I am able to fulfill a SingleLogout with my service:
await HttpContext.SignInAsync(user.SubjectId, user.Username, props, user.Claims.ToArray());
what you are using as a service provider.

JWT authentication concept

I am currently working on an interaction between Angular JS app and Node.js Server (as API) with an authentication based on JSON Web Token.
But I have a question I can't answer by myself : when you encode the JWT server-side putting a user as payload, how do you proceed to retrieve the user information client-side ?
Here is a small example to understand my question:
I am a basic user, I send my credentials to the API for authenticating. In exchange, I receive a JWT token but I don't have any information about the user since only the server has the secret key that is able to decode the JWT token. So does the server need to send me for example the id of the user so that I could call my api user/id for retrieving information about the user authenticated?
You retrieve the user's info by decoding the token on each request. So in your example after the token is returned to the client, the client makes a request to the server to grab the user's first and last name using the data stored in the encoded token which is sent along with the request back to the server. When making this GET request, you can send the token as a parameter. I'll use a non-cookie stored example. Here's how it goes down:
The user signs in with their password and username
The server encodes a json web token payload that contains the unique identifier (i.e. user_id) of the user that signed in using the secret_key. An example function call may look something like this.
payload = {user_id: 35}
user_token = JWT.encode(payload, "your_secret_key");
Return the user_token to the client and store said token in a hidden html tag or in a localStorage variable. Using Angular, I'd store it in localStorage.
Now that the user is signed_in and the token is client-side, you can submit a GET request that contains the user_token as a parameter. Remember, this user_token payload contains the user_id.
The server gets the parameter and decodes the user_token to get the user_id from the payload.
You query the database with the user_id and return the data (first and last name) as plain json, NOT ENCODED.
It's important to remember the only thing to encode in your example is the unique identifier (user_id). On each request you decode the token which itself is the authentication mechanism.
You have the payload on the client, If your needed data is in the payload you can easily do a Base64 Decode on payload to find it!
To understand this here are steps:
Client send username:user,password:pass to server.
The server starts the authentication business and finds that the user name and password is valid.
The server must return these information back to client. Here is where JWT has some rules. The server must return a token back to client. The token has three parts Header.PayLoad.Signature . Forget about signature right now, which is the part which make some confusion.
The part one is Header. Some thing like:
{"typ":"JWT","alg":"HS256"}
Which will be eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 after Base64 Decode. Please consider this is just a decode, no encryption at all! To see this you can go to https://www.base64decode.org/ and test.
After the header, the server needs to send a payload to user. The server may decide to send below json ( I said decide, because there is no standard requirement here, you can send more or less data as payload, for example, you may also set user privileges for example admin:true, or user first and last name, but keep in mind that the JWT size must be small as it will be send to server on each request)
{"username":"user","id":3,"iat":1465032622,"exp":1465050622}
Again according to JWT, the server needs a Base64 Decode here ( and again no encryption at all). The above json will be eyJ1c2VybmFtZSI6IjEiLCJpZCI6MywiaWF0IjoxNDY1MDMyNjIyLCJleHAiOjE0NjUwNTA2MjJ9.
Until now the server created the Header and Payload. Now time to make signature! It is very easy:
var encodedString=base64UrlEncode(header) + "." + base64UrlEncode(payload);
//As our example base64UrlEncode(header) is eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
//and the base64UrlEncode(payload) is eyJ1c2VybmFtZSI6IjEiLCJpZCI6MywiaWF0IjoxNDY1MDMyNjIyLCJleHAiOjE0NjUwNTA2MjJ9
var signature=HMACSHA256(encodedString, 'a secret string which is kept at server');
The signature is made with a secret key which you don't have it at clent!! You don't need it either. All token data is in the payload and can be accessed with decode ( again no decrypt ! ).
This signature is used at the server, when you send token back to server, the server check that signiature is correct to make sure he can trust the token data.
To summarize have a look at below token
//Header
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
//PayLoad
eyJ1c2VybmFtZSI6IjEiLCJpZCI6MywiaWF0IjoxNDY1MDMyNjIyLCJleHAiOjE0NjUwNTA2MjJ9.
//Signature
0K8TL1YS0XKnEIfI3lYs-bu2vbWHSNZsVJkN1mXtgWg
Header and payloads are Base64 Decoded and you can encode it on client. But you can not do any thing with signature.
The signature is only used by the server. The client send each request with his token, the server must be sure that the client did not change any part of token payload (for example change userid). This is where the signature string come importance is revealed, the server recheck the signature with it's secret key for every request!
Note:
Do you still wonder why the JWT use encode and decode ?! To make the hole token URL safe !
The strategy in the accepted answer works, but it misses the fact that the client can see the payload of a JWT. It is explained nicely in The Anatomy of a JSON Web Token.
A JWT has 3 parts. The first two, header and payload, are base64 encoded. The client can decode them easily. The payload has claims about the user, the client can use this data (user id, name, roles, token expiration) w/out having to make another request to the server.
The third part of the JWT is the signature. It is a hash of the header, the payload, and a secret that only the server knows. The server will validate the token and user's permissions on every request.
The client never knows the secret, it just has a token that claims to be the given user.
JWT (JSON web token) has become more and more popular in web development. It is
an open standard which allows transmitting data between parties as a JSON object in a secure and compact way. The data transmitting using JWT between parties are digitally signed so that it can be easily verified and trusted.
JWT in ASP.NET Core
The first step is to configure JWT based authentication in our project. we can add custom jwt auth middleware that fire in every request for Authorization.
Startup.cs
services.AddMvc(options => options.EnableEndpointRouting = false);
var tokenValidationParams = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey("Jwt_Key"),
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
RequireExpirationTime = false,
ValidIssuer = "Jwt_Issuer",
ValidAudience = "Jwt_Audience",
ClockSkew = TimeSpan.Zero
};
services.AddSingleton(tokenValidationParams);
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwt => {
jwt.SaveToken = true;
jwt.TokenValidationParameters = tokenValidationParams;
});
services.AddMvc();
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// custom jwt auth middleware
**app.UseMiddleware<JwtMiddleware>();**
app.UseAuthentication();
app.UseMvc();
app.Run(async (context) =>
{
await context.Response.WriteAsync("Welcome to DATA API");
});
}
Generete JWT
GenerateJSONWebToken(User userInfo)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Jwt_Key"));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[] {
new Claim(JwtRegisteredClaimNames.Sub, userInfo.UserID),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var token = new JwtSecurityToken("Jwt_Issuer","Jwt:Audience",
claims,
expires: DateTime.Now.AddHours(24),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
This Method return JWT Totken like
Token : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJKa
WduZXNoIFRyaXZlZGkiLCJlbWFpbCI6InRlc3QuYnRlc3RAZ21haWwuY29tIiwiRG
F0ZU9mSm9pbmciOiIwMDAxLTAxLTAxIiwianRpIjoiYzJkNTZjNzQtZTc3Yy00ZmU
xLTgyYzAtMzlhYjhmNzFmYzUzIiwiZXhwIjoxNTMyMzU2NjY5LCJpc3MiOiJUZXN0
LmNvbSIsImF1ZCI6IlRlc3QuY29tIn0.8hwQ3H9V8mdNYrFZSjbCpWSyR1CNyDYHc
Gf6GqqCGnY"
Calling Authorize Method
[Authorize]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2", "value3", "value4",
"value5" };
}
Validate Token in Jwt Middleware Class
JwtMiddleware
{
private readonly RequestDelegate _next;
private readonly TokenValidationParameters _tokenValidationParams;
public JwtMiddleware(RequestDelegate next, TokenValidationParameters
tokenValidationParams)
{
_next = next;
_tokenValidationParams = tokenValidationParams;
}
public async Task Invoke(HttpContext context)
{
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
var jwtTokenHandler = new JwtSecurityTokenHandler();
// Validation 1 - Validation JWT token format
var tokenInVerification = jwtTokenHandler.ValidateToken(token, _tokenValidationParams, out var validatedToken);
if (validatedToken is JwtSecurityToken jwtSecurityToken)
{
var result = jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase);
if (result == false)
{
Error Invalid = new Error()
{
Success = false,
Errors = "Token is Invalid"
};
context.Items["Error"] = Invalid;
}
}
await _next(context);
}
}
Authorize Attribute
public void OnAuthorization(AuthorizationFilterContext context)
{
var Error= (UserModel)context.HttpContext.Items["Error"];
if (AuthResult != null)
{
// not logged in
context.Result = new JsonResult(new { message = "Unauthorized Access" }) {
StatusCode = StatusCodes.Status401Unauthorized };
}
}
I hope this will work for you.