Spring boot OAuth 2 resource server + JWT filter backup authentication method - spring-boot

I'm working on a spring boot project with an OAuth2 resource server authentication method, the access token is verified on the authorization server.
This application provides also an endpoint to get a token signed by itself.
I would like to provide an authentication backup method with a WebFilter that verify the token signature if OAuth2 method failed like this :
#Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http.invoke {
csrf { disable() }
authorizeExchange {
authorize("/actuator/**", permitAll)
authorize("/api/login", permitAll)
authorize("/v3/api-docs", permitAll)
authorize("/**", authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = oauth2JwtConverter
}
}
addFilterAt(JWTFilter(tokenService), SecurityWebFiltersOrder.AUTHENTICATION)
}
}
My JWTFilter :
class JWTFilter(
private val tokenService: TokenService
) : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
return resolveToken(exchange.request)
?.takeIf { tokenService.validateToken(it) }
?.let { tokenService.getAuthentication(it) }
?.let { chain.filter(exchange).contextWrite(ReactiveSecurityContextHolder.withAuthentication(it)) }
?: chain.filter(exchange)
}
private fun resolveToken(request: ServerHttpRequest): String? {
return request.headers[AUTHORIZATION_HEADER]
?.get(0)
?.takeIf { it.startsWith("Bearer ") }
?.substring(7)
}
}
The problem is that the authentication context set by the filter is ignored if OAuth2 failed.
How can i manage that ?

Related

Too many arguments for public open fun http()

I'm following this tutorial to implement a authentication system in Kotlin using Spring Boot
The code works nicely until this part:
#Configuration
#EnableWebSecurity
class MultiHttpSecurityConfig {
#Bean
public fun userDetailsService(): UserDetailsService {
val users: User.UserBuilder = User.withDefaultPasswordEncoder()
val manager = InMemoryUserDetailsManager()
manager.createUser(users.username("user").password("password").roles("USER").build())
manager.createUser(users.username("admin").password("password").roles("USER","ADMIN").build())
return manager
but in the next part i receive the following error and cant import anything:
#Order(1)
#Bean
open fun apiFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
securityMatcher("/api/**")
authorizeRequests {
authorize(anyRequest, hasRole("ADMIN"))
}
httpBasic { }
}
return http.build()
}
#Bean
open fun formLoginFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
formLogin { }
}
return http.build()
}
}
Am I missing something? Is something necessary besides Spring Security dependency?
Try to add import :
import org.springframework.security.config.web.server.invoke

How to configure bearer-only = true when Using spring-boot-starter-oauth2-client

I am trying to setup o auth2 authentication in spring cloud gateway for my rest apis using keycloak. keyclcoak redirects my request to login page while passing access token as bearer token. In many places I found solution for this is to set bearer-only = true in keycloak adapter. where to set this while using spring-boot-starter-oauth2-client. I cannot use keycloak-spring-boot-starter to set this in application.yml
Thanks
I had the same question as you. Not having found an answer, I developed a filter with a light keycloak Client that calls the Endpoint instrospect of keycloak to validate the token
the client:
public class KeycloakClient {
private final KeycloakConfigurationProperties kcProperties;
private final WebClient client;
public KeycloakClient(final KeycloakConfigurationProperties keycloakConfigurationProperties) {
this.kcProperties = keycloakConfigurationProperties;
this.client = WebClient.builder()
.baseUrl(keycloakConfigurationProperties.getAuth_server_url())
.filter(logRequest())
.build();
}
public Mono<Boolean> validateBearerToken(String bearerToken) {
//todo: improve error management
return prepareAndRunRequest(bearerToken).retrieve()
.bodyToMono(KeycloakValidationResponse.class)
.flatMap(response -> Mono.just(response.getActive()))
.onErrorResume(WebClientResponseException.class,
ex -> Mono.just(false));
}
private WebClient.RequestHeadersSpec<?> prepareAndRunRequest(String bearerToken) {
return client.post()
.uri(uriBuilder -> uriBuilder.path("/auth/realms/")
.path(kcProperties.getRealm())
.path("/protocol/openid-connect/token/introspect")
.build())
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromFormData("client_id", kcProperties.getResource())
.with("client_secret", kcProperties.getCredentials_secret())
the filter:
public class ValidationTokenGatewayFilterFactory extends AbstractGatewayFilterFactory<ValidationTokenGatewayFilterFactory.Config> {
private final KeycloakClient client;
public ValidationTokenGatewayFilterFactory(KeycloakClient client) {
super(Config.class);
this.client = client;
}
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String token = exchange.getRequest()
.getHeaders()
.get(AUTHORIZATION)
.get(0);
token = token.substring(7);
log.trace("-- ValidationToken(): token={}", token);
return client.validateBearerToken(token)
.flatMap(validated -> {
if (validated) {
log.debug("-- ValidationToken(): Token valid");
return chain.filter(exchange);
} else {
log.debug("-- ValidationToken(): Token invalid");
exchange.getResponse()
.setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse()
.setComplete();
}
});
};
}
public static class Config {
}
}
You can find a full sample here: https://gitlab.com/-/snippets/2105967

Spring-Security: Getting 401s with CORS Preflights (despite http.cors())

For starters i want to secure a part of my rest-api with basic auth.
When I try to access endpoints from a react client, I keep getting 401's in the preflight requests.
I tried to follow this guide without success:
https://www.baeldung.com/spring-security-cors-preflight
i'm not sure if this is part of the problem, but another part can only be accessed with certain custom http headers.
I'm using Method Security:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = false)
class MethodSecurityConfig : GlobalMethodSecurityConfiguration() {
override fun customMethodSecurityMetadataSource(): MethodSecurityMetadataSource = SecurityMetadataSource()
override fun accessDecisionManager(): AccessDecisionManager = super.accessDecisionManager().apply {
this as AbstractAccessDecisionManager
decisionVoters.add(PrivilegeVoter())
}
}
And this is my Security config:
#Configuration
#EnableJpaAuditing(auditorAwareRef = "auditorProvider")
class SecurityConfig : WebSecurityConfigurerAdapter() {
private val deviceRequestHeaderName: String = "X-DEVICE-ID"
private val platformRequestHeaderName: String = "X-PLATFORM-ID"
#Autowired
lateinit var users: AppUserRepository
#Autowired
lateinit var backendUsers: BackendUserRepository
#Autowired
lateinit var roles: RoleRepository
val authManager by lazy { authenticationManager() }
private val authProvider by lazy {
PreAuthenticatedAuthenticationProvider().apply {
setPreAuthenticatedUserDetailsService {
val authId = it.principal as UserAuthId
if (authId.deviceId == null) throw UsernameNotFoundException("No device-id to search for.")
if (authId.platform == null) throw UsernameNotFoundException("Platform not specified.")
val platform = try {
ApplicationPlatform.valueOf(authId.platform)
} catch (e: IllegalArgumentException) {
throw UsernameNotFoundException("Unknown platform ${authId.platform}.")
}
val existingUser = users.findByUserDeviceIdAndPlatform(authId.deviceId, platform)
if (existingUser != null) return#setPreAuthenticatedUserDetailsService existingUser
users.save(AppUser(authId.deviceId, platform, roles))
}
}
}
val passwordEncoder by lazy { BCryptPasswordEncoder() }
private val deviceIdFilter by lazy {
HeaderFieldAuthFilter(deviceRequestHeaderName, platformRequestHeaderName).apply {
setAuthenticationManager(authManager)
}
}
override fun configure(auth: AuthenticationManagerBuilder) = auth {
authenticationProvider(authProvider)
val userDetailsService = BackendUserDetailsService(backendUsers)
userDetailsService(userDetailsService).passwordEncoder(passwordEncoder)
}
override fun configure(http: HttpSecurity) = http {
session {
sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
exceptionHandling()
addFilter(deviceIdFilter)
authorizeRequests().anyRequest().authenticated()
csrf().disable()
httpBasic()
cors().configurationSource { request ->
CorsConfiguration().apply {
allowedOrigins = listOf(ALL)
allowedMethods = listOf(GET, POST, DELETE, PUT, OPTIONS).map { it.name }
allowedHeaders = listOf(ALL)
allowCredentials = true
maxAge = 3600
}
}
}
#Bean
fun auditorProvider(): AuditorAware<User> = AuditorAware<User> {
val authentication = SecurityContextHolder.getContext().authentication
val user = authentication.run { if (isAuthenticated) principal as? User else null }
return#AuditorAware Optional.ofNullable(user)
}
}
I could solve by manually exclude the preflight requests from authentication.
adding this
antMatchers(OPTIONS, "/**").permitAll()
to the authorizeRequests() configuration accomplishes that.
Note that Options is a direct reference to the HttpMethod enum value, imported like this
import org.springframework.http.HttpMethod.*
Stackoverflow posts that helped me get there:
Response for preflight has invalid HTTP status code 401 - Spring
How to configure CORS in a Spring Boot + Spring Security application?
Originally I had assumed, that this should have been handled by the cors configuration - which it was apparently not.
To enable CORS for a single rest endpoint you can annotate it with:
#CrossOrigin
To allow for CORS for all endpoints you can have a bean like so:
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("GET", "POST", "PUT", "DELETE").allowedOrigins("*")
.allowedHeaders("*");
}
};

Spring reactive security

I am trying for reactive security and the unauthenticated calls are not going to auth manager.
#Configuration
#EnableWebFluxSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig{
#Autowired
private WebAuthenticationManager authenticationManager;
#Autowired
private ServerSecurityContextRepository securityContextRepository;
private static final String[] AUTH_WHITELIST = {
"/login/**",
"/logout/**",
"/authorize/**",
"/favicon.ico",
};
#Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
return http.exceptionHandling().authenticationEntryPoint((swe, e) -> {
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
});
}).accessDeniedHandler((swe, e) -> {
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
});
}).and().csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(authenticationManager)
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.authorizeExchange().pathMatchers(HttpMethod.OPTIONS).permitAll()
.pathMatchers(AUTH_WHITELIST).permitAll()
.anyExchange().authenticated().and().build();
}
#Bean
public PBKDF2Encoder passwordEncoder() {
return new PBKDF2Encoder();
}
}
WebAuthentication Manager,
#Component
public class WebAuthenticationManager implements ReactiveAuthenticationManager {
#Autowired
private JWTUtil jwtUtil;
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
String authToken = authentication.getCredentials().toString();
String username;
try {
username = jwtUtil.getUsernameFromToken(authToken);
} catch (Exception e) {
username = null;
}
if (username != null && jwtUtil.validateToken(authToken)) {
Claims claims = jwtUtil.getAllClaimsFromToken(authToken);
List<String> rolesMap = claims.get("role", List.class);
List<Role> roles = new ArrayList<>();
for (String rolemap : rolesMap) {
roles.add(Role.valueOf(rolemap));
}
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
username,
null,
roles.stream().map(authority -> new SimpleGrantedAuthority(authority.name())).collect(Collectors.toList())
);
return Mono.just(auth);
} else {
return Mono.empty();
}
}
}
Here, I have registered my WebAuthentication manager in Securityconfig. But, still the unauthenticated calls are not going to the WebAuthenticationManager.
It is expected to go to AuthenticationManager when the protected URL's are hit. For ex,
http://localhost:8080/api/v1/user.
Not sure, why the calls are not going to AuthManager.
In non reactive, we have OncePerRequestFilter and the auth is being taken care over there. Not sure, how to implement the same for reactive.
You disabled all authentication mechanisms hence there is nothing calling your authentication manager. As you mentioned, you can implement authentication flow through filters.
Sample implementation of authentication filter:
#Bean
public AuthenticationWebFilter webFilter() {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(authenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(tokenAuthenticationConverter());
authenticationWebFilter.setRequiresAuthenticationMatcher(serverWebExchangeMatcher());
authenticationWebFilter.setSecurityContextRepository(NoOpServerSecurityContextRepository.getInstance());
return authenticationWebFilter;
}
Then add this filter to ServerHttpSecurity: http.addFilterBefore(webFilter(),SecurityWebFiltersOrder.HTTP_BASIC)
Then finally your authentication manager will be called.
You must provide few additional things to make it working.
Matcher to check if Authorization header is added to request:
#Bean
public ServerWebExchangeMatcher serverWebExchangeMatcher() {
return exchange -> {
Mono<ServerHttpRequest> request = Mono.just(exchange).map(ServerWebExchange::getRequest);
return request.map(ServerHttpRequest::getHeaders)
.filter(h -> h.containsKey(HttpHeaders.AUTHORIZATION))
.flatMap($ -> ServerWebExchangeMatcher.MatchResult.match())
.switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());
};
}
Token converter responsible for getting token from request and preparing basic AbstractAuthenticationToken
#Bean
public ServerAuthenticationConverter tokenAuthenticationConverter() {
return exchange -> Mono.justOrEmpty(exchange)
.map(e -> getTokenFromRequest(e))
.filter(token -> !StringUtils.isEmpty(token))
.map(token -> getAuthentication(token));
}
I intentionally omitted implementation of getTokenFromRequest and getAuthentication because there is a lot of examples available.

Spring Boot 2 OIDC (OAuth2) client / resource server not propagating the access token in the WebClient

Sample project available on Github
I have successfully configured two Spring Boot 2 application2 as client/resource servers against Keycloak and SSO between them is fine.
Besides, I am testing authenticated REST calls to one another, propagating the access token as an Authorization: Bearer ACCESS_TOKEN header.
After starting Keycloak and the applications I access either http://localhost:8181/resource-server1 or http://localhost:8282/resource-server-2 and authenticate in the Keycloak login page. The HomeController uses a WebClient to invoke the HelloRestController /rest/hello endpoint of the other resource server.
#Controller
class HomeController(private val webClient: WebClient) {
#GetMapping
fun home(httpSession: HttpSession,
#RegisteredOAuth2AuthorizedClient authorizedClient: OAuth2AuthorizedClient,
#AuthenticationPrincipal oauth2User: OAuth2User): String {
val authentication = SecurityContextHolder.getContext().authentication
println(authentication)
val pair = webClient.get().uri("http://localhost:8282/resource-server-2/rest/hello").retrieve()
.bodyToMono(Pair::class.java)
.block()
return "home"
}
}
This call returns a 302 since the request is not authenticated (it's not propagating the access token):
2019-12-25 14:09:03.737 DEBUG 8322 --- [nio-8181-exec-5] o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is anonymous); redirecting to authentication entry point
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233) ~[spring-security-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]
OAuth2Configuration:
#Configuration
class OAuth2Config : WebSecurityConfigurerAdapter() {
#Bean
fun webClient(): WebClient {
return WebClient.builder()
.filter(ServletBearerExchangeFilterFunction())
.build()
}
#Bean
fun clientRegistrationRepository(): ClientRegistrationRepository {
return InMemoryClientRegistrationRepository(keycloakClientRegistration())
}
private fun keycloakClientRegistration(): ClientRegistration {
val clientRegistration = ClientRegistration
.withRegistrationId("resource-server-1")
.clientId("resource-server-1")
.clientSecret("c00670cc-8546-4d5f-946e-2a0e998b9d7f")
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
.scope("openid", "profile", "email", "address", "phone")
.authorizationUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/auth")
.tokenUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/token")
.userInfoUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.jwkSetUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/certs")
.clientName("Keycloak")
.providerConfigurationMetadata(mapOf("end_session_endpoint" to "http://localhost:8080/auth/realms/insight/protocol/openid-connect/logout"))
.build()
return clientRegistration
}
override fun configure(http: HttpSecurity) {
http.authorizeRequests { authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
}.oauth2Login(withDefaults())
.logout { logout ->
logout.logoutSuccessHandler(oidcLogoutSuccessHandler())
}
}
private fun oidcLogoutSuccessHandler(): LogoutSuccessHandler? {
val oidcLogoutSuccessHandler = OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository())
oidcLogoutSuccessHandler.setPostLogoutRedirectUri(URI.create("http://localhost:8181/resource-server-1"))
return oidcLogoutSuccessHandler
}
}
As you can see I'm setting a ServletBearerExchangeFilterFunction in the WebClient. This is what I've seen debugging:
The SubscriberContext isn't setting anything because authentication.getCredentials() instanceof AbstractOAuth2Token is false. Actually it is just a String:
public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
...
#Override
public Object getCredentials() {
// Credentials are never exposed (by the Provider) for an OAuth2 User
return "";
}
What's the problem here? How can I automate the propagation of the token?
There doesn't seem to be an out of the box solution for pure OAuth2/OIDC login applications, I've created a Github issue for this.
In the meantime, I've created a specific ServletBearerExchangeFilterFunction that retrieves the access token from the OAuth2AuthorizedClientRepository.
This is my custom solution:
#Autowired
lateinit var oAuth2AuthorizedClientRepository: OAuth2AuthorizedClientRepository
#Bean
fun webClient(): WebClient {
val servletBearerExchangeFilterFunction = ServletBearerExchangeFilterFunction("resource-server-1", oAuth2AuthorizedClientRepository)
return WebClient.builder()
.filter(servletBearerExchangeFilterFunction)
.build()
}
...
private fun keycloakClientRegistration(): ClientRegistration {
return ClientRegistration
.withRegistrationId("resource-server-1")
...
const val SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY = "org.springframework.security.SECURITY_CONTEXT_ATTRIBUTES"
class ServletBearerExchangeFilterFunction(private val clientRegistrationId: String,
private val oAuth2AuthorizedClientRepository: OAuth2AuthorizedClientRepository?) : ExchangeFilterFunction {
/**
* {#inheritDoc}
*/
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
return oauth2Token()
.map { token: AbstractOAuth2Token -> bearer(request, token) }
.defaultIfEmpty(request)
.flatMap { request: ClientRequest -> next.exchange(request) }
}
private fun oauth2Token(): Mono<AbstractOAuth2Token> {
return Mono.subscriberContext()
.flatMap { ctx: Context -> currentAuthentication(ctx) }
.map { authentication ->
val authorizedClient = oAuth2AuthorizedClientRepository?.loadAuthorizedClient<OAuth2AuthorizedClient>(clientRegistrationId, authentication, null)
if (authorizedClient != null) {
authorizedClient.accessToken
} else {
Unit
}
}
.filter { it != null }
.cast(AbstractOAuth2Token::class.java)
}
private fun currentAuthentication(ctx: Context): Mono<Authentication> {
return Mono.justOrEmpty(getAttribute(ctx, Authentication::class.java))
}
private fun <T> getAttribute(ctx: Context, clazz: Class<T>): T? { // NOTE: SecurityReactorContextConfiguration.SecurityReactorContextSubscriber adds this key
if (!ctx.hasKey(SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY)) {
return null
}
val attributes: Map<Class<T>, T> = ctx[SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY]
return attributes[clazz]
}
private fun bearer(request: ClientRequest, token: AbstractOAuth2Token): ClientRequest {
return ClientRequest.from(request)
.headers { headers: HttpHeaders -> headers.setBearerAuth(token.tokenValue) }
.build()
}
}

Resources