I'm currently developing an Integration Platform, but having some issues finding an idiotmatic way to set up the Spring Security Filter Chain.
Spring Security is used in 2 ways.
To authenticate the user of the application. (we'll call this the primary client registration)
To allow the user to authenticate with an external system.
In the second case, we saved their access/refresh tokens and use those later to interact with the external system(s) APIs and sync data.
When the user authenticated with the external system, the new token will overwrite the Security Context.
So far, the best solution we could come up with was overriding the SecurityContext Strategy.
class CompositeSecurityContextHolderStrategy(
val primaryClientRegistrationId: String = "primary"
) : SecurityContextHolderStrategy {
companion object {
val contextHolder = ThreadLocal<CompositeSecurityContext>()
}
override fun createEmptyContext(): SecurityContext = CompositeSecurityContext()
override fun clearContext() {
contextHolder.remove()
}
override fun getContext(): CompositeSecurityContext {
if (contextHolder.get() == null) {
contextHolder.set(createEmptyContext() as CompositeSecurityContext)
}
return contextHolder.get()
}
override fun setContext(context: SecurityContext) {
val currentContext = getContext()
val authentication = context.authentication
if (authentication is OAuth2AuthenticationToken) {
if (authentication.authorizedClientRegistrationId == primaryClientRegistrationId) currentContext.authentication = authentication
else currentContext.addAssociateAuthentication(authentication)
} else {
currentContext.authentication = context.authentication
}
}
}
class CompositeSecurityContext : SecurityContextImpl() {
val associatedAuthentications = mutableMapOf<String, Authentication>()
fun addAssociateAuthentication(authentication: Authentication) {
if (authentication is OAuth2AuthenticationToken) associatedAuthentications[authentication.authorizedClientRegistrationId] = authentication
}
fun retrieveAssociatedAuthentication(clientRegistrationId: String): Authentication? = associatedAuthentications[clientRegistrationId]
}
SecurityContextHolder.setContextHolderStrategy(CompositeSecurityContextHolderStrategy("primary"))
This works as intended. The Primary session is never overrwritten and associated token is available for consumption when our custom implementation of OAuth2AuthorizedClientService is invoked.
However, it still feels like a hack, but cannot find a better way to achieve authentication with token retrieval, without overwriting the security context.
I've seen a few older posts reference overriding OAuth2ClientAuthenticationProcessingFilter, but that is now deprecated in newer versions of SpringSecurity 5.
Any help will be appreciated.
Related
The app starts out not authenticated. When the user is logged in I need to send a auth token to my interceptor and notify my OkHttp Singleton about the change.
My OkHttp Singleton:
#Provides
#Singleton
fun provideOkHttp(interceptor: AuthInterceptor): OkHttpClient {
return OkHttpClient
.Builder()
.addInterceptor(interceptor)
.build()
}
My AuthInterceptor Singleton:
#Provides
#Singleton
fun provideAuthInterceptor(): AuthInterceptor = AuthInterceptor()
My AuthInterceptor Class: (I got it from here: https://github.com/apollographql/apollo-kotlin/issues/2030#issuecomment-596131870 not sure if I'm using it correctly)
class AuthInterceptor() : Interceptor {
// You can change authorization here
#get:Synchronized
#set:Synchronized
var tokenString: String? = null
override fun intercept(chain: Interceptor.Chain): Response {
return chain.proceed(chain.request().newBuilder()
.addHeader("Authorization", tokenString ?: "")
.build())
}
}
So I was hoping that I can simply call authInterceptor.tokenString = token in my repository before sending requests to my server but the OkHttp singleton doesn't care about that lol.
The Interceptor and OkHttp Instances gets created at app start and tokenString is and remains NULL.
Pretty new to Hilt, OkHttp and Interceptors so maybe a pretty obvious mistake.
P.S I'm using Apollo Android + MVVM + Clean Architecture
How can I pass the token to my Interceptor and OkHttp Singleton?
In my app I'm not using a singleton for the Interceptor, instead I have a class WebService that contains my OkHttpClient and ApolloClient, and an interceptor variable
Singleton:
#Singleton
#Provides
fun provideWebService(tokenStorage: TokenStorage) = WebService(tokenStorage)
WebService:
class WebService(tokenStorage: TokenStorage) {
// the Auth Interceptor
// if tokenStorage (basically encrypted shared preferences) has a token already, create an interceptor, otherwise it will be null
var interceptor: OkHttpAuthorizationInterceptor? = if (tokenStorage.activeToken != null) OkHttpAuthorizationInterceptor(tokenStorage.activeToken!!) else null
var okHttpClient = OkHttpClient.Builder().apply {
if (interceptor != null) {
this.addNetworkInterceptor(interceptor!!) // add the interceptor only if it's not null
}
}.build()
var apolloClient = ApolloClient
.Builder()
.serverUrl(Constant.ANILIST_URL)
.okHttpClient(okHttpClient) // add the OkHttpClient to ApolloClient
.build()
}
I've ran into the same problem and came up with the following solution: In my WebService class I created a method that rebuilds the clients with a new interceptor. I call this method when I need to change the token like when logging in or switching accounts
fun switchToken(token: String) {
// rebuild the OkHttpClient
okHttpClient = okHttpClient.newBuilder().apply {
networkInterceptors().removeAll {
it is OkHttpAuthorizationInterceptor // remove the interceptor from client if it already exists (useful for changing tokens)
}
interceptor = OkHttpAuthorizationInterceptor(token) // assign a new interceptor to the interceptor variable with the given token
addNetworkInterceptor(interceptor!!) // add the new interceptor to the client
}.build()
// rebuild the ApolloClient
apolloClient = apolloClient.newBuilder().okHttpClient(okHttpClient).build()
}
And this is how my interceptor looks like (same as yours more or less):
class OkHttpAuthorizationInterceptor(private val token: String) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response = chain.run {
proceed(
request().newBuilder()
.addHeader("Authorization", token)
.build()
)
}
}
(Edit: changed the code of Singletons, as it turns out even with this method providing the Apollo/OkHttpClient as a singleton won't refresh them so injecting the WebService and accessing the clients from there, like webService.okHttpClient is the only option, unless there is a better way to do this altogether)
I'm using a spring security in my app. I need it to secure two api's endpoints. Here is security config:
#Configuration
#EnableWebSecurity
class SecurityConfig(
private val authProps: AuthenticationProperties
) : WebSecurityConfigurerAdapter() {
override fun configure(auth: AuthenticationManagerBuilder) {
val encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
auth.inMemoryAuthentication()
.passwordEncoder(encoder)
.withUser(authProps.user)
.password(encoder.encode(authProps.password))
.roles("USER")
}
override fun configure(http: HttpSecurity) {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/firstapi/**", "/secondapi/**").authenticated()
.anyRequest().permitAll()
.and()
.httpBasic()
}
}
Also, i'm integrating with an external service, that uses oauth2, here it's restTemplate config -
#Configuration
class ExternalServiceConfiguration {
#Bean("externalProperties")
#ConfigurationProperties("http.external.api")
fun externalHttpClientConfig() = HttpClientProperties()
#Bean("externalDetails")
#ConfigurationProperties("http.external.api.security.oauth2")
fun externalOAuth2Details() = ResourceOwnerPasswordResourceDetails()
#Bean("externalRestTemplate")
fun externalClientRestTemplate(
#Qualifier("externalProperties") externalHttpClientProperties: HttpClientProperties,
#Qualifier("externalDetails") externalOAuth2Details: ResourceOwnerPasswordResourceDetails,
customizerProviders: ObjectProvider<RestTemplateCustomizer>,
objectMapper: ObjectMapper,
): RestTemplate {
val template = OAuth2RestTemplate(externalOAuth2Details).apply {
messageConverters = listOf(MappingJackson2HttpMessageConverter(objectMapper))
requestFactory = requestFactory(externalHttpClientProperties)
errorHandler = IntegrationResponseErrorHandler()
}
customizerProviders.orderedStream().forEach { it.customize(template) }
return template
}
}
Somehow spring-security-oauth clashes with spring-security. When i try to obtain a token i'm failing at lib class method:
AccessTokenProviderChain.obtainAccessToken(...)
, because instead of null i have an AnonymousAuthenticationToken authentication at context, when it calls
SecurityContextHolder.getContext().getAuthentication()
So spring security merges anonymous context somehow, i am not sure when and how, so that it affects external calls.
Could anyone help me with advice, how can i find the collision???
I am writing a Kotlin app and using Firebase for authentication.
As onActivityResult is now depraceted, I am trying to migrate my app to use registerForActivityResult. I have a link to Google account feature, that starts with the Google sign-in flow, as shown here. My code:
private fun initGoogleSignInClient() =
activity?.let {
// Configure Google Sign In
val gso =
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build()
// Build a GoogleSignInClient with the options specified by gso.
viewModel.googleSignInClient = GoogleSignIn.getClient(it, gso)
}
private fun showLinkWithGoogle() =
startActivityForResult(viewModel.googleSignInClient.signInIntent, RC_LINK_GOOGLE)
Where initGoogleSignInClient is called in the fragment's onCreateView, and showLinkWithGoogle is called when the user taps the button on the screen. This workes perfectly.
I looked for an example using registerForActivityResult, and the best one I found was at the bottom of this page. I added this code:
private val linkWithGoogle =
registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) {
viewModel.handleGoogleResult(it.data)
}
private fun showLinkWithGoogle() =
linkWithGoogle.launch(IntentSenderRequest.Builder(viewModel.googleSignInClient.signInIntent))
But realized that IntentSenderRequest.Builder needs an IntentSender and not an Intent. I haven't found any example of how to build an IntentSender from an Intent, nor a way to get one from my GoogleSignInClient.
Could anyone please provide a full example of using registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult())?
Thank you very much!
For this use-case, you don't need an ActivityResultContracts of type StartIntentSenderForResult but one of type StartActivityForResult. Here is an example (since you did not provide your full implementation):
Fragment
private val googleRegisterResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
result.checkResultAndExecute {
val task = GoogleSignIn.getSignedInAccountFromIntent(data)
val account = task.getResult(ApiException::class.java)
loginViewModel.onEvent(LoginRegistrationEvent.SignInWithGoogle(account))
}.onFailure { e -> toast("Error: ${e.message}") }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myGoogleSignInButton.setOnClickListener {
googleRegisterResult.launch(viewModel.googleSignInClient.signInIntent)
}
}
Then, in your viewmodel, you can handle the login as you would usually do, the only difference is, that you no longer need an RC_SIGN_IN
ViewModel Example
class YourViewModel : ViewModel() {
fun onEvent(event: LoginRegistrationEvent) {
when(event) {
is LoginRegistrationEvent.SignInWithGoogle -> {
viewModelScope.launch {
val credential = GoogleAuthProvider.getCredential(event.account.idToken)
Firebase.auth.signInWithCredential(credential).await()
}
}
}
}
}
To make my life easier, I created an extension function, that checks, if the login was successful and then executes a block of code (in this case, getting the account), while caching any exceptions. Futhermore, inside your block, you have access to an instance of ActivityResult as this:
inline fun ActivityResult.checkResultAndExecute(block: ActivityResult.() -> Unit) =
if (resultCode == Activity.RESULT_OK) runCatching(block)
else Result.failure(Exception("Something went wrong"))
I've tried to use WebClient with LoadBalancerExchangeFilterFunction:
WebClient config:
#Bean
public WebClient myWebClient(final LoadBalancerExchangeFilterFunction lbFunction) {
return WebClient.builder()
.filter(lbFunction)
.defaultHeader(ACCEPT, APPLICATION_JSON_VALUE)
.defaultHeader(CONTENT_ENCODING, APPLICATION_JSON_VALUE)
.build();
}
Then I've noticed that calls to underlying service are not properly load balanced - there is constant difference of RPS on each instance.
Then I've tried to move back to RestTemplate. And it's working fine.
Config for RestTemplate:
private static final int CONNECT_TIMEOUT_MILLIS = 18 * DateTimeConstants.MILLIS_PER_SECOND;
private static final int READ_TIMEOUT_MILLIS = 18 * DateTimeConstants.MILLIS_PER_SECOND;
#LoadBalanced
#Bean
public RestTemplate restTemplateSearch(final RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
.errorHandler(errorHandlerSearch())
.requestFactory(this::bufferedClientHttpRequestFactory)
.build();
}
private ClientHttpRequestFactory bufferedClientHttpRequestFactory() {
final SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(CONNECT_TIMEOUT_MILLIS);
requestFactory.setReadTimeout(READ_TIMEOUT_MILLIS);
return new BufferingClientHttpRequestFactory(requestFactory);
}
private ResponseErrorHandler errorHandlerSearch() {
return new DefaultResponseErrorHandler() {
#Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return response.getStatusCode().is5xxServerError();
}
};
}
Load balancing using WebClient config up to 11:25, then switching back to RestTemplate:
Is there a reason why there is such difference and how I can use WebClient to have same amount of RPS on each instance? Clue might be that older instances are getting more requests than new ones.
I've tried bit of debugging and same (defaults like ZoneAwareLoadBalancer) logic is being called.
I did simple POC and everything works exactly the same with web client and rest template for default configuration.
Rest server code:
#SpringBootApplication
internal class RestServerApplication
fun main(args: Array<String>) {
runApplication<RestServerApplication>(*args)
}
class BeansInitializer : ApplicationContextInitializer<GenericApplicationContext> {
override fun initialize(context: GenericApplicationContext) {
serverBeans().initialize(context)
}
}
fun serverBeans() = beans {
bean("serverRoutes") {
PingRoutes(ref()).router()
}
bean<PingHandler>()
}
internal class PingRoutes(private val pingHandler: PingHandler) {
fun router() = router {
GET("/api/ping", pingHandler::ping)
}
}
class PingHandler(private val env: Environment) {
fun ping(serverRequest: ServerRequest): Mono<ServerResponse> {
return Mono
.fromCallable {
// sleap added to simulate some work
Thread.sleep(2000)
}
.subscribeOn(elastic())
.flatMap {
ServerResponse.ok()
.syncBody("pong-${env["HOSTNAME"]}-${env["server.port"]}")
}
}
}
In application.yaml add:
context.initializer.classes: com.lbpoc.server.BeansInitializer
Dependencies in gradle:
implementation('org.springframework.boot:spring-boot-starter-webflux')
Rest client code:
#SpringBootApplication
internal class RestClientApplication {
#Bean
#LoadBalanced
fun webClientBuilder(): WebClient.Builder {
return WebClient.builder()
}
#Bean
#LoadBalanced
fun restTemplate() = RestTemplateBuilder().build()
}
fun main(args: Array<String>) {
runApplication<RestClientApplication>(*args)
}
class BeansInitializer : ApplicationContextInitializer<GenericApplicationContext> {
override fun initialize(context: GenericApplicationContext) {
clientBeans().initialize(context)
}
}
fun clientBeans() = beans {
bean("clientRoutes") {
PingRoutes(ref()).router()
}
bean<PingHandlerWithWebClient>()
bean<PingHandlerWithRestTemplate>()
}
internal class PingRoutes(private val pingHandlerWithWebClient: PingHandlerWithWebClient) {
fun router() = org.springframework.web.reactive.function.server.router {
GET("/api/ping", pingHandlerWithWebClient::ping)
}
}
class PingHandlerWithWebClient(private val webClientBuilder: WebClient.Builder) {
fun ping(serverRequest: ServerRequest) = webClientBuilder.build()
.get()
.uri("http://rest-server-poc/api/ping")
.retrieve()
.bodyToMono(String::class.java)
.onErrorReturn(TimeoutException::class.java, "Read/write timeout")
.flatMap {
ServerResponse.ok().syncBody(it)
}
}
class PingHandlerWithRestTemplate(private val restTemplate: RestTemplate) {
fun ping(serverRequest: ServerRequest) = Mono.fromCallable {
restTemplate.getForEntity("http://rest-server-poc/api/ping", String::class.java)
}.flatMap {
ServerResponse.ok().syncBody(it.body!!)
}
}
In application.yaml add:
context.initializer.classes: com.lbpoc.client.BeansInitializer
spring:
application:
name: rest-client-poc-for-load-balancing
logging:
level.org.springframework.cloud: DEBUG
level.com.netflix.loadbalancer: DEBUG
rest-server-poc:
listOfServers: localhost:8081,localhost:8082
Dependencies in gradle:
implementation('org.springframework.boot:spring-boot-starter-webflux')
implementation('org.springframework.cloud:spring-cloud-starter-netflix-ribbon')
You can try it with two or more instances for server and it works exactly the same with web client and rest template.
Ribbon use by default zoneAwareLoadBalancer and if you have only one zone all instances for server will be registered in "unknown" zone.
You might have a problem with keeping connections by web client. Web client reuse the same connection in multiple requests, rest template do not do that. If you have some kind of proxy between your client and server then you might have a problem with reusing connections by web client. To verify it you can modify web client bean like this and run tests:
#Bean
#LoadBalanced
fun webClientBuilder(): WebClient.Builder {
return WebClient.builder()
.clientConnector(ReactorClientHttpConnector { options ->
options
.compression(true)
.afterNettyContextInit { ctx ->
ctx.markPersistent(false)
}
})
}
Of course it's not a good solution for production but doing that you can check if you have a problem with configuration inside your client application or maybe problem is outside, something between your client and server. E.g. if you are using kubernetes and register your services in service discovery using server node IP address then every call to such service will go though kube-proxy load balancer and will be (by default round robin will be used) routed to some pod for that service.
You have to configure Ribbon config to modify the load balancing behavior (please read below).
By default (which you have found yourself) the ZoneAwareLoadBalancer is being used. In the source code for ZoneAwareLoadBalancer we read:
(highlighted by me are some mechanics which could result in the RPS pattern you see):
The key metric used to measure the zone condition is Average Active Requests, which is aggregated per rest client per zone. It is the
total outstanding requests in a zone divided by number of available targeted instances (excluding circuit breaker tripped instances).
This metric is very effective when timeout occurs slowly on a bad zone.
The LoadBalancer will calculate and examine zone stats of all available zones. If the Average Active Requests for any zone has reached a configured threshold, this zone will be dropped from the active server list. In case more than one zone has reached the threshold, the zone with the most active requests per server will be dropped.
Once the the worst zone is dropped, a zone will be chosen among the rest with the probability proportional to its number of instances.
If your traffic is being served by one zone (perhaps the same box?) then you might get into some additionally confusing situations.
Please also note that without using LoadBallancedFilterFunction the average RPS is the same as when you use it (on the graph all lines converge to the median line) after the change, so globally looking both load balancing strategies consume the same amount of available bandwidth but in a different manner.
To modify your Ribbon client settings, try following:
public class RibbonConfig {
#Autowired
IClientConfig ribbonClientConfig;
#Bean
public IPing ribbonPing (IClientConfig config) {
return new PingUrl();//default is a NoOpPing
}
#Bean
public IRule ribbonRule(IClientConfig config) {
return new AvailabilityFilteringRule(); // here override the default ZoneAvoidanceRule
}
}
Then don't forget to globally define your Ribbon client config:
#SpringBootApplication
#RibbonClient(name = "app", configuration = RibbonConfig.class)
public class App {
//...
}
Hope this helps!
I am using Webflux in Spring Boot 2.0.3.RELEASE to create REST API. With that implementation, I customize and use the webSessionManager as below.
#EnableWebFluxSecurity
#Configuration
class SecurityConfiguration {
#Bean
fun webSessionManager(): WebSessionManager {
return DefaultWebSessionManager().apply {
sessionIdResolver = HeaderWebSessionIdResolver().apply {
headerName = "X-Sample"
}
sessionStore = InMemoryWebSessionStore()
}
}
// ...
}
And in order to test the REST API, I created a test code as follows. (addUser and signin are extension functions.)
#RunWith(SpringRunner::class)
#SpringBootTest
#AutoConfigureWebTestClient
#FixMethodOrder(MethodSorters.NAME_ASCENDING)
class UserTests {
#Autowired
private lateinit var client: WebTestClient
#Test
fun testGetUserInfo() {
client.addUser(defaultUser)
val sessionKey = client.signin(defaultUser)
client.get().uri(userPath)
.header("X-Sample", sessionKey)
.exchange()
.expectStatus().isOk
.expectBody()
.jsonInStrict("""
{
"user": {
"mail_address": "user#example.com"
}
}
""".trimIndent())
}
// ...
}
The test failed. It is refused by authorization. However, if I start the server and run it from curl it will succeed in the authorization.
After investigating the cause, it turned out that org.springframework.test.web.reactive.server.AbstractMockServerSpec set webSessionManager to DefaultWebSessionManager. Default is used, not the webSessionManager I customized. For this reason, it could not get the session ID.
AbstractMockServerSpec.java#L41
AbstractMockServerSpec.java#L72-L78
How can I change the webSessionManager of AbstractMockServerSpec?
Also, I think that it is better to have the following implementation, what do you think?
abstract class AbstractMockServerSpec<B extends WebTestClient.MockServerSpec<B>>
implements WebTestClient.MockServerSpec<B> {
// ...
private WebSessionManager sessionManager = null;
// ...
#Override
public WebTestClient.Builder configureClient() {
WebHttpHandlerBuilder builder = initHttpHandlerBuilder();
builder.filters(theFilters -> theFilters.addAll(0, this.filters));
if (this.sessionManager != null) {
builder.sessionManager(this.sessionManager);
}
this.configurers.forEach(configurer -> configurer.beforeServerCreated(builder));
return new DefaultWebTestClientBuilder(builder);
}
// ...
}
Spring Framework's AbstractMockServerSpec is providing a method to customize the WebSessionManager already.
Thanks for opening SPR-17094, this problem will be solved with that ticket - the AbstractMockServerSpec is already looking into the application context for infrastructure bits, this should check for a WebSessionManager as well.