Spring Security WebFlux - body with Authentication - java

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"));
}
});
}
}

Related

Upgrading to Spring Boot 3 authorizeHttpRequests throwing 403 with oath2ResourceServer

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

WebSecurityConfiguration antMatcher() not ignoring specific url for external API in integration test

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.

Using tls with jwt

I want to use tls with my sharing of token and login process. I implemented it and it works fine. I create a new port for tls that is https://localhost:8443 but there is a security leak in my app. When i post https://localhost:8443/login and take the jwt and use it with GET https://localhost:8443/welcome everything great if i use http instead of https bad request is returned. But if i change my url with http://localhost:8080/welcome and gives the token that is taken before the app must return bad request but it returns succesful open. My server and security config is below. What am i doing wrong?
Server Config:
#Configuration
public class ServerConfig {
#Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
#Override
protected void postProcessContext(Context context) {
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
securityConstraint.addCollection(collection);
context.addConstraint(securityConstraint);
}
};
tomcat.addAdditionalTomcatConnectors(getHttpConnector());
return tomcat;
}
private Connector getHttpConnector() {
Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
connector.setScheme("http");
connector.setPort(8080);
connector.setSecure(false);
connector.setRedirectPort(8443);
return connector;
}
}
Security Config:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private JwtTokenFilter jwtTokenFilter;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
public void configurePasswordEncoder(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userDetailsService).passwordEncoder(getBCryptPasswordEncoder());
}
#Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public AuthenticationManager getAuthenticationManager() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
Auth Controller:
#RestController
#RequestMapping("/login")
public class AuthConroller {
#Autowired
private TokenManager tokenManager;
#Autowired
private AuthenticationManager authenticationManager;
#PostMapping
public ResponseEntity<String> login(#RequestBody LoginRequest loginRequest) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
return ResponseEntity.ok(tokenManager.generateToken(loginRequest.getUsername()));
} catch (Exception e) {
throw e;
}
}
}
Message Controller:
#RestController
#RequestMapping("/message")
public class MessageController {
#GetMapping
public ResponseEntity<String> getMessage() {
return ResponseEntity.ok("JWT demo");
}
}

Bean method 'clientRegistrationRepository' in 'OAuth2ClientRegistrationRepositoryConfiguration' not loaded

I have implemented facebook login using spring boot with security, when i try to run error occurs. I don't know from where it occurs.
how to solve this problem ? In my pom.xml already added spring-security-oauth2-client and spring-security-oauth2 dependency.
***************************
APPLICATION FAILED TO START
***************************
Description:
Method springSecurityFilterChain in org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration required a bean of type 'org.springframework.security.oauth2.client.registration.ClientRegistrationRepository' that could not be found.
The following candidates were found but could not be injected:
- Bean method 'clientRegistrationRepository' in 'OAuth2ClientRegistrationRepositoryConfiguration' not loaded because OAuth2 Clients Configured Condition registered clients is not available
Action:
Consider revisiting the entries above or defining a bean of type 'org.springframework.security.oauth2.client.registration.ClientRegistrationRepository' in your configuration.
2.WebSecurityConfig
#Configuration
#EnableWebSecurity
#EnableOAuth2Client
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Resource(name = "userService")
private UserDetailsService userDetailsService;
#Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
#Autowired
private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
#Autowired
private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
#Autowired
private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;
#Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
#Bean
public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() {
return new HttpCookieOAuth2AuthorizationRequestRepository();
}
#Bean
public BCryptPasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
#Autowired
public void globalUserDetails(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService)
.passwordEncoder(encoder());
}
#Bean
public JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationFilter();
}
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers("/login", "/register", "/auth/**", "/oauth2/**").permitAll()
.anyRequest()
.authenticated()
.and()
.oauth2Login()
.authorizationEndpoint()
.baseUri("/oauth2/authorize")
.authorizationRequestRepository(cookieAuthorizationRequestRepository())
.and()
.redirectionEndpoint()
.baseUri("/oauth2/callback/*")
.and()
.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler)
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
3.application.yml
security:
oauth2:
client:
registration:
facebook:
clientId: <clientId>
clientSecret: <clientSecret>
redirectUri: "{baseUrl}/oauth2/callback/{registrationId}"
scope:
- email
- public_profile
provider:
facebook:
authorizationUri: https://www.facebook.com/v3.0/dialog/oauth
tokenUri: https://graph.facebook.com/v3.0/oauth/access_token
userInfoUri: https://graph.facebook.com/v3.0/me?fields=id,first_name,middle_name,last_name,name,email,verified,is_verified,picture.width(250).height(250)
app:
auth:
tokenSecret: 926D96C90030DD58429D2751AC1BDBBC
tokenExpirationMsec: 864000000
oauth2:
authorizedRedirectUris:
- http://localhost:3000/oauth2/redirect
- myandroidapp://oauth2/redirect
- myiosapp://oauth2/redirect
4.MemberServiceImpl
#Service(value = "userService")
public class MembersServiceImpl extends DefaultOAuth2UserService implements MembersService, UserDetailsService {
#Autowired
private MembersDao membersDao;
#Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
try {
return processOAuth2User(oAuth2UserRequest, oAuth2User);
} catch (AuthenticationException ex) {
throw ex;
} catch (Exception ex) {
// Throwing an instance of AuthenticationException will trigger the OAuth2AuthenticationFailureHandler
throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
}
}
private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) {
OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(oAuth2UserRequest.getClientRegistration().getRegistrationId(), oAuth2User.getAttributes());
if(StringUtils.isEmpty(oAuth2UserInfo.getEmail())) {
throw new OAuth2AuthenticationProcessingException("Email not found from OAuth2 provider");
}
Optional<Members> membersOptional = membersDao.getByEmail(oAuth2UserInfo.getEmail());
Members members;
if(membersOptional.isPresent()) {
members = membersOptional.get();
if(!members.getProvider().equals(AuthProvider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId()))) {
throw new OAuth2AuthenticationProcessingException("Looks like you're signed up with " +
members.getProvider() + " account. Please use your " + members.getProvider() +
" account to login.");
}
members = updateExistingUser(members, oAuth2UserInfo);
} else {
members = registerNewUser(oAuth2UserRequest, oAuth2UserInfo);
}
return UserPrincipal.create(members, oAuth2User.getAttributes());
}
private Members registerNewUser(OAuth2UserRequest oAuth2UserRequest, OAuth2UserInfo oAuth2UserInfo) {
Members members = new Members();
long roleId = 2;
members.setProvider(AuthProvider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId()));
members.setProviderId(oAuth2UserInfo.getId());
members.setFirst_name(oAuth2UserInfo.getName());
members.setEmail(oAuth2UserInfo.getEmail());
members.setImage(oAuth2UserInfo.getImageUrl());
members.setRoles(new Role(roleId));
return membersDao.save(members);
}
private Members updateExistingUser(Members existingUser, OAuth2UserInfo oAuth2UserInfo) {
existingUser.setFirst_name(oAuth2UserInfo.getName());
existingUser.setImage(oAuth2UserInfo.getImageUrl());
return membersDao.save(existingUser);
}
}
5.OAuth2AuthenticationSuccessHandler
#Component
public class OAuth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private TokenProvider tokenProvider;
private AppProperties appProperties;
private HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;
#Autowired
OAuth2AuthenticationSuccessHandler(TokenProvider tokenProvider, AppProperties appProperties,
HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository) {
this.tokenProvider = tokenProvider;
this.appProperties = appProperties;
this.httpCookieOAuth2AuthorizationRequestRepository = httpCookieOAuth2AuthorizationRequestRepository;
}
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String targetUrl = determineTargetUrl(request, response, authentication);
if (response.isCommitted()) {
logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
return;
}
clearAuthenticationAttributes(request, response);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
Optional<String> redirectUri = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME)
.map(Cookie::getValue);
if(redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) {
throw new BadRequestException("Sorry! We've got an Unauthorized Redirect URI and can't proceed with the authentication");
}
String targetUrl = redirectUri.orElse(getDefaultTargetUrl());
String token = null;
try {
token = tokenProvider.generateToken(authentication);
} catch (IOException e) {
e.printStackTrace();
} catch (ServletException e) {
e.printStackTrace();
}
return UriComponentsBuilder.fromUriString(targetUrl)
.queryParam("token", token)
.build().toUriString();
}
protected void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
super.clearAuthenticationAttributes(request);
httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
}
private boolean isAuthorizedRedirectUri(String uri) {
URI clientRedirectUri = URI.create(uri);
return appProperties.getOauth2().getAuthorizedRedirectUris()
.stream()
.anyMatch(authorizedRedirectUri -> {
// Only validate host and port. Let the clients use different paths if they want to
URI authorizedURI = URI.create(authorizedRedirectUri);
if(authorizedURI.getHost().equalsIgnoreCase(clientRedirectUri.getHost())
&& authorizedURI.getPort() == clientRedirectUri.getPort()) {
return true;
}
return false;
});
}
6.AppProperties
#ConfigurationProperties(prefix = "app")
public class AppProperties {
private final Auth auth = new Auth();
private final OAuth2 oauth2 = new OAuth2();
public static class Auth {
private String tokenSecret;
private long tokenExpirationMsec;
public String getTokenSecret() {
return tokenSecret;
}
public void setTokenSecret(String tokenSecret) {
this.tokenSecret = tokenSecret;
}
public long getTokenExpirationMsec() {
return tokenExpirationMsec;
}
public void setTokenExpirationMsec(long tokenExpirationMsec) {
this.tokenExpirationMsec = tokenExpirationMsec;
}
}
public static final class OAuth2 {
private List<String> authorizedRedirectUris = new ArrayList<>();
public List<String> getAuthorizedRedirectUris() {
return authorizedRedirectUris;
}
public OAuth2 authorizedRedirectUris(List<String> authorizedRedirectUris) {
this.authorizedRedirectUris = authorizedRedirectUris;
return this;
}
}
public Auth getAuth() {
return auth;
}
public OAuth2 getOauth2() {
return oauth2;
}
}
spring.security.oauth2.client is the valid prefix for OAuth2ClientProperties. Yours is security.oauth2.client
UPDATE
Your AppProperties cannot be used for injection unless you define #EnableConfigurationProperties(AppProperties.class) on any of your #Configuration classes.

Spring MVC, Spring Security: Unable to load login template

When to access any route, I get redirected to my login page, including accessing my login page, which is expected. Now the problem is the login page does not even load. I have configured the controller method, hander, template configuration, and security configuration necessary to make this work, but all I get is the error below:
http://localhost:8080/login?errorMessage=Login+to+use+the+app
UserController.java
#Controller
public class UserController {
#Autowired
private UserService userService;
#GetMapping("/signup")
public String signup(Model model) {
model.addAttribute("user", new User());
return "signup";
}
#PostMapping("/users")
public String createUser(User user) {
// only create user if it does not exist
if (userService.findByUsername(user.getUsername()) == null) {
user.setRoles(new String[] {"ROLE_USER"});
userService.save(user);
return "redirect:/login";
}
else {
return "redirect:/signup";
}
}
#GetMapping("/login")
public String login(Model model) {
model.addAttribute("user", new User());
return "login";
}
#GetMapping("/profile")
public String currentUserProfile(Model model) {
User currentUser = (User) model.asMap().get("currentUser");
model.addAttribute("user", currentUser);
model.addAttribute("authorized", true);
return "profile";
}
#GetMapping("/users/{id}")
public String userProfile(#PathVariable Long id, Model model) {
User queriedUser = userService.findOne(id);
model.addAttribute("user", queriedUser);
User currentUser = (User) model.asMap().get("currentUser");
if (currentUser != null && currentUser.isAdmin()) {
model.addAttribute("authorized", true);
}
return "profile";
}
}
UserHandler.java
#ControllerAdvice(basePackages = "com.valencra.recipes.web.controller")
public class UserHandler {
public static final String USERNAME_NOT_FOUND_ERR_MSG = "Unable to find username";
public static final String ACCESS_DENIED_ERR_MSG = "Login to use the app";
#Autowired
private UserService userService;
#ModelAttribute("authenticatedUser")
public User addAuthenticatedUser() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
String username = authentication.getName();
User user = userService.findByUsername(username);
if (user != null) {
return user;
}
else {
throw new UsernameNotFoundException(USERNAME_NOT_FOUND_ERR_MSG);
}
}
else {
throw new AccessDeniedException(ACCESS_DENIED_ERR_MSG);
}
}
#ExceptionHandler(AccessDeniedException.class)
public String redirectIfUserNotAuthenticated(RedirectAttributes redirectAttributes) {
redirectAttributes.addAttribute("errorMessage", ACCESS_DENIED_ERR_MSG);
return "redirect:/login";
}
#ExceptionHandler(UsernameNotFoundException.class)
public String redirectIfUserNotFound(RedirectAttributes redirectAttributes) {
redirectAttributes.addAttribute("errorMessage", USERNAME_NOT_FOUND_ERR_MSG);
return "redirect:/login";
}
}
TemplateConfig.java
#Configuration
public class TemplateConfig {
#Bean
public SpringResourceTemplateResolver templateResolver() {
final SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setPrefix("classpath:/templates/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode("LEGACYHTML5");
return templateResolver;
}
#Bean
public SpringTemplateEngine templateEngine() {
final SpringTemplateEngine springTemplateEngine = new SpringTemplateEngine();
springTemplateEngine.addTemplateResolver(templateResolver());
springTemplateEngine.addDialect(new SpringSecurityDialect());
return springTemplateEngine;
}
#Bean
public ThymeleafViewResolver viewResolver() {
final ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine());
viewResolver.setOrder(1);
return viewResolver;
}
}
SecurityConfig.java
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private RecipesAppUserDetailsService recipesAppUserDetailsService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(recipesAppUserDetailsService)
.passwordEncoder(User.PASSWORD_ENCODER);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/signup").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.successHandler(loginSuccessHandler())
.failureHandler(loginFailureHandler())
.and()
.logout()
.permitAll()
.logoutSuccessUrl("/login")
.and()
.csrf().disable();
http.headers().frameOptions().disable();
}
public AuthenticationSuccessHandler loginSuccessHandler() {
return (request, response, authentication) -> response.sendRedirect("/");
}
public AuthenticationFailureHandler loginFailureHandler() {
return (request, response, exception) ->
response.sendRedirect("/login");
}
#Bean
public EvaluationContextExtension securityExtension() {
return new EvaluationContextExtensionSupport() {
#Override
public String getExtensionId() {
return "security";
}
#Override
public Object getRootObject() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return new SecurityExpressionRoot(authentication) {};
}
};
}
}
application.properties
# Package where our entities (models) are located
recipes.entity.package = com.valencra.recipes.model
# Details for our datasource
recipes.db.driver = org.h2.Driver
recipes.db.url = jdbc:h2:mem:recipes
# Hibernate properties
hibernate.dialect = org.hibernate.dialect.H2Dialect
hibernate.implicit_naming_strategy = org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
hibernate.format_sql = true
hibernate.show_sql = true
hibernate.hbm2ddl.auto = create-drop
spring.data.rest.basePath=/api/v1
resources directory
EDIT: See #lgaleazzi's answer, and the comments that follow. Essentially, removing the UserHandler class fixes it.
What does the stack trace say?
Looking at your UserHandler, you handle a null authenticated object, and a user you can find. You don't deal with what happens with an anonymous user. I think that's the issue.
If nobody is authenticated, you'll get an authentication object with an anonymous user. You can check that with the method isAuthenticated(). But you don't actually have to write this code, Spring Boot handles all this pretty well with its default configuration.
Try adding the following method to SecurityConfig:
#Override
public void configure(WebSecurity web) throws Exception {
// configuring here URLs for which security filters
// will be disabled (this is equivalent to using
// security="none")
web.ignoring().antMatchers("/login");
}
There you can specify URLs in your application for which authentication should not be applied (also useful for static resources).
In your case, /login is not excluded from authentication scope, so it causes one more redirection to /login, and you get a vicious circle.

Categories

Resources