I'm experiencing this difficulty and I still don't understand what's happening, I have a scenario where an api uses a feing client to obtain a user's profile from an external service, until this moment everything was going well, but when I solved it add the fallback, spring starts to inject the SecurityContextHolder.getContext() always with a null value, and with that the interceptor cannot get the token of the user who made the primary call of the application.
appliction.properties
application.authserver.url=http://localhost:8088/auth
feign.circuitbreaker.enabled=true
Interceptor Class
#Component
#Slf4j
public class FeingClientConfig {
#Value("${app.name}")
private String appName;
#Bean
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor() {
#Override
public void apply(RequestTemplate template) {
if (SecurityContextHolder.getContext().getAuthentication() != null) {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof Jwt) {
var jwt = (Jwt) principal;
log.info("-------------------- service integration -----------------");
log.info(String.format(" Endpoint: %s", template.method() + " " + template.feignTarget().url() + template.url()));
log.info(String.format(" User: %s", jwt.getClaim("user_name").toString()));
log.info(String.format(" Bearer token: %s", jwt.getTokenValue().substring(0, 30) + "....."));
log.info("----------------------------------------------------------");
template.header("Authorization", String.format("Bearer %s", jwt.getTokenValue()));
template.header("x-app-name", appName);
}
}
}
};
}
}
Client
#FeignClient(url = "${application.authserver.url}", name = "authClient",
fallbackFactory = AuthClientFallBackFactory.class)
public interface AuthClient {
#GetMapping("/user/profile")
public UserProfile getProfile();
}
Fallback
#Slf4j
#Component
public class AuthClientFallBackFactory implements FallbackFactory<AuthClient> {
#Override
public AuthClient create(Throwable cause) {
return new AuthClient() {
#Override
public UserProfile getProfile() {
log.error("*** Fallback: error on get profile.", cause);
return new UserProfile();
}
};
}
}
If the feign.circuitbreaker.enabled property was set to false, this ignores the use of the fallback, but the SecurityContextHolder.getContext() object is inject correctly
What am I doing wrong?
Using in this project:
JDK 11
Spring Boot [2.4.2]
Related
I want to create a form that I can submit it and data store in h2. If email address is already in h2 then throw an exception. I have four packages(Controller, Model, Service, Repository). I dont get my exception message when I use an email that already exist in h2. Can you please help me where is the issue?
Controller class:
#RestController
public class RegistrationController {
#Autowired
private RegistrationService service;
#PostMapping("/registeruser")
public User registerUser(#RequestBody User user) throws Exception {
String tempEmailId = user.getEmailId();
if(tempEmailId !=null && !"".equals(tempEmailId)) {
User userObject = service.fetchUserByEmailId(tempEmailId);
if(userObject!=null) {
throw new Exception("User with "+tempEmailId+" is already exist");
}
}
User userObject = null;
userObject = service.saveUser(user);
return userObject;
}
Repository:
public interface RegistrationRepository extends JpaRepository<User, Integer> {
public User findByEmailId(String emailId);
}
Service:
#Service
public class RegistrationService {
#Autowired
private RegistrationRepository repo;
public User saveUser(User user) {
return repo.save(user);
}
public User fetchUserByEmailId(String email) {
return repo.findByEmailId(email);
}
}
Here is JSON response so I want my message printed but somehow not happening:
{
"timestamp": "2020-08-26T06:28:01.369+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/registeruser"
}
Same kind of problem i got. what i did is rather than checking the object inside the if condition you better try to check with attribute of the object.
eg
if(userObject.getEmailId().isEmpty()) { throw new Exception("User with "+tempEmailId+" is already exist"); }
this worked for me.
You can configure a Spring ExceptionHandler to customize your response body with exception messages:
#ControllerAdvice
public class GlobalExceptionMapper extends ResponseEntityExceptionHandler {
#Autowired
public GlobalExceptionMapper() {
}
#ExceptionHandler(value = {Exception.class})
protected ResponseEntity<Object> handleBusinessException(Exception ex, WebRequest request) {
return handleExceptionInternal(ex, ex.getMessage(), new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
}
If you are using Spring Boot version 2.3 or higher, the property server.error.include-message must be set to always at application.properties file.
I am signing JWT with private key (authorization server) and I am using public key (resource server) to "verify" it...
How can I know whether the JWT has not been compromised? Or how can I do that?
The code is from resource server
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.txt");
String publicKey = null;
try {
publicKey = IOUtils.toString(resource.getInputStream());
} catch (final IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey);
return converter;
}
Spring Security will do the verification of the token based on configurations in authorization server.
For a standalone verification, the code would be like:
RsaVerifier verifier = new RsaVerifier(RSAPublicKey);
Jwt tokenDecoded = JwtHelper.decodeAndVerify(token, verifier);
Map<String, Object> claimsMap = (Map<String, Object>) new
ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class);
//Verify the claims then
// 1 Verify if the token has not already expired
// 2 Verify the issuance date ( should be before this date )
// 3 Verify if the issuer of this token is contained in verified authorities.
// 4 Verify if the token was issued for this client
// 5 Verify if the token contained any expected claims...
But the above is implemented by Spring Security for Oauth2 authentication process, client application just needs to provide configurations.
The trigger is OAuth2AuthenticationProcessingFilter in the Spring security filter chain. This filter is added when resources are protected by Oauth2 security.
In your application, the authorization server configuration would look like ( only relevant indicative configuration extracts below)
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
...
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
RSAPemKeyPairLoader keyPairLoader = new RSAPemKeyPairLoader();
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(...);
converter.setVerifierKey(...);
return converter;
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(...);
defaultTokenServices.setAccessTokenValiditySeconds(...);
defaultTokenServices.setRefreshTokenValiditySeconds(...);
return defaultTokenServices;
}
}
In your application, the Resource Server configuration would be like:
#EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
...
}
}
To trace in Spring implementation where the requested token is intercepted and verified look at the Spring OAUTH2 implementation - flow details below, where Authentication object, an instance of OAuth2Authentication would be attempted to be created for successful requests.
All below Code extracts are from spring-security-oauth2-2.0.8.RELEASE implementations.
public class OAuth2AuthenticationManager implements AuthenticationManager {
....
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
String token = (String) authentication.getPrincipal();
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
...
}
}
The loadAuthentication would be basically verifying the access token and attempting to convert it into OAuth2Authentication
public class DefaultTokenServices implements AuthorizationServerTokenServices ...{
public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException, InvalidTokenException {
OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
...
}
}
JwtTokenStore would create OAuth2AccessToken and in the process decode and verify the String token.
public class JwtTokenStore implements TokenStore {
public OAuth2AccessToken readAccessToken(String tokenValue) {
OAuth2AccessToken accessToken = convertAccessToken(tokenValue);
if (jwtTokenEnhancer.isRefreshToken(accessToken)) {
throw new InvalidTokenException("Encoded token is a refresh token");
}
return accessToken;
}
private OAuth2AccessToken convertAccessToken(String tokenValue) {
return jwtTokenEnhancer.extractAccessToken(tokenValue, jwtTokenEnhancer.decode(tokenValue));
}
}
JWTAccessTokenConverter does the decoding and extraction of token claims.
public class JwtAccessTokenConverter implements AccessTokenConverter {
protected Map<String, Object> decode(String token) {
try {
Jwt jwt = JwtHelper.decodeAndVerify(token, verifier);
String content = jwt.getClaims();
Map<String, Object> map = objectMapper.parseMap(content);
if (map.containsKey(EXP) && map.get(EXP) instanceof Integer) {
Integer intValue = (Integer) map.get(EXP);
map.put(EXP, new Long(intValue));
}
return map;
}
catch (Exception e) {
throw new InvalidTokenException("Cannot convert access token to JSON", e);
}
}
JwtHelper would do the decoding and request verification.
public static Jwt decodeAndVerify(String token, SignatureVerifier verifier) {
Jwt jwt = decode(token);
jwt.verifySignature(verifier);
return jwt;
}
JwttImpl invokes the verifier.
public void verifySignature(SignatureVerifier verifier) {
verifier.verify(signingInput(), crypto);
}
For example, RSA Signature verifier would finally do the verification:
public class RsaVerifier implements SignatureVerifier {
public void verify(byte[] content, byte[] sig) {
try {
Signature signature = Signature.getInstance(algorithm);
signature.initVerify(key);
signature.update(content);
if (!signature.verify(sig)) {
throw new InvalidSignatureException("RSA Signature did not match content");
}
}
catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
}
I have added custom token based authentication for my spring-web app and extending the same for spring websocket as shown below
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
config.setUserDestinationPrefix("/user");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/gs-guide-websocket").setAllowedOrigins("*").withSockJS();
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String jwtToken = accessor.getFirstNativeHeader("Auth-Token");
if (!StringUtils.isEmpty(jwtToken)) {
Authentication auth = tokenService.retrieveUserAuthToken(jwtToken);
SecurityContextHolder.getContext().setAuthentication(auth);
accessor.setUser(auth);
//for Auth-Token '12345token' the user name is 'user1' as auth.getName() returns 'user1'
}
}
return message;
}
});
}
}
The client side code to connect to the socket is
var socket = new SockJS('http://localhost:8080/gs-guide-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({'Auth-Token': '12345token'}, function (frame) {
stompClient.subscribe('/user/queue/greetings', function (greeting) {
alert(greeting.body);
});
});
And from my controller I am sending message as
messagingTemplate.convertAndSendToUser("user1", "/queue/greetings", "Hi User1");
For the auth token 12345token the user name is user1. But when I send a message to user1, its not received at the client end. Is there anything I am missing with this?
In your Websocket controller you should do something like this :
#Controller
public class GreetingController {
#Autowired
private SimpMessagingTemplate messagingTemplate;
#MessageMapping("/hello")
public void greeting(Principal principal, HelloMessage message) throws Exception {
Greeting greeting = new Greeting();
greeting.setContent("Hello!");
messagingTemplate.convertAndSendToUser(message.getToUser(), "/queue/reply", greeting);
}
}
On the client side, your user should subscribe to topic /user/queue/reply.
You must also add some destination prefixes :
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue" ,"/user");
config.setApplicationDestinationPrefixes("/app");
config.setUserDestinationPrefix("/user");
}
/*...*/
}
When your server receive a message on the /app/hello queue, it should send a message to the user in your dto. User must be equal to the user's principal.
I think the only problem in your code is that your "/user" is not in your destination prefixes. Your greetings messages are blocked because you sent them in a queue that begin with /user and this prefixe is not registered.
You can check the sources at git repo :
https://github.com/simvetanylen/test-spring-websocket
Hope it works!
In my previous project I sent messages to one specific user; in detail I wrote the following:
CLIENT SIDE:
function stompConnect(notificationTmpl)
{
var socket = new SockJS('/comm-svr');
stompClient = Stomp.over(socket);
var theUserId
stompClient.connect({userId:theUserId}, function (frame) {
debug('Connected: ' + frame);
stompClient.subscribe('/topic/connect/'+theUserId, function (data) {
//Handle data
}
});
}
SERVER SIDE
Spring websocket listener:
#Component
public class WebSocketSessionListener
{
private static final Logger logger = LoggerFactory.getLogger(WebSocketSessionListener.class.getName());
private List<String> connectedClientId = new ArrayList<String>();
#EventListener
public void connectionEstablished(SessionConnectedEvent sce)
{
MessageHeaders msgHeaders = sce.getMessage().getHeaders();
Principal princ = (Principal) msgHeaders.get("simpUser");
StompHeaderAccessor sha = StompHeaderAccessor.wrap(sce.getMessage());
List<String> nativeHeaders = sha.getNativeHeader("userId");
if( nativeHeaders != null )
{
String userId = nativeHeaders.get(0);
connectedClientId.add(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Connessione websocket stabilita. ID Utente "+userId);
}
}
else
{
String userId = princ.getName();
connectedClientId.add(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Connessione websocket stabilita. ID Utente "+userId);
}
}
}
#EventListener
public void webSockectDisconnect(SessionDisconnectEvent sde)
{
MessageHeaders msgHeaders = sde.getMessage().getHeaders();
Principal princ = (Principal) msgHeaders.get("simpUser");
StompHeaderAccessor sha = StompHeaderAccessor.wrap(sde.getMessage());
List<String> nativeHeaders = sha.getNativeHeader("userId");
if( nativeHeaders != null )
{
String userId = nativeHeaders.get(0);
connectedClientId.remove(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Connessione websocket stabilita. ID Utente "+userId);
}
}
else
{
String userId = princ.getName();
connectedClientId.remove(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Connessione websocket stabilita. ID Utente "+userId);
}
}
}
public List<String> getConnectedClientId()
{
return connectedClientId;
}
public void setConnectedClientId(List<String> connectedClientId)
{
this.connectedClientId = connectedClientId;
}
}
Spring websocket message sender:
#Autowired
private SimpMessagingTemplate msgTmp;
private void propagateDvcMsg( WebDeviceStatusInfo device )
{
String msg = "";
String userId =((Principal)SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getName()
msgTmp.convertAndSend("/topic/connect"+userId, msg);
}
I hope it's useful
I want to secure my API's based on the user role, but #PreAuthorize Annotations seem to not work properly. No matter what role the user has the server throws a 403 error. How to make this work ?
This is where I retrieve the User Details in my custom authentication provider:
#Override
protected UserDetails retrieveUser(String userName, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken){
final String password = (String) usernamePasswordAuthenticationToken.getCredentials();
if (!StringUtils.hasText(password)) {
this.logger.warn("Username {}: no password provided", userName);
}
userName = parseCredentials(userName);
try {
DirContext ctx = ldapConfiguration.openConnection(userName, password);
ctx.close();
} catch (NamingException e) {
throw new LdapException("User not found in Active Directory", e);
} catch (NullPointerException e) {
throw new CredentialsNotProvidedException("Entered data may be null", e);
}
User user = userRepository.findOneByLogin(userName);
if (user == null) {
this.logger.warn("Username {}: user not found", userName);
throw new BadCredentialsException("Invalid Username/Password for user " + userName);
}
final List<GrantedAuthority> auths = new ArrayList<GrantedAuthority>();
GrantedAuthority r = (GrantedAuthority) () -> "ROLE_" + user.getRole().getName().toUpperCase();
auths.add(r);
// enabled, account not expired, credentials not expired, account not locked
UserDetails userDetails = new org.springframework.security.core.userdetails.User(userName, password, true, true, true, true, auths);
return userDetails;
}
This is the controller:
#PreAuthorize("hasRole('ROLE_HR')") //I don't have acces even if I am HR
#RestController
public class SettingsController {
#Autowired
private LocationRepository locationRepository;
#Autowired
private DepartmentRepository departmentRepository;
#Autowired
private RoleRepository roleRepository;
#RequestMapping(value = "/api/locations", method = RequestMethod.POST)
public ResponseEntity addLocation(#RequestBody Location location) {
if (location == null) {
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
locationRepository.save(new Location(location.getName()));
return new ResponseEntity(HttpStatus.CREATED);
}
#RequestMapping(value = "/api/roles", method = RequestMethod.POST)
public ResponseEntity addRole(#RequestBody Role role) {
if (role == null) {
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
roleRepository.save(new Role(role.getName()));
return new ResponseEntity(HttpStatus.CREATED);
}
#RequestMapping(value = "/api/departments", method = RequestMethod.POST)
public ResponseEntity addDepartment(#RequestBody Department department) {
if (department == null) {
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
departmentRepository.save(new Department(department.getName()));
return new ResponseEntity(HttpStatus.CREATED);
}
}
And security config:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private DatabaseAuthenticationProvider authenticationProvider;
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/js/**", "/css/**", "/img/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/login").failureUrl("/login").defaultSuccessUrl("/")
.and().logout().logoutSuccessUrl("/login")
.and().authorizeRequests().antMatchers("/login").permitAll()
/*.antMatchers("/settings.html").access("hasRole('HR')")
.antMatchers("/pendingRequests.html").access("hasRole('MANAGER')")
.antMatchers("/settings.html","/pendingRequests.html").access("hasRole('ADMIN')")*/
.anyRequest().authenticated().and().csrf().disable();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider).eraseCredentials(false);
}
}
According to your commented line in security config class
.antMatchers("/settings.html").access("hasRole('HR')") user role is HR.
If role is HR then you should use
#PreAuthorize("hasRole('HR')")
and also #PreAuthorize should be placed first and then mention #RestController
#RestController
#PreAuthorize("hasRole('HR')")
public class SettingsController
We added property isBlocked for users.
When following property is set user cannot login on our site.
We want to render error message "your account is temporary blocked..."
But I have not ideas how to pass this message to loginFailed controller method.
I have following spring-security configuration:
public class XXXSecurityServiceImpl implements UserDetailsService {
#Autowired
private TerminalAdminDao terminalAdminDao;
#Override
public UserDetails loadUserByUsername(String adminName) throws UsernameNotFoundException,
DataAccessException {
TerminalAdmin admin = terminalAdminDao.findAdminByEmail(adminName);
UserDetails userDetails = null;
if (admin != null) {
Set<SimpleGrantedAuthority> authorities = new HashSet<SimpleGrantedAuthority>();
for (AdminRole adminRole : admin.getAdminRoles()) {
authorities.add(new SimpleGrantedAuthority(adminRole.getRole()));
}
userDetails = new User(admin.getEmail(), admin.getPassword(), true, true, true, !admin.isBlocked(),
authorities);
}
return userDetails;
}
admin failed controlled method:
#RequestMapping(value = "/loginAdminFailed", method = RequestMethod.GET)
public String loginError(HttpSession session) {
session.setAttribute("login_error", "true");
return "admin/login";
}
How to understand in controller that user is blocked?
Basically you need to register custom authentication provider and custom authentication failure handler to handle authentication exceptions.
Check this SO question.
This works
added to controller:
#RequestMapping(value = "/loginAdminFailed", method = RequestMethod.GET)
public String loginError(HttpSession session, HttpServletRequest request) {
session.setAttribute("message", getErrorMessage(request, "SPRING_SECURITY_LAST_EXCEPTION"));
return "admin/login";
}
// customize the error message
private String getErrorMessage(HttpServletRequest request, String key) {
Exception exception = (Exception) request.getSession().getAttribute(key);
String error = "";
if (exception instanceof BadCredentialsException) {
error = messageSource.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", null, LOCALE_RU);
} else if (exception instanceof LockedException) {
error = messageSource.getMessage(
"AbstractUserDetailsAuthenticationProvider.accountIsLocked", null, LOCALE_RU);
} else {
error = messageSource.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", null, LOCALE_RU);
}
return error;
}
But this looks ugly