Migrating Spring Security AbstractAuthenticationProcessingFilter to WebFlux - java

I'm updating an old application to use WebFlux but I've gotten a bit lost when it comes to handling JWT validation with Spring Security.
The existing code (which works with standard Spring Web) looks like:
(Validating a Firebase Token)
public class FirebaseAuthenticationTokenFilter extends AbstractAuthenticationProcessingFilter {
private static final String TOKEN_HEADER = "X-Firebase-Auth";
public FirebaseAuthenticationTokenFilter() {
super("/v1/**");
}
#Override
public Authentication attemptAuthentication(
final HttpServletRequest request, final HttpServletResponse response) {
for (final Enumeration<?> e = request.getHeaderNames(); e.hasMoreElements(); ) {
final String nextHeaderName = (String) e.nextElement();
final String headerValue = request.getHeader(nextHeaderName);
}
final String authToken = request.getHeader(TOKEN_HEADER);
if (Strings.isNullOrEmpty(authToken)) {
throw new RuntimeException("Invaild auth token");
}
return getAuthenticationManager().authenticate(new FirebaseAuthenticationToken(authToken));
}
However when switching to WebFlux we lose HttpServletRequest and HttpServletResponse. There is a GitHub issue which suggests there is an alternative method/fix https://github.com/spring-projects/spring-security/issues/5328 however following it through I'm not able to identify what was actually changed to make this work.
The Spring Security docs while great, don't really explain how to handle the use-case.
Any tips on how to proceed?

Got there in the end:
First need to update the filter chain with a custom filter just like before
#Configuration
public class SecurityConfig {
private final FirebaseAuth firebaseAuth;
public SecurityConfig(final FirebaseAuth firebaseAuth) {
this.firebaseAuth = firebaseAuth;
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(final ServerHttpSecurity http) {
http.authorizeExchange()
.and()
.authorizeExchange()
.pathMatchers("/v1/**")
.authenticated()
.and()
.addFilterAt(firebaseAuthenticationFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
.csrf()
.disable();
return http.build();
}
private AuthenticationWebFilter firebaseAuthenticationFilter() {
final AuthenticationWebFilter webFilter =
new AuthenticationWebFilter(new BearerTokenReactiveAuthenticationManager());
webFilter.setServerAuthenticationConverter(new FirebaseAuthenticationConverter(firebaseAuth));
webFilter.setRequiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers("/v1/**"));
return webFilter;
}
}
The main workhorse of the process is FirebaseAuthenticationConverter where I validate the incoming JWT against Firebase, and perform some standard logic against it.
#Slf4j
#Component
#RequiredArgsConstructor
public class FirebaseAuthenticationConverter implements ServerAuthenticationConverter {
private static final String BEARER = "Bearer ";
private static final Predicate<String> matchBearerLength =
authValue -> authValue.length() > BEARER.length();
private static final Function<String, Mono<String>> isolateBearerValue =
authValue -> Mono.justOrEmpty(authValue.substring(BEARER.length()));
private final FirebaseAuth firebaseAuth;
private Mono<FirebaseToken> verifyToken(final String unverifiedToken) {
try {
final ApiFuture<FirebaseToken> task = firebaseAuth.verifyIdTokenAsync(unverifiedToken);
return Mono.justOrEmpty(task.get());
} catch (final Exception e) {
throw new SessionAuthenticationException(e.getMessage());
}
}
private Mono<FirebaseUserDetails> buildUserDetails(final FirebaseToken firebaseToken) {
return Mono.just(
FirebaseUserDetails.builder()
.email(firebaseToken.getEmail())
.picture(firebaseToken.getPicture())
.userId(firebaseToken.getUid())
.username(firebaseToken.getName())
.build());
}
private Mono<Authentication> create(final FirebaseUserDetails userDetails) {
return Mono.justOrEmpty(
new UsernamePasswordAuthenticationToken(
userDetails.getEmail(), null, userDetails.getAuthorities()));
}
#Override
public Mono<Authentication> convert(final ServerWebExchange exchange) {
return Mono.justOrEmpty(exchange)
.flatMap(AuthorizationHeaderPayload::extract)
.filter(matchBearerLength)
.flatMap(isolateBearerValue)
.flatMap(this::verifyToken)
.flatMap(this::buildUserDetails)
.flatMap(this::create);
}
}

To the previous answer there could be added that this method also works fine:
private Mono<FirebaseToken> verifyToken(final String unverifiedToken) {
try {
return Mono.just(FirebaseAuth.getInstance().verifyIdToken(unverifiedToken));
} catch (final Exception e) {
throw new SessionAuthenticationException(e.getMessage());
}
}
And this one does not provid warnings regarding unnecessary use of blocking methods (like get())

Related

Spring Rest Forbidden Access when hit Exception

I'm using Spring Boot 3.0. The authorization just works as expected but when it hit SomeException like MethodArgumentNotValidException it just only show 403 Forbidden Access with empty body. Before I'm using Spring Boot 3.0 everything just work as I'm expected when hitting Exception like they give me the exception JSON result.
SecurityConfiguration
#Configuration
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
var secret = System.getProperty("app.secret");
var authorizationFilter = new AuthorizationFilter(secret);
var authenticationFilter = new AuthenticationFilter(secret, authenticationManager);
authenticationFilter.setFilterProcessesUrl("/login");
authenticationFilter.setPostOnly(true);
return http
.cors().and()
.csrf((csrf) -> csrf.disable())
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/login/**", "/trackers/camera/**").permitAll()
.requestMatchers("/sites/**").hasAnyRole(Role.OWNER.name())
.anyRequest().authenticated()
)
.addFilter(authenticationFilter)
.addFilterBefore(authorizationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
var config = new CorsConfiguration();
var all = Arrays.asList("*");
config.setAllowedOrigins(all);
config.setAllowedHeaders(all);
config.setAllowedMethods(all);
config.setExposedHeaders(all);
var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
AuthenticationFilter
#RequiredArgsConstructor
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final String secretToken;
private final AuthenticationManager authenticationManager;
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username, password;
try {
var requestMap = new ObjectMapper().readValue(request.getInputStream(), LoginRequest.class);
username = requestMap.getUsername();
password = requestMap.getPassword();
} catch (Exception e) {
throw new AuthenticationServiceException(e.getMessage(), e);
}
var token = new UsernamePasswordAuthenticationToken(username, password);
return authenticationManager.authenticate(token);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
var user = (UserDetails) authResult.getPrincipal();
var algorithm = Algorithm.HMAC512(secretToken.getBytes());
var token = JWT.create()
.withSubject(user.getUsername())
.withIssuer(request.getRequestURL().toString())
.withClaim("roles", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
.sign(algorithm);
var jsonMap = new HashMap<String, String>();
jsonMap.put("token", token);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), jsonMap);
response.flushBuffer();
}
}
AuthorizationFilter
#RequiredArgsConstructor
public class AuthorizationFilter extends OncePerRequestFilter {
private final String secretToken;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
var authentication = request.getHeader(HttpHeaders.AUTHORIZATION);
if(authentication != null) {
if(authentication.startsWith("Bearer")) {
var token = authentication.substring("Bearer ".length());
var algorithm = Algorithm.HMAC512(secretToken.getBytes());
var verifier = JWT.require(algorithm).build();
var message = verifier.verify(token);
var subject = message.getSubject();
var roles = message.getClaim("roles").asArray(String.class);
var authorities = new ArrayList<SimpleGrantedAuthority>();
Arrays.stream(roles).forEach(role -> authorities.add(new SimpleGrantedAuthority(role)));
var authenticationToken = new UsernamePasswordAuthenticationToken(subject, token, authorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} else if(authentication.startsWith("Basic")) {
var token = authentication.substring("Basic ".length());
var bundle = new String(Base64.getDecoder().decode(token)).split(":", 2);
if(bundle.length == 2 && bundle[0].equals(System.getProperty("app.camera.username")) && bundle[1].equals(System.getProperty("app.camera.password"))) {
var authenticationToken = new UsernamePasswordAuthenticationToken("camera1", null, Arrays.asList(new SimpleGrantedAuthority(Role.USER.getAuthority())));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(request, response);
}
}
If I understood you correctly, you want the exception-message shown in the return body of the request.
I solved this problem by implementing a (global) exception handler.
(Optional) Create a custom exception, extending some sort of other exception.
public class ApiException extends RuntimeException {
// Not really needed here, as Throwable.java has a message too
// I added it for better readability
#Getter
private String message;
public ApiException() {
super();
}
public ApiException(String message) {
this.message = message;
}
public ApiException(String message, Throwable cause) {
super(message, cause);
}
}
(Optional) A Wrapper, with custom information. (This is the object returned in the body).
// I've used a record, as the wrapper simply has to store data
public record ApiError(String message, HttpStatus status, Throwable cause, ZonedDateTime timestamp) {}
The handler
To create the handler, you simply have to create a custom class, which extends the ResponseEntityExceptionHandler.java
#ControllerAdvice
#Order(Ordered.HIGHEST_PRECEDENCE)
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
// The annotation's value can be replaced by any exception.
// Use Throwable.class to handle **all** exceptions.
// For this example I used the previously created exception.
#ExceptionHandler(value = { ApiException.class })
#ResponseBody
#ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<Object> handleApiRequestException(ApiException e) {
// At this point, you can create the exception wrapper to create a
// formatted JSON-response, but you could also just get the info
// required from the exception and return that.
ApiError error = new ApiError(
e.getMessage(),
HttpStatus.BAD_REQUEST,
null,
ZonedDateTime.now(ZoneId.of("Z"))
);
return new ResponseEntity<>(error, error.status());
}
}
Also: To handle different kinds of exceptions differently, like e.g. you want a ApiException to return a 403 and a FooException to return a 404, just create another method inside of the handler and adjust it to your likings.
I hope this helped!
Cheers

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.

How to rewrite if statment logic to the reactive approach in web security filter?

I am sensibly new to project reactor and web flux, I want to rewrite my current traditional, blocking security filter to the reactive one, namely:
the current filter looks like this:
#Component
#Slf4j
public class WhitelistingFilter extends OncePerRequestFilter {
private static final String SECURITY_PROPERTIES = "security.properties";
private final Properties securityProperties = readConfigurationFile(SECURITY_PROPERTIES);
private final String whitelistingEnabled = securityProperties.getProperty("whitelisting.enabled", FALSE.toString());
private final RedisTemplate<String, Object> whitelistingRedisTemplate;
private final AwsCognitoIdTokenProcessor awsCognitoIdTokenProcessor;
public WhitelistingFilter(
#Qualifier("whitelistingRedisTemplate")
RedisTemplate<String, Object> whitelistingRedisTemplate,
AwsCognitoIdTokenProcessor awsCognitoIdTokenProcessor) {
this.whitelistingRedisTemplate = whitelistingRedisTemplate;
this.awsCognitoIdTokenProcessor = awsCognitoIdTokenProcessor;
}
#Override
protected boolean shouldNotFilter(#NonNull HttpServletRequest request) {
AntPathMatcher pathMatcher = new AntPathMatcher();
return Stream.of(USER_LOGIN_URL, ADMIN_LOGIN_URL, SIGNUP_BY_ADMIN_URL, SIGNUP_URL, LOGOUT_URL)
.anyMatch(p -> pathMatcher.match(p, request.getServletPath())) || whitelistingDisabled();
}
private boolean whitelistingDisabled() {
return FALSE.toString().equalsIgnoreCase(whitelistingEnabled);
}
#Override
protected void doFilterInternal(#NonNull HttpServletRequest httpServletRequest, #NonNull HttpServletResponse httpServletResponse, #NonNull FilterChain filterChain) {
try {
Authentication authentication = awsCognitoIdTokenProcessor.getAuthentication(httpServletRequest);
Optional<String> username = Optional.ofNullable(authentication.getName());
if (username.isPresent() && usernameWhitelisted(username.get())) {
log.info("User with username: {} is present in whitelisting", username.get());
filterChain.doFilter(httpServletRequest, httpServletResponse);
} else {
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
log.error("Username: {} not whitelisted or empty", username.orElse(""));
}
} catch (Exception e) {
logger.error("Error occurred while checking user in redis whitelisting", e);
SecurityContextHolder.clearContext();
}
}
private boolean usernameWhitelisted(String username) {
return Boolean.TRUE.equals(whitelistingRedisTemplate.hasKey(WHITELISTING_PREFIX + username));
}
}
New, incomplete, reactive approach class looks like this:
#Component
#Slf4j
public class WhitelistingFilter implements WebFilter {
private static final String SECURITY_PROPERTIES = "security.properties";
public final List<String> whitelistedUrls =
List.of(USER_LOGIN_URL, ADMIN_LOGIN_URL, SIGNUP_BY_ADMIN_URL, SIGNUP_URL, LOGOUT_URL);
private final Properties securityProperties = readConfigurationFile(SECURITY_PROPERTIES);
private final String whitelistingEnabled = securityProperties.getProperty("whitelisting.enabled", FALSE.toString());
private final ReactiveRedisOperations<String, Object> whitelistingRedisTemplate;
private final AuthenticationManager authenticationManager;
public WhitelistingFilter(
#Qualifier("reactiveWhitelistingRedisTemplate")
ReactiveRedisOperations<String, Object> whitelistingRedisTemplate,
AuthenticationManager authenticationManager) {
this.whitelistingRedisTemplate = whitelistingRedisTemplate;
this.authenticationManager = authenticationManager;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
Mono<String> username =
ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.map(Authentication::getName);
//logic here
}
private Mono<Boolean> whitelistingDisabled() {
return Mono.just(FALSE.toString().equalsIgnoreCase(whitelistingEnabled));
}
private Mono<Boolean> usernameWhitelisted(Mono<String> username) {
return whitelistingRedisTemplate.hasKey(WHITELISTING_PREFIX + username);
}
}
I changed the usernameWhitelisted() and whitelistingDisabled() methods to return Mono's but I cannot figure out how to verify if the username is whitelisted and whitelisting enabled in reactive approach. I tried to do sth
username.flatMap(u -> {
if(two conditions here)
})
but with this approach, I am providing Mono to the if statment which is contradictory with Java semantic. I will be grateful for suggestions on how to rewrite the code and make it works in a reactive approach.
Having a reactive stream or pipe doesn't mean that everything needs to be reactive, you can just leave the mono out of the boolean operators
As you return a Mono of type void and since you need (?) logging, I guess you could just throw a custom exception in here as well and catch it on top op the stream/pipe by using the doOnError and in there check for the exception type and have according logic (pass a msg to the exception should you need one)
ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.map(Authentication::getName)
.doOnNext(this::doBla)
public void doBla(String username) {
if (!whitelistingDisabled() || !usernameWhitelisted(username)) {
throw new TypeException(ms);
}
}
FYI doOnNext is only being executed if you reach that part, its kind of like a peek in a java stream.
The doOnError its functionality could also be extracted, a stream is easier to read to keep it simple and not add too much into it
myStream
....otherOperators
.filter(bla)
...
.doOnError(e -> {
if ( e instanceOf TypeException) {
// do stuff
}
}

Can I generate a JWT for my Spring Security app without have a user?

I want to generate a JWT with expiration date for people to access the system without have to register and create a user. Is this posible? I have tried with JwtTokenProvider but it needs a LoginRequest to work also with Jwts.builder() also needs a user.
if you want to use spring security you can create security configration and extends WebSecurityConfigurerAdapter. Then important point is custom provider.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
#Autowired
private JWTConfigurer securityConfigurerAdapter;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//you can write customAuth provider
auth.authenticationProvider(customAuthenticationProvider);
}
#Override
public void configure(WebSecurity web) throws Exception {
//Some ignore etc.
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.csrf()
.disable().and()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//important here
.antMatchers("/api/v1/authentication/**").permitAll()
.anyRequest().authenticated()
.and()
.apply(securityConfigurerAdapter);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return this.authenticationManager();
}
}
This is Filter class which extends genericFilterBean. Every request is monitored in this class
You will check to it is right token
I create token TokenProvider class and depend into JWTFilter then use valideToken method.
if token is sended and not validate then throw exception
if token is not sended then go super method so the flow is continue and works auth.authenticationProvider. Spring knows to start customAuthenticationProvider behind the scene becouse of you set into SecurityConfiguration class
#Component
public class JWTFilter extends GenericFilterBean {
private final Logger log = LoggerFactory.getLogger(JWTFilter.class);
#Autowired
private TokenProvider tokenProvider;
#Autowired
private MessageSource msgSource;
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
//Resolve method is optional what you want to use
String jwt = resolveToken(httpServletRequest);
if (StringUtils.hasText(jwt)) {
//token validation is important becouse of expires date into token
// and you will check expired date
if (this.tokenProvider.validateToken(jwt)) {
String jwtMd5 = DigestUtils.md5Hex(jwt);
MDC.put("jwt",jwtMd5);
Authentication authentication = this.tokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(servletRequest, servletResponse);
}catch(Exception ex){
handleException((HttpServletResponse) servletResponse,ex);
}
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(JWTConfigurer.AUTHENTICATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
String jwt = bearerToken.substring(7, bearerToken.length());
return jwt;
}
String jwt = request.getParameter(JWTConfigurer.AUTHENTICATION_TOKEN);
if (StringUtils.hasText(jwt)) {
return jwt;
}
return null;
}
}
You can use this class for create token or validate token
you define expire date for token expiration into create method.
#Component public class TokenProvider {
private final Logger log = LoggerFactory.getLogger(TokenProvider.class);
private static final String AUTHORITIES_KEY = "auth";
private static final String WTS_USER_ID = "wtsUserId";
private static final String CHANNEL_PERMISSIONS = "channelPermissions";
private static final String APP_ROLES = "appRoles";
private String secretKey;
private long tokenValidityInSeconds;
#Autowired private ApplicationProperties applicationProperties;
#PostConstruct public void init() {
this.tokenValidityInSeconds = 1000;
}
public String createToken(Authentication authentication, Boolean rememberMe) { List<String> authorities = authentication.getAuthorities().stream().map(authority -> authority.getAuthority())
.collect(Collectors.toList());
//Token creation format is this
// token will be three part important parts are claims and sign
// claims refers to body to use datas
// sign will use to validation
return Jwts.builder().setSubject(authentication.getName()).claim(AUTHORITIES_KEY, authorities)
.claim(WTS_USER_ID, ((JWTAuthentication) authentication).getWtsUserId())
.claim(CHANNEL_PERMISSIONS, ((JWTAuthentication) authentication).getChannelPermissions())
.claim(APP_ROLES, ((JWTAuthentication) authentication).getAppRoles())
.signWith(SignatureAlgorithm.HS512, secretKey).setExpiration(tokenValidityInSeconds).compact(); }
#SuppressWarnings("unchecked") public Authentication getAuthentication(String token) { Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
List<String> list = (List<String>) claims.get(AUTHORITIES_KEY); Collection<? extends GrantedAuthority> authorities = list.stream()
.map(authority -> new SimpleGrantedAuthority(authority)).collect(Collectors.toList()); Integer wtsUserId = (Integer) claims.get(WTS_USER_ID); List<String> appRoles = (List<String>) claims.get(APP_ROLES);
ObjectMapper objectMapper = new ObjectMapper(); List<ChannelPermission> channelPermissions = objectMapper.convertValue(claims.get(CHANNEL_PERMISSIONS),
new TypeReference<List<ChannelPermission>>() {
});
return new JWTAuthentication(token, wtsUserId, claims.getSubject(), authorities, channelPermissions, appRoles); }
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
log.info("Invalid JWT signature: " + e.getMessage());
return false;
} } }
This is controller who anonymous people get a JWT token .You can give a new JWT token all request and this JWT has expires date becouse of you set a expiration date into provider class.
#RequestMapping(value = "/login", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse login(#RequestBody #Validated AuthenticationRequestDTO authenticationRequest) {
Authentication authentication = this.authenticationManager.authenticate(new JWTAuthentication(
RandomUid, RandomPwd, "anonymous"));
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = tokenProvider.createToken(authentication, false);
return new ApiResponse(ApiResponseStatus.SUCCESS, new AuthenticationResponseDTO(token));
}

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

Categories