I implement a custom security feature and I implement a login entry point :
#PostMapping("/login")
public ResponseEntity<UserRestResponseModel> userLogin(#RequestBody UserDetailsRequestModel userDetails) {
if(userDetails.getEmail().isEmpty() || userDetails.getPassword().isEmpty()) {
throw new UserServiceException(ErrorMessages.MISSING_REQUIRED_FIELD.getErrorMessage());
}
authenticate(userDetails.getEmail(), userDetails.getPassword());
UserRestResponseModel userRestResponseModel = new UserRestResponseModel();
ModelMapper modelMapper = new CustomMapper();
//modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
UserDto loggedInUser = userService.getUser(userDetails.getEmail());
if (loggedInUser == null)
throw new UserServiceException("Error !!");
if(loggedInUser.getIsAccountNonLocked() == Boolean.FALSE)
throw new UserServiceException("User account is locked");
userRestResponseModel = modelMapper.map(loggedInUser, UserRestResponseModel.class);
UserPrincipal userPrincipal = modelMapper.map(loggedInUser, UserPrincipal.class);
HttpHeaders jwtHeader = getJwtHeader(userPrincipal);
ResponseEntity<UserRestResponseModel> returnValue =
new ResponseEntity<>(userRestResponseModel, jwtHeader, HttpStatus.OK);
return returnValue;
}
private void authenticate(String userName, String password) {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userName, password));
}
private HttpHeaders getJwtHeader(UserPrincipal userPrincipal) {
HttpHeaders headers = new HttpHeaders();
String token = jwtTokenProvider.generateJwtToken(userPrincipal);
headers.add(SecurityConstants.TOKEN_PREFIX, token);
return headers;
}
I also implement my UserPrincipal class :
public class UserPrincipal implements UserDetails {
private static final long serialVersionUID = 7464059818443209139L;
private UserEntity userEntity;
private String userKeyId;
public UserPrincipal(){ }
public UserPrincipal(UserEntity userEntity) {
this.userEntity = userEntity;
this.userKeyId = userEntity.getUserKeyId();
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new HashSet<>();
Collection<AuthorityEntity> authorityEntities = new HashSet<>();
// get user roles
Collection<RoleEntity> roles = userEntity.getRoles();
if (roles == null) {
return authorities; // null
}
// get user roles
roles.forEach((role) ->{
authorities.add(new SimpleGrantedAuthority(role.getName()));
authorityEntities.addAll(role.getAuthorities());
});
// get user authorities
authorityEntities.forEach(authorityEntity ->
authorities.add(
new SimpleGrantedAuthority(authorityEntity.getName())
)
);
return authorities;
}
#Override
public String getPassword() {
return this.userEntity.getEncryptedPassword();
}
#Override
public String getUsername() {
return this.userEntity.getEmail();
}
#Override
public boolean isAccountNonExpired() {
return this.userEntity.getIsAccountNonExpired();
}
I also have a JwtTokenProvider class that handle token.
#Component
public class JwtTokenProvider {
public String generateJwtToken(UserPrincipal userPrincipal) {
String[] claims = getClaimsFromUser(userPrincipal);
return JWT.create()
.withIssuer(SecurityConstants.TOKEN_ISSUER)
.withAudience(SecurityConstants.TOKEN_AUDIENCE)
.withIssuedAt(new Date())
.withSubject(userPrincipal.getUsername())
.withArrayClaim(SecurityConstants.AUTHORITIES, claims)
.withExpiresAt(new Date(System.currentTimeMillis() + SecurityConstants.EXPIRATION_TIME))
.sign(Algorithm.HMAC512(SecurityConstants.getTokenSecret().getBytes()));
}
public List<GrantedAuthority> getAuthorities(String token) {
String[] claims = getClaimsFromToken(token);
return stream(claims).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
public Authentication getAuthentication(String userName,
List<GrantedAuthority> authorities,
HttpServletRequest request) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userName, null, authorities);
usernamePasswordAuthenticationToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
return usernamePasswordAuthenticationToken;
}
public boolean isTokenValid(String userName, String token) {
JWTVerifier verifier = getJWTVerifier();
return StringUtils.isNotEmpty(userName) && !isTokenExpired(verifier, token);
}
public String getSubject(String token) {
JWTVerifier jwtVerifier = getJWTVerifier();
return jwtVerifier.verify(token).getSubject();
}
private boolean isTokenExpired(JWTVerifier verifier, String token) {
Date expirationDate = verifier.verify(token).getExpiresAt();
return expirationDate.before(new Date());
}
private String[] getClaimsFromToken(String token) {
JWTVerifier verifier = getJWTVerifier();
return verifier.verify(token).getClaim("Authorities").asArray(String.class);
}
private JWTVerifier getJWTVerifier() {
JWTVerifier verifier;
try {
Algorithm algorithm = Algorithm.HMAC512(SecurityConstants.getTokenSecret());
verifier = JWT.require(algorithm).withIssuer(SecurityConstants.TOKEN_ISSUER).build();
} catch (JWTVerificationException e) {
throw new JWTVerificationException(SecurityConstants.TOKEN_CANNOT_BE_VERIFIED);
}
return verifier;
}
private String[] getClaimsFromUser(UserPrincipal userPrincipal) {
List<String> authorities = new ArrayList<>();
for(GrantedAuthority grantedAuthority: userPrincipal.getAuthorities()) {
authorities.add(grantedAuthority.getAuthority());
}
return authorities.toArray(new String[0]);
}
}
Everything works fine except that I have 2 issues :
If i don't have a no argument constructor in my UserPrincipal class, model mapper returns an error...
"message": "An error occurred while processing the request ModelMapper mapping errors: Failed to instantiate instance of destination UserPrincipal. Ensure that UserPrincipal has a non-private no-argument constructor. 1 error".
And if i do add a no argument constructor, I have a null pointer exception (userPrincipal is null) with a nice 500 error. it drives me crazy.
My issue is around this place in my code:
UserDto loggedInUser = userService.getUser(userDetails.getEmail());
...
userRestResponseModel = modelMapper.map(loggedInUser, UserRestResponseModel.class);
UserPrincipal userPrincipal = modelMapper.map(loggedInUser, UserPrincipal.class);
HttpHeaders jwtHeader = getJwtHeader(userPrincipal);
Related
I want to login in spring using jwt and I want to use email or social number (social) and password but I don't know how. Please help me.
This is my domain dao in table user
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "idUser")
private Long idUser;
#Column(name = "username", nullable = false)
#Email(message = "EMAIL IS NOT VALID")
#Pattern(regexp=".+#.+\\..+", message="Please provide a valid email address")
private String username;
#Column(name = "password", nullable = false)
private String password;
#Column(name = "social", unique = true)
private String social;
#Column(name = "name")
private String name;}
This is my custom user detail
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
private final UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("user " + username + " not found"));
log.info("User '{}' found", username);
return UserDetailsImpl.build(user);
}
}
This is my jwt token provider
public class JwtTokenProvider {
private final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
public String generateToken(Authentication auth) {
UserDetailsImpl user = (UserDetailsImpl) auth.getPrincipal();
return Jwts.builder()
.setId(user.getId().toString())
.setSubject(user.getUsername())
.signWith(key)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
}
catch(MalformedJwtException ex){
log.error("Invalid Jwt Token: {}", ex.getMessage());
}catch(UnsupportedJwtException ex){
log.error("Unsupported Jwt Token: {}", ex.getMessage());
}catch(ExpiredJwtException ex){
log.error("Expired Jwt Token: {}", ex.getMessage());
}catch(IllegalArgumentException ex){
log.error("Jwt claim string is empty: {}", ex.getMessage());
}
return false;
}
public String getUsername(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().getSubject();
}
public String getId(String token){
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().getId();
}
}
This is my authentication service for validate token
public TokenResponse generateToken(UserDTO req) {
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(req.getEmail().toLowerCase(),req.getPassword())
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtTokenProvider.generateToken(authentication);
TokenResponse tokenResponse = new TokenResponse();
tokenResponse.setToken(jwt);
log.info("Token created");
return tokenResponse;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e.getMessage(), e);
}
}
This is my authentication login controller
#PostMapping(value = "/login")
public ResponseEntity<?> generateToken(#RequestBody UserDTO req) {
TokenResponse token = authService.generateToken(req);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set("Authorization", token.getToken());
return ResponseEntity.ok().headers(responseHeaders).body(token);
}
I have endpoints that will be called only from authenticated users. I have valid JWT token that I am passing as a Bearer Token in Authorization. When I do get request to http://localhost:8080/api/students/all, I am getting 401:
The logger logs in the console Authenticated user test#gmail.com[ROLE_STUDENT], setting security context, which means that the token is successfully validated. Why am I still gettin 401? How can I solve this issue?
Here is my StudentController.java:
#RestController
#CrossOrigin(origins = "http://localhost:3000")
#Slf4j
#RequestMapping("/api/students")
public class StudentController {
#Autowired
private StudentService studentService;
#GetMapping("/all")
public ResponseEntity<List<Student>> getAllStudents(){
log.info("test");
return new ResponseEntity<List<Student>>(studentService.getAll(), HttpStatus.OK);
}
}
JwtTokenUtil class is:
#Component
public class JwtTokenUtil implements Serializable{
#Value("${jwt.public.key}")
private String publicKey;
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(publicKey)
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public String generateToken(User user, String scope) {
return doGenerateToken(user.getEmail(), scope);
}
private String doGenerateToken(String subject, String scope) {
Claims claims = Jwts.claims().setSubject(subject);
claims.put("roles",scope);
return Jwts.builder()
.setClaims(claims)
.setIssuer("http://uni.pu.com")
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 5*60*60*1000))
.signWith(SignatureAlgorithm.HS256, publicKey)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (
username.equals(userDetails.getUsername())
&& !isTokenExpired(token));
}
}
JWTAuthenticationFilter class:
public class JWTAuthenticationFilter extends OncePerRequestFilter {
#Autowired
private MyUserDetailsService userDetailsService;
#Autowired
private JwtTokenUtil jwtTokenUtil;
public static final String TOKEN_PREFIX = "Bearer ";
public static final String HEADER_STRING = "Authorization";
#Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
String header = req.getHeader(HEADER_STRING);
String username = null;
String authToken = null;
if (header != null && header.startsWith(TOKEN_PREFIX)) {
authToken = header.replace(TOKEN_PREFIX, "");
try {
username = jwtTokenUtil.getUsernameFromToken(authToken);
} catch (IllegalArgumentException e) {
logger.error("an error occured during getting username from token", e);
} catch (ExpiredJwtException e) {
logger.warn("the token is expired and not valid anymore", e);
} catch (SignatureException e) {
logger.error("Authentication Failed. Username or Password not valid.");
}
} else {
logger.warn("couldn't find bearer string, will ignore the header");
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
logger.info("authenticated user " + username + userDetails.getAuthorities()+ ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(req, res);
}
}
WebSecurityConfig:
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
public class WebSecurityConfig {
private static final String[] WHITE_LIST_URLS = { "/", "/api/students/register", "/api/students/register/*",
"/api/user/reset-password", "/api/user/save-password", "/api/user/login", "/api/user/logout",
// -- Swagger UI v2
"/v2/api-docs", "/swagger-resources", "/swagger-resources/**", "/configuration/ui", "/configuration/**",
"/configuration/security", "/swagger-ui.html", "/webjars/**",
// -- Swagger UI v3 (OpenAPI)
"/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui/", "/swagger-ui" };
#Autowired
private JwtAuthenticationEntryPoint authenticationEntryPoint;
#Value("${jwt.public.key}")
private RSAPublicKey rsaPublicKey;
#Value("${jwt.private.key}")
private RSAPrivateKey rsaPrivateKey;
#Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder(10);
}
#Bean
public JWTAuthenticationFilter authenticationTokenFilterBean() throws Exception {
return new JWTAuthenticationFilter();
}
#Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// Enable CORS and disable CSRF
http = http.cors().and().csrf().disable();
// Set session management to stateless
http = http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();
// Set unauthorized requests exception handler
http = http.exceptionHandling(
(exceptions) -> exceptions.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(new BearerTokenAccessDeniedHandler()));
http = http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
http = http.formLogin().disable();
// Set permissions on endpoints
http.authorizeHttpRequests().antMatchers(WHITE_LIST_URLS).permitAll()
// private endpoints
.anyRequest().authenticated()
// Set up oauth2 resource server
.and().httpBasic(Customizer.withDefaults()).oauth2ResourceServer().jwt();
return http.build();
}
#Bean
public JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(this.rsaPublicKey).privateKey(this.rsaPrivateKey).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
// Used by JwtAuthenticationProvider to decode and validate JWT tokens
#Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.rsaPublicKey).build();
}
// Extract authorities from the roles claim
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
#Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
#Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_ADMIN > ROLE_INSPECTOR \n ROLE_INSPECTOR > ROLE_STUDENT";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
}
MyUserDetailsService:
#Service
#Transactional
public class MyUserDetailsService implements UserDetailsService {
#Autowired
private UserRepository userRepository;
public UserDetails loadUserByUsername(String email) throws UserNotFoundException {
Optional<User> optionalUser = userRepository.findByEmail(email);
if(!optionalUser.isPresent()) {
throw new UserNotFoundException("No user found with email: " + email);
}
User user = optionalUser.get();
boolean enabled = user.isEnabled();
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(),
enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, getAuthorities(user.getRoles()));
}
public Collection<? extends GrantedAuthority> getAuthorities(Collection<Role> roles) {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
}
I have two AuthenticaionProvider's in my application which use AWS Cognito to authenticate users.
first for Admin
#Component
#RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final CognitoAuthenticationService cognitoService;
private final AuthenticationHelper authenticationHelper;
#SuppressWarnings("unchecked")
#Override
public Authentication authenticate(Authentication authentication) {
AuthenticationRequest authenticationRequest;
if (isNotNull(authentication)) {
authenticationRequest = new AuthenticationRequest();
Map<String, String> credentials = (Map<String, String>) authentication.getCredentials();
authenticationRequest.setNewPassword(credentials.get(NEW_PASSWORD_KEY));
authenticationRequest.setPassword(credentials.get(PASSWORD));
authenticationRequest.setUsername(authentication.getName());
SpringSecurityUser userAuthenticated = cognitoService.authenticate(authenticationRequest);
if (isNotNull(userAuthenticated)) {
Map<String, String> authenticatedCredentials =
authenticationHelper.prepareAuthenticationResponse(userAuthenticated);
return new UsernamePasswordAuthenticationToken(
userAuthenticated.getUsername(),
authenticatedCredentials,
userAuthenticated.getAuthorities());
} else {
return null;
}
} else {
throw new UsernameNotFoundException("No application user for given username");
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
second for traditional User
#Component
#RequiredArgsConstructor
public class UserAuthenticationProvider implements AuthenticationProvider {
private final CognitoAuthenticationService cognitoAuthenticationService;
private final AuthenticationHelper authenticationHelper;
#SuppressWarnings("unchecked")
#Override
public Authentication authenticate(Authentication authentication) {
if (isNotNull(authentication)) {
AuthenticationRequest authenticationRequest = new AuthenticationRequest();
Map<String, String> credentials = (Map<String, String>) authentication.getCredentials();
authenticationRequest.setPassword(credentials.get(PASSWORD));
authenticationRequest.setUsername(authentication.getName());
SpringSecurityUser userAuthenticated =
cognitoAuthenticationService.authenticateUser(authenticationRequest);
if (isNotNull(userAuthenticated)) {
Map<String, String> authenticatedCredentials =
authenticationHelper.prepareAuthenticationResponse(userAuthenticated);
return new UsernamePasswordAuthenticationToken(
userAuthenticated.getUsername(),
authenticatedCredentials,
userAuthenticated.getAuthorities());
} else {
return null;
}
} else {
throw new UsernameNotFoundException("No application user for given username");
}
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
Above providers use methods authenticate and authenticateUser
public SpringSecurityUser authenticate(AuthenticationRequest authenticationRequest) {
AuthenticationResultType authenticationResult;
try {
final Map<String, String> authParams = new HashMap<>();
authParams.put(USERNAME, authenticationRequest.getUsername());
authParams.put(PASSWORD, authenticationRequest.getPassword());
final AdminInitiateAuthRequest authRequest =
new AdminInitiateAuthRequest()
.withAuthFlow(AuthFlowType.ADMIN_NO_SRP_AUTH)
.withClientId(cognitoConfig.getClientId())
.withUserPoolId(cognitoConfig.getPoolId())
.withAuthParameters(authParams);
AdminInitiateAuthResult result = awsCognitoIdentityProvider.adminInitiateAuth(authRequest);
// Has a Challenge
if (StringUtils.isNotBlank(result.getChallengeName())) {
// If the challenge is required new Password validates if it has the new password variable.
if (NEW_PASSWORD_REQUIRED.equals(result.getChallengeName())) {
if (isNull(authenticationRequest.getNewPassword())) {
throw new CognitoException(
"User must provide a new password",
CognitoException.USER_MUST_CHANGE_PASS_WORD_EXCEPTION_CODE,
result.getChallengeName());
} else {
// we still need the username
final Map<String, String> challengeResponses = new HashMap<>();
challengeResponses.put(USERNAME, authenticationRequest.getUsername());
challengeResponses.put(PASSWORD, authenticationRequest.getPassword());
// add the new password to the params map
challengeResponses.put(NEW_PASSWORD, authenticationRequest.getNewPassword());
// populate the challenge response
final AdminRespondToAuthChallengeRequest request =
new AdminRespondToAuthChallengeRequest()
.withChallengeName(ChallengeNameType.NEW_PASSWORD_REQUIRED)
.withChallengeResponses(challengeResponses)
.withClientId(cognitoConfig.getClientId())
.withUserPoolId(cognitoConfig.getPoolId())
.withSession(result.getSession());
AdminRespondToAuthChallengeResult resultChallenge =
awsCognitoIdentityProvider.adminRespondToAuthChallenge(request);
authenticationResult = resultChallenge.getAuthenticationResult();
}
} else {
// has another challenge
throw new CognitoException(
result.getChallengeName(), CognitoException.USER_MUST_DO_ANOTHER_CHALLENGE);
}
} else {
// Doesn't have a challenge
authenticationResult = result.getAuthenticationResult();
}
var userAuthenticated =
SpringSecurityUser.builder()
.username(authenticationRequest.getUsername())
.password(authenticationRequest.getPassword())
.accessToken(authenticationResult.getAccessToken())
.refreshToken(authenticationResult.getRefreshToken())
.expiresIn(authenticationResult.getExpiresIn())
.tokenType(authenticationResult.getTokenType())
.idToken(authenticationResult.getIdToken())
.build();
log.info("User with username: {} authenticated", authenticationRequest.getUsername());
return userAuthenticated;
} catch (AWSCognitoIdentityProviderException e) {
log.error(e.getMessage(), e);
throw new CognitoException(
e.getMessage(), e.getErrorCode(), e.getMessage() + e.getErrorCode());
} catch (CognitoException cognitoException) {
throw cognitoException;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new CognitoException(
e.getMessage(), CognitoException.GENERIC_EXCEPTION_CODE, e.getMessage());
}
}
and
public SpringSecurityUser authenticateUser(AuthenticationRequest request) {
try {
final var authRequest =
new InitiateAuthRequest()
.withAuthFlow(AuthFlowType.USER_PASSWORD_AUTH)
.withClientId(cognitoConfig.getClientId())
.withAuthParameters(prepareAuthorizationParameters(request));
var initiateAuthResult = awsCognitoIdentityProvider.initiateAuth(authRequest);
var authenticationResult = initiateAuthResult.getAuthenticationResult();
Set<GrantedAuthority> grantedAuthorities = prepareUserAuthorities(request.getUsername());
var userAuthenticated =
SpringSecurityUser.builder()
.username(request.getUsername())
.password(request.getPassword())
.accessToken(authenticationResult.getAccessToken())
.refreshToken(authenticationResult.getRefreshToken())
.expiresIn(authenticationResult.getExpiresIn())
.tokenType(authenticationResult.getTokenType())
.idToken(authenticationResult.getIdToken())
.authorities(grantedAuthorities)
.build();
log.info(USER_SUCCESSFULLY_AUTHENTICATED, request.getUsername(), grantedAuthorities);
return userAuthenticated;
} catch (AWSCognitoIdentityProviderException e) {
log.error(e.getMessage(), e);
throw new CognitoException(
e.getMessage(), e.getErrorCode(), e.getMessage() + e.getErrorCode());
} catch (CognitoException cognitoException) {
throw cognitoException;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new CognitoException(
e.getMessage(), CognitoException.GENERIC_EXCEPTION_CODE, e.getMessage());
}
}
My configuration looks like:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableTransactionManagement
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
private CustomAuthenticationProvider authProvider;
private UserAuthenticationProvider userAuthProvider;
private AccountControllerExceptionHandler exceptionHandler;
private static final String LOGIN_URL = "/auth/login";
private static final String LOGOUT_URL = "/auth/signOut";
#Autowired
public WebSecurityConfiguration(
CustomAuthenticationProvider authProvider,
UserAuthenticationProvider userAuthProvider,
AccountControllerExceptionHandler exceptionHandler) {
this.authProvider = authProvider;
this.userAuthProvider = userAuthProvider;
this.exceptionHandler = exceptionHandler;
}
public WebSecurityConfiguration() {
super(true);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authProvider).eraseCredentials(false);
auth.authenticationProvider(userAuthProvider).eraseCredentials(false);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
public void configure(WebSecurity web) {
// TokenAuthenticationFilter will ignore the below paths
web.ignoring().antMatchers("/auth");
web.ignoring().antMatchers("/auth/**");
web.ignoring().antMatchers("/v2/api-docs");
web.ignoring().antMatchers(GET, "/nutrition/api/**");
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.addFilterAfter(corsFilter(), ExceptionTranslationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(new SecurityAuthenticationEntryPoint())
.accessDeniedHandler(new RestAccessDeniedHandler())
.and()
.anonymous()
.and()
.sessionManagement()
.sessionCreationPolicy(STATELESS)
.and()
.authorizeRequests()
.antMatchers("/auth")
.permitAll()
.anyRequest()
.authenticated()
.and()
// Instantiate a new instance of the filter
.addFilterBefore(
awsCognitoJwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.formLogin(
formLogin -> formLogin.loginProcessingUrl(LOGIN_URL).failureHandler(exceptionHandler))
.logout(logout -> logout.permitAll().logoutUrl(LOGOUT_URL))
.csrf(AbstractHttpConfigurer::disable);
}
private CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader(ORIGIN);
config.addAllowedHeader(CONTENT_TYPE);
config.addAllowedHeader(ACCEPT);
config.addAllowedHeader(AUTHORIZATION);
config.addAllowedMethod(GET);
config.addAllowedMethod(PUT);
config.addAllowedMethod(POST);
config.addAllowedMethod(OPTIONS);
config.addAllowedMethod(DELETE);
config.addAllowedMethod(PATCH);
config.setMaxAge(3600L);
source.registerCorsConfiguration("/v2/api-docs", config);
source.registerCorsConfiguration("/**", config);
return new CorsFilter();
}
private AwsCognitoJwtAuthenticationFilter awsCognitoJwtAuthenticationFilter() {
return new AwsCognitoJwtAuthenticationFilter(new AwsCognitoIdTokenProcessor(), exceptionHandler);
}
}
My controller looks like:
#RestController
#RequestMapping("/auth")
#RequiredArgsConstructor
public class AuthenticationController {
private final AuthenticationManager authenticationManager;
private final CognitoAuthenticationService authService;
#SuppressWarnings("unchecked")
#CrossOrigin
#PostMapping("/login")
public ResponseEntity<AuthenticationResponse> authenticationRequest(
#RequestBody AuthenticationRequest authRequest) {
String accessToken;
Map<String, String> authorizationParameters =
authService.prepareAuthorizationParameters(authRequest);
Authentication authentication =
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
authRequest.getUsername(), authorizationParameters));
//todo verify if above can be simplified
Map<String, String> authenticatedCredentials =
(Map<String, String>) authentication.getCredentials();
accessToken = authenticatedCredentials.get(ACCESS_TOKEN_KEY);
UserResponse userResponse = authService.getUserInfo(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
AuthenticationResponse authResponse =
AuthenticationResponse.builder()
.accessToken(authenticatedCredentials.get(ID_TOKEN_KEY))
.expiresIn(authenticatedCredentials.get(EXPIRES_IN_KEY))
.sessionToken(accessToken)
.userData(userResponse)
.build();
return ResponseEntity.ok(authResponse);
}
#SuppressWarnings("unchecked")
#CrossOrigin
#PostMapping("/loginUser")
public ResponseEntity<AuthenticationResponse> authenticationUserRequest(
#RequestBody AuthenticationRequest authRequest) {
String accessToken;
Map<String, String> authorizationParameters =
authService.prepareAuthorizationParameters(authRequest);
Authentication authentication =
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
authRequest.getUsername(), authorizationParameters));
Map<String, String> authenticatedCredentials =
(Map<String, String>) authentication.getCredentials();
accessToken = authenticatedCredentials.get(ACCESS_TOKEN_KEY);
UserResponse userResponse = authService.getUserInfo(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
AuthenticationResponse authResponse =
AuthenticationResponse.builder()
.accessToken(authenticatedCredentials.get(ID_TOKEN_KEY))
.expiresIn(authenticatedCredentials.get(EXPIRES_IN_KEY))
.sessionToken(accessToken)
.userData(userResponse)
.build();
return ResponseEntity.ok(authResponse);
}
}
and there lies a problem, namely, I cannot pick which method under the hood will be called by AuthenticationManager in the Controller line: authenticationManager.authenticate(). I know that splitting authentication to two ways, like admin/user is not the best option and in the future, I will get rid off this one but it doesn't change the fact that I would like to create another Provider for Social's authentication and there will the same problem because so far I don't have an option to pick proper authentication() method. I will be grateful for suggestions on how to solve this problem.
I have multiple clients registered for my oauth2 auth server. lets say user1 have roles such as ROLE_A, ROLE_B for client1, same user has roes such as ROLE_C, ROLE_D for client2. now when the user logins either using client1 or client2 he is able to see all the four roles ie. ROLE_A, ROLE_B, ROLE_C and ROLE_D.
My requirement was when the user1 logins to client1 it should return only the roles ROLE_A and ROLE_B. when he logins using client2 it should return only ROLE_C and ROLE_D
For achieving this, what I planned is within the authenticate function, I need to get the clientId. so using the clientId and the username I can find the corresponding roles allocated to the user from the db (client-user-roles-mapping table). .But the issue is I don't know how to get the clientId within the authenticate function
#Override
public Authentication authenticate(final Authentication authentication) throws AuthenticationException {
String userName = ((String) authentication.getPrincipal()).toLowerCase();
String password = (String) authentication.getCredentials();
if (userName != null && authentication.getCredentials() != null) {
String clientId = // HERE HOW TO GET THE CLIENT ID
Set<String> userRoles = authRepository.getUserRoleDetails(userName.toLowerCase(), clientId);
Collection<SimpleGrantedAuthority> authorities = fillUserAuthorities(userRoles);
Authentication token = new UsernamePasswordAuthenticationToken(userName, StringUtils.EMPTY, authorities);
return token;
} else {
throw new BadCredentialsException("Authentication Failed!!!");
}
} else {
throw new BadCredentialsException("Username or Password cannot be empty!!!");
}
}
Can anyone please help me on this
UPDATE 1
CustomAuthenticationProvider.java
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final Logger log = LoggerFactory.getLogger(getClass());
#Autowired
private LDAPAuthenticationProvider ldapAuthentication;
#Autowired
private AuthRepository authRepository;
public CustomAuthenticationProvider() {
super();
}
#Override
public Authentication authenticate(final Authentication authentication) throws AuthenticationException {
String userName = ((String) authentication.getPrincipal()).toLowerCase();
String password = (String) authentication.getCredentials();
if (userName != null && authentication.getCredentials() != null) {
String clientId = // HERE HOW TO GET THE CLIENT ID
Set<String> userRoles = authRepository.getUserRoleDetails(userName.toLowerCase(), clientId);
Collection<SimpleGrantedAuthority> authorities = fillUserAuthorities(userRoles);
Authentication token = new UsernamePasswordAuthenticationToken(userName, StringUtils.EMPTY, authorities);
return token;
} else {
throw new BadCredentialsException("Authentication Failed!!!");
}
} else {
throw new BadCredentialsException("Username or Password cannot be empty!!!");
}
}
public boolean invokeAuthentication(String username, String password, Boolean isClientValidation) {
try {
Map<String, Object> userDetails = ldapAuthentication.authenticateUser(username, password);
if(Boolean.parseBoolean(userDetails.get("success").toString())) {
return true;
}
} catch (Exception exception) {
log.error("Exception in invokeAuthentication::: " + exception.getMessage());
}
return false;
}
#Override
public boolean supports(Class<? extends Object> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
private Collection<SimpleGrantedAuthority> fillUserAuthorities(Set<String> roles) {
Collection<SimpleGrantedAuthority> authorties = new ArrayList<SimpleGrantedAuthority>();
for(String role : roles) {
authorties.add(new SimpleGrantedAuthority(role));
}
return authorties;
}
}
Here is you code after modification
#Override
public Authentication authenticate(final Authentication authentication) throws AuthenticationException {
String userName = ((String) authentication.getPrincipal()).toLowerCase();
String password = (String) authentication.getCredentials();
if (userName != null && authentication.getCredentials() != null) {
String clientId = getClientId();
// validate client ID before use
Set<String> userRoles = authRepository.getUserRoleDetails(userName.toLowerCase(), clientId);
Collection<SimpleGrantedAuthority> authorities = fillUserAuthorities(userRoles);
Authentication token = new UsernamePasswordAuthenticationToken(userName, StringUtils.EMPTY, authorities);
return token;
} else {
throw new BadCredentialsException("Authentication Failed!!!");
}
} else {
throw new BadCredentialsException("Username or Password cannot be empty!!!");
}
private String getClientId(){
final HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
final String authorizationHeaderValue = request.getHeader("Authorization");
final String base64AuthorizationHeader = Optional.ofNullable(authorizationHeaderValue)
.map(headerValue->headerValue.substring("Basic ".length())).orElse("");
if(StringUtils.isNotEmpty(base64AuthorizationHeader)){
String decodedAuthorizationHeader = new String(Base64.getDecoder().decode(base64AuthorizationHeader), Charset.forName("UTF-8"));
return decodedAuthorizationHeader.split(":")[0];
}
return "";
}
more info about RequestContextHolder
Extend UsernamePasswordAuthenticationToken
A POJO is required to hold not just the username and the password but also the client identifier.
public ExtendedUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
private final String clientId;
public ExtendedUsernamePasswordAuthenticationToken(Object principal
, Object credentials
, String clientId) {
super(principal, credentials);
this.clientId = clientId;
}
public String getClientId() { return clientId; }
}
Extend UsernamePasswordAuthenticationFilter
The authentication process needs to be tweaked so that the client identifier is passed to the authentication code in addition to the username and password.
public class ExtendedUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public ExtendedUsernamePasswordAuthenticationFilter () { super(); }
#Override
public public Authentication attemptAuthentication(HttpServletRequest request
, HttpServletResponse response)
throws AuthenticationException {
// See the source code of UsernamePasswordAuthenticationFilter
// to implement this. Instead of creating an instance of
// UsernamePasswordAuthenticationToken, create an instance of
// ExtendedUsernamePasswordAuthenticationToken, something along
// the lines of:
final String username = obtainUsername(request);
final String password = obtainPassword(request);
final String clientId = obtainClientId(request);
...
final Authentication authentication = new ExtendedUsernamePasswordAuthenticationToken(username, password, clientId);
return getAuthenticationManager().authenticate(authentication);
}
}
Use the extra information available for logging in
public CustomAuthenticationProvider implements AuthenticationProvider {
...
#Override
public boolean supports(final Class<?> authentication) {
return authentication.isAssignableFrom(ExtendedUsernamePasswordAuthenticationToken.class);
}
#Override
public Authentication authenticate(final Authentication authentication)
throws AuthenticationException {
}
}
Force Spring Security to use the custom filter
<bean class="com.path.to.filter.ExtendedUsernamePasswordAuthenticationFilter" id="formAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
</bean>
<http ... >
<security:custom-filter position="FORM_LOGIN_FILTER" ref="formAuthenticationFilter"/>
...
</http>
or, if using Java configuration:
#Bean
public ExtendedUsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter(final AuthenticationManager authenticationManager) {
final ExtendedUsernamePasswordAuthenticationFilter filter = new ExtendedUsernamePasswordAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager);
return filter;
}
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAt(usernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
...
}
For your requirement, since you want to just access additional parameters from the request, you could try out the following in your CustomAuthenticationProvider class
#Autowired
private HttpServletRequest request;
Add the following logic to read the httpRequest parameters and add your logic to access the authorization key
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Enumeration<String> headerNames = request.getHeaderNames();
while(headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
System.out.println("Header Name - " + headerName + ", Value - " + request.getHeader(headerName));
}
}
Now, you will have the encode Basic Authentication field which you can decode like the one below
if (authorization != null && authorization.startsWith("Basic")) {
// Authorization: Basic base64credentials
String base64Credentials = authorization.substring("Basic".length()).trim();
String credentials = new String(Base64.getDecoder().decode(base64Credentials),
Charset.forName("UTF-8"));
// client/secret = clientId:secret
final String[] values = credentials.split(":",2);
I am developing a application for test the resources of the Spring Security, and in the current state of it, I need one solution to implement Roles AND Permissions for my methods. I already using this annotation:
#Secured("ROLE_ADMIN")
to define which role should be able to access each method. In my database, I have this structure:
which means for each Role I can have a list of permissions. Anyone can point me a solution for define both restrictions for each one of my methods? I am configuring the Spring Security via Java code, this way:
https://github.com/klebermo/webapp2/tree/master/src/com/spring/webapp/lojavirtual/config/security
UPDATE
Following the presented sugestions, I change my AuthenticationService (which extends UserDetailsService) to this:
#Service
public class AuthenticationService implements UserDetailsService {
#Autowired
private UsuarioHome accountDao;
#Override
#Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Usuario account = accountDao.findByField("login", username);
if(account==null) {
System.out.println("No such user: " + username);
throw new UsernameNotFoundException("No such user: " + username);
} else if (account.getAutorizacao().isEmpty()) {
System.out.println("User " + username + " has no authorities");
throw new UsernameNotFoundException("User " + username + " has no authorities");
}
List<Permission> lista = new ArrayList<Permission>();
for(int i=0; i<account.getAutorizacao().size(); i++) {
for(int j=0; j<account.getAutorizacao().get(i).getPermissao().size(); j++) {
lista.add(account.getAutorizacao().get(i).getPermissao().get(j));
}
}
boolean accountIsEnabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
return new User(account.getLogin(), account.getSenha(), accountIsEnabled, accountNonExpired, credentialsNonExpired, accountNonLocked, getAuthorities(lista));
}
public List<String> getRolesAsList(List<Permission> list) {
List <String> rolesAsList = new ArrayList<String>();
for(Permission role : list){
rolesAsList.add(role.getNome());
}
return rolesAsList;
}
public static List<GrantedAuthority> getGrantedAuthorities(List<String> roles) {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
return authorities;
}
public Collection<? extends GrantedAuthority> getAuthorities(List<Permission> list) {
List<GrantedAuthority> authList = getGrantedAuthorities(getRolesAsList(list));
return authList;
}
and in my methods, I change to that:
#Controller
#RequestMapping(value="privado")
public class PrivadoController {
#RequestMapping(value="admin")
#Secured("admin_main")
public ModelAndView admin() {
ModelAndView mav = new ModelAndView();
mav.setViewName("privado/admin");
return mav;
}
#RequestMapping(value="customer")
#Secured("customer_main")
public ModelAndView customer() {
ModelAndView mav = new ModelAndView();
mav.setViewName("privado/customer");
return mav;
}
}
But now when I try acess the page, I get my no_permit page. what's wrong now?
UPDATE
after some changes, I finally get to this code:
AuthenticationService.java
#Service
public class AuthenticationService implements UserDetailsService {
#Autowired
private UsuarioHome accountDao;
#Override
#Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Usuario account = accountDao.findByField("login", username);
if(account==null) {
System.out.println("No such user: " + username);
throw new UsernameNotFoundException("No such user: " + username);
} else if (account.getAutorizacao().isEmpty()) {
System.out.println("User " + username + " has no authorities");
throw new UsernameNotFoundException("User " + username + " has no authorities");
}
List<Permission> lista = new ArrayList<Permission>();
int max = account.getAutorizacao().size();
for(int i=0; i<max; i++) {
for(int j=0; j<max; j++) {
lista.add(account.getAutorizacao().get(i).getPermissao().get(j));
}
}
boolean accountIsEnabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
return new User(account.getLogin(), account.getSenha(), accountIsEnabled, accountNonExpired, credentialsNonExpired, accountNonLocked, getAuthorities(lista));
}
public List<String> getRolesAsList(List<Permission> list) {
List <String> rolesAsList = new ArrayList<String>();
for(Permission role : list){
rolesAsList.add(role.getNome());
}
return rolesAsList;
}
public static List<GrantedAuthority> getGrantedAuthorities(List<String> roles) {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
return authorities;
}
public Collection<? extends GrantedAuthority> getAuthorities(List<Permission> list) {
List<GrantedAuthority> authList = getGrantedAuthorities(getRolesAsList(list));
return authList;
}
}
In my controller, I try use either the annotation #Secured or #PreAuthorize, in this way:
#Controller
#RequestMapping(value="privado")
public class PrivadoController {
#RequestMapping(value="admin")
//#Secured("admin_main")
#PreAuthorize("hasRole('admin_main')")
public ModelAndView admin() {
ModelAndView mav = new ModelAndView();
mav.setViewName("privado/admin");
return mav;
}
#RequestMapping(value="customer")
//#Secured("customer_main")
#PreAuthorize("hasRole('customer_main')")
public ModelAndView customer() {
ModelAndView mav = new ModelAndView();
mav.setViewName("privado/customer");
return mav;
}
}
But I am not getting access to any user, despite informing the right credencials. What's wrong in this current scenario?
UPDATE 2
Ok, it happens the problem with the access denied was becasue an error in the code in my CustomAuthenticationSuccessHandler, which now it is like that:
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException, ServletException {
HttpSession session = request.getSession();
SavedRequest savedReq = (SavedRequest) session.getAttribute(WebAttributes.ACCESS_DENIED_403);
boolean isAdmin = false;
boolean isUser = false;
if (savedReq == null) {
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
for (GrantedAuthority grantedAuthority : authorities) {
if(grantedAuthority.getAuthority().equals("admin_main"))
isAdmin = true;
else if(grantedAuthority.getAuthority().equals("customer_main"))
isUser = true;
}
if(isAdmin)
response.sendRedirect(request.getContextPath() + "/privado/admin");
else if(isUser)
response.sendRedirect(request.getContextPath() + "/privado/customer");
else
response.sendRedirect(request.getContextPath() + "/erro/no_permit");
}
else {
response.sendRedirect(savedReq.getRedirectUrl());
}
}
}
But now when I try login in my appliation, I get the error described in this topic:
SpelEvaluationException: EL1004E:(pos 0): Method call: Method hasPermission(java.lang.String) cannot be found on MethodSecurityExpressionRoot type
Any ideas in how to solve this one?