I'm currently upgrading from Spring Boot 2.7 to Spring 3 but I'm having issues when upgrading my HTTP Security Config.
I am using Azure B2C for my oauth server and that part is working fine as I can see the valid JWT being returned.
I've changed from using authorizeRequests to authorizeHttpRequests as suggested but all my HTTP requests are returning 403.
If I use authorizeRequests with all of the other Spring 3 suggested updates it still works as expected.
Configuration File:
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Autowired
private AuthenticationProvider authenticationProvider;
#Autowired
#Qualifier("preAuthProvider")
private AuthenticationProvider preAuthProvider;
#Value("${oauthTokenChecks.audience}")
private String audience;
#Value("${oauthTokenChecks.issuer}")
private String issuer;
#Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
private String jwkUri;
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
configuration.setAllowedHeaders(Collections.singletonList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().
configurationSource(corsConfigurationSource()).and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests((authorize) -> authorize
.requestMatchers("/api/**")
.authenticated()
)
.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
.csrf()
.disable()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(new Auth0TokenConverter());
return http.build();
}
#Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.authenticationProvider(authenticationProvider);
authenticationManagerBuilder.authenticationProvider(preAuthProvider);
return authenticationManagerBuilder.build();
}
#Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder =
NimbusJwtDecoder.withJwkSetUri(jwkUri).build();
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
OAuth2TokenValidator<Jwt> withAudience =
new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
// jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
}
Auth Converter:
public class Auth0TokenConverter implements Converter<Jwt, AbstractAuthenticationToken> {
#Override
public AbstractAuthenticationToken convert(final Jwt jwt) {
var emails = jwt.getClaimAsStringList("emails");
if (emails == null || emails.size() != 1) {
throw new RuntimeException("One and only one email expected in token claim");
}
var user =
ImplementUser.builder()
.username(emails.get(0))
.auth0Id(null)
.enabled(true)
.accountNonExpired(true)
.accountNonLocked(true)
.credentialsNonExpired(true)
.build();
return new PreAuthenticatedAuthenticationToken(user, jwt);
}
}
CustomPreAuthProvider:
#Component("preAuthProvider")
public class CustomPreAuthProvider extends PreAuthenticatedAuthenticationProvider {
#Autowired
private CustomPreAuthUserDetailsService userService;
public CustomPreAuthProvider() {
super();
}
#PostConstruct
public void init() {
super.setPreAuthenticatedUserDetailsService(userService);
}
}
CustomPreAuthUserDetailsService:
#Service
#RequiredArgsConstructor(onConstructor = #__(#Autowired))
public class CustomPreAuthUserDetailsService
implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
private final AuthUserDetailsService authUserDetailsService;
#Override
public final UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) {
var principal = (ImplementUser) token.getPrincipal();
var email = principal.getUsername();
var user = authUserDetailsService.loadUserDetails(email);
var implementUser = (ImplementUser) token.getPrincipal();
return implementUser.toBuilder()
.firstName(user.getFirstName())
.lastName(user.getLastName())
.globalUserId(user.getGlobalUserId())
.profiles(user.getProfiles())
.activeProfile(user.getActiveProfile())
.defaultProfileId(user.getDefaultProfileId())
.build();
}
}
Below are the error logs
Log output
Related
I am while implementation of AWS Cognito security mechanism in my Spring Boot application. I met a problem with already existing integration test for external API after enabling security.
As a test result I am receiving an error:
2020-11-15 18:18:20.033 ERROR 12072 --- [ main]
.c.s.f.AwsCognitoJwtAuthenticationFilter : Invalid Action, no token
found MockHttpServletResponse:
Status = 401
Error message = null
Headers = [Access-Control-Allow-Origin:"*", Access-Control-Allow-Methods:"POST, GET, OPTIONS, PUT, DELETE",
Access-Control-Max-Age:"3600",
Access-Control-Allow-Credentials:"true",
Access-Control-Allow-Headers:"content-type,Authorization",
Content-Type:"application/json"]
Content type = application/json
Body = {"data":null,"exception":{"message":"JWT Handle exception","httpStatusCode":"INTERNAL_SERVER_ERROR","detail":null}}
My WebSecurityConfiguration looks like:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableTransactionManagement
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
private CustomAuthenticationProvider authProvider;
private AwsCognitoJwtAuthenticationFilter awsCognitoJwtAuthenticationFilter;
private AccountControllerExceptionHandler exceptionHandler;
private static final String LOGIN_URL = "/auth/login";
private static final String LOGOUT_URL = "/auth/signOut";
#Autowired
public WebSecurityConfiguration(
CustomAuthenticationProvider authProvider,
AwsCognitoJwtAuthenticationFilter awsCognitoJwtAuthenticationFilter,
AccountControllerExceptionHandler exceptionHandler) {
this.authProvider = authProvider;
this.awsCognitoJwtAuthenticationFilter = awsCognitoJwtAuthenticationFilter;
this.exceptionHandler = exceptionHandler;
}
public WebSecurityConfiguration() {
super(true);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authProvider).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(GET, "/**");
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()
.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();
}
}
AwsCognitoJwtAuthenticationFilter
#Slf4j
public class AwsCognitoJwtAuthenticationFilter extends OncePerRequestFilter {
private static final String ERROR_OCCURRED_WHILE_PROCESSING_THE_TOKEN =
"Error occured while processing the token";
private static final String INVALID_TOKEN_MESSAGE = "Invalid Token";
private final AwsCognitoIdTokenProcessor awsCognitoIdTokenProcessor;
#Autowired private ApplicationContext appContext;
public AwsCognitoJwtAuthenticationFilter(AwsCognitoIdTokenProcessor awsCognitoIdTokenProcessor) {
this.awsCognitoIdTokenProcessor = awsCognitoIdTokenProcessor;
}
private void createExceptionResponse(
ServletRequest request, ServletResponse response, CognitoException exception)
throws IOException {
HttpServletRequest req = (HttpServletRequest) request;
ExceptionController exceptionController;
ObjectMapper objMapper = new ObjectMapper();
exceptionController = appContext.getBean(ExceptionController.class);
ResponseData<Object> responseData = exceptionController.handleJwtException(req, exception);
HttpServletResponse httpResponse = CorsHelper.addResponseHeaders(response);
final HttpServletResponseWrapper wrapper = new HttpServletResponseWrapper(httpResponse);
wrapper.setStatus(HttpStatus.UNAUTHORIZED.value());
wrapper.setContentType(APPLICATION_JSON_VALUE);
wrapper.getWriter().println(objMapper.writeValueAsString(responseData));
wrapper.getWriter().flush();
}
#Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Authentication authentication;
try {
authentication = awsCognitoIdTokenProcessor.getAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (BadJOSEException e) {
SecurityContextHolder.clearContext();
log.error(e.getMessage());
createExceptionResponse(
request,
response,
new CognitoException(
INVALID_TOKEN_MESSAGE,
CognitoException.INVALID_TOKEN_EXCEPTION_CODE,
e.getMessage()));
return;
} catch (CognitoException e) {
SecurityContextHolder.clearContext();
log.error(e.getMessage());
createExceptionResponse(
request,
response,
new CognitoException(
e.getErrorMessage(),
CognitoException.INVALID_TOKEN_EXCEPTION_CODE,
e.getDetailErrorMessage()));
return;
} catch (Exception e) {
SecurityContextHolder.clearContext();
log.error(e.getMessage());
createExceptionResponse(
request,
response,
new CognitoException(
ERROR_OCCURRED_WHILE_PROCESSING_THE_TOKEN,
CognitoException.INVALID_TOKEN_EXCEPTION_CODE,
e.getMessage()));
return;
}
filterChain.doFilter(request, response);
}
}
AwsCognitoIdTokenProcessor
#AllArgsConstructor
#NoArgsConstructor
public class AwsCognitoIdTokenProcessor {
private static final String INVALID_TOKEN = "Invalid Token";
private static final String NO_TOKEN_FOUND = "Invalid Action, no token found";
private static final String ROLE_PREFIX = "ROLE_";
private static final String EMPTY_STRING = "";
private ConfigurableJWTProcessor<SecurityContext> configurableJWTProcessor;
private AWSConfig jwtConfiguration;
private String extractAndDecodeJwt(String token) {
String tokenResult = token;
if (token != null && token.startsWith("Bearer ")) {
tokenResult = token.substring("Bearer ".length());
}
return tokenResult;
}
#SuppressWarnings("unchecked")
public Authentication getAuthentication(HttpServletRequest request)
throws ParseException, BadJOSEException, JOSEException {
String idToken = request.getHeader(HTTP_HEADER);
if (idToken == null) {
throw new CognitoException(
NO_TOKEN_FOUND,
NO_TOKEN_PROVIDED_EXCEPTION,
"No token found in Http Authorization Header");
} else {
idToken = extractAndDecodeJwt(idToken);
JWTClaimsSet claimsSet;
claimsSet = configurableJWTProcessor.process(idToken, null);
if (!isIssuedCorrectly(claimsSet)) {
throw new CognitoException(
INVALID_TOKEN,
INVALID_TOKEN_EXCEPTION_CODE,
String.format(
"Issuer %s in JWT token doesn't match cognito idp %s",
claimsSet.getIssuer(), jwtConfiguration.getCognitoIdentityPoolUrl()));
}
if (!isIdToken(claimsSet)) {
throw new CognitoException(
INVALID_TOKEN, NOT_A_TOKEN_EXCEPTION, "JWT Token doesn't seem to be an ID Token");
}
String username = claimsSet.getClaims().get(USER_NAME_FIELD).toString();
List<String> groups = (List<String>) claimsSet.getClaims().get(COGNITO_GROUPS);
List<GrantedAuthority> grantedAuthorities =
convertList(
groups, group -> new SimpleGrantedAuthority(ROLE_PREFIX + group.toUpperCase()));
User user = new User(username, EMPTY_STRING, grantedAuthorities);
return new CognitoJwtAuthentication(user, claimsSet, grantedAuthorities);
}
}
private boolean isIssuedCorrectly(JWTClaimsSet claimsSet) {
return claimsSet.getIssuer().equals(jwtConfiguration.getCognitoIdentityPoolUrl());
}
private boolean isIdToken(JWTClaimsSet claimsSet) {
return claimsSet.getClaim("token_use").equals("id");
}
private static <T, U> List<U> convertList(List<T> from, Function<T, U> func) {
return from.stream().map(func).collect(Collectors.toList());
}
}
CognitoJwtAutoConfiguration
#Configuration
#Import(AWSConfig.class)
#ConditionalOnClass({AwsCognitoJwtAuthenticationFilter.class, AwsCognitoIdTokenProcessor.class})
public class CognitoJwtAutoConfiguration {
private final AWSConfig jwtConfiguration;
public CognitoJwtAutoConfiguration(AWSConfig jwtConfiguration) {
this.jwtConfiguration = jwtConfiguration;
}
#Bean
#Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public CognitoJwtIdTokenCredentialsHolder awsCognitoCredentialsHolder() {
return new CognitoJwtIdTokenCredentialsHolder();
}
#Bean
public AwsCognitoIdTokenProcessor awsCognitoIdTokenProcessor() {
return new AwsCognitoIdTokenProcessor();
}
#Bean
public CognitoJwtAuthenticationProvider jwtAuthenticationProvider() {
return new CognitoJwtAuthenticationProvider();
}
#Bean
public AwsCognitoJwtAuthenticationFilter awsCognitoJwtAuthenticationFilter() {
return new AwsCognitoJwtAuthenticationFilter(awsCognitoIdTokenProcessor());
}
#SuppressWarnings({"rawtypes", "unchecked"})
#Bean
public ConfigurableJWTProcessor configurableJWTProcessor() throws MalformedURLException {
ResourceRetriever resourceRetriever =
new DefaultResourceRetriever(CONNECTION_TIMEOUT, READ_TIMEOUT);
// https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json.
URL jwkSetURL = new URL(jwtConfiguration.getJwkUrl());
// Creates the JSON Web Key (JWK)
JWKSource keySource = new RemoteJWKSet(jwkSetURL, resourceRetriever);
ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor();
JWSKeySelector keySelector = new JWSVerificationKeySelector(RS256, keySource);
jwtProcessor.setJWSKeySelector(keySelector);
return jwtProcessor;
}
#Bean
public AWSCognitoIdentityProvider awsCognitoIdentityProvider() {
return AWSCognitoIdentityProviderClientBuilder.standard()
.withRegion(Regions.EU_CENTRAL_1)
.withCredentials(getCredentialsProvider())
.build();
}
#Bean
public AWSCredentialsProvider getCredentialsProvider() {
return new ClasspathPropertiesFileCredentialsProvider();
}
}
I want to exclude my controller URL from being considered as an endpoint which requires authorization.
Based on sight tested controller looks like:
#RestController
#RequestMapping("/nutrition/api/")
class NutritionixApiController {
private ProductFacadeImpl productFacadeImpl;
public NutritionixApiController(
ProductFacadeImpl productFacadeImpl) {
this.productFacadeImpl = productFacadeImpl;
}
#GetMapping("/productDetails")
public ResponseEntity<Set<RecipeIngredient>> productsDetails(#RequestParam String query) {
//logic here
}
}
I have tried to whitelist URL "/nutrition/api/**" in method configure(WebSecurity web)
aby adding:
web.ignoring().antMatchers(GET, "/nutrition/api/**");
or
web.ignoring().antMatchers(GET, "/**");
but without desirable effect. I am a little bit confused about why ignoring.antMatchers() not working so I will be grateful for suggestions on how to fix the above problem.
EDIT
I came back to the topic but with the same result. In WebSecurityConfiguration I commented out #EnableGlobalMethodSecurity(prePostEnabled = true) to try configuration without prePostEnabled = true but without desirable effect. I have the same problem with endpoint /auth which is ignored in the configuration.
I patterned after tutorial which is working and available here click
but I refactored my code a little to get rid of field injection with #Autowired but without doing radical changes and logic under the hood.
Moreover class CustomAuthenticationProvider looks like:
#Component
#RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final CognitoAuthenticationService cognitoService;
#SuppressWarnings("unchecked")
#Override
public Authentication authenticate(Authentication authentication) {
AuthenticationRequest authenticationRequest;
if (authentication != null) {
authenticationRequest = new AuthenticationRequest();
Map<String, String> credentials = (Map<String, String>) authentication.getCredentials();
authenticationRequest.setNewPassword(credentials.get(NEW_PASS_WORD_KEY));
authenticationRequest.setPassword(credentials.get(PASS_WORD_KEY));
authenticationRequest.setUsername(authentication.getName());
SpringSecurityUser userAuthenticated = cognitoService.authenticate(authenticationRequest);
if (userAuthenticated != null) {
Map<String, String> authenticatedCredentials = new HashMap<>();
authenticatedCredentials.put(ACCESS_TOKEN_KEY, userAuthenticated.getAccessToken());
authenticatedCredentials.put(EXPIRES_IN_KEY, userAuthenticated.getExpiresIn().toString());
authenticatedCredentials.put(ID_TOKEN_KEY, userAuthenticated.getIdToken());
authenticatedCredentials.put(PASS_WORD_KEY, userAuthenticated.getPassword());
authenticatedCredentials.put(REFRESH_TOKEN_KEY, userAuthenticated.getRefreshToken());
authenticatedCredentials.put(TOKEN_TYPE_KEY, userAuthenticated.getTokenType());
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);
}
}
To be honest I don't know what can be done more to solve this problem with not working filter. Will be grateful for help.
Although you indicated the right ignoring pattern and Spring Security is actually ignoring the filter, I think it is being still executed because probably Spring is registering again the filter outside of the security chain because you exposed the filter with #Bean in CognitoJwtAutoConfiguration.
To avoid the problem, perform the following modifications in your code (basically, be sure that only one instance of your filter is in place). First, in WebSecurityConfiguration:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableTransactionManagement
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
private CustomAuthenticationProvider authProvider;
private AccountControllerExceptionHandler exceptionHandler;
private static final String LOGIN_URL = "/auth/login";
private static final String LOGOUT_URL = "/auth/signOut";
#Autowired
public WebSecurityConfiguration(
CustomAuthenticationProvider authProvider,
AccountControllerExceptionHandler exceptionHandler) {
// Do not provide AwsCognitoJwtAuthenticationFilter() as instance filed any more
this.authProvider = authProvider;
this.exceptionHandler = exceptionHandler;
}
public WebSecurityConfiguration() {
super(true);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authProvider).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(GET, "/**");
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();
}
// It will also be possible to inject AwsCognitoIdTokenProcessor
private AwsCognitoJwtAuthenticationFilter awsCognitoJwtAuthenticationFilter() {
return new AwsCognitoJwtAuthenticationFilter(new AwsCognitoIdTokenProcessor());
}
}
You also need to remove the unnecessary stuff from CognitoJwtAutoConfiguration:
#Configuration
#Import(AWSConfig.class)
#ConditionalOnClass({AwsCognitoJwtAuthenticationFilter.class, AwsCognitoIdTokenProcessor.class})
public class CognitoJwtAutoConfiguration {
private final AWSConfig jwtConfiguration;
public CognitoJwtAutoConfiguration(AWSConfig jwtConfiguration) {
this.jwtConfiguration = jwtConfiguration;
}
#Bean
#Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public CognitoJwtIdTokenCredentialsHolder awsCognitoCredentialsHolder() {
return new CognitoJwtIdTokenCredentialsHolder();
}
/* No longer needed
#Bean
public AwsCognitoIdTokenProcessor awsCognitoIdTokenProcessor() {
return new AwsCognitoIdTokenProcessor();
}*/
#Bean
public CognitoJwtAuthenticationProvider jwtAuthenticationProvider() {
return new CognitoJwtAuthenticationProvider();
}
/* No longer needed
#Bean
public AwsCognitoJwtAuthenticationFilter awsCognitoJwtAuthenticationFilter() {
return new AwsCognitoJwtAuthenticationFilter(awsCognitoIdTokenProcessor());
}*/
#SuppressWarnings({"rawtypes", "unchecked"})
#Bean
public ConfigurableJWTProcessor configurableJWTProcessor() throws MalformedURLException {
ResourceRetriever resourceRetriever =
new DefaultResourceRetriever(CONNECTION_TIMEOUT, READ_TIMEOUT);
// https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json.
URL jwkSetURL = new URL(jwtConfiguration.getJwkUrl());
// Creates the JSON Web Key (JWK)
JWKSource keySource = new RemoteJWKSet(jwkSetURL, resourceRetriever);
ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor();
JWSKeySelector keySelector = new JWSVerificationKeySelector(RS256, keySource);
jwtProcessor.setJWSKeySelector(keySelector);
return jwtProcessor;
}
#Bean
public AWSCognitoIdentityProvider awsCognitoIdentityProvider() {
return AWSCognitoIdentityProviderClientBuilder.standard()
.withRegion(Regions.EU_CENTRAL_1)
.withCredentials(getCredentialsProvider())
.build();
}
#Bean
public AWSCredentialsProvider getCredentialsProvider() {
return new ClasspathPropertiesFileCredentialsProvider();
}
}
I think this SO question also could be of help.
I am trying to configure Spring Boot OAuth2 with keycloak. Code is working fine when I am using properties from application.properties file. Current config is as follow:
rest.security.issuer-uri=http://172.30.30.172:8080/auth/realms/<REALM_NAME>
security.oauth2.resource.id=test
security.oauth2.resource.token-info-uri=${rest.security.issuer-uri}/protocol/openid-connect/token/introspect
security.oauth2.resource.user-info-uri=${rest.security.issuer-uri}/protocol/openid-connect/userinfo
security.oauth2.resource.jwt.key-value=<PUBLIC KEY>
security.oauth2.client.client-id=<CLIENT ID>
security.oauth2.client.client-secret=<CLIENT SECRET>
security.oauth2.client.user-authorization-uri=${rest.security.issuer-uri}/protocol/openid-connect/auth
security.oauth2.client.access-token-uri=${rest.security.issuer-uri}/protocol/openid-connect/token
security.oauth2.client.scope=openid
security.oauth2.client.grant-type=client_credentials
I want to configure the client config settings in a dynammic manner by fetching the client config from database for each client (its own realm) request. What should be the Java Spring Boot Security configuration for setting the client properties in dynamic manner.
Current Security configuration is as follow:
#Configuration
#EnableWebSecurity
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true)
#ConditionalOnProperty(prefix = "rest.security", value = "enabled", havingValue = "true")
#Import({SecurityProperties.class})
public class SecurityConfigurer extends ResourceServerConfigurerAdapter{
private ResourceServerProperties resourceServerProperties;
private SecurityProperties securityProperties;
/* Using spring constructor injection, #Autowired is implicit */
public SecurityConfigurer(ResourceServerProperties resourceServerProperties, SecurityProperties securityProperties) {
this.resourceServerProperties = resourceServerProperties;
this.securityProperties = securityProperties;
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(resourceServerProperties.getResourceId());
}
#Override
public void configure(final HttpSecurity http) throws Exception {
http.cors()
.configurationSource(corsConfigurationSource())
.and()
.headers()
.frameOptions()
.disable()
.and()
.csrf()
.disable()
.authorizeRequests()
.antMatchers(securityProperties.getApiMatcher())
.authenticated();
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
if (null != securityProperties.getCorsConfiguration()) {
source.registerCorsConfiguration("/**", securityProperties.getCorsConfiguration());
}
return source;
}
#Bean
public JwtAccessTokenCustomizer jwtAccessTokenCustomizer(ObjectMapper mapper) {
return new JwtAccessTokenCustomizer(mapper);
}
}
I tried to override bean ResourceServerProperties, using following code. However its throwing NoUniqueBeanDefinitionException exception.
#Configuration
#Import({ResourceServerProperties.class})
public class ResourceSecurityProperties {
#Primary
#Bean
ResourceServerProperties resourceServerProperties(){
ResourceServerProperties resourceServerProperties= new ResourceServerProperties("<CLIENT-ID>", "<CLIENT-SECRET>");
ResourceServerProperties.Jwt jwt= resourceServerProperties.new Jwt();
resourceServerProperties.setId("test001001");
resourceServerProperties.setTokenInfoUri("http://172.30.30.172:8080/auth/realms/conf/protocol/openid-connect/token/introspect");
resourceServerProperties.setUserInfoUri("http://172.30.30.172:8080/auth/realms/conf/protocol/openid-connect/userinfo");
jwt.setKeyValue("<PUBLIC KEY>");
resourceServerProperties.setJwt(jwt);
return resourceServerProperties;
}
}
I try to generate a JWT Token with Spring Security but I don't how to do it correctly (with the best practice).
Do I should "intercept" the authenticate method within UsernamePasswordAuthenticationFilter somewhere and generate token inside it ?
Or it is better to use AuthenticationManager autowired in the controller '/login' ?
I'm afraid to authenticate the user twice if I use the controller mechanism.
I used this tutorial : tutorial Jwt Token
Here is my code :
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
UserService userService;
#Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
// #Autowired
// private UserDetailsService jwtUserDetailsService;
#Autowired
private JwtTokenFilter jwtTokenFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.and()
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.cors().and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/css/**", "/login/**", "/register/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
//.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
.formLogin()
.usernameParameter("email")
//.loginPage("http://localhost:4200/login").failureUrl("/login-error")
.and()
.logout()
.permitAll();
http
.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
#Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Bean
public CustomDaoAuthenticationProvider authenticationProvider() {
CustomDaoAuthenticationProvider authenticationProvider = new CustomDaoAuthenticationProvider();
authenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
authenticationProvider.setUserDetailsService(userService);
return authenticationProvider;
}
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebConfig() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(
"http://localhost:4200")
.allowedMethods("GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS")
.allowedHeaders("Content-Type", "X-Requested-With", "accept", "Origin", "Access-Control-Request-Method",
"Access-Control-Request-Headers", "Authorization", "Cache-Control",
"Access-Control-Allow-Origin")
.exposedHeaders("Access-Control-Allow-Origin", "Access-Control-Allow-Credentials")
.allowCredentials(true).maxAge(3600);
}
};
}
}
Token Filter
public class JwtTokenFilter extends GenericFilterBean {
private JwtTokenProvider jwtTokenProvider;
public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException {
String token = jwtTokenProvider.resolveToken((HttpServletRequest) req);
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = token != null ? jwtTokenProvider.getAuthentication(token) : null;
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(req, res);
}
}
Token Provider
#Component
public class JwtTokenProvider {
#Value("${security.jwt.token.secret-key:secret}")
private String secretKey = "secret";
#Value("${security.jwt.token.expire-length:3600000}")
private long validityInMilliseconds = 3600000; // 1h
#Autowired
private UserDetailsService userDetailsService;
#PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
public String createToken(String username, List<String> roles) {
Claims claims = Jwts.claims().setSubject(username);
claims.put("roles", roles);
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()//
.setClaims(claims)//
.setIssuedAt(now)//
.setExpiration(validity)//
.signWith(SignatureAlgorithm.HS256, secretKey)//
.compact();
}
public Authentication getAuthentication(String token) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(getUsername(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
public String getUsername(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
public String resolveToken(HttpServletRequest req) {
String bearerToken = req.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
public boolean validateToken(String token) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
if (claims.getBody().getExpiration().before(new Date())) {
return false;
}
return true;
} catch (JwtException | IllegalArgumentException e) {
throw new InvalidJwtAuthenticationException("Expired or invalid JWT token");
}
}
}
Given your context, the controller is responsible for issuing a new token (after validating credentials) while the filter is responsible for authenticating the user against the given token. The controller should not populate the security context (authenticate user), it is the filter's responsibility.
To better understand the two phases:
Spring uses two filters to authenticate and log in a user.
See UsernamePasswordAuthenticationFilter and SecurityContextPersistenceFilter in a "username/password" scenario, from the Spring Security project: the first one processes an authentication attempt (username/password) while the latter populates the security context from a SecurityContextRepository (from a session in general).
Despite lot of subject, i cant figure out how to authenticate with my angular project to my back with spring boot so i try to post with my setup.
So far, all my authentification is handle by spring boot and work
#Configuration
#EnableOAuth2Sso
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout().clearAuthentication(true)
.logoutSuccessUrl("/")
.permitAll();
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://localhost:4200"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
configuration.setAllowedHeaders(Arrays.asList("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
i started a new angular project and try to bind it with angular-oauth2-oidc.
in auth.config.js
import { AuthConfig } from 'angular-oauth2-oidc';
export const authConfig: AuthConfig = {
clientId: 'xxxxxx',
issuer: 'https://accounts.google.com/',
// loginUrl: 'http://localhost:8080',
redirectUri: window.location.origin + '/user.html',
scope: 'openid profile email',
tokenEndpoint: 'https://www.googleapis.com/oauth2/v3/token',
// strictDiscoveryDocumentValidation: false,
userinfoEndpoint: 'http://localhost:8080/user',
// disableAtHashCheck: true,
// nonceStateSeparator: ',',
// clearHashAfterLogin: false,
};
in login.component.ts
import { Component, OnInit } from '#angular/core';
import { OAuthService, JwksValidationHandler } from 'angular-oauth2-oidc';
import { authConfig } from '../auth.config';
#Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
constructor(private oauthService: OAuthService) {
this.oauthService.configure(authConfig);
this.oauthService.tokenValidationHandler = new JwksValidationHandler();
this.oauthService.loadDiscoveryDocumentAndTryLogin();
}
ngOnInit() {
this.oauthService.initImplicitFlow(encodeURIComponent('http://localhost8080/'));
}
}
I dont understand how the authentication must be handle in this config.
The annotation #EnableOAuth2Sso transforms your spring application in an OAuth2 client
In your scenario, instead, you want that your application is a ResourceServer
So you should use the #EnableResourceServer annotation.
Spring security should be configured like this:
#Configuration
#EnableWebSecurity
#EnableResourceServer
#PropertySource(value = { "classpath:application.properties" }, encoding = "UTF-8", ignoreResourceNotFound = false)
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Autowired
private Environment env;
#Override
public void configure(final HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/swagger-ui.html","/webjars/**","/swagger-resources/**", "/v2/**","/csrf")
.permitAll()
.antMatchers("/**")
.authenticated()
.and()
.cors()
.configurationSource(corsConfigurationSource())
.and()
.exceptionHandling()
.accessDeniedHandler(new OAuth2AccessDeniedHandler());
}
#Override
public void configure(final ResourceServerSecurityConfigurer config) {
config
.tokenServices(tokenServices())
.resourceId("RES_ID");
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
final DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
return tokenServices;
}
#Bean
public TokenStore tokenStore()
{
JwkTokenStore result = new JwkTokenStore("JWTKS_URL", accessTokenConverter());
return result;
}
#Bean
public JwtAccessTokenConverter accessTokenConverter()
{
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setAccessTokenConverter(new DefaultAccessTokenConverter() {
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
final OAuth2Authentication auth = super.extractAuthentication(map);
auth.setDetails(map);
return auth;
}
});
return converter;
}
#Bean
public JwtClaimsSetVerifier jwtClaimsSetVerifier() {
return new DelegatingJwtClaimsSetVerifier(Arrays.asList(issuerClaimVerifier(), customJwtClaimVerifier()));
}
#Bean
public JwtClaimsSetVerifier issuerClaimVerifier() {
try {
return new IssuerClaimVerifier(new URL("ISSUER CLAIMS URL"));
} catch (final MalformedURLException e) {
throw new RuntimeException(e);
}
}
#Bean
public JwtClaimsSetVerifier customJwtClaimVerifier() {
return new CustomClaimVerifier();
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
boolean abilitaCors = new Boolean(env.getProperty("profile.manager.web.cors.enbaled"));
if( abilitaCors )
{
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.addAllowedOrigin("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.setExposedHeaders(Arrays.asList("X-Auth-Token","x-auth-token", "x-requested-with", "x-xsrf-token","Access-Control-Allow-Origin", "content-type"));
source.registerCorsConfiguration("/**", configuration);
}
return source;
}
}
On angular side I suggest to you to use angulat-oauth2-oidc plugin https://github.com/manfredsteyer/angular-oauth2-oidc
I want to implement simple Spring Security WebFlux application.
I want to use JSON message like
{
'username': 'admin',
'password': 'adminPassword'
}
in body (POST request to /signin) to sign in my app.
What did I do?
I created this configuration
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity(proxyTargetClass = true)
public class WebFluxSecurityConfig {
#Autowired
private ReactiveUserDetailsService userDetailsService;
#Autowired
private ObjectMapper mapper;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(11);
}
#Bean
public ServerSecurityContextRepository securityContextRepository() {
WebSessionServerSecurityContextRepository securityContextRepository =
new WebSessionServerSecurityContextRepository();
securityContextRepository.setSpringSecurityContextAttrName("securityContext");
return securityContextRepository;
}
#Bean
public ReactiveAuthenticationManager authenticationManager() {
UserDetailsRepositoryReactiveAuthenticationManager authenticationManager =
new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
authenticationManager.setPasswordEncoder(passwordEncoder());
return authenticationManager;
}
#Bean
public AuthenticationWebFilter authenticationWebFilter() {
AuthenticationWebFilter filter = new AuthenticationWebFilter(authenticationManager());
filter.setSecurityContextRepository(securityContextRepository());
filter.setAuthenticationConverter(jsonBodyAuthenticationConverter());
filter.setRequiresAuthenticationMatcher(
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/signin")
);
return filter;
}
#Bean
public Function<ServerWebExchange, Mono<Authentication>> jsonBodyAuthenticationConverter() {
return exchange -> {
return exchange.getRequest().getBody()
.cache()
.next()
.flatMap(body -> {
byte[] bodyBytes = new byte[body.capacity()];
body.read(bodyBytes);
String bodyString = new String(bodyBytes);
body.readPosition(0);
body.writePosition(0);
body.write(bodyBytes);
try {
UserController.SignInForm signInForm = mapper.readValue(bodyString, UserController.SignInForm.class);
return Mono.just(
new UsernamePasswordAuthenticationToken(
signInForm.getUsername(),
signInForm.getPassword()
)
);
} catch (IOException e) {
return Mono.error(new LangDopeException("Error while parsing credentials"));
}
});
};
}
#Bean
public SecurityWebFilterChain securityWebFiltersOrder(ServerHttpSecurity httpSecurity,
ReactiveAuthenticationManager authenticationManager) {
return httpSecurity
.csrf().disable()
.httpBasic().disable()
.logout().disable()
.formLogin().disable()
.securityContextRepository(securityContextRepository())
.authenticationManager(authenticationManager)
.authorizeExchange()
.anyExchange().permitAll()
.and()
.addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
.build();
}
}
BUT I use jsonBodyAuthenticationConverter() and it reads Body of the incoming request. Body can be read only once, so I have an error
java.lang.IllegalStateException: Only one connection receive subscriber allowed.
Actually it's working but with exception (session is set in cookies). How can I remake it without this error?
Now I only created something like:
#PostMapping("/signin")
public Mono<Void> signIn(#RequestBody SignInForm signInForm, ServerWebExchange webExchange) {
return Mono.just(signInForm)
.flatMap(form -> {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
form.getUsername(),
form.getPassword()
);
return authenticationManager
.authenticate(token)
.doOnError(err -> {
System.out.println(err.getMessage());
})
.flatMap(authentication -> {
SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
return securityContextRepository
.save(webExchange, securityContext)
.subscriberContext(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)));
});
});
}
And removed AuthenticationWebFilter from config.
You are almost there. The following converter worked for me:
public class LoginJsonAuthConverter implements Function<ServerWebExchange, Mono<Authentication>> {
private final ObjectMapper mapper;
#Override
public Mono<Authentication> apply(ServerWebExchange exchange) {
return exchange.getRequest().getBody()
.next()
.flatMap(buffer -> {
try {
SignInRequest request = mapper.readValue(buffer.asInputStream(), SignInRequest.class);
return Mono.just(request);
} catch (IOException e) {
log.debug("Can't read login request from JSON");
return Mono.error(e);
}
})
.map(request -> new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
}
}
Furthermore, you don't need the sign in controller; spring-security will check each request for you in the filter. Here's how I configured spring-security with an ServerAuthenticationEntryPoint:
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
ReactiveAuthenticationManager authManager) {
return http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/api/**").authenticated()
.pathMatchers("/**", "/login", "/logout").permitAll()
.and().exceptionHandling().authenticationEntryPoint(restAuthEntryPoint)
.and().addFilterAt(authenticationWebFilter(authManager), SecurityWebFiltersOrder.AUTHENTICATION)
.logout()
.and().build();
}
Hope this helps.
Finally I config WebFlux security so (pay attention to logout handling, logout doesn't have any standard ready-for-use configuration for 5.0.4.RELEASE, you must disable default logout config anyway, because default logout spec creates new SecurityContextRepository by default and doesn't allow you to set your repository).
UPDATE: default logout configuration doesn't work only in case when you set custom SpringSecurityContextAttributeName in SecurityContextRepository for web session.
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity(proxyTargetClass = true)
public class WebFluxSecurityConfig {
#Autowired
private ReactiveUserDetailsService userDetailsService;
#Autowired
private ObjectMapper mapper;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(11);
}
#Bean
public ServerSecurityContextRepository securityContextRepository() {
WebSessionServerSecurityContextRepository securityContextRepository =
new WebSessionServerSecurityContextRepository();
securityContextRepository.setSpringSecurityContextAttrName("langdope-security-context");
return securityContextRepository;
}
#Bean
public ReactiveAuthenticationManager authenticationManager() {
UserDetailsRepositoryReactiveAuthenticationManager authenticationManager =
new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
authenticationManager.setPasswordEncoder(passwordEncoder());
return authenticationManager;
}
#Bean
public SecurityWebFilterChain securityWebFiltersOrder(ServerHttpSecurity httpSecurity) {
return httpSecurity
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.securityContextRepository(securityContextRepository())
.authorizeExchange()
.anyExchange().permitAll() // Currently
.and()
.addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
.addFilterAt(logoutWebFilter(), SecurityWebFiltersOrder.LOGOUT)
.build();
}
private AuthenticationWebFilter authenticationWebFilter() {
AuthenticationWebFilter filter = new AuthenticationWebFilter(authenticationManager());
filter.setSecurityContextRepository(securityContextRepository());
filter.setAuthenticationConverter(jsonBodyAuthenticationConverter());
filter.setAuthenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/home"));
filter.setAuthenticationFailureHandler(
new ServerAuthenticationEntryPointFailureHandler(
new RedirectServerAuthenticationEntryPoint("/authentication-failure")
)
);
filter.setRequiresAuthenticationMatcher(
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/signin")
);
return filter;
}
private LogoutWebFilter logoutWebFilter() {
LogoutWebFilter logoutWebFilter = new LogoutWebFilter();
SecurityContextServerLogoutHandler logoutHandler = new SecurityContextServerLogoutHandler();
logoutHandler.setSecurityContextRepository(securityContextRepository());
RedirectServerLogoutSuccessHandler logoutSuccessHandler = new RedirectServerLogoutSuccessHandler();
logoutSuccessHandler.setLogoutSuccessUrl(URI.create("/"));
logoutWebFilter.setLogoutHandler(logoutHandler);
logoutWebFilter.setLogoutSuccessHandler(logoutSuccessHandler);
logoutWebFilter.setRequiresLogoutMatcher(
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout")
);
return logoutWebFilter;
}
private Function<ServerWebExchange, Mono<Authentication>> jsonBodyAuthenticationConverter() {
return exchange -> exchange
.getRequest()
.getBody()
.next()
.flatMap(body -> {
try {
UserController.SignInForm signInForm =
mapper.readValue(body.asInputStream(), UserController.SignInForm.class);
return Mono.just(
new UsernamePasswordAuthenticationToken(
signInForm.getUsername(),
signInForm.getPassword()
)
);
} catch (IOException e) {
return Mono.error(new LangDopeException("Error while parsing credentials"));
}
});
}
}