Allow OPTIONS HTTP Method for oauth/token request - spring

I'm trying to enable oauth2 token fetching for my angular application. My configuration is working fine (authentication is working correctly for all requests, token fetching is working fine as well) but there is one problem.
CORS requests require that before GET an OPTIONS request is sent to the server. To make it worse, that request does not contain any authentication headers.
I would like to have this request always returning with 200 status without any authentication done on the server. Is it possible? Maybe I'm missing something
my spring security config:
#Configuration
#EnableWebSecurity
#EnableAuthorizationServer
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
#Inject
private UserService userService;
#Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
#Bean
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
#Bean
public WebResponseExceptionTranslator webResponseExceptionTranslator() {
return new DefaultWebResponseExceptionTranslator() {
#Override
public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
ResponseEntity<OAuth2Exception> responseEntity = super.translate(e);
OAuth2Exception body = responseEntity.getBody();
HttpHeaders headers = new HttpHeaders();
headers.setAll(responseEntity.getHeaders().toSingleValueMap());
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
headers.set("Access-Control-Max-Age", "3600");
headers.set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
return new ResponseEntity<>(body, headers, responseEntity.getStatusCode());
}
};
}
#Bean
public AuthorizationServerConfigurer authorizationServerConfigurer() {
return new AuthorizationServerConfigurer() {
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
OAuth2AuthenticationEntryPoint oAuth2AuthenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
oAuth2AuthenticationEntryPoint.setExceptionTranslator(webResponseExceptionTranslator());
security.authenticationEntryPoint(oAuth2AuthenticationEntryPoint);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("secret-client")
.secret("secret")
.authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
.authorities("ROLE_LOGIN")
.scopes("read", "write", "trust")
.accessTokenValiditySeconds(60 * 60 * 12); // 12 hours
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenServices(tokenServices());
endpoints.authenticationManager(authenticationManager());
}
};
}
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return new AuthenticationManager() {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
log.warn("FIX ME: REMOVE AFTER DEBUG!!!!!!!!!!!!");
log.debug("authenticate: " + authentication.getPrincipal() + ":" + authentication.getCredentials());
final Collection<GrantedAuthority> authorities = new ArrayList<>();
WomarUser user = userService.findUser(authentication.getPrincipal().toString(), authentication.getCredentials().toString());
for (UserRole userRole : user.getRoles()) {
authorities.add(new SimpleGrantedAuthority(userRole.getName()));
}
return new UsernamePasswordAuthenticationToken(user.getLogin(), user.getPassword(), authorities);
}
};
}
#Bean
public OAuth2AuthenticationManager auth2AuthenticationManager() {
OAuth2AuthenticationManager oAuth2AuthenticationManager = new OAuth2AuthenticationManager();
oAuth2AuthenticationManager.setTokenServices(tokenServices());
return oAuth2AuthenticationManager;
}
#Bean
public OAuth2AuthenticationProcessingFilter auth2AuthenticationProcessingFilter() throws Exception {
OAuth2AuthenticationProcessingFilter oAuth2AuthenticationProcessingFilter = new OAuth2AuthenticationProcessingFilter();
oAuth2AuthenticationProcessingFilter.setAuthenticationManager(auth2AuthenticationManager());
return oAuth2AuthenticationProcessingFilter;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
OAuth2AuthenticationEntryPoint oAuth2AuthenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
oAuth2AuthenticationEntryPoint.setRealmName("realmName");
oAuth2AuthenticationEntryPoint.setTypeName("Basic");
oAuth2AuthenticationEntryPoint.setExceptionTranslator(webResponseExceptionTranslator());
http
.antMatcher("/**").httpBasic()
.authenticationEntryPoint(oAuth2AuthenticationEntryPoint)
.and().addFilterBefore(auth2AuthenticationProcessingFilter(), BasicAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/rest/womar/admin/**").hasRole("ADMIN")
.antMatchers("/rest/womar/**").hasRole("USER");
}
}
angular request:
var config = {
params: {
grant_type: 'password',
username: login,
password: password
},
headers: {
Authorization: 'Basic ' + Base64.encode('secret-client' + ':' + 'secret')
}
};
$http.get("http://localhost:8080/oauth/token", config)
.success(function(data, status) {
$log.log('success');
$log.log(data);
$log.log(status);
})
.error(function(data, status) {
$log.log('error');
$log.log(data);
$log.log(status);
});

#EnableAuthorizationServer is adding http security configuration for endpoints like /oauth/token, /oauth/token_key etc at order 0. So what you should do is to define a http security rule for /oauth/token endpoint only for the OPTIONS http method which is at a higher order.
Something like this:
#Order(-1)
#Configuration
public class MyWebSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/oauth/token").permitAll()
}
}

I was using the solution proposed by idursun. The OPTION call started to work, but still had problems with Access-Control-Allow-Origin.
This filter implementation definitively worked for me:
Standalone Spring OAuth2 JWT Authorization Server + CORS

I just add
#Order(Ordered.HIGHEST_PRECEDENCE)
in
public class OAuth2SecurityConfig extends WebSecurityConfigurerAdapter {....}
and config the support of spring
#Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
Worked for me.

Same problem with Spring-Boot 1.4.7.RELEASE
My WebSecurityConfigurerAdapter was using SecurityProperties.ACCESS_OVERRIDE_ORDER so, selected answer did not work.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class AuthServerSecurityConfig extends WebSecurityConfigurerAdapter
Thus, I added the following filter configuration with preceding order:
#Bean
public FilterRegistrationBean corsFilter() {
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(corsConfigurationSource()));
bean.setOrder(SecurityProperties.DEFAULT_FILTER_ORDER);
return bean;
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return source;
}
and it got the job done.
Note: equivalent result can be achieved with a javax.servlet.Filter bean with #Order(SecurityProperties.DEFAULT_FILTER_ORDER) annotation as below:
#Component
#Order(SecurityProperties.DEFAULT_FILTER_ORDER)
public class CorsFilter implements Filter {
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
final HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin" , "*" );
response.setHeader("Access-Control-Allow-Methods" , "POST, PUT, GET, OPTIONS, DELETE" );
response.setHeader("Access-Control-Allow-Headers" , "Authorization, Content-Type" );
response.setHeader("Access-Control-Max-Age" , "3600" );
if("OPTIONS".equalsIgnoreCase(((HttpServletRequest) req).getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
}
else {
chain.doFilter(req, res);
}
}
// ...
}

The following works for Spring Boot 2. It does not pick up other CORS configurations otherwise.
#Configuration
#EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
// this is a Spring ConfigurationProperty use any way to get the CORS values
#Autowired
private CorsProperties corsProperties;
// other things
//...
#Override
public void configure(
AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenStore(tokenStore())
.authenticationManager(authenticationManager);
if (corsProperties.getAllowedOrigins() != null) {
Map<String, CorsConfiguration> corsConfigMap = new HashMap<>();
Arrays.asList(corsProperties.getAllowedOrigins().split(",")).stream()
.filter(StringUtils::isNotBlank).forEach(s -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin(s.trim());
if (corsProperties.getAllowedMethods() != null) {
config.setAllowedMethods(Arrays.asList(corsProperties.getAllowedMethods().split(",")));
}
if (corsProperties.getAllowedHeaders() != null) {
config.setAllowedHeaders(Arrays.asList(corsProperties.getAllowedHeaders().split(",")));
}
// here the /oauth/token is used
corsConfigMap.put("/oauth/token", config);
});
endpoints.getFrameworkEndpointHandlerMapping()
.setCorsConfigurations(corsConfigMap);
}
}
}
And in addition the already mentioned allowance of the OPTIONS request:
#Order(-1)
#Configuration
public class MyWebSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
authorizeRequests()
.antMatchers("/**/oauth/token").permitAll()
.and().httpBasic().realmName(securityRealm)
// would throw a 403 otherwise
.and().csrf().disable()
// optional, but with a token a sesion is not needed anymore
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}

Related

Method security ignored when using Jersey with Spring Security

I'm having trouble getting Spring-Security to work with Jersey within my Spring Boot project. It seems to be something to do with the way the two are integrated but I can't figure out why...
In Jersey endpoints the Spring Security enforced authentication requirement (configured in the WebSecurityConfigurerAdapter) works fine however some method security annotations are ignored. JSR-250 annotations (such as #DenyAll, #RolesAllowed, etc) seem to work if one registers the RolesAllowedDynamicFeature feature. However, #PreAuthorize, #PostAuthorize, #PreFilter, #PostFilter don't work.
Also, when logging in via the /login endpoint the XSRF-TOKEN cookie is not sent. Normally Spring security handles this and if I rewrite the endpoint with Spring MVC, I get both the XSRF-TOKEN and my own ACCESS-TOKEN (generated and sent by the endpoint itself).
(I found a workaround to this - see end of post)
Here's some code (I left out the obvious stuff like JwtUtils, general bean config, etc). Full source (not that there's much - this is a pared-down version of the real project) can be found at https://github.com/ChambreNoire/jersey-issue.
Thanks!
(I'm using Spring Boot 2.3.10.REALEASE and upgrading isn't currently an option)
application.yaml
spring:
jersey:
application-path: /resources
servlet:
load-on-startup: 1
type: filter
JerseyConfig
#Configuration
public class JerseyConfig {
#Bean
public ResourceConfig resourceConfig(ObjectMapper objectMapper) {
return new ResourceConfig()
.property(ServletProperties.FILTER_FORWARD_ON_404, true)
.register((ContextResolver<ObjectMapper>) aClass -> objectMapper)
.register(JacksonFeature.class)
.register(HttpMethodOverrideFilter.class)
.register(AuthResource.class)
.register(UserResource.class);
}
}
SecurityConfigurationAdapter
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
private static final String[] PUBLIC_PATTERNS;
private final JwtTokenAuthFilter jwtTokenAuthFilter;
private final JwtTokenAuthEntryPoint unauthorizedHandler;
#Autowired
public SecurityConfigurationAdapter(final JwtTokenAuthFilter jwtTokenAuthFilter, final JwtTokenAuthEntryPoint unauthorizedHandler) {
this.jwtTokenAuthFilter = jwtTokenAuthFilter;
this.unauthorizedHandler = unauthorizedHandler;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors()
.configurationSource(corsConfig()).and()
.csrf()
.ignoringAntMatchers("/login", "/login2")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler).and()
.authorizeRequests()
.antMatchers("/login", "/login2").permitAll()
.anyRequest().authenticated().and()
.addFilterBefore(jwtTokenAuthFilter, UsernamePasswordAuthenticationFilter.class)
.headers()
.xssProtection().and()
.contentSecurityPolicy("script-src 'self';require-trusted-types-for 'script';object-src 'none';");
}
private CorsConfigurationSource corsConfig() {
return request -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(singletonList("http://localhost:9000"));
config.setAllowedMethods(singletonList("*"));
config.setAllowCredentials(true);
config.setAllowedHeaders(singletonList("*"));
config.setExposedHeaders(singletonList("Authorization"));
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return config;
};
}
}
JwtTokenAuthFilter
#Priority(Priorities.AUTHENTICATION)
public class JwtTokenAuthFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final JwtTokenUtils jwtTokenUtils;
public JwtTokenAuthFilter(final UserDetailsService userDetailsService, final JwtTokenUtils jwtTokenUtils) {
this.userDetailsService = userDetailsService;
this.jwtTokenUtils = jwtTokenUtils;
}
#Override
protected void doFilterInternal(final HttpServletRequest req, final HttpServletResponse resp, final FilterChain chain)
throws ServletException, IOException {
if (req.getCookies() == null) {
chain.doFilter(req, resp);
return;
}
String token = Arrays.stream(req.getCookies())
.filter(c -> "ACCESS-TOKEN".equals(c.getName()))
.findFirst()
.map(Cookie::getValue)
.orElse(null);
if (isEmpty(token) || !jwtTokenUtils.validateJwtToken(token)) {
chain.doFilter(req, resp);
return;
}
String username = jwtTokenUtils.getUserNameFromJwtToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, resp);
}
}
AuthResource
#Singleton
#Path("")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public class AuthResource {
private final AuthenticationManager authenticationManager;
private final JwtTokenUtils jwtTokenUtils;
#Inject
public AuthResource(AuthenticationManager authenticationManager, JwtTokenUtils jwtTokenUtils) {
this.authenticationManager = authenticationManager;
this.jwtTokenUtils = jwtTokenUtils;
}
#POST
#Path("/login")
public Response login(#RequestBody LoginRequest request) {
try {
org.springframework.security.core.Authentication authentication = authenticationManager
.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(), request.getPassword()
)
);
UserDetailsImpl user = (UserDetailsImpl) authentication.getPrincipal();
return Response.ok()
.cookie(new NewCookie(
"ACCESS-TOKEN",
jwtTokenUtils.generateJwtToken(user),
"/", null, null,
NewCookie.DEFAULT_MAX_AGE,
true, true
))
.build();
} catch (BadCredentialsException ex) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}
}
}
If anyone could enlighten me on why this is the case that would be most appreciated, or indeed any general comments/improvements you may have !
Cheers
UPDATE
It looks like adding the JWT cookie to the javax.ws.rs.core.Response is causing the issue although I'm not certain how. If I add it directly to the HttpServletResponse, the XSRF-TOKEN cookie is present in the response.
Here's the updated Jersey authentication endpoint :
#POST
#Path("/login")
public Response authenticateUser(#Context HttpServletResponse response, #RequestBody LoginRequest request) {
try {
Authentication authentication = authenticationManager
.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(), request.getPassword()
)
);
UserDetailsImpl user = (UserDetailsImpl) authentication.getPrincipal();
Cookie cookie = new Cookie("ACCESS-TOKEN", jwtTokenUtils.generateJwtToken(user));
cookie.setPath("/");
cookie.setSecure(true);
cookie.setHttpOnly(true);
cookie.setMaxAge(-1);
response.addCookie(cookie);
return Response.ok().build();
} catch (BadCredentialsException ex) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}
}

JWT login wont work on deployed spring boot application

I'm following this JWT tutorial to secure my application.
I've ended up with the following WebSecurity configuration:
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private MyUserDetailsService userDetailsService;
private MyPasswordEncoder passwordEncoder;
public SecurityConfiguration(MyUserDetailsService userService) {
this.userDetailsService = userService;
this.passwordEncoder = new MyPasswordEncoder();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().authorizeRequests()
//SIGN_UP_URL = "/login";
.antMatchers(HttpMethod.GET, SIGN_UP_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.logout().permitAll();
http.logout(logout -> logout
.logoutUrl("/logout")
.addLogoutHandler(new SecurityContextLogoutHandler())
.permitAll()
.clearAuthentication(true));
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
and the following JWTAuthenticationFilter:
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
//SIGN_UP_URL= "/login"
setFilterProcessesUrl(MySettings.SIGN_UP_URL);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
User creds = new ObjectMapper()
.readValue(req.getInputStream(), User.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException {
String token;
token = JWT.create()
.withSubject(((User) auth.getPrincipal()).getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + MySettings.EXPIRATION_TIME))
.sign(Algorithm.HMAC512(MySettings.SECRET.getBytes()));
String body = ((User) auth.getPrincipal()).getUsername() + " " + token;
res.getWriter().write(body);
res.getWriter().flush();
}
Problem
At the moment, the app accepts GET requests on the /login URL when starting the app on my computer/localhost. I use postman and Im able to login and receive the token.
When I deploy the application to the server, the /login automatically replies with 403 forbidden.
The databases are equal.
What am I doing wrong?
References
Set custom login url in Spring Security UsernamePasswordAuthenticationFilter JWT authentication
https://www.freecodecamp.org/news/how-to-setup-jwt-authorization-and-authentication-in-spring/
try to add
#CrossOrigin(origins = "*", allowedHeaders = "*")
above your login Api in your controller

Angular 6 Http Interceptors, request headers not modified

I created an interceptor to add an authorization header to each request sent by the client, here is the code :
import { HttpInterceptor, HttpRequest, HttpHandler, HttpHeaderResponse, HttpSentEvent, HttpProgressEvent, HttpResponse, HttpUserEvent, HttpEvent, HttpHeaders } from "#angular/common/http";
import { Observable } from "rxjs";
import { Injectable } from "#angular/core";
#Injectable()
export class AuthenticationInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log(localStorage.getItem('jwtToken'));
if(localStorage.getItem('jwtToken')){
const request = req.clone({
setHeaders: {
Authorization: `bearer ${localStorage.getItem('jwtToken')}`
}
});
console.log(request.headers.get("Authorization"));
return next.handle(request);
}
return next.handle(req);
}
}
When a request is sent the function intercept is called and the authorization header is correclty set with the token value in the variable "request" as you can see there :
token console screenshot
But the authorization header doesn't appear in the request sent by my browser : network request headers and the backend cannot resolve the token.
Do you know why ?
Here is my spring config:
WebSecurityConfig.java
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public final static String AUTHORIZATION_HEADER = "Authorization";
#Autowired
UserService userService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(getProvider());
}
#Bean
public JwtTokenFilter jwtAuthenticationFilter() {
return new JwtTokenFilter();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/auth/**")
.permitAll()
.anyRequest()
.authenticated();
}
#Bean
public AuthenticationProvider getProvider() {
AppAuthProvider provider = new AppAuthProvider();
provider.setUserDetailsService(userService);
return provider;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
CorsConfig.java
#Configuration
public class CorsConfiguration {
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:4200")
.allowedMethods("*")
.allowedHeaders("*");
}
};
}
}
Your problem resides into backend services. For security reasons by default only some headers are accepted, the others are ignored.
To fix your problem you need to setup custom accepted headers. Authorization header, even if is like a standard for JWT, is considered a custom header.
I can give you an example of my Spring Security configuration:
#Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("GET");
config.addAllowedMethod("POST");
config.addAllowedMethod("PUT");
config.addAllowedMethod("DELETE");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
Note the line
config.addAllowedHeader("*");
That means that my REST services accept all possible headers sent by the client.
Obviously it's not a good configuration, you should limit allowed headers and other things to match your needs, as restricted as is possible.
Obviously if you don't use Spring Security you need to find the way to do the same thing with yout language/framework.
This is my SecurityConfig.java It's a bit different from yours.
Try this and let me know
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
#Autowired
private JwtTokenUtil jwtTokenUtil;
#Autowired
private WLUserDetailsService userDetailsService;
#Value("${jwt.header}")
private String tokenHeader;
#Value("${jwt.route.authentication.path}")
private String authenticationPath;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoderBean());
}
#Bean
public PasswordEncoder passwordEncoderBean() {
return new BCryptPasswordEncoder();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// we don't need CSRF because our token is invulnerable
.csrf().disable()
// TODO adjust CORS management
.cors().and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// don't create session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated();
// Custom JWT based security filter
JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(userDetailsService(), jwtTokenUtil, tokenHeader);
httpSecurity
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// // disable page caching
// httpSecurity
// .headers()
// .frameOptions().sameOrigin() // required to set for H2 else H2 Console will be blank.
// .cacheControl();
}
#Override
public void configure(WebSecurity web) {
// AuthenticationTokenFilter will ignore the below paths
web
.ignoring()
.antMatchers(
HttpMethod.POST,
authenticationPath
)
// allow anonymous resource requests
.and()
.ignoring()
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js"
);
}
#Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
// config.addExposedHeader("Authorization, x-xsrf-token, Access-Control-Allow-Headers, Origin, Accept, X-Requested-With, " +
// "Content-Type, Access-Control-Request-Method, Custom-Filter-Header");
config.addAllowedHeader("*");
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("GET");
config.addAllowedMethod("POST");
config.addAllowedMethod("PUT");
config.addAllowedMethod("DELETE");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}

spirng boot 2 jwt oauth2 + angular 5 can't get the JWT

I'm new working with spring boot and spring security and I'm trying to implement oauth2 to generate a JWT and used this token in an angular5 application, my situation is that after implementation I can get the token if a use postman or curl but when I use my web client in angular I can't get the token.
this is what I did.
My login method is angular
login(username: string, password: string ) {
const params: HttpParams = new HttpParams();
const headers: Headers = new Headers();
params.set('username', 'GDELOSSANTOS');
params.set('password', 'ADMIN');
params.set('client_id', 'ADMIN');
params.set('client_secret', 'ADMIN');
params.set('grant_type', 'password');
params.set('scope', '*');
headers.set('Content-Type', 'application/x-www-form-urlencoded');
return this.http.post(Constante.BACKEND_TOKEN_REQUEST, {headers}, {params} ).subscribe
(res => this.setSession);
}
My authorization server
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private static final Logger logger = LogManager.getLogger(AuthorizationServerConfig.class);
#Value("${security.oauth2.resource.id}")
private String resourceId;
#Autowired
#Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
#Override
public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
#Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
logger.traceEntry();
clients
.inMemory()
.withClient(ConstanteUtil.Seguridad.CLIEN_ID)
.secret(Seguridad.CLIENT_SECRET)
.authorizedGrantTypes(Seguridad.GRANT_TYPE_PASSWORD, Seguridad.AUTHORIZATION_CODE, Seguridad.REFRESH_TOKEN, Seguridad.IMPLICIT )
.authorities(UsusarioRoles.ROLE_ADMIN, UsusarioRoles.ROLE_USER)
.resourceIds(resourceId)
.scopes(Seguridad.SCOPE_READ, Seguridad.SCOPE_WRITE, Seguridad.TRUST)
.accessTokenValiditySeconds(Seguridad.ACCESS_TOKEN_VALIDITY_SECONDS).
refreshTokenValiditySeconds(Seguridad.FREFRESH_TOKEN_VALIDITY_SECONDS);
logger.info("Configuracion " + clients);
logger.traceExit();
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
#Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager);
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
}
My Resource Server
#Configuration
#EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private static final Logger logger = LogManager.getLogger(AuthorizationServerConfig.class);
#Value("${security.oauth2.resource.id}")
private String resourceId;
#Override
public void configure(final HttpSecurity http) throws Exception {
logger.traceEntry("Entrada configure");
// #formatter:off
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.authorizeRequests().anyRequest().permitAll();
logger.info("Ejecucion de metodo " + http);
// #formatter:on
}
#Override
public void configure(final ResourceServerSecurityConfigurer config) {
config.resourceId(resourceId).stateless(true); }
}
The WebSecurity
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final Logger logger = LogManager.getLogger(WebSecurityConfig.class);
#Autowired
#Resource(name = "UsuarioService")
private UserDetailsService userDetailsService;
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
logger.traceEntry("globalUserDetails", auth);
auth.userDetailsService(userDetailsService)
.passwordEncoder(encoder());
logger.traceExit("globalUserDetails", auth);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
logger.traceEntry();
logger.info("ejecutando configuracion " + http);
http.cors().disable()
.csrf().disable()
.anonymous().disable()
.authorizeRequests()
.antMatchers("/login", "/logout.do").permitAll()
.antMatchers("/**").authenticated()
.and().formLogin().loginPage("/login").permitAll()
.and().httpBasic();
logger.info("se ejecuto configuracion " + http);
}
#Bean
public BCryptPasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
#Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/auth/token").allowedOrigins("http://localhost:9000");
}
};
}
}
The implementation of loadUserDetail of UserDetailService
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.traceEntry("Iniciando loadUserByUsername");
/Here we are using dummy data, you need to load user data from
database or other third party application/
try {
Usuario usuario = findAllUsuarioRoleByName(username);
logger.info("Se encontro el usaurio " + usuario);
UserBuilder builder = null;
if (usuario != null) {
List<String> roles = new ArrayList<>();
Collection<UsuarioRole> usuarioRoleByUsuarioName = usuarioRoleRepository.findAllUsuarioRoleByUsuarioName(usuario.getNombreUsuario());
logger.info("Roles encontrados " + usuarioRoleByUsuarioName.size());
for(UsuarioRole usuarioRole : usuarioRoleByUsuarioName) {
roles.add(usuarioRole.getRole().getNombreRole());
}
String[] rolesArray = new String[roles.size()];
rolesArray = roles.toArray(rolesArray);
builder = org.springframework.security.core.userdetails.User.withUsername(username);
builder.password(new BCryptPasswordEncoder().encode(usuario.getClaveUsuario()));
for (String string : rolesArray) {
logger.debug("**** " + string);
}
builder.roles(rolesArray);
} else {
throw new UsernameNotFoundException("User not found.");
}
return builder.build();
}finally {
logger.traceExit("Finalizando loadUserByUsername");
}
}
Make the following adjustments to your angular code.
Pass client_id and client_secret through Authorization header.
Serialize the object before post (you can reference this answer).
login(username: string, password: string ) {
let body = {
username: 'GDELOSSANTOS',
password: 'ADMIN',
grant_type: 'password'
};
// Serialize body object
let bodySerialized = 'grant_type=password&password=ADMIN&username=GDELOSSANTOS';
let headers = new HttpHeaders()
.set('Content-Type', 'application/x-www-form-urlencoded')
.set('Authorization', 'Basic ' + btoa("ADMIN:ADMIN"));
return this.http.post(Constante.BACKEND_TOKEN_REQUEST,
bodySerialized,
{
headers: headers
}).subscribe(res => this.setSession);
}

Handling OPTIONS and CORS when using a sign in filter instead of controller

I've got an AbstractAuthenticationProcessingFilter that I'm using to handle POST requests at path /sign-in. CORS preflight requests are coming back 404 because there is no path that matches. This makes sense to me.
What I would like to know is if there is a way to inform Spring that there is a filter handling the POST (rather than a controller), so that Spring can dispatch the OPTIONS in the same way it would if a controller were handling the POST. Would it be bad practice to write a controller with one PostMapping? I'm not sure how that would behave since technically the filter handles the POST.
Thanks for your help!
Update
Here's my setup. I originally posted from my phone so wasn't able to add these details then. See below. To reiterate, there is no controller for /sign-in. The POST is handled by the JwtSignInFilter.
CORS Config
#EnableWebMvc
#Configuration
public class CorsConfig extends WebMvcConfigurerAdapter {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*") // TODO: Lock this down before deploying
.allowedHeaders("*")
.allowedMethods(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name())
.allowCredentials(true);
}
}
Security Config
#EnableWebSecurity
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public JwtSignInFilter signInFilter() throws Exception {
return new JwtSignInFilter(
new AntPathRequestMatcher("/sign-in", HttpMethod.POST.name()),
authenticationManager()
);
}
#Bean
public JwtAuthenticationFilter authFilter() {
return new JwtAuthenticationFilter();
}
#Autowired
private UserDetailsService userDetailsService;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers(HttpMethod.POST, "/sign-in").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(
signInFilter(),
UsernamePasswordAuthenticationFilter.class
)
.addFilterBefore(
authFilter(),
UsernamePasswordAuthenticationFilter.class
);
}
}
Sign In Filter
public class JwtSignInFilter extends AbstractAuthenticationProcessingFilter {
#Autowired
private TokenAuthenticationService tokenAuthService;
public JwtSignInFilter(RequestMatcher requestMatcher, AuthenticationManager authManager) {
super(requestMatcher);
setAuthenticationManager(authManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException, IOException, ServletException {
SignInRequest creds = new ObjectMapper().readValue(
req.getInputStream(),
SignInRequest.class
);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
creds.getEmail(),
creds.getPassword(),
emptyList()
)
);
}
#Override
protected void successfulAuthentication(
HttpServletRequest req,
HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
tokenAuthService.addAuthentication(res, auth.getName());
}
}
Authentication Filter
public class JwtAuthenticationFilter extends GenericFilterBean {
#Autowired
private TokenAuthenticationService tokenAuthService;
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
Authentication authentication = tokenAuthService.getAuthentication((HttpServletRequest)request);
SecurityContextHolder
.getContext()
.setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
Alright, finally found out how to fix this. After hours of tinkering and searching, I found that I needed to use a filter-based CORS configuration and then handle CORS preflights (OPTIONS requests) in the sign-in filter by simply returning 200 OK. The CORS filter will then add appropriate headers.
Updated configuration below (note that my CorsConfig is no longer needed, since we have a CORS filter in SecurityConfig, and JwtAuthenticationFilter is the same as before).
Security Config
#EnableWebSecurity
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*"); // TODO: lock down before deploying
config.addAllowedHeader("*");
config.addExposedHeader(HttpHeaders.AUTHORIZATION);
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
#Bean
public JwtSignInFilter signInFilter() throws Exception {
return new JwtSignInFilter(
new AntPathRequestMatcher("/sign-in"),
authenticationManager()
);
}
#Bean
public JwtAuthenticationFilter authFilter() {
return new JwtAuthenticationFilter();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/sign-in").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(
signInFilter(),
UsernamePasswordAuthenticationFilter.class
)
.addFilterBefore(
authFilter(),
UsernamePasswordAuthenticationFilter.class
);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
#Autowired
private UserDetailsService userDetailsService;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Sign In Filter
public class JwtSignInFilter extends AbstractAuthenticationProcessingFilter {
#Autowired
private TokenAuthenticationService tokenAuthService;
public JwtSignInFilter(RequestMatcher requestMatcher, AuthenticationManager authManager) {
super(requestMatcher);
setAuthenticationManager(authManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException, IOException, ServletException {
if (CorsUtils.isPreFlightRequest(req)) {
res.setStatus(HttpServletResponse.SC_OK);
return null;
}
if (!req.getMethod().equals(HttpMethod.POST.name())) {
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
return null;
}
SignInRequest creds = new ObjectMapper().readValue(
req.getInputStream(),
SignInRequest.class
);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
creds.getEmail(),
creds.getPassword(),
emptyList()
)
);
}
#Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException {
tokenAuthService.addAuthentication(res, auth.getName());
}
}

Resources