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 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");
}
}
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.
I am building microservice applications with spring cloud, oauth and JWT. My Oauth2 server generates JWT token but when I am trying to validate the token in gateway (implemented using ZUUL) I am getting below Error
Could you please let me know what is wrong and what could be the solution.
I am using Spring 4.3, Spring boot 1.5.8, Spring cloud Dalston.SR4
org.springframework.security.jwt.crypto.sign.InvalidSignatureException: Calculated signature did not match actual value
at org.springframework.security.jwt.crypto.sign.MacSigner.verify(MacSigner.java:62) ~[spring-security-jwt-1.0.8.RELEASE.jar:na]
at org.springframework.security.jwt.JwtImpl.verifySignature(JwtHelper.java:287) ~[spring-security-jwt-1.0.8.RELEASE.jar:na]
at org.springframework.security.jwt.JwtHelper.decodeAndVerify(JwtHelper.java:77) ~[spring-security-jwt-1.0.8.RELEASE.jar:na]
at com.debopam.gateway.filter.CustomPostZuulFilter.run(CustomPostZuulFilter.java:57) ~[classes/:na]
at com.netflix.zuul.ZuulFilter.runFilter(ZuulFilter.java:112) [zuul-core-1.3.0.jar:1.3.0]
at com.netflix.zuul.FilterProcessor.processZuulFilter(FilterProcessor.java:193) [zuul-core-1.3.0.jar:1.3.0]
at com.netflix.zuul.FilterProcessor.runFilters(FilterProcessor.java:157) [zuul-core-1.3.0.jar:1.3.0]
at com.netflix.zuul.FilterProcessor.postRoute(FilterProcessor.java:92) [zuul-core-1.3.0.jar:1.3.0]
at com.netflix.zuul.ZuulRunner.postRoute(ZuulRunner.java:87) [zuul-core-1.3.0.jar:1.3.0]
I have used signing key 12345AsDfG in both Auth server and gateway server.
Below Are the code snippet
Auth Server
#Configuration
public class JWTTokenStoreConfig {
#Autowired
private ServiceConfig serviceConfig;
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
defaultTokenServices.setAccessTokenValiditySeconds(60*30);
return defaultTokenServices;
}
#Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(serviceConfig.getJwtSigningKey());
return converter;
}
#Bean
public TokenEnhancer jwtTokenEnhancer() {
return new JWTTokenEnhancer();
}
}
#Configuration
public class JWTOAuth2Config extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private TokenStore tokenStore;
#Autowired
private DefaultTokenServices tokenServices;
#Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
#Autowired
private TokenEnhancer jwtTokenEnhancer;
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception
{
//oauthServer.checkTokenAccess("permitAll()");
oauthServer
.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')")
.checkTokenAccess("permitAll()");
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));
endpoints.tokenStore(tokenStore) //JWT
.accessTokenConverter(jwtAccessTokenConverter) //JWT
.tokenEnhancer(tokenEnhancerChain) //JWT
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("uiapp")
.secret("secret")
.authorizedGrantTypes("refresh_token", "password", "client_credentials")
.scopes("webclient", "mobileclient");
}
}
In the Gateway application, I am using below code to verify the Token
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
try {
InputStream is = ctx.getResponseDataStream();
String responseBody = IOUtils.toString(is);
if (StringUtils.hasText(responseBody)
&& responseBody.contains("access_token")) {
Map<String, Object> responseMap = objectMapper.readValue(
responseBody, new TypeReference<Map<String, Object>>() {});
String accesToken = responseMap.get("access_token").toString();
Jwt jwt = JwtHelper.decodeAndVerify(accesToken, new MacSigner(serviceConfig.getJwtSigningKey()));
System.out.println(jwt.getClaims());
//System.out.println(jwt.getBody());
}
ctx.setResponseBody(responseBody);
} catch (Exception e) {
logger.error("Error occured in zuul post filter", e);
}
return null;
}
There was a signing key mismatch between the services.
I have a working Spring Boot 1.3.1 application with an AngularJS client using the "password" grant type with OAuth2. I know this is not very good as the client_id and client_secret are visible in the AngularJS code by everybody.
I would like to change to the "implicit" grant. My authorisation server and resource server are running in the same application.
This is my current configuration of the authorisation server:
#Configuration
#EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
#Autowired
private MyApplicationSecuritySettings securitySettings;
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private PasswordEncoder passwordEncoder;
#Autowired
private DataSource dataSource;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(securitySettings.getAngularClientId())
.authorizedGrantTypes("password","refresh_token")
.scopes("read", "write")
.resourceIds(RESOURCE_ID)
.secret(passwordEncoder.encode(securitySettings.getAngularClientSecret()));
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore())
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.passwordEncoder(passwordEncoder);
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(tokenStore());
return tokenServices;
}
#Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource) {
// Workaround for https://github.com/spring-projects/spring-boot/issues/5071
#Override
protected OAuth2Authentication deserializeAuthentication(byte[] authentication) {
ObjectInputStream input = null;
try {
input = new ConfigurableObjectInputStream(
new ByteArrayInputStream(authentication),
Thread.currentThread().getContextClassLoader());
return (OAuth2Authentication) input.readObject();
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
} finally {
if (input != null) {
try {
input.close();
} catch (IOException ex) {
// Continue
}
}
}
}
};
}
}
I also have this WebSecurityConfigurerAdapter subclass:
#Configuration
#EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private PasswordEncoder passwordEncoder;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}
#Override
public void configure(WebSecurity web) throws Exception {
// Disable security for bower components
web.ignoring().antMatchers("/components/**");
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
and this is the configuration of the resource server:
#Configuration
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin", "/admin/", "/index.html", "/home.html", "/", "/logout",
"/partials/login.html"
)
.permitAll()
.authorizeRequests()
.antMatchers("/management/**").hasRole("ADMIN")
.anyRequest().authenticated();
}
}
With this configuration, a token can be requested via /oauth/token. This is implemented in AngularJS when the user visits http://localhost:8080/admin/#/login and types his username and password in that HTML form.
For the implicit grant, I am changing the ClientDetailsServiceConfigurer to:
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(securitySettings.getAngularClientId())
.authorizedGrantTypes("implicit")
.scopes("read", "write")
.resourceIds(RESOURCE_ID);
}
If I then try the following url: http://localhost:8080/oauth/authorize?client_id=angularClient&response_type=token
The browser redirects to http://localhost:8080/login and shows:
<oauth>
<error_description>Full authentication is required to access this resource</error_description>
<error>unauthorized</error>
</oauth>
Am I testing this implicit flow wrongly?
Do I need to change something in the configuration so that the redirect goes to http://localhost:8080/admin/#/login instead? Or does the angular app need to go from http://localhost:8080/admin/#/login to http://localhost:8080/oauth/authorize passing in the user's username and password somewhere?