Spring websocket upgraderequest - java

I have a spring application.
I need to put a value to the initial handshake.
The url looks like: ws://localhost:8080/chat?key=value
I need this key=value in my Websocket Handler.
How can I access it?
Websocket Configuration:
#Configuration
#EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// alle origins erlauben
registry.addHandler(chatWebSocketController(), "/chat").addInterceptors(new HttpSessionHandshakeInterceptor())
.setAllowedOrigins("*");
}
#Bean
public ChatWebSocketController chatWebSocketController() {
return new ChatWebSocketController();
}
}
Websocket Handler method:
#Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
if (session.getAttributes().containsKey("key")) {
List<String> userMap = session.getHandshakeHeaders().get("key");
JwtTokenUtil jwtTokenUtil = new JwtTokenUtil();
String token = userMap.get(0);
if (jwtTokenUtil.validateToken(token)) {
User userToStore = new User(jwtTokenUtil.getUsernameFromToken(token));
userUsernameMap.put(session, userToStore);
LOGGER.info("User with name " + jwtTokenUtil.getUsernameFromToken(token) + "and IP "
+ session.getRemoteAddress() + " successfully connected");
sendConnectMessage(session, userToStore);
}
} else {
session.close(CloseStatus.POLICY_VIOLATION);
}
}

Found the solution by myself. You have to write your own HandshakeInterceptor, there you have access to the http parameter. so you can put this to your attribbutes map.
public class HttpHandshakeInterceptor implements HandshakeInterceptor {
#Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession();
attributes.put("sessionId", session.getId());
attributes.put("key", servletRequest.getServletRequest().getParameterMap().get("key"));
}
return true;
}
#Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
//nothing to do
}
}

Related

How can I add a Principal in configureClientInboundChannel?

I am using SockJS + STOMP + Spring WebSocket with a configuration that receives user credentials during the handshake. The credentials are received in the configureClientInboundChannel method:
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message,
StompHeaderAccessor.class);
if (accessor != null && (StompCommand.CONNECT.equals(accessor.getCommand())
|| StompCommand.SEND.equals(accessor.getCommand()))) {
List<String> auth = accessor.getNativeHeader("Authorization");
System.out.printf("Authorization: %s%n", auth.get(0));
}
return message;
}
});
}
I would like to add a Principal based on the token I receive in the header. I have a handler that registers Principals:
public class PrincipalHandshakeHandler extends DefaultHandshakeHandler {
#Override
protected Principal determineUser(ServerHttpRequest request,
WebSocketHandler handler, Map<String, Object> attributes) {
System.out.println(attributes);
return new Principal() {
#Override
public String getName() {
return userId;
}
};
}
}
But how do I get pass the token I get in configureClientInboundChannel to the above handshake handler? Basically, the token should go there in place of userId.

Springboot #DeleteMapping respond 404, but response body is empty

I have problem with #DeleteMapping.
Situation is like below.
If I request to /v1/cache/{cacheEntry} with method DELETE,
It respond with 404, but body was empty. no message, no spring default json 404 response message.
If i request to /v1/cache/{cacheEntry} with method POST,
It respond with 405 and body was below. (This action is correct, not a bug.)
If I change #DeleteMapping to #PostMapping, and request /v1/cache/{cacheEntry} with method POST, It respond success with code 200.
{
"timestamp": 1643348039913,
"status": 405,
"error": "Method Not Allowed",
"message": "",
"path": "/v1/cache/{cacheEntry}"
}
// Controller
#Slf4j
#RestController
#RequestMapping("/v1/cache")
#RequiredArgsConstructor
public class CacheController {
private final CacheService cacheService;
#PostMapping("/{cacheEntry}")
public CacheClearResponse clearCacheEntry(#PathVariable("cacheEntry") CacheChannels cacheEntry) {
try {
log.info("Cache entry :: " + cacheEntry);
cacheService.evictCacheEntry(cacheEntry);
return CacheClearResponse.builder()
.result(
RequestResult.builder()
.code(9200)
.message("SUCCESS")
.build()
)
.common(
Common.builder().build()
)
.date(LocalDateTime.now())
.build();
} catch (Exception e) {
e.printStackTrace();
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
return CacheClearResponse.builder()
.result(
RequestResult.builder()
.code(9999)
.message(sw.toString())
.build()
)
.common(
Common.builder().build()
)
.date(LocalDateTime.now())
.build();
}
}
}
}
// CacheService
#Service
#RequiredArgsConstructor
public class CacheService {
private final CacheManager cacheManager;
public void evictCacheEntry(CacheChannels cacheEntry) {
Cache cache = cacheManager.getCache(cacheEntry.getCacheName());
if (cache != null) {
cache.clear();
}
}
public void evictCache(CacheChannels cacheEntry, String cacheKey) {
Cache cache = cacheManager.getCache(cacheEntry.getCacheName());
if (cache != null) {
cache.evict(cacheKey);
}
}
}
// Enum
#Getter
#AllArgsConstructor
public enum CacheChannels {
CACHE_TEN_MIN(Names.CACHE_TEN_MIN, Duration.ofMinutes(10)),
CACHE_HALF_HR(Names.CACHE_HALF_HR, Duration.ofMinutes(30)),
CACHE_ONE_HR(Names.CACHE_ONE_HR, Duration.ofHours(1)),
CACHE_THREE_HR(Names.CACHE_THREE_HR, Duration.ofHours(3)),
CACHE_SIX_HR(Names.CACHE_SIX_HR, Duration.ofHours(6)),
CACHE_ONE_DAY(Names.CACHE_ONE_DAY, Duration.ofDays(1));
private final String cacheName;
private final Duration cacheTTL;
public static CacheChannels from(String value) {
return Arrays.stream(values())
.filter(cacheChannel -> cacheChannel.cacheName.equalsIgnoreCase(value))
.findAny()
.orElse(null);
}
public static class Names {
public static final String CACHE_TEN_MIN = "cache10Minutes";
public static final String CACHE_HALF_HR = "cache30Minutes";
public static final String CACHE_ONE_HR = "cache1Hour";
public static final String CACHE_THREE_HR = "cache3Hours";
public static final String CACHE_SIX_HR = "cache6Hours";
public static final String CACHE_ONE_DAY = "cache1Day";
}
}
// Converter
#Slf4j
public class StringToCacheChannelConverter implements Converter<String, CacheChannels> {
#Override
public CacheChannels convert(String source) {
log.info("Convert Target: " + source);
return CacheChannels.from(source);
}
}
// Security Config
#Configuration
#EnableWebSecurity
#Order(1)
public class APISecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${spring.security.auth-token-header-name:Authorization}")
private String apiKeyHeader;
#Value("${spring.security.secret}")
private String privateApiKey;
#Override
protected void configure(HttpSecurity http) throws Exception {
APIKeyAuthFilter filter = new APIKeyAuthFilter(apiKeyHeader);
filter.setAuthenticationManager(new AuthenticationManager() {
#Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String requestedApiKey = (String) authentication.getPrincipal();
if (!privateApiKey.equals(requestedApiKey)) {
throw new BadCredentialsException("The API Key was not found or not the expected value");
}
authentication.setAuthenticated(true);
return authentication;
}
});
http
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(filter)
.authorizeRequests()
.antMatchers("/v1/cache/**")
.authenticated();
}
}
// Filter
#Slf4j
public class APIKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
private String apiKeyHeader;
public APIKeyAuthFilter(String apiKeyHeader) {
this.apiKeyHeader = apiKeyHeader;
}
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest httpServletRequest) {
log.info("Check authenticated.");
return httpServletRequest.getHeader(apiKeyHeader);
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest httpServletRequest) {
return "N/A";
}
}
// Web Config
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToCacheChannelConverter());
}
#Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new HiddenHttpMethodFilter();
}
}
This can be expected the controller was loaded, endpoint was mapped.
I tried change #DeleteMapping to #PostMapping and it was successfully respond against to POST request.
What am I missing?
I found reason why received 404 without any messages.
My tomcat is on remote server. It configured with security-constraint and disable DELETE method for all enpoints.
I just comment out it and It work properly with delete method.

Spring token security with credential security (springboot)

I have a question regarding security implementation on my server. I am making a SpringBoot application which has a control panel like website on it, where 1 single admin inputs needed data and i have managed to secure that part fine like this :
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/*").authorizeRequests().anyRequest().hasRole("ADMIN")
.and().formLogin().loginPage("/login.jsp")
.failureUrl("/login.jsp?error=1").loginProcessingUrl("/login")
.permitAll().and().logout()
.logoutSuccessUrl("/login.jsp");
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// Create a default account
auth.inMemoryAuthentication()
.withUser("admin")
.password("admin")
.roles("ADMIN");
}
Every website url is on /*, and that works fine. The next thing i need to do is to retrieve data from my mobile app and it needs to be secure. urls that the app should use is /rest/**. I have a Student class that stores email(username) and password that is created by that admin on web site. As far as i've read i need token implementation.
How can I implement token authentication?
To implement token based authentication for a mobile app, with Spring Boot and Spring Security.
Create a TokenAuthenticationFilter
public class TokenAuthenticationFilter extends GenericFilterBean {
private AuthenticationManager authenticationManager;
public TokenAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String apiKey = httpRequest.getHeader("API-Key");
String token = httpRequest.getHeader("Access-Token");
try {
if (!StringUtils.isEmpty(apiKey)) {
processTokenAuthentication(apiKey);
}
chain.doFilter(request, response);
} catch (InternalAuthenticationServiceException internalAuthenticationServiceException)
{
SecurityContextHolder.clearContext();
logger.error("Internal authentication service exception", internalAuthenticationServiceException);
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
catch(AuthenticationException authenticationException)
{
SecurityContextHolder.clearContext();
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
}
}
private void processTokenAuthentication(String apiKey) {
SessionCredentials authCredentials = new SessionCredentials(apiKey);
Authentication requestAuthentication = new PreAuthenticatedAuthenticationToken(authCredentials, authCredentials);
Authentication resultOfAuthentication = tryToAuthenticate(requestAuthentication);
SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication);
}
private Authentication tryToAuthenticate(Authentication requestAuthentication) {
Authentication responseAuthentication = authenticationManager.authenticate(requestAuthentication);
if (responseAuthentication == null || !responseAuthentication.isAuthenticated()) {
throw new InternalAuthenticationServiceException("Unable to authenticate Domain User for provided credentials");
}
return responseAuthentication;
}
}
public class TokenAuthenticationProvider implements AuthenticationProvider {
private String apiKey;
public TokenAuthenticationProvider(String apiKey) {
this.apiKey = apiKey;
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SessionCredentials credentials = (SessionCredentials) authentication.getCredentials();
if (credentials != null && credentials.apiKey.equals(this.apiKey)) {
//Also evaluate the token here
Authentication newAuthentication = new PreAuthenticatedAuthenticationToken(apiKey, credentials);
newAuthentication.setAuthenticated(true);
return newAuthentication;
}
throw new BadCredentialsException("Bad credentials given.");
}
#Override
public boolean supports(Class<?> aClass) {
return aClass.equals(PreAuthenticatedAuthenticationToken.class);
}
}
Create Session Credentials Holder
public class SessionCredentials {
String apiKey;
String accessToken;
public SessionCredentials(String apiKey, String accessToken) {
this.apiKey = apiKey;
this.accessToken = accessToken;
}
public String getApiKey() {
return apiKey;
}
public String getAccessToken() {
return accessToken;
}
}
Finally Register These in your Security Config
//Leave whatever you had here
#Override
public void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new TokenAuthenticationFilter(authenticationManager()), BasicAuthenticationFilter.class);
String contentPathDir = String.format("/%s/**", contentPath);
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers("/authorization/**", "/public/**", "/management/**", "/health/**", contentPathDir).permitAll()
.antMatchers("/**").authenticated();
}
//Add these two below.
#Override
public void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(apiKeyAuthenticationProvider());
}
#Bean
public TokenAuthenticationProvider apiKeyAuthenticationProvider() {
return new TokenAuthenticationProvider(apiKey);
}

How to make Shiro return 403 Forbidden with Spring Boot rather than redirect to login.jsp

I have a server that is just an API endpoint, no client front-end, no jsp, no html. It uses Spring Boot and I'm trying to secure it with Shiro. The relevent parts of my SpringBootServletInitializer look like this. I'm trying to get Shiro to return a 403 response if it fails the roles lookup as defined in BasicRealm. Yet it seems to default to redirecting to a non-existent login.jsp and no matter what solution I seem to use. I can't override that. Any help would be greatly appreciated.
#SpringBootApplication
public class RestApplication extends SpringBootServletInitializer {
...
#Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
Map<String, String> filterChain = new HashMap<>();
filterChain.put("/admin/**", "roles[admin]");
shiroFilter.setFilterChainDefinitionMap(filterChain);
shiroFilter.setSecurityManager(securityManager());
return shiroFilter;
}
#Bean
public org.apache.shiro.mgt.SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
CookieRememberMeManager rmm = new CookieRememberMeManager();
rmm.setCipherKey(Base64.decode("XXXXXXXXXXXXXXXXXXXXXX"));
securityManager.setRememberMeManager(rmm);
return securityManager;
}
#Bean(name = "userRealm")
#DependsOn("lifecycleBeanPostProcessor")
public BasicRealm userRealm() {
return new BasicRealm();
}
#Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
public class BasicRealm extends AuthorizingRealm {
private static Logger logger = UserService.logger;
private static final String REALM_NAME = "BASIC";
public BasicRealm() {
super();
}
#Override
protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken token)
throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String userid = upToken.getUsername();
User user = Global.INST.getUserService().getUserById(userid);
if (user == null) {
throw new UnknownAccountException("No account found for user [" + userid + "]");
}
return new SimpleAuthenticationInfo(userid, user.getHashedPass().toCharArray(), REALM_NAME);
}
#Override
protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) {
String userid = (String) principals.getPrimaryPrincipal();
if (userid == null) {
return new SimpleAuthorizationInfo();
}
return new SimpleAuthorizationInfo(Global.INST.getUserService().getRoles(userid));
}
}
OK, here is how I solved it. I created a class ...
public class AuthFilter extends RolesAuthorizationFilter {
private static final String MESSAGE = "Access denied.";
#Override
protected boolean onAccessDenied(final ServletRequest request, final ServletResponse response) throws IOException {
HttpServletResponse httpResponse ;
try {
httpResponse = WebUtils.toHttp(response);
}
catch (ClassCastException ex) {
// Not a HTTP Servlet operation
return super.onAccessDenied(request, response) ;
}
if (MESSAGE == null) {
httpResponse.sendError(403);
} else {
httpResponse.sendError(403, MESSAGE);
}
return false; // No further processing.
}
}
... and then in my shiroFilter() method above I added this code ...
Map<String, Filter> filters = new HashMap<>();
filters.put("roles", new AuthFilter());
shiroFilter.setFilters(filters);
... hope this helps someone else.
In Shiro 1.4+ you can set the login url in your application.properties:
https://github.com/apache/shiro/blob/master/samples/spring-boot-web/src/main/resources/application.properties#L20
Earlier versions you should be able to set ShiroFilterFactoryBean.setLoginUrl("/login")
https://shiro.apache.org/static/current/apidocs/org/apache/shiro/spring/web/ShiroFilterFactoryBean.html

Spring Security: AuthenticationProcessingFilter is called twice

I try configure Spring Security via token authorization in RESTful application.
My AuthenticationFilter looks like:
#Configurable
public class CustomTokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final Logger logger = LoggerFactory.getLogger(CustomTokenAuthenticationFilter.class);
private final static String SECRET_KEY = "ThisIsASecretKey";
public final String HEADER_SECURITY_TOKEN = "X-Token";
#Inject
private Users usres;
public CustomTokenAuthenticationFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(defaultFilterProcessesUrl));
setAuthenticationManager(new NoOpAuthenticationManager());
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException,
ServletException {
String token = request.getHeader(HEADER_SECURITY_TOKEN);
logger.info("token found:" + token);
TokenInfo tokenInfo = new TokenInfo(token, SECRET_KEY);
AbstractAuthenticationToken userAuthenticationToken;
try {
userAuthenticationToken = authUserByToken(tokenInfo);
if (userAuthenticationToken == null)
throw new AuthenticationServiceException(MessageFormat.format("Error | {0}", "Bad Token"));
return userAuthenticationToken;
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
private AbstractAuthenticationToken authUserByToken(TokenInfo token) throws ParseException {
if (token == null) {
return null;
}
UserInfo userInfo = usres.findUser(token.getUsername());
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration().setProvider(new UserProvider());
User userDetails = mapper.map(userInfo, User.class);
AbstractAuthenticationToken authToken = new AuthenticationToken(userDetails);
try {
return authToken;
} catch (Exception e) {
logger.error("Authenticate user by token error: ", e);
}
return authToken;
}
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler() {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
chain.doFilter(request, response);
}
});
super.doFilter(req, res, chain);
}
}
and Spring Security config:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Inject
AuthenticationManager authenticationManager;
#Bean
protected AbstractAuthenticationProcessingFilter getTokenAuthFilter() throws Exception {
CustomTokenAuthenticationFilter tapf = new CustomTokenAuthenticationFilter("/api/secure-module/admin/**");
tapf.setAuthenticationManager(authenticationManager);
return tapf;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.csrf().disable().addFilterBefore(getTokenAuthFilter(), AnonymousAuthenticationFilter.class).exceptionHandling()
.authenticationEntryPoint(new RestAuthenticationEntryPoint());
}
}
It works fine but CustomTokenAuthenticationFilter is called twice and I don't know why. Any ideas?
I found problem, it is #Bean annotation in getTokenAuthFilter method. Then I had 2 registered filter in chain (additionalFilters, originalChain).
I had a similar experience when the Filter was generating an exception causing a redirect to /error which triggered the Filter again. I had to specify
#Override
public void configure(WebSecurity web) throws Exception {
// ignoring security for /error
web.ignoring().antMatchers("/error");
}

Categories