I am trying to use Spring Boot RSocket with Security using JWT Tokens. It is giving me an Access Denied error with no other useful information to help debug with?
Access Denied.
ApplicationErrorException (0x201): Access Denied at
app//io.rsocket.exceptions.Exceptions.from(Exceptions.java:76) at
app//io.rsocket.core.RSocketRequester.handleFrame(RSocketRequester.java:261)
at
app//io.rsocket.core.RSocketRequester.handleIncomingFrames(RSocketRequester.java:211)
at
app//reactor.core.publisher.LambdaSubscriber.onNext(LambdaSubscriber.java:160)
at
app//io.rsocket.core.ClientServerInputMultiplexer$InternalDuplexConnection.onNext(ClientServerInputMultiplexer.java:248)
at
app//io.rsocket.core.ClientServerInputMultiplexer.onNext(ClientServerInputMultiplexer.java:129)
at
app//io.rsocket.core.ClientServerInputMultiplexer.onNext(ClientServerInputMultiplexer.java:48)
at
app//reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
at
app//reactor.netty.channel.FluxReceive.onInboundNext(FluxReceive.java:364)
at
app//reactor.netty.channel.ChannelOperations.onInboundNext(ChannelOperations.java:404)
at
app//reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:725)
at
app//reactor.netty.http.client.WebsocketClientOperations.onInboundNext(WebsocketClientOperations.java:161)
at
app//reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:93)
at
app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at
app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at
app//io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at
app//io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:327)
at
app//io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:299)
at
app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at
app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at
app//io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
at
app//io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
at
app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
at
app//io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
at
app//io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
at
app//io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
at
app//io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:722)
at
app//io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:658)
at
app//io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:584)
at app//io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:496)
at
app//io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
at
app//io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at
app//io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base#11.0.14/java.lang.Thread.run(Thread.java:834)
Security Config file
#Configuration
#EnableRSocketSecurity
#EnableReactiveMethodSecurity
class SecurityConfig {
#Bean
fun authorization(rsocketSecurity: RSocketSecurity): PayloadSocketAcceptorInterceptor {
val security: RSocketSecurity =
rsocketSecurity.authorizePayload { authorize: RSocketSecurity.AuthorizePayloadsSpec ->
authorize
.anyRequest().authenticated()
.anyExchange().permitAll()
}
.jwt { jwtSpec ->
jwtSpec.authenticationManager(jwtReactiveAuthenticationManager(jwtDecoder()))
}
return security.build()
}
#Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return TokenUtils.jwtAccessTokenDecoder()
}
#Bean
fun jwtReactiveAuthenticationManager(decoder: ReactiveJwtDecoder): JwtReactiveAuthenticationManager {
val converter = JwtAuthenticationConverter()
val authoritiesConverter = JwtGrantedAuthoritiesConverter()
authoritiesConverter.setAuthorityPrefix("ROLE_")
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter)
val manager = JwtReactiveAuthenticationManager(decoder)
manager.setJwtAuthenticationConverter(ReactiveJwtAuthenticationConverterAdapter(converter))
return manager
}
#Bean
fun rsocketMessageHandler() = RSocketMessageHandler() .apply {
argumentResolverConfigurer.addCustomResolver(AuthenticationPrincipalArgumentResolver())
routeMatcher = PathPatternRouteMatcher()
rSocketStrategies = rsocketStrategies()
}
#Bean
fun rsocketStrategies() = RSocketStrategies.builder()
.routeMatcher(PathPatternRouteMatcher())
.build()
}
Message Controller file
#MessageMapping("api.v1.messages")
#Controller
class MessageController {
#MessageMapping("stream")
suspend fun receive(
#Payload inboundMessages: Flow<String>,
#AuthenticationPrincipal jwt: String
) {
println("MessageController: jwt: $jwt")
println("MessageController: inbound message: " + inboundMessages.first())
}
}
Testing using MessageControllerTest file
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MessageControllerTest(
#Autowired val rsocketBuilder: RSocketRequester.Builder,
#LocalServerPort val serverPort: Int
) {
#ExperimentalTime
#ExperimentalCoroutinesApi
#Test
fun `test that messages API streams latest messages`() {
val admin = HelloUser(userId = "9527", password = "password", role = HelloRole.ADMIN)
val token: UserToken = TokenUtils.generateAccessToken(admin)!!
val authenticationMimeType: MimeType =
MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string)
runBlocking {
val rSocketRequester = rsocketBuilder.websocket(URI("ws://localhost:${serverPort}/rsocket"))
launch {
rSocketRequester.route("api.v1.messages.stream")
.metadata(token.token!!, authenticationMimeType)
.dataWithType(flow {
emit(
"Hey from test class"
)
})
.retrieveFlow<Void>()
.collect()
}
}
}
}
I've add the rest of the code example I did to GitHub https://github.com/CJMobileApps/rsocket-jwt-security-example
I figured it out. RSocket currently has a bug or just bad documentation. The MimeType for JWTs BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE says that it is deprecated and to use MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string) instead, however that does not work.
When passing tokens continue using/use BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE along with the token string.
.metadata(token.token!!, BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE)
Related
My objective is to hash a password that is passed from the command line argument and save it to the application.yaml. Then when the user authenticates using the password to match it and allow users in.
I am passing the value like this:
mvn spring-boot:run -Dspring-boot.run.arguments="--user.password=admin"
The fragment of the security section in application.yaml looks like this:
security:
user:
name: admin
password: ${user.password}
I have written some password encoder classes in my spring boot application.
class MyPasswordEncoder : PasswordEncoder {
private val logger = LoggerFactory.getLogger(javaClass)
private val argon2PasswordEncoder = Argon2PasswordEncoder(SALT_LENGTH, HASH_LENGTH, PARALLELISM, MEMORY_USE, ITERATIONS)
/**
* Encrypt the password (Algorithm Used: Argon2id)
*/
override fun encode(decryptedPassword: CharSequence?): String {
val hashedPassword : String = argon2PasswordEncoder.encode(decryptedPassword)
logger.debug("Password Encoding Successful!")
return hashedPassword
}
/**
* Matches the password with the encoded password (Algorithm Used: Argon2id)
*/
override fun matches(rawPassword: CharSequence?, encodedPassword: String?): Boolean {
return argon2PasswordEncoder.matches(rawPassword, encodedPassword)
}
}
And the Delegation class:
class MyPasswordDelegation {
fun createDelegatingPasswordEncoder(): PasswordEncoder {
val idForEncode = "myEncoder"
val encoders: MutableMap<String, PasswordEncoder> = mutableMapOf()
encoders[idForEncode] = NCaaSPasswordEncoder()
return DelegatingPasswordEncoder(idForEncode, encoders)
}
}
My Security Config class looks like this:
#Configuration
#EnableWebSecurity
class SecurityConfig {
#Value("\${spring.security.user.name}")
private val userName: String? = null
#Value("\${spring.security.user.password}")
private val password: String? = null
#Autowired
lateinit var appAuthenticationEntryPoint: AppAuthenticationEntryPoint
#Bean
fun passwordEncoder(): MyPasswordEncoder {
return MyPasswordEncoder()
}
#Bean
#Throws(Exception::class)
fun userDetailsService(): InMemoryUserDetailsManager? {
val userDetails : UserDetails = User.withUsername(userName).password(passwordEncoder().encode(password).roles("USER").build()
return InMemoryUserDetailsManager(userDetails)
}
#Throws(Exception::class)
#Bean
fun filterChain(httpSecurity : HttpSecurity): SecurityFilterChain {
httpSecurity.csrf().disable()
// Allow only HTTPS Requests
httpSecurity.requiresChannel {
channel -> channel.anyRequest().requiresSecure()
}.authorizeRequests {
authorize -> authorize.anyRequest().fullyAuthenticated().and().httpBasic().and().exceptionHandling().authenticationEntryPoint(appAuthenticationEntryPoint)
}
return httpSecurity.build()
}
}
But when I try to authenticate using admin/admin, I get the following error:
Failed to process authentication request
org.springframework.security.authentication.BadCredentialsException: Bad credentials
at org.springframework.security.authentication.dao.DaoAuthenticationProvider.additionalAuthenticationChecks(DaoAuthenticationProvider.java:79)
at org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:147)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:201)
at org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilterInternal(BasicAuthenticationFilter.java:172)
I know I am going wrong somewhere. But since this is all new to me, I am not able to quite put it all together.
Any help would be appreciated. Thanks!
I am trying to connect to the google cloud platform pub/sub from behind a proxy.
Using Spring lib "org.springframework.cloud:spring-cloud-gcp-starter-pubsub" which uses the google pub sub client, which in order to make the pull call for the subscription uses gRPC calls.
In order to set the proxy I can use GRPC_PROXY_EXP environment variable but I also need credentials to go through this proxy.
I've tries several approaches, including configuring the org.springframework.cloud.gcp.pubsub.support.SubscriberFactory similar to here https://medium.com/google-cloud/accessing-google-cloud-apis-though-a-proxy-fe46658b5f2a
#Bean
fun inboundQuotationsChannelAdapter(
#Qualifier("inboundQuotationsMessageChannel") quotationsChannel: MessageChannel,
mpProperties: ConfigurationProperties,
defaultSubscriberFactory: SubscriberFactory
): PubSubInboundChannelAdapter {
Authenticator.setDefault(ProxyAuthenticator("ala","bala"))
val proxySubscriberFactory: DefaultSubscriberFactory = defaultSubscriberFactory as DefaultSubscriberFactory
proxySubscriberFactory.setCredentialsProvider(ProxyCredentialsProvider(getCredentials()))
val headers = mutableMapOf(Pair("Proxy-Authorization", getBasicAuth()))
proxySubscriberFactory.setChannelProvider(SubscriberStubSettings.defaultGrpcTransportProviderBuilder()
.setHeaderProvider(FixedHeaderProvider.create(headers)).build())
val proxySubscriberTemplate = PubSubSubscriberTemplate(proxySubscriberFactory)
val adapter = PubSubInboundChannelAdapter(proxySubscriberTemplate, mpProperties.gcp.quotationSubscription)
adapter.outputChannel = quotationsChannel
adapter.ackMode = AckMode.MANUAL
adapter.payloadType = ActivityStateChanged::class.java
return adapter
}
#Throws(IOException::class)
fun getCredentials(): GoogleCredentials {
val httpTransportFactory = getHttpTransportFactory(
"127.0.0.1", 3128, "ala", "bala"
)
return GoogleCredentials.getApplicationDefault(httpTransportFactory)
}
fun getHttpTransportFactory(
proxyHost: String?,
proxyPort: Int,
proxyUsername: String?,
proxyPassword: String?
): HttpTransportFactory? {
val proxyHostDetails = HttpHost(proxyHost, proxyPort)
val httpRoutePlanner: HttpRoutePlanner = DefaultProxyRoutePlanner(proxyHostDetails)
val credentialsProvider: CredentialsProvider = BasicCredentialsProvider()
credentialsProvider.setCredentials(
AuthScope(proxyHostDetails.hostName, proxyHostDetails.port),
UsernamePasswordCredentials(proxyUsername, proxyPassword)
)
val httpClient: HttpClient = ApacheHttpTransport.newDefaultHttpClientBuilder()
.setRoutePlanner(httpRoutePlanner)
.setProxyAuthenticationStrategy(ProxyAuthenticationStrategy.INSTANCE)
.setDefaultCredentialsProvider(credentialsProvider)
.setDefaultRequestConfig(
RequestConfig.custom()
.setAuthenticationEnabled(true)
.setProxy(proxyHostDetails)
.build())
.addInterceptorLast(HttpRequestInterceptor { request, context ->
request.addHeader(
BasicHeader(
"Proxy-Authorization",
getBasicAuth()
)
)
})
.build()
val httpTransport: HttpTransport = ApacheHttpTransport(httpClient)
return HttpTransportFactory { httpTransport }
}
Also tried using #GRpcGlobalInterceptor from LogNet
https://github.com/LogNet/grpc-spring-boot-starter
#Bean
#GRpcGlobalInterceptor
fun globalServerInterceptor(): ServerInterceptor {
return GrpcServerInterceptor(configurationProperties)
}
#Bean
#GRpcGlobalInterceptor
fun globalClientInterceptor(): ClientInterceptor {
return GrpcClientInterceptor(configurationProperties)
}
with
class GrpcClientInterceptor(private val configurationProperties: ConfigurationProperties) :
ClientInterceptor {
private val proxyUsername = configurationProperties.http.proxy.username
private val proxyPassword = configurationProperties.http.proxy.password
private val proxyHeaderKey = Metadata.Key.of("Proxy-Authorization", Metadata.ASCII_STRING_MARSHALLER)
private fun getBasicAuth(): String {
val usernameAndPassword = "$proxyUsername:$proxyPassword"
val encoded = Base64.getEncoder().encodeToString(usernameAndPassword.toByteArray())
return "Basic $encoded"
}
override fun <ReqT, RespT> interceptCall(
method: MethodDescriptor<ReqT, RespT>?,
callOptions: CallOptions?, next: Channel
): ClientCall<ReqT, RespT>? {
return object : SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
override fun start(responseListener: Listener<RespT>?, headers: Metadata) {
headers.put(proxyHeaderKey, getBasicAuth())
super.start(object : SimpleForwardingClientCallListener<RespT>(responseListener) {
override fun onHeaders(headers: Metadata) {
super.onHeaders(headers)
}
}, headers)
}
}
}
}
class GrpcServerInterceptor(private val configurationProperties: ConfigurationProperties) :
ServerInterceptor {
private val proxyUsername = configurationProperties.http.proxy.username
private val proxyPassword = configurationProperties.http.proxy.password
override fun <ReqT : Any?, RespT : Any?> interceptCall(
call: ServerCall<ReqT, RespT>?,
headers: io.grpc.Metadata?,
next: ServerCallHandler<ReqT, RespT>?
): ServerCall.Listener<ReqT> {
val proxyHeaderKey = Metadata.Key.of("Proxy-Authorization", Metadata.ASCII_STRING_MARSHALLER)
if (!headers!!.containsKey(proxyHeaderKey))
headers!!.put(proxyHeaderKey, getBasicAuth())
return next!!.startCall(call, headers)
}
private fun getBasicAuth(): String {
val usernameAndPassword = "$proxyUsername:$proxyPassword"
val encoded = Base64.getEncoder().encodeToString(usernameAndPassword.toByteArray())
return "Basic $encoded"
}
}
(also tried the annotation directly on class level - ofc it did not work)
Also tried using #GrpcGlobalServerInterceptor and #GrpcGlobalClientInterceptor from https://github.com/yidongnan/grpc-spring-boot-starter/tree/v2.12.0.RELEASE but this dependency crashed the app entirely
Here you can find an example on how to set the proxy credentials from the Java API documentation to configuring-a-proxy ;
public CloudTasksClient getService() throws IOException {
TransportChannelProvider transportChannelProvider =
CloudTasksStubSettings.defaultGrpcTransportProviderBuilder()
.setChannelConfigurator(
new ApiFunction<ManagedChannelBuilder, ManagedChannelBuilder>() {
#Override
public ManagedChannelBuilder apply(ManagedChannelBuilder managedChannelBuilder) {
return managedChannelBuilder.proxyDetector(
new ProxyDetector() {
#Nullable
#Override
public ProxiedSocketAddress proxyFor(SocketAddress socketAddress)
throws IOException {
return HttpConnectProxiedSocketAddress.newBuilder()
.setUsername(PROXY_USERNAME)
.setPassword(PROXY_PASSWORD)
.setProxyAddress(new InetSocketAddress(PROXY_HOST, PROXY_PORT))
.setTargetAddress((InetSocketAddress) socketAddress)
.build();
}
});
}
})
.build();
CloudTasksSettings cloudTasksSettings =
CloudTasksSettings.newBuilder()
.setTransportChannelProvider(transportChannelProvider)
.build();
return CloudTasksClient.create(cloudTasksSettings);
}
Take into consideration the note where it says that gRPC proxy is currently experimental.
There are two clients that communicate with google. One using "http" and one using "gRPC". (https instead of http is also possible)
The Solution Dan posted is just the solution for gRPC.
Here my Solution for http by using an Apache-Http-Client.
try (InputStream jsonCredentialsFile = json-file as InputStream) {
GoogleCredentials credentials = GoogleCredentials.fromStream(jsonCredentialsFile, new HttpTransportFactory() {
public HttpTransport create() {
DefaultHttpClient httpClient = new DefaultHttpClient();
httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, new HttpHost("myProxyHost", myProxyPort, "http"));
// DefaultHttpClient is deprecated but this recommended solution does not work in this context.
// org.apache.http.impl.client.InternalHttpClient.getParams() always throws UnsupportedOperationException
// HttpClientBuilder builder = HttpClientBuilder.create();
// if (StringUtils.isBlank(proxyServer) || proxyport < 0) {
// builder.setProxy(new HttpHost(proxyServer, proxyport, "http"));
// }
// CloseableHttpClient httpClient = builder.build();
return new ApacheHttpTransport(httpClient);
}
})
.createScoped(Lists.newArrayList("https://www.googleapis.com/auth/cloud-platform"));
DocumentProcessorServiceSettings.Builder dpssBuilder = DocumentProcessorServiceSettings.newBuilder()
.setEndpoint(endpoint)
.setCredentialsProvider(FixedCredentialsProvider.create(credentials));
dpssBuilder.setTransportChannelProvider(transportProvider);
DocumentProcessorServiceClient client = DocumentProcessorServiceClient.create(dpssBuilder.build());
// use the client
}
Expected result: connecting to the RSocket websocket based endpoint from front end that includes authentication information as metadata will trigger PayloadSocketAcceptorInterceptor's jwt authentication system.
Actual result: This only happens when sending responseRequest from JS frontend, fails when doing the same with streamRequest. No errors. Not one of the authentication related methods get called in the classes below. I've logged all of them.
Code for RSocketConfig:
#Configuration
#EnableRSocketSecurity
#EnableReactiveMethodSecurity
class RSocketConfig {
#Autowired
lateinit var rSocketAuthenticationManager: RSocketAuthenticationManager
#Bean
fun rSocketMessageHandler(strategies: RSocketStrategies?): RSocketMessageHandler? {
val handler = RSocketMessageHandler()
handler.argumentResolverConfigurer.addCustomResolver(AuthenticationPrincipalArgumentResolver())
handler.rSocketStrategies = strategies!!
return handler
}
#Bean
fun authorization(rsocket: RSocketSecurity): PayloadSocketAcceptorInterceptor {
rsocket.authorizePayload { authorize: AuthorizePayloadsSpec ->
authorize
.route("flux-stream").authenticated()
.anyRequest().authenticated()
.anyExchange().permitAll()
}
.jwt { jwtSpec: RSocketSecurity.JwtSpec ->
try {
jwtSpec.authenticationManager(rSocketAuthenticationManager)
} catch (e: Exception) {
throw RuntimeException(e)
}
}
return rsocket.build()
}
#Bean
fun rSocketRequester(strategies: RSocketStrategies, props: RSocketProperties): Mono<RSocketRequester> =
RSocketRequester.builder()
.rsocketStrategies(strategies)
.connectWebSocket(getUri(props))
fun getUri(props: RSocketProperties): URI =
URI.create(String.format("ws://localhost:${props.server.port}${props.server.mappingPath}"))
}
Code for RSocketAuthenticationManager:
#Component
class RSocketAuthenticationManager(): ReactiveAuthenticationManager {
#Autowired
lateinit var cognitoConfig: CognitoConfig
#Override
override fun authenticate(authentication: Authentication): Mono<Authentication> {
val authToken: String = authentication.credentials.toString()
try {
return if(isTokenValid(authToken)) {
val decoded = JWT.decode(authToken)
decoded.claims.entries.forEach { (key, value) -> println("$key = ${value.asString()}") }
val authorities: MutableList<GrantedAuthority> = ArrayList()
println("authentication successful!")
Mono.just(UsernamePasswordAuthenticationToken(decoded.subject, null, authorities))
} else {
println("invalid authentication token")
Mono.empty<Authentication>();
}
} catch (e: Exception) {
println("authentication errored")
e.printStackTrace()
return Mono.empty<Authentication>()
}
}
#Throws(Exception::class)
fun isTokenValid(token: String): Boolean {
// code borrowed from
// https://github.com/awslabs/cognito-proxy-rest-service/blob/2f9a9ffcc742c8ab8a694b7cf39dc5d8b3247898/src/main/kotlin/com/budilov/cognito/services/CognitoService.kt#L41
// Decode the key and set the kid
val decodedJwtToken = JWT.decode(token)
val kid = decodedJwtToken.keyId
val http = UrlJwkProvider(URL(cognitoConfig.jwksUrl))
// Let's cache the result from Cognito for the default of 10 hours
val provider = GuavaCachedJwkProvider(http)
val jwk = provider.get(kid)
val algorithm = Algorithm.RSA256(jwk.publicKey as RSAKey)
val verifier = JWT.require(algorithm)
.withIssuer(cognitoConfig.jwtTokenIssuer)
.build() //Reusable verifier instance
val jwt = try {
verifier.verify(token)
} catch (e: Exception) {
false
}
return (jwt != null)
}
}
Dependencies related to the issue:
implementation("org.springframework.boot:spring-boot-starter-webflux:2.3.0.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-websocket:2.3.0.RELEASE")
implementation("org.springframework.boot:spring-boot-configuration-processor:2.3.0.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-rsocket:2.3.0.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-integration:2.3.0.RELEASE")
implementation("org.springframework.boot:spring-boot-starter-security:2.3.0.RELEASE")
implementation("org.springframework.security:spring-security-rsocket:5.4.2")
implementation("org.springframework.security:spring-security-messaging:5.4.2")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server:2.3.0.RELEASE")
implementation("com.auth0:java-jwt:3.3.0")
implementation("com.auth0:jwks-rsa:0.1.0")
I'm not too familiar with Spring Security, so maybe I'm missing something obvious.
You should have a service method annotated to receive the authentication principal.
#MessageMapping("runCommand")
suspend fun runCommand(request: CommandRequest, rSocketRequester: RSocketRequester, #AuthenticationPrincipal jwt: String): Flow<CommandResponse> {
Can you extract a simpler project that you can share on github to work through why it's not working?
A full example is here https://spring.io/blog/2020/06/17/getting-started-with-rsocket-spring-security
The issue has been solved, for anyone in need check the repos history with fixes: https://github.com/Braffolk/spring-rsocket-stream-jwt-authentication/commits/master
I have an application.yml with some configuration properties required by my application.
SF:
baseurl: https://xxxxx
case:
recordTypeId: 0124a0000004Ifb
application:
recordTypeId: 0125P000000MkDa
address:
personal:
recordTypeId: 0125P000000MnuO
business:
recordTypeId: 0125P000000MnuT
I have defined a configuration class to read those properties as follows:
#Configuration
class SFProperties(
#Value("\${sf.case.recordTypeId}") val caseRecordTypeId: String,
#Value("\${sf.application.recordTypeId}") val applicationRecordTypeId: String,
#Value("\${sf.address.personal.recordTypeId}") val addressPersonalRecordTypeId:String,
#Value("\${sf.address.business.recordTypeId}") val addressBusinessRecordTypeId: String
)
The class is wired within a service without any issues,
#Service
class SFClientManagementServiceImpl( val webClientBuilder: WebClient.Builder):
ClientManagementService {
....
#Autowired
lateinit var sfProperties: SFProperties
override fun createCase(caseRequest: CaseRequestDto): Mono<CaseResponseDto> {
...
var myValue= sfProperties.caseRecordTypeId
....
}
}
When trying to test this service, I get a "lateinit property sfProperties has not been initialized" exception:
The test looks as follows:
#SpringBootTest(classes = [SFProperties::class])
class SalesforceClientManagementServiceImplTests {
#Autowired
open lateinit var sfProperties: SFProperties
#Test
fun `createCase should return case id when case is created`() {
val clientResponse: ClientResponse = ClientResponse
.create(HttpStatus.OK)
.header("Content-Type", "application/json")
.body(ObjectMapper().writeValueAsString(Fakes().GetFakeCaseResponseDto())).build()
val shortCircuitingExchangeFunction = ExchangeFunction {
Mono.just(clientResponse)
}
val webClientBuilder: WebClient.Builder = WebClient.builder().exchangeFunction(shortCircuitingExchangeFunction)
val sfClientManagementServiceImpl =
SFClientManagementServiceImpl(webClientBuilder)
var caseResponseDto =
salesforceClientManagementServiceImpl.createCase(Fakes().GetFakeCaseRequestDto())
var response = caseResponseDto.block()
if (response != null) {
assertEquals(Fakes().GetFakeCaseResponseDto().id, response.id)
}
}
I have tried many other annotations on the Test class but without success, I would appreciate any ideas.
I have tried all 3 solutions suggested in what is the right way to handle errors in spring-webflux, but WebExceptionHandler is not getting called. I am using Spring Boot 2.0.0.M7. Github repo here
#Configuration
class RoutesConfiguration {
#Autowired
private lateinit var testService: TestService
#Autowired
private lateinit var globalErrorHandler: GlobalErrorHandler
#Bean
fun routerFunction():
RouterFunction<ServerResponse> = router {
("/test").nest {
GET("/") {
ServerResponse.ok().body(testService.test())
}
}
}
}
#Component
class GlobalErrorHandler() : WebExceptionHandler {
companion object {
private val log = LoggerFactory.getLogger(GlobalErrorHandler::class.java)
}
override fun handle(exchange: ServerWebExchange?, ex: Throwable?): Mono<Void> {
log.info("inside handle")
/* Handle different exceptions here */
when(ex!!) {
is ClientException -> exchange!!.response.statusCode = HttpStatus.BAD_REQUEST
is Exception -> exchange!!.response.statusCode = HttpStatus.INTERNAL_SERVER_ERROR
}
return Mono.empty()
}
}
UPDATE:
When I change Spring Boot version to 2.0.0.M2, the WebExceptionHandler is getting called. Do I need to do something for 2.0.0.M7?
SOLUTION:
As per Brian's suggestion, it worked as
#Bean
#Order(-2)
fun globalErrorHandler() = GlobalErrorHandler()
You can provide your own WebExceptionHandler, but you have to order it relatively to others, otherwise they might handle the error before yours get a chance to try.
the DefaultErrorWebExceptionHandler provided by Spring Boot for error handling (see reference documentation) is ordered at -1
the ResponseStatusExceptionHandler provided by Spring Framework is ordered at 0
So you can add #Order(-2) on your error handling component, to order it before the existing ones.
An error response should have standard payload info. This can be done by extending AbstractErrorWebExceptionHandler
ErrorResponse: Data Class
data class ErrorResponse(
val timestamp: String,
val path: String,
val status: Int,
val error: String,
val message: String
)
ServerResponseBuilder: 2 different methods to build an error response
default: handle standard errors
webClient: handle webClient exceptions (WebClientResponseException), not for this case
class ServerResponseBuilder(
private val request: ServerRequest,
private val status: HttpStatus) {
fun default(): Mono<ServerResponse> =
ServerResponse
.status(status)
.body(BodyInserters.fromObject(ErrorResponse(
Date().format(),
request.path(),
status.value(),
status.name,
status.reasonPhrase)))
fun webClient(e: WebClientResponseException): Mono<ServerResponse> =
ServerResponse
.status(status)
.body(BodyInserters.fromObject(ErrorResponse(
Date().format(),
request.path(),
e.statusCode.value(),
e.message.toString(),
e.responseBodyAsString)))
}
GlobalErrorHandlerConfiguration: Error handler
#Configuration
#Order(-2)
class GlobalErrorHandlerConfiguration #Autowired constructor(
errorAttributes: ErrorAttributes,
resourceProperties: ResourceProperties,
applicationContext: ApplicationContext,
viewResolversProvider: ObjectProvider<List<ViewResolver>>,
serverCodecConfigurer: ServerCodecConfigurer) :
AbstractErrorWebExceptionHandler(
errorAttributes,
resourceProperties,
applicationContext
) {
init {
setViewResolvers(viewResolversProvider.getIfAvailable { emptyList() })
setMessageWriters(serverCodecConfigurer.writers)
setMessageReaders(serverCodecConfigurer.readers)
}
override fun getRoutingFunction(errorAttributes: ErrorAttributes?): RouterFunction<ServerResponse> =
RouterFunctions.route(RequestPredicates.all(), HandlerFunction<ServerResponse> { response(it, errorAttributes) })
private fun response(request: ServerRequest, errorAttributes: ErrorAttributes?): Mono<ServerResponse> =
ServerResponseBuilder(request, status(request, errorAttributes)).default()
private fun status(request: ServerRequest, errorAttributes: ErrorAttributes?) =
HttpStatus.valueOf(errorAttributesMap(request, errorAttributes)["status"] as Int)
private fun errorAttributesMap(request: ServerRequest, errorAttributes: ErrorAttributes?) =
errorAttributes!!.getErrorAttributes(request, false)
}