Adding client secret and client id to request in API Gateway - spring

I have an Auth server sitting behind my Spring Cloud Gateway. I want to perform JWT auth through the Gateway. When I call the respective API Endpoint, I'm having to pass my username, password, client-id and client-secret to generate a JWT token.
User simply calls the endpoint with username and password, and the API gateway forwards the request to the Auth server after attaching client-id and client-secret. This is my whole plan.
My question is, how can I attach client id and client secret to my request, using Spring Cloud Gateway?
Thanks in advance!

You can create a java configuration as shown below:
#Configuration
public class SpringCloudConfig {
#Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/oauth/token")
.uri("http://localhost:8081/oauth/token")
.id("auth"))
.build();
}
}
In this case, the original request and response will simply be proxied through the spring cloud gateway.
For example, if spring cloud gateway is running on port 8080, the request will be (the authorization server is running on port 8081):
curl --location --request POST 'http://localhost:8080/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic c2VydmVyX2FwcDpzZWNyZXQ=' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=server_app'
You can add client-id, client-secret, or other data on the client.
If you need to modify the request body you can add a filter:
#Configuration
public class SpringCloudConfig {
#Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/oauth/token")
.filters(f -> f.modifyRequestBody(String.class, String.class, MediaType.APPLICATION_JSON_VALUE,
(exchange, body) -> {
String modifiedBody = someService.modify(body);
return Mono.just(modifiedBody);
})
)
.uri("http://localhost:8081/oauth/token")
.id("auth"))
.build();
}
}

Related

spring gateway server fail when gateway server protected witeh csrf and request content type is application/x-www-form-urlencoded

spring gateway server cannot get response from upsteam server when gateway server protected witeh csrf and request content type is application/x-www-form-urlencoded
there are two simple server.
the 1st is gateway server.
it's only import gateway and security.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
and it's security config like this. they are very simple. so i write them in the same class
#SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http.authorizeExchange().anyExchange().permitAll();
http.csrf().csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse());
return http.build();
}
/**
* add csrf cookie response on every response
* #return
*/
#Bean
public WebFilter addCsrfTokenFilter() {
return (exchange, next) -> Mono.just(exchange)
.flatMap(ex -> ex.<Mono<CsrfToken>>getAttribute(CsrfToken.class.getName()))
.doOnNext(ex -> {
})
.then(next.filter(exchange));
}
}
the gateway routes
spring:
cloud:
gateway:
routes:
- id: test
uri: http://localhost:8888
predicates:
- Path=/test/**
filters:
- StripPrefix=1
the 2nd is an upstream server
it is a simple web server
#SpringBootApplication
#RestController
public class UpstreamApplication {
#PostMapping("/test")
public String postData(Dto dto) {
return "post success";
}
public static void main(String[] args) {
SpringApplication.run(UpstreamApplication.class, args);
}
}
the dto class has 2 properties a and b
public class Dto {
private String a;
private String b;
public String getA() {
return a;
}
public void setA(String a) {
this.a = a;
}
public String getB() {
return b;
}
public void setB(String b) {
this.b = b;
}
}
and i config the server start on port 8888
server:
port: 8888
and then ,i use post man to test them.
the 1st time ,i have no csrf value.
curl --location --request POST 'http://localhost:8080/test/test' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'a=1' \
--data-urlencode 'b=2'
the server response the invalidate csrf token exception.
and then i get csrf token value from cookie values.
and then i send request with csrf header.
curl --location --request POST 'http://localhost:8080/test/test' \
--header 'X-XSRF-TOKEN: f8db31f3-8be6-4103-8e15-5ef4594a08f0' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Cookie: XSRF-TOKEN=f8db31f3-8be6-4103-8e15-5ef4594a08f0' \
--data-urlencode 'a=1' \
--data-urlencode 'b=2'
the client is wait for response more than 3 minutes and not completed.
i test the other content type ,they are completed normal.
if the upstream server's function params is empty or the client request params is empty. it can be completed.
the test project is here https://github.com/ldwqh0/spring_gateway_csrf_bug

Keycloak login page not showing, I get Spring Security login instead

Hi I'm trying to add security to a Kotlin Spring Boot project with Java 15 I've started. I want to use Keycloak as Auth Server.
I don't have a frontend yet (REACT app), I'm building the REST API first.
My problem is when I try to hit a protected endpoint instead of having the Keycloak login page I think the Spring Security login page pops up because it says invalid credentials and the looks are a basic form instead of the style Keycloak has for login. I don't know what's missing on my config.
This is the SecurityConfig:
#Configuration
#EnableWebSecurity
#ComponentScan(basePackageClasses = [KeycloakSecurityComponents::class])
internal class SecurityConfig : KeycloakWebSecurityConfigurerAdapter() {
#Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
val keycloakAuthenticationProvider: KeycloakAuthenticationProvider =
keycloakAuthenticationProvider()
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(
SimpleAuthorityMapper()
)
auth.authenticationProvider(keycloakAuthenticationProvider)
}
#Bean
fun keycloakConfigResolver(): KeycloakSpringBootConfigResolver {
return KeycloakSpringBootConfigResolver()
}
#Bean
override fun sessionAuthenticationStrategy(): SessionAuthenticationStrategy {
return RegisterSessionAuthenticationStrategy(
SessionRegistryImpl()
)
}
override fun configure(http: HttpSecurity) {
super.configure(http)
http.authorizeRequests()
.antMatchers("/carts*")
.hasRole("user")
.anyRequest()
.permitAll()
}
}
Controller:
#RestController
#RequestMapping("/carts")
class CartController(private val cartService: CartService) {
#GetMapping("/{id}")
fun getCart(#PathVariable id: String): Cart? {
return cartService.findById(id)
}
}
application-yml:
keycloak:
auth-server-url: http://localhost:8081/auth
realm: TestRealm
resource: login-app
public-client: true
principal-attribute: preferred_username
build.gradle dependencies:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.+")
implementation("org.keycloak:keycloak-spring-boot-starter")
developmentOnly("org.springframework.boot:spring-boot-devtools")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
testImplementation("io.mockk:mockk:1.10.6")
}
dependencyManagement {
imports {
mavenBom("org.keycloak.bom:keycloak-adapter-bom:12.0.4")
}
}
When I do a direct request I get a valid token so I'm assuming Keycloak is working (I'm running it on a docker container at port 8081).
curl --location --request POST 'http://localhost:8081/auth/realms/TestRealm/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=login-app' \
--data-urlencode 'username=user2' \
--data-urlencode 'password=user2' \
--data-urlencode 'grant_type=password'
A request to "localhost:8080/carts/605271fa9f2ad0418ca4858d" redirects to "http://localhost:8080/login" and this shows:
Instead what I'd expect to be similar to this (taken from another example):
Every guide I've seen they are redirected to the Keycloak login out of the box. I'm kind of lost here, any ideas?
Thanks!!
You have to disable the auto configuration for security
use this in your Main class
#EnableAutoConfiguration(exclude = { SecurityAutoConfiguration.class})
Lost the afternoon for not adding the package to my SecurityConfig (copied the example from somewhere else and probably overwrote the package definition) class so the component scan was skipping completely this config.
Nevertheless the way I made it work is requesting the token directly to the keycloak url and sending it with the header --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiA...'.
I guess the Keycloak login page is something I have to configure later on from the frontend directly since I'm not using Spring MVC

Spring OAuth2 server cannot refresh token with Resource owner credentials (password) grant flow

I have configured an OAuth2 authorisation server with spring security oauth, using jwt tokens:
#Configuration
#EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
...
#Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource).passwordEncoder(passwordEncoder);
}
#Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource);
}
#Bean
public TokenStore tokenStore() {
var jwtTokenStore = new JwtTokenStore(tokenConverter());
jwtTokenStore.setApprovalStore(approvalStore());
return jwtTokenStore;
}
#Bean
public JwtAccessTokenConverter tokenConverter() {
var converter = new JwtAccessTokenConverter();
var keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource(jwtKeyStore), jwtKeyPass.toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwtkey"));
return converter;
}
}
There is a client that has password and refresh_token grants. I can get access and refresh tokens with the following request:
curl --request POST \
--url 'http://localhost:8080/oauth/token?grant_type=password&scope=read' \
--header 'authorization: Basic <xxxxxxx>' \
--header 'content-type: application/x-www-form-urlencoded' \
--data 'username=xxxxxxx&password=xxxxxxx'
Response:
{
"access_token": "<long access token>",
"token_type": "bearer",
"refresh_token": "<long refresh token>",
"expires_in": 599,
"scope": "read",
"subject": "xxx",
"jti": "xxx"
}
However, when I try to refresh the token, I get an error Invalid refresh token. Further debugging into Spring codes I see that on the first request, it doesn't insert a row into oauth_approvals table. And on the second request (refreshing the token) it thinks that the user has not approved the scope (although I have autoapprove=true).
This is not the case with implicit or authorization_code grant flow: in those cases it does insert a row into oauth_approvals table, and the token is refreshed successfully.
Is this a bug in Spring OAuth or is there any workaround?
After digging more into Spring's codes, I came to conclusion that this is indeed a bug there. So I extended JdbcApprovalStore and used that one instead. Here is pseudo-code
public class JdbcApprovalStoreAutoApprove extends JdbcApprovalStore {
...
#Override
public List<Approval> getApprovals(String userName, String clientId) {
if (client has auto approved scopes) {
return those scopes;
}
return super.getApprovals(userName, clientId);
}

Spring Security OAuth2 clientId and clientSecret

I am evaluating the Spring Security OAuth2 implementation. I am confused by clientId and clientSecret.
I follow https://spring.io/guides/tutorials/spring-security-and-angular-js/ to build auth server.
I can get generate code by
http://localhost:9999/uaa/oauth/authorize?response_type=code&client_id=acme&redirect_uri=http://example.com
I also can obtain the accesstoken by
curl acme:acmesecret#localhost:9999/uaa/oauth/token \
-d grant_type=authorization_code -d client_id=acme \
-d redirect_uri=http://example.com -d code=jYWioI
{"access_token":"2219199c-966e-4466-8b7e-12bb9038c9bb","token_type":"bearer","refresh_token":"d193caf4-5643-4988-9a4a-1c03c9d657aa","expires_in":43199,"scope":"openid"}
When getting access token, the clientId and clientSecret is required.
But if I have multiple clients, should I start multiple auth server? It cannot work in this way.
How do I build OAuth2 server without clientId and clientSecret?
The code is here: https://github.com/yigubigu/spring-security-auth
You can setup may clients
Ex In Memory :-
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("acme")
.secret("acmesecret")
.authorizedGrantTypes("authorization_code", "refresh_token",
"password").scopes("openid")
.and()
.withClient("xx")
.secret("xx")
.authorizedGrantTypes("xxx");
}
Or you can add Database record for client
REF - Spring oauth2 DB Schema
In order to achieve dynamic client registration, you need to store the credentials in database, instead of hardcoded configuration.
#Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource())
// ...
}
Please refer to this tutorial for more info.

Is it possible to get an access_token from Spring OAuth2 server without client secret?

I am using Spring Security's OAuth2 server implementation. I am trying to get the access_token from the servers' /oauth/token endpoint using the OAuth2 "Password" grant type by only supplying username and password and the client id without the client secret.
This works fine as long as I provide the client id and the client secret in the Authorization header of my HTTP request like so:
curl -u clientid:clientsecret http://myhost ... -d "grant_type=password&username=user&password=pw&client_id=OAUTH_CLIENT"
Following the advice here: Spring OAuth2 disable HTTP Basic Auth for TokenEndpoint, I managed to disable HTTP Basic authentication for the /auth/token endpoint. But when I tried to get the access_token via cURL like so:
curl http://myhost ... -d "grant_type=password&username=user&password=pw&client_id=OAUTH_CLIENT"
I got a BadCredentialsException and could see the message:
Authentication failed: password does not match stored value
in my servers' log. At this point I was slightly irritated, because it was my understanding that this message only shows up when there's something wrong with the username and/or password, not the client id and/or secret. After additionally supplying the client secret in the cURL command like so:
curl http://myhost ... -d "grant_type=password&username=user&password=pw&client_id=OAUTH_CLIENT&client_secret=SECRET"
everything was fine again.
So does that mean I have to supply the client secret one way or another to access the /auth/token endpoint?
PS: I am aware of the fact that regarding security it is generally a good idea to protect this endpoint via HTTP Basic authentication, but there are some use cases where one would rather be able to do without.
Edit:
I seem to have found a way to omit the client secret. Here's my OAuth2 server configuration (notice the calls to allowFormAuthenticationForClients() and autoApprove(true)):
#Configuration
#EnableAuthorizationServer
class OAuth2Config extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager;
public OAuth2Config(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(this.authenticationManager);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauth) throws Exception {
// allows access of /auth/token endpoint without HTTP Basic authentication
oauth.allowFormAuthenticationForClients();
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("acme")
.autoApprove(true) // <- allows for client id only
.authorizedGrantTypes("authorization_code", "refresh_token", "password").scopes("openid");
}
}
Edit II:
The question here: Spring Security OAuth 2.0 - client secret always required for authorization code grant is very closely related to this one but deals with the OAuth2 grant type "Authorization Code", which results in a different workflow like the one you get with grant type "Password".
According to the specification (RFC 6749), if the client type of your application is public, a client secret is not required. On the contrary, if the client type is confidential, a client secret is required.
If Spring offers an API to set the client type, try to set the client type to public.
Spring Boot's implementation requires that a client-secret be passed in to authenticate. You can however override this by creating a bean of type AuthorizationServerConfigurer and configuring it yourself. This is the link to the documenation...
Use basic auth but leave the password empty.
In the implementation of AuthorizationServerConfigurerAdapter override configure and set password encoder to raw text encoder (do not use it as a default password encoder!).
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.passwordEncoder(plainTextPasswordEncoder())
.allowFormAuthenticationForClients();
}
private PasswordEncoder plainTextPasswordEncoder() {
return new PasswordEncoder() {
#Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return !StringUtils.hasText(encodedPassword) || passwordEncoder.matches(rawPassword, encodedPassword);
}
#Override
public String encode(CharSequence rawPassword) {
return passwordEncoder.encode(rawPassword);
}
};
}
}
Now, for OAuth client details (in memory or in a database), set the client secret to null. In this case, the client will be treated as public and will not require client_secret parameter. If you set client secret for OAuth client details (e.g. BCrypt hash), then the client will be treated as confidential. It will rely on default password encoder (e.g. BCrypt) and require client_secret parameter to be sent in order to obtain an access token.

Resources