Syntax highlighter header

Wednesday, 7 February 2024

Creating a proxy for other service in Spring Boot

Recently we had a requirement to translate from HTTP headers to query parameter to expose a service to a client which can't send query parameter to the service. The client could send the information as HTTP header although.

We decided to build it in our spring boot application using FeignClient. We accepted API name as path variable so that same code can work for multiple APIs.

Following is the code our FeignClient interface which accept API name as a path variable:

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "genericClient", configuration = GenericClientConfiguration.class,
        url = "${base-url}" )
public interface GenericClient {
    String LOGIN_ID = "_loginid";
    String USER_TOKEN = "_token";

    @PostMapping(value = "/prefix/{apiName}", consumes = "application/json", produces = "application/json")
    ResponseEntity<String> callAPI( @PathVariable String apiName,
            @RequestParam(value = LOGIN_ID) String loginId,
            @RequestParam(value = USER_TOKEN) String userToken,
            @RequestBody String requestBody);
}

Following is the code for our controller class which receive the request from client. This controller catches errors returned by the server and passes them to the client.

import feign.FeignException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Optional;

@RestController
@Slf4j
@RequestMapping("/api/public")
public class ProxyController {

    @Autowired
    private GenericClient genericClient;

    @PostMapping(value = "/proxyPrefix/{api}", consumes = "application/json", produces = "application/json")
    public ResponseEntity<?> omsApiCall(@PathVariable String api,
                                           @RequestHeader String userName,
                                           @RequestHeader String userToken,
                                           @RequestBody String reqBody) {
        try {
            ResponseEntity<String> resp= genericClient.callAPI(api, userName, userToken, reqBody);
            log.info("Generic call successful API={}",api);
            return resp;
        } catch(FeignException.FeignClientException fce) {
            log.info("Generic call failed: API={}", api, fce);
            Optional<ByteBuffer> respBody = fce.responseBody();
            if(respBody.isPresent()) {
                String errorText = StandardCharsets.UTF_8.decode(respBody.get()).toString();
                return ResponseEntity.status(fce.status()).body(errorText);
            } else {
                return ResponseEntity.status(fce.status()).build();
            }
        } catch(FeignException.FeignServerException fse) {
            log.error("Generic call failed with server error for API={}", api, fse);
            Optional<ByteBuffer> respBody = fse.responseBody();
            if(respBody.isPresent()) {
                String errorText = StandardCharsets.UTF_8.decode(respBody.get()).toString();
                return ResponseEntity.status(fse.status()).body(errorText);
            } else {
                return ResponseEntity.status(fse.status()).build();
            }
        } catch(FeignException fe) {
            log.error("Generic call failed with unknown error for API={}", api, fe);
            Optional<ByteBuffer> respBody = fe.responseBody();
            if(respBody.isPresent()) {
                String errorText = StandardCharsets.UTF_8.decode(respBody.get()).toString();
                return ResponseEntity.status(fe.status()).body(errorText);
            } else {
                return ResponseEntity.status(fe.status()).build();
            }
        }
    }
}

Following is the code for our trusting Feign Client configuration which is needed incase you are connecting to a https url with self signed certificate:

import feign.Client;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.ssl.TrustStrategy;
import org.springframework.context.annotation.Bean;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;

public class GenericClientConfiguration {

    @Bean
    public Client feignClient()
            throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
        TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;
        HostnameVerifier hostnameVerifier = (s, sslSession) -> true;
        SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
        return new Client.Default(sslContext.getSocketFactory(),hostnameVerifier);
    }
}

Tuesday, 12 September 2023

Uploading a large file to S3 from flutter

We were recently trying to upload a large zip file from flutter browser app and we discovered that when we try to upload files larger than 500 MB it was throwing Buffer Allocation error. We needed to upload files larger than 10 GB.

So we started exploring other options. We finally solved the problem using S3 Multipart upload. We did not want to expose S3 credentials to client due to security reasons. So we implemented 3 APIs on server:

  1. InitiateMultipartUpload - This API initiates multipart upload on S3 and return uploadId to client
  2. UploadPart - This API receives a part from client and sends it to S3 and returns PartETag to client
  3. ComplateMultipartUpload - This API completes multipart upload in S3 using PartETags received from client.
Now for client side support we developed a custom component to upload file to S3 taking inspiration from https://github.com/Taskulu/chunked_uploader/blob/master/lib/chunked_uploader.dart

After these changes I was able to upload 5GB file to S3 in 20 minutes without getting buffer allocation error.

Monday, 11 September 2023

Uploading file to S3 using flutter

Recently were working on uploading a zip file to S3 using presigned url. We exposed an API to client for generating a presigned url over an authenticated API call. After generating presigned URL client is supposed to send content in a PUT call to S3 on the presigned url. But when we tried sending the file using MultipartRequest the file got corrupted. After struggling for a long time we found StreamedRequest using which we were able to upload the file to S3 without getting it corrupted. Following is the code for uploading the file to S3.

Future<http.StreamedResponse?> uploadFileToS3(
  String s3Url,
  file_picker.PlatformFile file,
) async {
  var request = http.StreamedRequest('PUT', Uri.parse(s3Url)); 
  final streamSink = request.sink as StreamSink<List<int>>;
  print('Uploading file '+file.name+' to '+s3Url);
  var resp = request.send();
  await streamSink.addStream(file.readStream!);
  streamSink.close();
  print('returning response for '+file.name);
  return await resp;
}

Please note readStream will be null if you don't pass required arguments to file_picker. The required arguments for file picker are:

FilePickerResult? result = await FilePicker.platform.pickFiles(withData:false, withReadStream: true);

Please let me know if you face any problem in using this approach.

Thursday, 24 August 2023

Using new HttpClient API for downloading file

Recently we were required to download a CSV file from our partner website in our Java application. So I explored new Java HttpClient API. The APIs are asynchronous which is good for scalability but confusing for the users. After struggling for a long time I was able to figure out how to download the file which was protected by a username and password. 

The code for downloading file is provided below, it also include a call to get just headers using HEAD call.

import java.io.IOException;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class HttpClientMainClass {
    public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
        HttpClient httpClient = HttpClient.newBuilder().authenticator(new Authenticator() {
                    @Override
                    protected PasswordAuthentication getPasswordAuthentication() {
                        return new PasswordAuthentication("username","password".toCharArray());
                    }
                })
                .build();

        HttpRequest httpRequest2 = HttpRequest.newBuilder().method("HEAD", HttpRequest.BodyPublishers.noBody())
                .uri(new URI("http://example.com/data.csv"))
                .build();
        HttpResponse<Void> resp2 = httpClient.send(httpRequest2,
                HttpResponse.BodyHandlers.discarding());

        System.out.println(resp2);
        System.out.println(resp2.headers());

        HttpRequest httpRequest = HttpRequest.newBuilder().GET()
                .uri(new URI("http://example.com/data.csv"))
                .build();
        HttpResponse<Path> resp = httpClient.send(httpRequest,
                HttpResponse.BodyHandlers.ofFile(Path.of("/tmp","data.csv"),
                        StandardOpenOption.CREATE, StandardOpenOption.WRITE));

        if(resp.statusCode()==200) {
            System.out.println("File downloaded At : "+resp.body());
        }
    }
}

Hope it will be useful for the readers.

Ref: https://www.baeldung.com/java-9-http-client

Friday, 4 August 2023

Spring Security 6.0 with Oauth2 and AWS Cognito Pool

Recently we were migrating our old application from Spring Security 5 to Spring security 6. The major problem we faced was there was no documentation available on internet for Spring Security 6. After struggling for a long time we were able to move our old application to latest version of spring which is 6.0 . Here I am documenting our finding in hope that it will be helpful for the readers.

The first class you need to write is SecurityConfiguration. The content of file is provided below:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfiguration {

    @Autowired
    private JwtDecoder jwtDecoder;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // @formatter:off
        http.cors(c-> {
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowCredentials(true);
            config.addAllowedOriginPattern("*");
            config.addAllowedHeader("*");
            config.setAllowedMethods(Arrays.asList("OPTIONS", "GET", "POST", "PUT", "DELETE", "PATCH"));
            source.registerCorsConfiguration("/**", config);

            c.configurationSource(source);
        });
        http.csrf(AbstractHttpConfigurer::disable);

        http
                .authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers(AntPathRequestMatcher.antMatcher("/error")).permitAll()
                        .requestMatchers(AntPathRequestMatcher.antMatcher("/api/public/**")).permitAll()
                        .requestMatchers(AntPathRequestMatcher.antMatcher("/api/test/**")).permitAll()
                        .requestMatchers(AntPathRequestMatcher.antMatcher("/actuator/**")).permitAll()
                        .anyRequest().authenticated()
                ).sessionManagement(sm-> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .oauth2ResourceServer(rs-> rs.jwt(j-> j.decoder(jwtDecoder)));
        // @formatter:on
        return http.build();
    }
}

In the above class I defined a CorsFilter to allow all domains. CSRF is disabled and few URLs were allowed without authentication. We need to use OAuth2 for integrating Cognito pool JWT tokens, for that we specified jwtDecoder. The JwtDecoder is defined in next class:

import com.nimbusds.jose.*;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.MappedJwtClaimSetConverter;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import java.security.Key;
import java.util.*;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import java.text.ParseException;

@Configuration
public class JwtConfiguration {

    public static final String COGNITO_GROUPS = "cognito:groups";
    private static final String SPRING_AUTHORITIES = "scope";
    public static final String COGNITO_USERNAME = "username";
    private static final String SPRING_USER_NAME = "sub";

    @Value("${security.oauth2.resource.jwk.key-set-uri}")
    private String keySetUri;

    @Bean
    JwtDecoder jwtDecoder() throws ParseException {
        // Obtain the JWKS from the endpoint
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> jwksResponse = restTemplate.getForEntity(keySetUri, String.class);
        String jwksJson = jwksResponse.getBody();

        JWKSet jwkSet = JWKSet.parse(jwksJson);

        DefaultJWTProcessor<SecurityContext> defaultJWTProcessor =  new DefaultJWTProcessor<>();

        defaultJWTProcessor.setJWSKeySelector(new JWSKeySelector<SecurityContext>() {
            @Override
            public List<? extends Key> selectJWSKeys(JWSHeader header, SecurityContext context) throws KeySourceException {
                RSAKey rsaKey = (RSAKey) jwkSet.getKeyByKeyId(header.getKeyID());
                try {
                    return new ArrayList<Key>(Arrays.asList(rsaKey.toPublicKey()));
                } catch (JOSEException e) {
                    e.printStackTrace();
                }
                return null;
            }
        });
        NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(defaultJWTProcessor);

        Converter<Map<String, Object>, Map<String, Object>> claimSetConverter = MappedJwtClaimSetConverter
                .withDefaults(Collections.emptyMap());

        jwtDecoder.setClaimSetConverter( claims -> {
            claims = claimSetConverter.convert(claims);

            HashMap<String, Object> hashMap = new HashMap<>(claims);
            if (claims.containsKey(COGNITO_GROUPS))
                ((Map<String, Object>) hashMap).put(SPRING_AUTHORITIES, claims.get(COGNITO_GROUPS));
            if (claims.containsKey(COGNITO_USERNAME))
                ((Map<String, Object>) hashMap).put(SPRING_USER_NAME, claims.get(COGNITO_USERNAME));

            return hashMap;
        });
        return jwtDecoder;
    }
}

In the above class we are trying to decode the JWT token generated by Cognito pool which is passed to our API as bearer token. The JWT tokens are signed using a private key and validated using a public key. The public keys are provided as a JSON Web Key Set(JWKS) at the URL https://cognito-idp.<aws-region>.amazonaws.com/<cognito-pool-id>/.well-known/jwks.json by AWS. It contains public keys for validating tokens. There can be multiple keys in this set so you need to parse this JSON Web Key Set(JWKS) and pick the correct key as per header of JWT token.

After that you need to do some customization in claimSetConverter. The customization we required are that we want to use username of the cognito pool as user identification rather than UUID generated by cognito for the user. For doing that we overwritten value of "sub" with username received in claim. This value will be returned by principal.getName() when used in the code. 

The second customization we did was to overwrite "scope" with "cognito:groups" because we want to use congnito groups as authority for authenticating our API calls. Assume cognito group name is ROLE_ADMIN then the authority it will be mapped to will be SCOPE_ROLE_ADMIN and we can use @PreAuthorize("hasAuthority('SCOPE_ROLE_ADMIN')") for pre authorizing the API calls. One sample API is implemented below:

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Validated
@Slf4j
public class UserNameTestController {
    @GetMapping("/userName")
    @PreAuthorize("hasAuthority('SCOPE_ROLE_ADMIN')")
    public  ResponseEntity<String>  getCurrentUserInfo(final Principal principal) {
        return ResponseEntity.status(200).body(principal.getName());
    }
}

The important maven dependencies used in this code are:

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
        </dependency>
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-resource-server</artifactId>
            <version>6.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-jose</artifactId>
            <version>6.0.5</version>
        </dependency>
        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>9.8.1</version>
        </dependency>

I hope this will be helpful for the readers. Please drop me a comment if you need some more information.

References:

https://medium.com/javarevisited/json-web-key-set-jwks-94dc26847a34