Hashing a Password and Saving to application.yaml (Spring Boot 2.7.3) - spring

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!

Related

Spring reactive OAuth2 resource-server: include both access and ID tokens claims in the `Authentication`

I have a AWS Cognito user pool issuing tokens to my frontend application. The frontend application then uses the tokens to talk to my backend service.
This flow is working as intended. I am validating the tokens that hit my backend service using org.springframework.security:spring-security-oauth2-resource-server:6.0.1 which is configured to point back to Cognito
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://cognito-idp.us-east-1.amazonaws.com/my_pool_endpoint
I have a simple SecurityConfig
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity(useAuthorizationManager = true)
public class SecurityConfig {
#Bean
SecurityWebFilterChain securityWebFilterChain(final ServerHttpSecurity http) {
return http.authorizeExchange()
.pathMatchers("/v3/api-docs/**")
.permitAll()
.anyExchange()
.authenticated()
.and()
.oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt)
.build();
}
So far everything is looking good.
But how do I gather additional information from incoming tokens, for example things such as email and username are not included in the token response from Cognito. An example of a decoded token looks like:
{
"sub": "00000000000000000000",
"cognito:groups": [
"00000000000000000000"
],
"iss": "https://cognito-idp.us-east-1.amazonaws.com/00000000000000000000",
"version": 2,
"client_id": "00000000000000000000",
"origin_jti": "00000000000000000000",
"token_use": "access",
"scope": "openid profile email",
"auth_time": 1676066347,
"exp": 1676186814,
"iat": 1676143614,
"jti": "00000000000000000000",
"username": "google_00000000000000000000"
}
When I need extra information from the token, I'm calling https://my-congito-pool.auth.us-east-1.amazoncognito.com/oauth2/userInfo and passing the JWT as the Bearer token, which works and returns the information I'm looking for such as email, picture, username etc.
My question is I don't think doing this manually every time I want additional information is the 'correct' way of handling it.
Should I be using something like a UserDetailsService to perform this once and transforming the incoming JWT into my own User which holds this information?
If so, how do I do this using ReactiveSpringSecurity?
It looks like Cognito allows to enrich ID tokens, but not access tokens. That's sad, most competitors allow it and it makes spring resource-servers configurations much easier.
I can think of two solutions:
configure your resource-server with access-token introspection (with http.oauth2ResourceServer().opaqueToken()), using your /oauth2/userInfo as introspection endpoint and the JWT access-token as "opaque" token
require clients to add the ID token in a dedicated header (let's say X-ID-Token) in addition to the access token (provided in the Authorization header as usual). Then in the authentication converter, retrieve and decode this additional header and build an Authentication of your own with both access and ID tokens strings and claims
I will only develop the second solution for two reasons:
the first has the usual performance cost of token introspection (a call is made from the resource-server to the authorization-server before each request is processed)
the second permits to add any data from as many headers as we need to the Authentication instance for authentication and authorization (not only ID token as we demo here) with very little performance impact
Spoiler: here is what I got:
with valid access and ID tokens
with just the access-token
Isn't it exactly what you are looking for: an Authentication instance with the roles from the access token and the email from the ID token (or Unauthorized if authorization data is missing / invalid / incomplete)?
Detailed Security Configuration
Here is the security configuration for a reactive app. For Servlets, main lines are the same, only the tooling to statically access the request context is quite different. You can refer to this tutorial I just added to my collection for details.
#Configuration
#EnableReactiveMethodSecurity
#EnableWebFluxSecurity
public class SecurityConfig {
static final String ID_TOKEN_HEADER_NAME = "X-ID-Token";
public static Mono<ServerHttpRequest> getServerHttpRequest() {
return Mono.deferContextual(Mono::just)
.map(contextView -> contextView.get(ServerWebExchange.class).getRequest());
}
public static Mono<String> getIdTokenHeader() {
return getServerHttpRequest().map(req -> {
final var headers = req.getHeaders().getOrEmpty(ID_TOKEN_HEADER_NAME).stream()
.filter(StringUtils::hasLength).toList();
if (headers.size() == 0) {
throw new MissingIdTokenException();
}
if (headers.size() > 1) {
throw new MultiValuedIdTokenException();
}
return headers.get(0);
});
}
#Bean
SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http, ReactiveJwtDecoder jwtDecoder) {
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(accessToken -> getIdTokenHeader()
.flatMap(idTokenString -> jwtDecoder.decode(idTokenString).doOnError(JwtException.class, e -> {
throw new InvalidIdTokenException();
}).map(idToken -> {
final var idClaims = idToken.getClaims();
#SuppressWarnings("unchecked")
final var authorities = ((List<String>) accessToken.getClaims().getOrDefault("cognito:groups",
List.of())).stream().map(SimpleGrantedAuthority::new).toList();
return new MyAuth(authorities, accessToken.getTokenValue(), idTokenString, accessToken.getClaims(),
idClaims);
})));
http.securityContextRepository(NoOpServerSecurityContextRepository.getInstance()).csrf().disable();
http.authorizeExchange().anyExchange().authenticated();
return http.build();
}
public static class MyAuth extends AbstractAuthenticationToken {
private static final long serialVersionUID = 9115947200114995708L;
// Save access and ID tokens strings just in case we need to call another
// micro-service on behalf of the user who initiated the request and as so,
// position "Authorization" and "X-ID-Token" headers
private final String accessTokenString;
private final String idTokenString;
private final Map<String, Object> accessClaims;
private final Map<String, Object> idClaims;
public MyAuth(Collection<? extends GrantedAuthority> authorities, String accessTokenString,
String idTokenString, Map<String, Object> accessClaims, Map<String, Object> idClaims) {
super(authorities);
this.accessTokenString = accessTokenString;
this.accessClaims = Collections.unmodifiableMap(accessClaims);
this.idTokenString = idTokenString;
this.idClaims = Collections.unmodifiableMap(idClaims);
// Minimal security checks: assert that issuer and subject claims are the same
// in access and ID tokens.
if (!Objects.equals(accessClaims.get(IdTokenClaimNames.ISS), idClaims.get(IdTokenClaimNames.ISS))
|| !Objects.equals(accessClaims.get(StandardClaimNames.SUB), idClaims.get(IdTokenClaimNames.SUB))) {
throw new InvalidIdTokenException();
}
// You could also make assertions on ID token audience, but this will require
// adding a custom property for expected ID tokens audience.
// You can't just check for audience equality with already validated access
// token one.
this.setAuthenticated(true);
}
#Override
public String getCredentials() {
return accessTokenString;
}
#Override
public String getPrincipal() {
return (String) accessClaims.get(StandardClaimNames.SUB);
}
public String getAccessTokenString() {
return accessTokenString;
}
public String getIdTokenString() {
return idTokenString;
}
public Map<String, Object> getAccessClaims() {
return accessClaims;
}
public Map<String, Object> getIdClaims() {
return idClaims;
}
}
#ResponseStatus(code = HttpStatus.UNAUTHORIZED, reason = ID_TOKEN_HEADER_NAME + " is missing")
static class MissingIdTokenException extends RuntimeException {
private static final long serialVersionUID = -4894061353773464761L;
}
#ResponseStatus(code = HttpStatus.UNAUTHORIZED, reason = ID_TOKEN_HEADER_NAME + " is not unique")
static class MultiValuedIdTokenException extends RuntimeException {
private static final long serialVersionUID = 1654993007508549674L;
}
#ResponseStatus(code = HttpStatus.UNAUTHORIZED, reason = ID_TOKEN_HEADER_NAME + " is not valid")
static class InvalidIdTokenException extends RuntimeException {
private static final long serialVersionUID = -6233252290377524340L;
}
}
Now, each time an authorization succeeds (isAuthenticated() is true), you'll have a MyAuth instance in the security context and it contains both the access and ID tokens claims!
Sample Controller
#RestController
public class GreetingController {
#GetMapping("/greet")
#PreAuthorize("isAuthenticated()")
Mono<String> greet(MyAuth auth) {
return Mono.just("Hello %s! You are granted with %s".formatted(
auth.getIdClaims().get("email"),
auth.getAuthorities()));
}
}
You may also build your #PreAuthorize expressions based on it. Something like:
#RequiredArgsConstructor
#RestController
#RequestMapping("/something/protected")
#PreAuthorize("isAuthenticated()")
public class ProtectedResourceController {
private final SomeResourceRepository resourceRepo;
#GetMapping("/{resourceId}")
#PreAuthorize("#auth.idClaims['email'] == #resource.email")
ResourceDto getProtectedResource(MyAuth auth, #RequestParam("resourceId") SomeResource resource) {
...
}
}
EDIT: Code reusability & spring-addons starters
I maintain wrappers around spring-boot-starter-oauth2-resource-server. It is very thin and opensource. If you don't want to use it, you should have a look at how it is done to get inspiration from it:
inspect resources to find out what it takes to build your own spring-boot starters
inspect beans to pick ideas for creating your own configurable ones
browse to dependencies like OpenidClaimSet and OAuthentication which could be of inspiration
Here is what the sample above becomes with "my" starter for reactive resource-servers with JWT decoders:
#Configuration
#EnableReactiveMethodSecurity
#EnableWebFluxSecurity
public class SecurityConfig {
static final String ID_TOKEN_HEADER_NAME = "X-ID-Token";
#Bean
OAuth2AuthenticationFactory authenticationFactory(
Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter,
ReactiveJwtDecoder jwtDecoder) {
return (accessBearerString, accessClaims) -> ServerHttpRequestSupport.getUniqueHeader(ID_TOKEN_HEADER_NAME)
.flatMap(idTokenString -> jwtDecoder.decode(idTokenString).doOnError(JwtException.class, e -> {
throw new InvalidHeaderException(ID_TOKEN_HEADER_NAME);
}).map(idToken -> new MyAuth(
authoritiesConverter.convert(accessClaims),
accessBearerString,
new OpenidClaimSet(accessClaims),
idTokenString,
new OpenidClaimSet(idToken.getClaims()))));
}
#Data
#EqualsAndHashCode(callSuper = true)
public static class MyAuth extends OAuthentication<OpenidClaimSet> {
private static final long serialVersionUID = 1734079415899000362L;
private final String idTokenString;
private final OpenidClaimSet idClaims;
public MyAuth(Collection<? extends GrantedAuthority> authorities, String accessTokenString,
OpenidClaimSet accessClaims, String idTokenString, OpenidClaimSet idClaims) {
super(accessClaims, authorities, accessTokenString);
this.idTokenString = idTokenString;
this.idClaims = idClaims;
}
}
}
Update #Controller (pay attention to the direct accessor to email claim):
#RestController
public class GreetingController {
#GetMapping("/greet")
#PreAuthorize("isAuthenticated()")
Mono<String> greet(MyAuth auth) {
return Mono.just("Hello %s! You are granted with %s".formatted(
auth.getIdClaims().getEmail(),
auth.getAuthorities()));
}
}
This are the configuration properties (with different claims used as authorities source depending on the authorization-server configured in the profile):
server:
error.include-message: always
spring:
lifecycle.timeout-per-shutdown-phase: 30s
security.oauth2.resourceserver.jwt.issuer-uri: https://localhost:8443/realms/master
com:
c4-soft:
springaddons:
security:
issuers:
- location: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}
authorities:
claims:
- realm_access.roles
- resource_access.spring-addons-public.roles
- resource_access.spring-addons-confidential.roles
caze: upper
prefix: ROLE_
cors:
- path: /greet
---
spring.config.activate.on-profile: cognito
spring.security.oauth2.resourceserver.jwt.issuer-uri: https://cognito-idp.us-west-2.amazonaws.com/us-west-2_RzhmgLwjl
com.c4-soft.springaddons.security.issuers:
- location: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}
authorities:
claims:
- cognito:groups
caze: upper
prefix: ROLE_
---
spring.config.activate.on-profile: auth0
com.c4-soft.springaddons.security.issuers:
- location: https://dev-ch4mpy.eu.auth0.com/
authorities:
claims:
- roles
- permissions
caze: upper
prefix: ROLE_
Unit-tests with mocked identity for the #Controller above can be as simple as:
#WebFluxTest(controllers = GreetingController.class)
#AutoConfigureAddonsWebSecurity
#Import(SecurityConfig.class)
class GreetingControllerTest {
#Autowired
WebTestClientSupport api;
#Test
#WithMyAuth(authorities = { "AUTHOR" }, idClaims = #OpenIdClaims(email = "ch4mp#c4-soft.com"))
void givenUserIsAuthenticated_whenGreet_thenOk() throws Exception {
api.get("/greet").expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello ch4mp#c4-soft.com! You are granted with [AUTHOR]");
}
#Test
void givenRequestIsAnonymous_whenGreet_thenUnauthorized() throws Exception {
api.get("/greet").expectStatus().isUnauthorized();
}
}
With annotation definition (to build the custom Authentication implementation and set it in the security context):
#Target({ ElementType.METHOD, ElementType.TYPE })
#Retention(RetentionPolicy.RUNTIME)
#Inherited
#Documented
#WithSecurityContext(factory = WithMyAuth.MyAuthFactory.class)
public #interface WithMyAuth {
#AliasFor("authorities")
String[] value() default {};
#AliasFor("value")
String[] authorities() default {};
OpenIdClaims accessClaims() default #OpenIdClaims();
OpenIdClaims idClaims() default #OpenIdClaims();
String accessTokenString() default "machin.truc.chose";
String idTokenString() default "machin.bidule.chose";
#AliasFor(annotation = WithSecurityContext.class)
TestExecutionEvent setupBefore()
default TestExecutionEvent.TEST_METHOD;
#Target({ ElementType.METHOD, ElementType.TYPE })
#Retention(RetentionPolicy.RUNTIME)
public static #interface Proxy {
String onBehalfOf();
String[] can() default {};
}
public static final class MyAuthFactory extends AbstractAnnotatedAuthenticationBuilder<WithMyAuth, MyAuth> {
#Override
public MyAuth authentication(WithMyAuth annotation) {
final var accessClaims = new OpenidClaimSet(super.claims(annotation.accessClaims()));
final var idClaims = new OpenidClaimSet(super.claims(annotation.idClaims()));
return new MyAuth(super.authorities(annotation.authorities()), annotation.accessTokenString(), accessClaims, annotation.idTokenString(), idClaims);
}
}
}
And this is the pom body:
<properties>
<java.version>17</java.version>
<spring-addons.version>6.0.13</spring-addons.version>
</properties>
<dependencies>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webflux-jwt-resource-server</artifactId>
<version>${spring-addons.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webflux-jwt-test</artifactId>
<version>${spring-addons.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

Spring RSocket Security JWT Access Denied Error

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)

Spring unable to authenticate RSocket streams using JWT, while able to auth response requests

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

How to handle spring boot stomp websocket subscribe endpoint to authorize users

We have successfully developed webosocket+stomp+rabbitmq for our project with security. It is working fine although we have some problems in solving the following case:
The workflow of this websocket works as following:
First user subscribes to the websocket endpoint which is working as expected
Second after being authorized by user's token, the user tries to subscribe the following endpoint '/user/queue/' + chatRoomId +
'.messages'. Here chatroomId defines to which chatroom user connects
to, which is also working fine, however here user can connect any
chatroomid which is not being validated in the backend which is also
the big matter we are trying to solve.
stompClient.subscribe('/user/queue/' + chatRoomId + '.messages', incomingMessages);
My question is how can i validate any users when they try to subscribe to this endpoint? I mean is there any way to handle each specific subscriptions
This is our front end code. if u need complete page i will upload it
function connect() {
socket = new SockJS('http://localhost:9600/wsss/messages');
stompClient = Stomp.over(socket);
// var stompClient = Stomp.client("ws://localhost:9600/ws/messages");
// stompClient.connect({ 'chat_id' : chatRoomId,
// 'X-Authorization' : 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0Iiwic2NvcGVzIjoiUk9MRV9BRE1JTiIsImVtYWlsIjoiYWRtaW5AZ21haWwuY29tIiwiaWF0IjoxNTc5MDgxMzg5LCJleHAiOjE1ODE2NzMzODl9.H3mnti0ZNtH6uLe-sOfrr5jzwssvGNcBiHGg-nUQ6xY' },
// stompSuccess, stompFailure);
stompClient.connect({ 'chatRoomId' : chatRoomId,
'login' : 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyODg5Iiwic2NvcGVzIjoiUk9MRV9VU0VSLFJPTEVfTU9ERVJBVE9SIiwiaWF0IjoxNTgyMDMxMDA0LCJleHAiOjE1ODQ2MjMwMDR9.NGAAed4R46FgrtgyDmrLSrmd-o3tkqbF60vOg8vAWYg' },
stompSuccess, stompFailure);
}
function stompSuccess(frame) {
enableInputMessage();
successMessage("Your WebSocket connection was successfuly established!");
console.log(frame);
stompClient.subscribe('/user/queue/' + chatRoomId + '.messages', incomingMessages);
stompClient.subscribe('/topic/notification', incomingNotificationMessage);
// stompClient.subscribe('/app/join/notification', incomingNotificationMessage);
}
And here is the code I am using for my backend
#Configuration
#EnableWebSocketMessageBroker
#Order(Ordered.HIGHEST_PRECEDENCE + 99)
class WebSocketConfig #Autowired constructor(
val jwtTokenUtil: TokenProvider
) : WebSocketMessageBrokerConfigurer {
#Autowired
#Resource(name = "userService")
private val userDetailsService: UserDetailsService? = null
#Autowired
private lateinit var authenticationManager: AuthenticationManager
#Value("\${spring.rabbitmq.username}")
private val userName: String? = null
#Value("\${spring.rabbitmq.password}")
private val password: String? = null
#Value("\${spring.rabbitmq.host}")
private val host: String? = null
#Value("\${spring.rabbitmq.port}")
private val port: Int = 0
#Value("\${endpoint}")
private val endpoint: String? = null
#Value("\${destination.prefix}")
private val destinationPrefix: String? = null
#Value("\${stomp.broker.relay}")
private val stompBrokerRelay: String? = null
override fun configureMessageBroker(config: MessageBrokerRegistry) {
config.enableStompBrokerRelay("/queue/", "/topic/")
.setRelayHost(host!!)
.setRelayPort(port)
.setSystemLogin(userName!!)
.setSystemPasscode(password!!)
config.setApplicationDestinationPrefixes(destinationPrefix!!)
}
override fun registerStompEndpoints(registry: StompEndpointRegistry) {
registry.addEndpoint("/websocket").setAllowedOrigins("*").setAllowedOrigins("*")
registry.addEndpoint("/websocket/messages").addInterceptors(customHttpSessionHandshakeInterceptor()).setAllowedOrigins("*")
registry.addEndpoint("/wsss/messages").addInterceptors(customHttpSessionHandshakeInterceptor()).setAllowedOrigins("*").withSockJS()
}
#Bean
fun customHttpSessionHandshakeInterceptor(): CustomHttpSessionHandshakeInterceptor {
return CustomHttpSessionHandshakeInterceptor()
}
override fun configureClientInboundChannel(registration: ChannelRegistration) {
registration.interceptors(object : ChannelInterceptor {
override fun preSend(message: Message<*>, channel: MessageChannel): Message<*> {
val accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor::class.java)
if (StompCommand.CONNECT == accessor!!.command || StompCommand.STOMP == accessor.command) {
val authorization = accessor.getNativeHeader("login")
println("X-Authorization: {$authorization}")
val authToken = authorization!![0].split(" ")[1]
val username = jwtTokenUtil.getUsernameFromToken(authToken)
if (username != null) {
if(username.contains("#")) {
val userDetails = userDetailsService!!.loadUserByUsername(username)
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
val authentication = jwtTokenUtil.getAuthentication(authToken, SecurityContextHolder.getContext().authentication, userDetails)
accessor.user = authentication
}
} else {
val authorities = jwtTokenUtil.getAuthoritiesFromToken(authToken)
val usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken(username, "", authorities)
val authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken)
accessor.user = authentication
}
}
}
return message
}
})
}
}
Here is the events handler
#Component
class WebSocketEvents {
#EventListener
fun handleSessionConnected(event: SessionConnectEvent) {
val headers = SimpMessageHeaderAccessor.wrap(event.message)
if ( headers.getNativeHeader("chatRoomId") != null && headers.getNativeHeader("chatRoomId")!!.isNotEmpty()){
val chatId = headers.getNativeHeader("chatRoomId")!![0]
if (headers.sessionAttributes != null)
headers.sessionAttributes!!["chatRoomId"] = chatId
}
}
#EventListener
fun handleSessionDisconnect(event: SessionDisconnectEvent) {
val headers = SimpMessageHeaderAccessor.wrap(event.message)
val chatRoomId = headers.sessionAttributes!!["chatRoomId"].toString()
}
}
So far what I have tried: As you can see above when user first connecting to the websocket endpoint http://localhost:9600/wsss/messages it is sending token and chatroom id (headers) and I am handling this in events listener component by resetting chatroomid into header attributes.
What I really need to do is take chatroom id while user subscribing to this specific destionation and apply validation whether he belongs to this chatroom and if so just give him permission | let him join the chat if not return error
I really appreciate any thought or workarounds!
I have spent couple of day searching for an answer but did not find any so I have figured out by myself. Here is my solution for this problem, though it is not complete one.
I have created separate interceptor class for handling all connection types as I was doing while catching subscribe command. So
it came to my mind, why not to use Subscribe command to listen users
actions and respond to it properly. For instance like this
#Component
class WebSocketTopicHandlerInterceptor constructor() : ChannelInterceptor {
override fun preSend(message: Message<*>, channel: MessageChannel): Message<*>? {
val accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor::class.java)
if (StompCommand.CONNECT == accessor!!.command || StompCommand.STOMP == accessor.command) {
val authorization = accessor.getNativeHeader("login").apply { if (isNullOrEmpty()) throw LoggedError(AuthorizationException()) }
val authToken = authorization!![0].split(" ").apply { if (size <= 1) throw LoggedError(InvalidTokenException("Token is not valid")) }[1]
val username = jwtTokenUtil.getUsernameFromToken(authToken)
//DO YOUR AUTHENTICATION HERE
}
if (StompCommand.SUBSCRIBE == accessor.command) {
val destination = accessor.destination
if (destination.isNullOrBlank()) throw LoggedError(CustomBadRequestException("Subscription destionation cannot be null! U DUMB IDIOT!"))
val chatPattern = "/user/queue/+[a-zA-Z0-9-]+.messages".toRegex()
val notificationPattern = "/topic/notification".toRegex()
if (chatPattern.matches(accessor.destination!!)) println("working")
// FINDING OUT WHERE USER IS TRYING TO SUBSCRIBE ALL ROUTING LOGIC GOES HERE...
when {
chatPattern.matches(destination) -> {
//do your all logic here
}
notificationPattern.matches(destination) -> {
//do your all logic here
}
}
}
return message
}
}
IS THERE ANYTHING DOES NOT MAKE SENSE JUST LET ME KNOW I WILL BE VERY HAPPY TO ELABORATE ON ANYTHING FURTHER.
What I have done in my case is that I have figured out where the user is going and do my all validation there otherwise user cannot subscribe to any channel which means it is very secure.

Spring 5 Reactive - WebExceptionHandler is not getting called

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)
}

Resources