Reputation: 848
Good moorning everyone !
I'm working on a project with spring boot (JWT & Spring security ) and Angular5 in front-End .
I'm now in a phase where i need to send the authenticated user's ID with the JWT token so i can use it front-End in many operations ..
First Question :
I'm not using Auth2 , only JWT , How can i implement this ? I can not find any complet exemple with JWT .
Second Question :
I saw some exemples where they use class UserTokenEnhancer that implements TokenEnhancer , it's used in a class that extends AuthorizationServerConfigurerAdapter while my config class is extending the WebSecurityConfigurerAdapter .
Are there any similar way on how to do the same with JWT ?
Searching for these information for along time but with no result , Hope i get any answer here .
Thank you in advance :) .
Edit
This is JWTAuthenticationFilter.java
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
@Autowired
private UserRepo userRepo;
@Autowired
private AccountService accountService;
@Autowired
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
@Autowired
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
AppUser appUser = null;
try {
appUser=new ObjectMapper().readValue(request.getInputStream(),AppUser.class);
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(appUser.getUsername(),appUser.getPassword()));
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
User springUser = (User)authResult.getPrincipal();
String jwt = Jwts.builder()
.setSubject(springUser.getUsername())
.setExpiration(new Date(System.currentTimeMillis()+SecurityConstants.EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS256,SecurityConstants.SECRET)
.claim("roles",springUser.getAuthorities())
.compact();
System.out.println("****"+jwt);
AppUser app = userRepo.findByUsername(springUser.getUsername());
Long id = app.getId();
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonJwt = objectMapper.readTree(jwt);
((ObjectNode)jsonJwt).put("userId", id);
response.addHeader(SecurityConstants.HEADER_STRING,SecurityConstants.TOKEN_PREFIX+objectMapper.writeValueAsString(jsonJwt));
}
}
EDIT
Erros i'm fascing concerns JsonParseException at line JsonNode jsonJwt = objectMapper.readTree(jwt);
Here is the error :
com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'eyJhbGciOiJIUzI1NiJ9': was expecting ('true', 'false' or 'null')
at [Source: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTUyNzU5NjA2Mywicm9sZXMiOlt7ImF1dGhvcml0eSI6IkFETUlOIn1dfQ.gkoGLsdVxgJURhfGQkn5pbs6KyO6ikCAILQeR4j0ikk; line: 1, column: 21]
at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:1702) ~[jackson-core-2.8.10.jar:2.8.10]
at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:558) ~[jackson-core-2.8.10.jar:2.8.10]
at com.fasterxml.jackson.core.json.ReaderBasedJsonParser._reportInvalidToken(ReaderBasedJsonParser.java:2839) ~[jackson-core-2.8.10.jar:2.8.10]
at com.fasterxml.jackson.core.json.ReaderBasedJsonParser._handleOddValue(ReaderBasedJsonParser.java:1903) ~[jackson-core-2.8.10.jar:2.8.10]
at com.fasterxml.jackson.core.json.ReaderBasedJsonParser.nextToken(ReaderBasedJsonParser.java:749) ~[jackson-core-2.8.10.jar:2.8.10]
at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:3850) ~[jackson-databind-2.8.10.jar:2.8.10]
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3799) ~[jackson-databind-2.8.10.jar:2.8.10]
at com.fasterxml.jackson.databind.ObjectMapper.readTree(ObjectMapper.java:2397) ~[jackson-databind-2.8.10.jar:2.8.10]
at interv.Web.security.JWTAuthenticationFilter.successfulAuthentication(JWTAuthenticationFilter.java:81) ~[classes/:na]
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:240) ~[spring-security-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) ~[spring-security-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
at interv.Web.service.JWTAutorizationFilter.doFilterInternal(JWTAutorizationFilter.java:43) ~[classes/:na]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.14.RELEASE.jar:4.3.14.RELEASE]
Class JWTAuthorizationFilter.java
package interv.Web.service;
import interv.Web.security.SecurityConstants;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
public class JWTAutorizationFilter extends OncePerRequestFilter{
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
httpServletResponse.addHeader("Access-Control-Allow-Origin","*");
httpServletResponse.addHeader("Access-Control-Allow-Headers", " Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers, Authorization");
httpServletResponse.addHeader("Access-Control-Expose-Headers",
"Access-Control-Allow-Origin, Access-Control-Allow-Credentials,Authorization");
httpServletResponse.addHeader("Access-Control-Allow-Methods","GET,PUT,POST,DELETE");
String jwtToken = httpServletRequest.getHeader(SecurityConstants.HEADER_STRING);
if(httpServletRequest.getMethod().equals("OPTIONS")){
httpServletResponse.setStatus(httpServletResponse.SC_OK);
}
else {
if(jwtToken==null || !jwtToken.startsWith(SecurityConstants.TOKEN_PREFIX)){
filterChain.doFilter(httpServletRequest,httpServletResponse);
return ;
}
Claims claims = Jwts.parser()
.setSigningKey(SecurityConstants.SECRET)
.parseClaimsJws(jwtToken.replace(SecurityConstants.TOKEN_PREFIX,""))
.getBody();
String username = claims.getSubject();
ArrayList<Map<String,String>> roles = (ArrayList<Map<String,String>>)claims.get("roles");
Collection<GrantedAuthority> authorities = new ArrayList<>();
roles.forEach(r->{
authorities.add(new SimpleGrantedAuthority(r.get("authority")));
});
UsernamePasswordAuthenticationToken authenticationToken=
new UsernamePasswordAuthenticationToken(username,null,authorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
}
Upvotes: 0
Views: 9437
Reputation: 3634
I'm not 100% sure if the following is the absolute best "Spring way" to do this, but you should be following these rough steps:
First, you will need to authenticate your user against your data store (e.g. using UsernamePasswordAuthenticationToken
and a DaoAuthenticationProvider
) and build a User
object. At the end of successful authentication you should have a valid User
object in your SecurityContext
.
After that, create and sign the token. I highly recommend using JWE (there are libraries that support this). Remember, a signed token only verifies that it was not tampered with: unless the subject is encrypted in addition to being signed, anyone can read the token. "Create" the token means that you should serialize your User
and put it into the subject
of the token payload.
Then, assumming that your clients will supply the token back to you using some header (i.e. x-my-token
), you will need to register a Filter
that snags the incoming HttpServletRequest
and checks for the presence of this header. Optionally depending on your requirements, you check the expiration of the token (that you set when creating it).
Still in the filter, after decoding the payload and decrypting your subject (hopefully!), you deserialize the subject into a User
object that you place in the SecurityContext
. If the token is expired (if you checked that), or if you cannot deserialize a valid User
(perhaps the user was deactivated in the meantime, something you might check with a DB CHECK / user store cache, or the subject in the token is somehow malformed), you would throw an exception. On success, you continue the filter chain as normal.
Once this is done you will have a valid User
that you can retrieve (i.e. using SecurityContext.getPrincipal()
) and use in your application to perform authorization (i.e. using @PreAuthorize(hasRole(...))
, @PreAuthorize(hasPermission(...))
, etc.).
If you wish to only allow authenticated users access to your infrastructure, that's all you will need to do.
If you would like to allow anonymous access, there are two ways that I know of:
Do not throw an exception upon failing to find an authentication header, and simply continue the filter chain. This results in there not being a valid User
in the SecurityContext
, and any attempt to access a protected resource will result in an exception.
Add an additional Filter
component that runs after your normal authentication filter, i.e. AnonymousAuthenticationFilter
. In it, you would populate a stub User
if none was found. This user should have ROLE_ANONYMOUS
and no otherwise valid ID values that might conflict with other users of your system (i.e. id = -1
).
You might also take a look at the filter chain hierarchy and precedence, as there are lots of problems that you might run into if your authentication filters run at the wrong time, i.e. too early or too late.
' How can i send the authenticated user's id from back end to front end which is angular . Any idea ? im using only jwt not auth2 .
As I mentioned, all your client needs to send is the token that was returned upon authentication.
So, the normal workflow would look like:
Client performs POST /login
with the supplied user credentials, and receives a JWT
. Client stores this token.
Client sends the token as an additional header on every request. I'm not well-versed in Angular, but this seems to be one way to do it.
After that, the process starting with the Authentication Filter
described above takes over.
Note that the client should have some additional handling to be able to gracefully handle token expiration (if you check it). One way to do it is: add the plaintext expiration date and time to the token payload, and check it in the client prior to sending the request. If expired, prompt user to log in again.
The last error you have probably hinges on the following lines where you create the token:
String jwt = Jwts.builder()
.setSubject(springUser.getUsername()) <---
.setExpiration(new Date(System.currentTimeMillis()+SecurityConstants.EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS256,SecurityConstants.SECRET)
.claim("roles",springUser.getAuthorities())
.compact();
System.out.println("****"+jwt);
AppUser app = userRepo.findByUsername(springUser.getUsername());
Long id = app.getId();
ObjectMapper objectMapper = new ObjectMapper(); <---
JsonNode jsonJwt = objectMapper.readTree(jwt); <---
((ObjectNode)jsonJwt).put("userId", id); <---
String jwt
already contains the encoded JWT string, which means that objectMapper.readTree(jwt)
attempts to parse eyJhbGciOiJIUzI1NiJ9...
(i.e. the actual token), which will fail. After creation, the token is essentially immutable.
In addition, when creating the token you only say .setSubject(springUser.getUsername())
. If you wish additional fields to be inside the subject, then you will need to serialize (using setSubject()
) the additional fields here.
One way to do that is to simply create a class that will hold your user information, i.e. MyTokenUser, which you will fill with all required fields (i.e.
username,
userId`, etc.). So:
User springUser = (User)authResult.getPrincipal();
AppUser app = userRepo.findByUsername(springUser.getUsername());
Long id = app.getId();
MyTokenUser tokenUser = new MyTokenUser(springUser.getUsername(), id);
String jwt = Jwts.builder()
.setSubject(tokenUser)
.setExpiration(new Date(System.currentTimeMillis()+SecurityConstants.EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS256,SecurityConstants.SECRET)
.claim("roles",springUser.getAuthorities())
.compact();
response.addHeader(SecurityConstants.HEADER_STRING,SecurityConstants.TOKEN_PREFIX+jwt);
Alternatively, you might just serialize your AppUser
, as it seems to have all the required fields already.
Upvotes: 3
Reputation: 434
Looks Right to me, Just do something like this,, forward the Request to a Controller and return the Body.
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authentication) throws IOException, ServletException {
//Write token in the Response as response.addHeader();
tokenAuthenticationService.addAuthentication();
chain.doFilter(request, response);
}
you added the response Header, after that just forward that to the Controller like
@RequestMapping(value = "/login", method = RequestMethod.POST)
public Output login(
HttpServletRequest request,@RequestBody UserDto
userDto) throws UserServiceException {
logger.info("Entering login ");
return Obj;
}
Then you can return whatever Output you want in the response in addition to JWT Token Header.
Upvotes: 1