HOAB

History of a bug

SpringBoot 2, OAUTH 2 and tokenStore

Rédigé par gorki Aucun commentaire

Problem :

Following the previous post (this one and this one) I configured a Authorization and Resource server on a a same JVM.

All was working well in my local machine, but when I send the springboot on the server, I get an "Invalid access token". The authorization request was accepted, I get an access token but it was refused by Resource server.

 

Solution :

I activate remote DEBUG, perform tests (I also had a reverse proxy but that's was not the problem). The issue was due to the tokenStore which :

  • on my local machine was the same instance
  • on the server was two different instance

In fact depending on the bean initialization order, the token store could be shared or not according to optional Autowire field of the authorization and resource server. If not available at init time, it could use a local instance.

So here is my updated configuration :

My AuthorizationServer is now :

package com.example;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {


    @Autowired
    public TokenStore tokenStore;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(new MyClientDetailsService());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore);
    }
}

And my resource server :

package com.hexagon.hpa.security;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler;

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    private static final String RESOURCE_ID = "RESSOURCE_ID";

    @Autowired
    TokenStore tokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {

        resources.resourceId(RESOURCE_ID);
        resources.tokenStore(tokenStore);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.anonymous().disable()
                .authorizeRequests()
                .antMatchers("/api/**").authenticated()
                .and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
    }

}

A simple configurer :

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

@Configuration
public class TokenStoreProvider {

    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }
}

 

SpringBoot 2, OAUTH 2 and client_credentials in POST parameters

Rédigé par gorki Aucun commentaire

Problem :

Edited : due to a bug, see also (https://www.hoab.fr/springboot-2-oauth-2-and-tokenstore)

I try to implements client credentials OAUTH flow for a server which is resource and authentication server. (Goody summary here : https://www.slideshare.net/halyph/oauth2-and-spring-security)

There is plenty of posts on how to make an oauth 2 authentication server, (like authentication code flow  less with client_credentials mode...

And in these examples :

But if I easily found how to perform this kind of request : POST + BasicAuth(client_id/client_secret), I wasn't able to do :

POST + optional BasicAuth + client_id/client_secret in post parameters

Solution :

I activate DEBUG on spring security and see that filter chains do not have UsernamePasswordAuthenticationFilter, I plan to add the filter in websecurity as in some examples but it was not working.

After a lot of unsuccessful tries, I finally understand that I have to add a kind of UsernamePasswordAuthenticationFilter on OAUTH2 filter chain and not on the others (yes, there is multiple security filter chain....)

Going back to origins, I check AuthorizationServerConfigurerAdapter override method and ... eurêka !

As simple as add a

ClientCredentialsTokenEndpointFilter

with

security.allowFormAuthenticationForClients()

method... So simple, so hard to find.

But my AuthorizationServer is now :

Due to a bug, the code is now in (https://www.hoab.fr/springboot-2-oauth-2-and-tokenstore)

And a simple (for now) client detail service :

package com.example;

import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;

import java.util.Arrays;

public class MyClientDetailsService implements ClientDetailsService {

    private static final String CLIENT_CREDENTIALS = "client_credentials";
    private static final String REFRESH_TOKEN = "refresh_token";
    private static final String SCOPE_READ = "read";
    private static final String SCOPE_WRITE = "write";
    private static final String TRUST = "trust";
    private static final int VALID_FOREVER = -1;


    private static final String CLIENT_ID = "my-client";
    // encoding method prefix is required for DelegatingPasswordEncoder which is default since Spring Security 5.0.0.RC1
    // you can use one of bcrypt/noop/pbkdf2/scrypt/sha256
    // you can change default behaviour by providing a bean with the encoder you want
    // more: https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-encoding
    static final String CLIENT_SECRET = "{noop}my-secret";


    @Override
    public ClientDetails loadClientByClientId(String s) throws ClientRegistrationException {
        if (s.equals(CLIENT_ID)) {
            BaseClientDetails client = new BaseClientDetails();
            client.setClientId(s);
            client.setClientSecret(CLIENT_SECRET);
            client.setAuthorizedGrantTypes(Arrays.asList(CLIENT_CREDENTIALS, REFRESH_TOKEN));
            client.setScope(Arrays.asList(SCOPE_READ, SCOPE_WRITE, TRUST));
            client.setAccessTokenValiditySeconds(VALID_FOREVER);
            client.setRefreshTokenValiditySeconds(VALID_FOREVER);
            return client;
        }
        return null;
    }


}

And my resource server :

Due to a bug, the code is now in (https://www.hoab.fr/springboot-2-oauth-2-and-tokenstore)

The CORS filter from (https://www.hoab.fr/springboot-2-oauth-2-options-and-cors) :

package com.example;

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleCorsFilter implements Filter {

    public SimpleCorsFilter() {
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with, authorization, content-type");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(req, res);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void destroy() {
    }
}

And a protected resource :

package com.example;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/api/metrics")
@RestController
public class Metrics {

    @GetMapping
    String getToken() {
        return "OK";
    }
}

 

 

 

 

SpringBoot 2, OAUTH 2, OPTIONS and CORS

Rédigé par gorki Aucun commentaire

Problem :

I have a standalone application Angular running on localhost:4200 and a standalone SpringBoot2 Oauth running on 8080.

OAUTH is configured with "client_credentials".

As indicate by many articles, the OPTIONS preflight request is issued by browsers to request CORS configuration supported by servers.

I added the CORS filters thanks to this post, normal Webrequest CORS configuration is not working for OAUTH as this one is not handle by Spring MVC.

But still no POST request after the preflight OPTIONS request...

Solution :

I was checking my network requests in the browser console... and the solution was simply in the console... Yeah. Too early this morning.

The error was explicity written : "need to enable content-type headers in the Access-Control-Allow-Headers".

The preflight request say to the browser what kind of request can be emit, if the response of the OPTIONS does not match the original request, this one is not sent.

The preflight request has no authentication headers and must return 200.

Here is the filter code :

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleCorsFilter implements Filter {

    public SimpleCorsFilter() {
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with, authorization, content-type");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(req, res);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void destroy() {
    }
}

 

 

 

 

Spring data and MongoDB aggregation (GroupBy)

Rédigé par gorki Aucun commentaire

Problem :

I try to create an aggregation (count and groupBy) on my documents.

Thanks to these links :

I nearly resolve my problem... nearly :)

Solution :

Documents to analyze :

  1. the _id of the application is defined by an id attribute...
  2. instance is not present is all documents
{
   phase : "Production",
   application {
      _id : 1234
   },
   instance: "myInstance"
}

The first try is to use springdata easy way : there is not groupBy ! but count is working if you need :

# If instance is set to null, returns the document which do not have the field
countByApplicationIdAndPhaseAndInstanceId(Long applicationId, Phase phase, String instance)

# Another way to test instance presence
countByApplicationIdAndPhaseAndInstanceIdExists(Long applicationId, Phase phase, Boolean exists)

So, thanks to baeldung (many thanks) ! Here is a working solution, take care about :

  1. filtering order ! the Match operation works either on the document, either on the result of the group by ! (see Baeldung example for testing result of the group by)
#Filter on document
        Aggregation aggregation = newAggregation(filterStates, agg);

#Filter on result of the aggregation
        Aggregation aggregation = newAggregation(agg, filterStates);
  1. my applicationId as used in Springdata easy way must be written with Mongo ID : application._id
  2. and application_id could not be present in the group clause as it is filtered (but as it takes me 2 long hours to make it works....)
  3. Don't forget @Id on the result bean. I see somewhere that we can execute the generic command in Mongo and do not take care about the result bean.
  4. The @Id must be on the first item of the group
  5. Do not miss the collection name in the aggregation command (here MONGO_EVALUATION_COLLECTION_NAME)
@Service
public class EvaluationAdditionalRepository {

    private static final Logger LOGGER = LoggerFactory.getLogger(EvaluationAdditionalRepository.class);

    @Autowired
    private MongoTemplate mongoTemplate;


    public List<InstanceCount> getInstanceByApplicationAndPhase(Long applicationId) {

        GroupOperation agg = group("application._id", "instanceId", "phase").count().as("countInstance");
        MatchOperation filterStates = match(new Criteria("application._id").is(applicationId));

        Aggregation aggregation = newAggregation(filterStates, agg);
        AggregationResults<InstanceCount> result = mongoTemplate.aggregate(aggregation, MONGO_EVALUATION_COLLECTION_NAME, InstanceCount.class);

        return result.getMappedResults();
    }
}

And the result bean :

import org.springframework.data.annotation.Id;

public class InstanceCount {

    @Id
    private Long applicationId;

    // Enum are authorized
    private Phase phase;

    private long countInstance;

    private String instanceId;

    public Phase getPhase() {
        return phase;
    }

    public void setPhase(Phase phase) {
        this.phase = phase;
    }

    public long getCountInstance() {
        return countInstance;
    }

    public void setCountInstance(long countInstance) {
        this.countInstance = countInstance;
    }

    public Long getApplicationId() {
        return applicationId;
    }

    public void setApplicationId(Long applicationId) {
        this.applicationId = applicationId;
    }

    public String getInstanceId() {
        return instanceId;
    }

    public void setInstanceId(String instanceId) {
        this.instanceId = instanceId;
    }

    @Override
    public String toString() {
        return "InstanceCount{" +
            ", applicationId='" + applicationId + '\'' +
            ", phase='" + phase + '\'' +
            ", instanceId='" + instanceId + '\'' +
            ", countInstance=" + countInstance +
            '}';
    }
}

 

Jmeter POST without paramter name and multipart header missing

Rédigé par gorki Aucun commentaire

Problem :

I was looking why my multipart header was not sent when suddendly, JMeter sends my POST HTTP request in a raw format.

Although I have in my GUI HTTP Request sampler a normal list of parameters : param1=value1, etc..., it sends

param1param2

 

Solution :

No solution on google, but it was "simple" : in my default HTTP Request, I changed the sheet "parameters" to "body data", even if the both was empty it was sufficient to invite chaos...

The first problem was that my Multipartform-data header was not sent : because a default one was set on default HTTP Header...

End of day....

 

 

Fil RSS des articles de cette catégorie