Getting 401 error status from swagger response when providing authentication using oauth2 - spring-boot

I am trying to setup swagger with spring security and oauth2.0 in my application.I have created all the files required for this , i am able to get the access_token and also able to authenticate using that token when trying with postman.
but now i need to do the same using swagger and i am getting status 401
and error: "Auth ErrorTypeError: Failed to fetch" on swagger.
also when i am hitting the authenticate button i am getting method type options.
i have used cors filter in my application and have removed the security for HttpMethod.options.
Why i am getting this error? what i have missed in this?
authorazation server code:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().and().cors().and()
.authorizeRequests()
.antMatchers("/login","/logout.do").permitAll()
.antMatchers(HttpMethod.OPTIONS,
"/oauth/token").permitAll()
// .antMatchers("/**").authenticated()
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login.do")
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/login")
.and()
.logout()
.logoutRequestMatcher(new
AntPathRequestMatcher("/logout.do"))
.and()
.userDetailsService(userDetailsServiceBean());
}
Resource server:
#Override
public void configure(HttpSecurity http) throws Exception{
http
.csrf().disable().authorizeRequests().antMatchers("/v2/api-docs", "/configuration/ui", "/swagger-resources", "/configuration/security",
"/swagger-ui.html", "/webjars/**", "/swagger-resources/configuration/ui", "/swagger-ui.html",
"/swagger-resources/configuration/security")
.permitAll();
http.csrf().and()
//.addFilterBefore(new CORSFilter(), ChannelProcessingFilter.class)
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/oauth/token").permitAll()
.antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
.antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')");
}
swagger configuration:
#Bean
public Docket edstrumentsApi() {
List<ResponseMessage> list = new ArrayList<>();
list.add(new ResponseMessageBuilder().code(500).message("500 message")
.responseModel(new ModelRef("Result")).build());
list.add(new ResponseMessageBuilder().code(401).message("Unauthorized")
.responseModel(new ModelRef("Result")).build());
list.add(new ResponseMessageBuilder().code(406).message("Not Acceptable")
.responseModel(new ModelRef("Result")).build());
return new Docket(DocumentationType.SWAGGER_2)
.select().apis(RequestHandlerSelectors.basePackage("backend"))
.paths(PathSelectors.any())
.build()
.directModelSubstitute(LocalDate.class, String.class)
.genericModelSubstitutes(ResponseEntity.class)
.securitySchemes(Collections.singletonList(securitySchema()))
.securityContexts(Collections.singletonList(securityContext())).pathMapping("/")
.useDefaultResponseMessages(false).apiInfo(apiInfo()).globalResponseMessage(RequestMethod.GET, list)
.globalResponseMessage(RequestMethod.POST, list)
.apiInfo(apiInfo());
}
#Bean
public OAuth securitySchema() {
List<AuthorizationScope> authorizationScopeList = new ArrayList();
authorizationScopeList.add(new AuthorizationScope(Constants.SCOPE_READ, "read all"));
authorizationScopeList.add(new AuthorizationScope(Constants.SCOPE_TRUST, "trust all"));
authorizationScopeList.add(new AuthorizationScope(Constants.SCOPE_WRITE, "access all"));
List<GrantType> grantTypes = new ArrayList();
GrantType creGrant = new ResourceOwnerPasswordCredentialsGrant( "http://localhost:8081/oauth/token");
grantTypes.add(creGrant);
return new OAuth("oauth2schema", authorizationScopeList, grantTypes);
}

yes i found the cause of the error.
i was creating the cors filter at the resource side instead of authorisation server.
That's why i was getting unable to get the option request on the server.

Related

Migrating from WebSecurityConfigurerAdapter to SecurityFilterChain

Here is my working security conf before migration :
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/auth/**")
.antMatchers("/swagger-ui/**")
.antMatchers("/swagger-ui.html")
.antMatchers("/swagger-resources/**")
.antMatchers("/v2/api-docs/**")
.antMatchers("/v3/api-docs/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedPortalRoleConverter);
http
.csrf().disable()
.cors()
.and()
.exceptionHandling()
.authenticationEntryPoint(new AuthenticationFallbackEntryPoint())
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests(authorize -> authorize.anyRequest().authenticated())
.oauth2ResourceServer()
.jwt().jwtAuthenticationConverter(jwtAuthenticationConverter);
}
And here is my Security chain config after migration :
#Bean
#Order(1)
public SecurityFilterChain ignorePathsSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.antMatchers(
"/auth/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v3/api-docs/**")
.permitAll());
return http.build();
}
#Bean
#Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, GrantedPortalRoleConverter grantedPortalRoleConverter) throws Exception {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedPortalRoleConverter);
http
.csrf().disable()
.cors(Customizer.withDefaults())
.exceptionHandling(configurer -> configurer.authenticationEntryPoint(new AuthenticationFallbackEntryPoint()))
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.oauth2ResourceServer(configurer -> configurer.jwt().jwtAuthenticationConverter(jwtAuthenticationConverter));
return http.build();
}
With the original conf, when I call a random non existing path :
#Test
void should_not_authenticate_or_return_not_found() throws Exception {
logger.info("should_not_authenticate_or_return_not_found");
mvc.perform(get("/toto/tata"))
.andExpect(status().isUnauthorized());
}
I get :
15:44:00.230 [main] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Failed to authorize filter invocation [GET /toto/tata] with attributes [authenticated]
With the new conf, I'm just getting HTTP 404, what am I missing here please ? I can't see any difference and debug logs don't show much.
Here is the first line of log missing using the non working conf :
16:24:58.651 [main] DEBUG o.s.s.w.a.e.ExpressionBasedFilterInvocationSecurityMetadataSource - Adding web access control expression [authenticated] for any request
But in both logs, I can see (2 lines of this for the new conf since there are 2 security chains) :
o.s.s.web.DefaultSecurityFilterChain - Will secure any request with (...)
Explanation
When you have multiple SecurityFilterChains, you have to specify a request matcher, otherwise all requests will be processed by the first SecurityFilterChain, annotated with #Order(1), and never reach the second SecurityFilterChain, annotated with #Order(2).
In the code you shared above, this means configuring .requestMatchers() in ignorePathsSecurityFilterChain:
#Bean
#Order(1)
public SecurityFilterChain ignorePathsSecurityFilterChain(HttpSecurity http) throws Exception {
http
.requestMatchers(requests -> requests // add this block
.antMatchers(
"/auth/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v3/api-docs/**")
)
.authorizeHttpRequests(authorize -> authorize
.antMatchers(
"/auth/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v3/api-docs/**")
.permitAll());
return http.build();
}
This means that only the requests matching /auth/**, /swagger-ui/** etc will be processed by ignorePathsSecurityFilterChain, while the rest of the requests will move on to defaultSecurityFilterChain.
To understand the difference between requestMatchers and authorizeHttpRequests you can check out this StackOverflow question.
Solution
An even better option is to combine the SecurityFilterChains into a single one. I don't see any reason why you would separate them in this case.
The resulting configuration would be:
#Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, GrantedPortalRoleConverter grantedPortalRoleConverter) throws Exception {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedPortalRoleConverter);
http
.authorizeHttpRequests(authorize -> authorize
.antMatchers(
"/auth/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v3/api-docs/**")
.permitAll()
.anyRequest().authenticated()
)
.csrf().disable()
.cors(Customizer.withDefaults())
.exceptionHandling(configurer -> configurer.authenticationEntryPoint(new AuthenticationFallbackEntryPoint()))
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(configurer -> configurer.jwt().jwtAuthenticationConverter(jwtAuthenticationConverter));
return http.build();
}
Alternative
Alternatively you can use a WebSecurityCustomizer to ignore certain endpoints:
#Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers(
"/auth/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v3/api-docs/**");
}
Then you would use defaultSecurityFilterChain as your only SecurityFilterChain.

Jhipster public api encounter Unauthorized status 401

My Project needs to set up a callback API to receive a biometric punchlog. I exposed a public API [.antMatchers("/api/daily-time-records").permitAll()] but I encounter error when I test it in postman
"type": "https://www.jhipster.tech/problem/problem-with-message",
"title": "Unauthorized",
"status": 401,
"detail": "Full authentication is required to access this resource",
"path": "/api/daily-time-records%0A",
"message": "error.http.401"
Resource
#PostMapping("/daily-time-records")
public ResponseEntity<DailyTimeRecordDTO> createDailyTimeRecord(#Valid #RequestBody DailyTimeRecordDTO dailyTimeRecordDTO)
throws URISyntaxException {
log.debug("REST request to save DailyTimeRecord : {}", dailyTimeRecordDTO);
if (dailyTimeRecordDTO.getId() != null) {
throw new BadRequestAlertException("A new dailyTimeRecord cannot already have an ID", ENTITY_NAME, "idexists");
}
DailyTimeRecordDTO result = dailyTimeRecordService.save(dailyTimeRecordDTO);
return ResponseEntity
.created(new URI("/api/daily-time-records/" + result.getId()))
.headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, result.getId().toString()))
.body(result);
}
Security Configuration
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.csrf()
.disable()
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.headers()
.contentSecurityPolicy(jHipsterProperties.getSecurity().getContentSecurityPolicy())
.and()
.referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
.and()
.permissionsPolicy().policy("camera=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=()")
.and()
.frameOptions()
.deny()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/daily-time-records").permitAll()
.antMatchers("/api/authenticate").permitAll()
.antMatchers("/api/register").permitAll()
.antMatchers("/api/activate").permitAll()
.antMatchers("/api/account/reset-password/init").permitAll()
.antMatchers("/api/account/reset-password/finish").permitAll()
.antMatchers("/api/admin/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/health/**").permitAll()
.antMatchers("/management/info").permitAll()
.antMatchers("/management/prometheus").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.and()
.httpBasic()
.and()
.apply(securityConfigurerAdapter());
// #formatter:on
}

Sleuth in Webflux Spring Security

I have a webflux app and need to have sleuth context in my authentication logs (move from DefaultWebFilterChain to SecurityWebFilterChain).
I have tried to add manually in my security chain:
public GwApiSecurityConfig(Tracer tracer, HttpServerHandler httpServerHandler, CurrentTraceContext currentTraceContext){
this.tracer = tracer;
this.httpServerHandler = httpServerHandler;
this.currentTraceContext = currentTraceContext;
}
#Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
return http
.csrf().disable()
.authorizeExchange()
.pathMatchers(HttpMethod.GET,"/swagger", "/v3/**", "/webjars/**", "/actuator/**").permitAll()
.anyExchange().authenticated()
.and()
.httpBasic()
.authenticationEntryPoint((exchange, exception) -> Mono.error(new GwException(HttpStatus.UNAUTHORIZED, GwError.AUTHENTICATION)))
.and()
.formLogin().disable()
.redirectToHttps()
.and()
.addFilterBefore(new TraceWebFilter(tracer, httpServerHandler, currentTraceContext), SecurityWebFiltersOrder.HTTP_BASIC)
.build();
}
But I got an error:
09:11:06.117 ERROR[reactor-http-nio-2] [,] GwApiErrorHandler - null
java.lang.NullPointerException: null
at org.springframework.cloud.sleuth.instrument.web.TraceWebFilter.spanFromContextRetriever(TraceWebFilter.java:139)
Also I checked this property:
spring.sleuth.web.filter-order=1
but I think that only affect the DefaultWebFilterChain order, no SecurityWebFilterChain.
And how to remove the filter from DefaultWebFilterChain to avoid filtering twice?
Any ideas?
Thanks!

Spring Security - fallback to basic authentication if Kerberos fails

How can I enable fallback to basic auth if Kerberos authentication fails (e.g. client is not on the domain)? With the configuration below no browser authentication window appears and the following exceptions are thrown:
org.springframework.security.authentication.BadCredentialsException: Kerberos validation not successful
org.ietf.jgss.GSSException: Defective token detected (Mechanism level: GSSHeader did not find the right tag)
Relevant part of my WebSecurityConfigurerAdapter implementation:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(spnegoEntryPoint())
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.logout()
.permitAll()
.and()
.addFilterBefore(
spnegoAuthenticationProcessingFilter(),
BasicAuthenticationFilter.class);
}
#Bean
public SpnegoEntryPoint spnegoEntryPoint() {
return new SpnegoEntryPoint("/");
}
#Bean
public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter() {
SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
try {
filter.setAuthenticationManager(authenticationManagerBean());
} catch (Exception e) {
log.error("Failed to set AuthenticationManager on SpnegoAuthenticationProcessingFilter.", e);
}
return filter;
}

Spring Security custom authentication failure handler redirect with parameter

I have a problem with Spring Security authentication failure handler redirect with parameter.
In security config when I use
failureUrl("/login.html?error=true")
it works. But when I use custom authentication failure handler (as shown below), it always returns: url/login.html
getRedirectStrategy().sendRedirect(request, response, "/login.html?error=true");
or
response.sendRedirect(request.getContextPath() + "/login.html?error=true");
I don't know whats wrong. Why does it not show the parameter ?error=true?
Info: I am using Spring + JSF + Hibernate + Spring Security
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.usernameParameter("j_username")
.passwordParameter("j_password")
.loginProcessingUrl("/j_spring_security_check")
.failureHandler(customAuthenticationFailureHandler)// .failureUrl("/login.html?error=true")//.successHandler(authSuccsessHandler)
.defaultSuccessUrl("/dashboard.html")
.permitAll()
.and()
.logout()
.invalidateHttpSession(true)
.logoutSuccessUrl("/")
.permitAll()
.and()
.exceptionHandling()
.accessDeniedPage("/access.html")
.and()
.headers()
.defaultsDisabled()
.frameOptions()
.sameOrigin()
.cacheControl();
http
.csrf().disable();
}
This is custom authentication failure handler:
#Component
public class CustomAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
getRedirectStrategy().sendRedirect(request, response, "/login.html?error=true");
}
}
I will change parameter for some cases.
You didn't allow anonymous access to URL /login.html?error=true, so you are redirected to the login page (/login.html).
AbstractAuthenticationFilterConfigurer#permitAll allows access (for anyone) to failure URL but not for custom failure handler:
Ensures the urls for failureUrl(String) as well as for the HttpSecurityBuilder, the getLoginPage() and getLoginProcessingUrl() are granted access to any user.
You have to allow access explicitly with AbstractRequestMatcherRegistry#antMatchers:
Maps a List of AntPathRequestMatcher instances that do not care which HttpMethod is used.
and ExpressionUrlAuthorizationConfigurer.AuthorizedUrl#permitAll:
Specify that URLs are allowed by anyone.
You don't have to allow the exact URL /login.html?error=true, because AntPathRequestMatcher ignores the query string:
Matcher which compares a pre-defined ant-style pattern against the URL ( servletPath + pathInfo) of an HttpServletRequest. The query string of the URL is ignored and matching is case-insensitive or case-sensitive depending on the arguments passed into the constructor.
Your modified configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.usernameParameter("j_username")
.passwordParameter("j_password")
.loginProcessingUrl("/j_spring_security_check")
.failureHandler(customAuthenticationFailureHandler)// .failureUrl("/login.html?error=true")//.successHandler(authSuccsessHandler)
.defaultSuccessUrl("/dashboard.html")
.permitAll()
.and()
.logout()
.invalidateHttpSession(true)
.logoutSuccessUrl("/")
.permitAll()
.and()
.exceptionHandling()
.accessDeniedPage("/access.html")
.and()
.headers()
.defaultsDisabled()
.frameOptions()
.sameOrigin()
.cacheControl();
http
.csrf().disable();
}
In the case of OAuth token failure, I am getting below response, which is inconsistent with app response style.
{
"error": "invalid_token",
"error_description": "Invalid access token: 4cbc6f1c-4d47-44bd-89bc-92a8c86d88dbsdfsdfs"
}
I just wanted to use common response object for the consistency.
Following approach worked for me.
Build your resource server with your custom entry-point object
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.authenticationEntryPoint(new CustomOAuth2AuthenticationEntryPoint());
}
and here is your custom entry point
public class CustomOAuth2AuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint{
public CustomOAuth2AuthenticationEntryPoint() {
super.setExceptionTranslator(new CustomOAuth2WebResponseExceptionTranslator());
}
}
here is your custom WebResponseExceptionTranslator, In my case I have just used a replica of DefaultWebResponseExceptionTranslator and rewritten handleOAuth2Exception method.
CustomOAuth2WebResponseExceptionTranslator implements WebResponseExceptionTranslator<Response> {
....
.....
private ResponseEntity<Response> handleOAuth2Exception(OAuth2Exception e) throws IOException {
int status = e.getHttpErrorCode();
HttpHeaders headers = new HttpHeaders();
headers.set("Cache-Control", "no-store");
headers.set("Pragma", "no-cache");
if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) {
headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary()));
}
ResponseEntity<Response> response =new ResponseEntity<>(new Response().message(e.getMessage()).status(StatusEnum.ERROR)
.errorType(e.getClass().getName()), HttpStatus.UNAUTHORIZED);
return response;
}
Result looks like
{
"status": "error",
"message": "Invalid access token: 4cbc6f1c-4d47-44bd-89bc-92a8c86d88dbsdfsdfs",
"error_type": "org.springframework.security.oauth2.common.exceptions.InvalidTokenException"
}

Resources