Securing Cloud Storage Access: Approach to Limiting Document Access Attempts
Explore how to secure cloud documents and images by limiting access attempts using GUIDs, tokens, and tracking, enhancing security for image rendering.
Join the DZone community and get the full member experience.
Join For FreeIn today's digital age, cloud-hosted applications frequently use storage solutions like AWS S3 or Azure Blob Storage for images, documents, and more. Public URLs allow direct access to publicly accessible resources.
However, sensitive images require protection and are not readily accessible via public URLs. Accessing such an image involves a JWT-protected API endpoint, which returns the needed image. We must pass the JWT token in the header to fetch the image using the GET API. The standard method for rendering these images in HTML uses JavaScript, which binds the byte content from the API to the img src
attribute. Though straightforward, this approach might not always be suitable, especially when avoiding JavaScript execution.
Could we simplify the process by assigning a direct URL to the img src
attribute to render images without JavaScript and don't need to pass the JWT token in the header? This is possible with pre-signed URLs provided by AWS S3 and Azure Blob Storage, which grant temporary access to private resources by appending a unique, expiring token to the URL. While enhancing security by limiting access time, pre-signed URLs don't restrict the number of access attempts, allowing potentially unlimited access within the time window.
Acknowledging this, a solution is needed that restricts access time for images within the HTML attribute and limits access attempts, ensuring sensitive images are safeguarded against unauthorized distribution.
Time and Attempts Limited Cloud Storage Resource Access
To address this challenge, we developed a solution combining cloud resources and associated database mapping with unique identifiers (GUIDs) and a token system that gets appended to the URL. We employ a GET API for secure image rendering that combines the base URL, document identifier, and token as query parameters. This method circumvents the limitations of embedding tokens in headers for image src
attributes.
Unique Identifier for the Image
For the image for which we have the requirement to be rendered in an HTML document through the image src
attribute, we generate a unique identifier for this image and persist the image cloud storage path and associated unique identifier (GUID) in the database; there is a dedicated API which does this functionality. This API is part of the microservice responsible for managing all the cloud documents for us.
Token Management
In a master token table, we define token types with attributes such as description, reusability, expiry, and access limits. Using the token type above, we generate a limited-time use token for the image identifier. Each image identifier is assigned a token, stored in a transaction token table with the image identifier GUID, enabling us to track access attempts. We have a microservice to manage these tokens. We will generate this limited-time token using one of the APIs from that microservice.
Now we have both a unique identifier for the image and a time use token; based on that, we build the URL for the cloud storage resource something like below:
{{baseUrl}}/v1/document/{{imageIdentifier}}?token={{limitedTimeUseToken}}
Sample URL:
- https://api.fabrikam.com/v1/document/e8655967-3d85-4a5c-b1a8-bb885cc4b81b?token=d5c68f04-b674-4df8-8729-081fe7a8f6b7
The above URL is for the API endpoint, which will send image bytes as a response once the token is validated. We will be covering this API in detail below.
API To Render Image
This API is straightforward. It will fetch the image cloud storage path based on the document UUID sent and then go to AWS S3 to pull the image. But before it performs all this, it goes through the token validation process through a filter.
Sometimes, we need to perform certain operations on client requests before they reach the controller. Similarly, we may need to process controller responses before they are returned to clients. We can accomplish this by utilizing filters in Spring web applications. The above URL is an API endpoint in the documents microservice; the call goes through the filter DocAccessTokenFilter
before reaching the controller. DocAccessTokenFilter
acts as a gatekeeper for incoming requests to our documents or images serving API. This filter intercepts HTTP requests before they reach their intended controller, performing token validation to ensure that the requestor has permission to access the requested resource.
Below is the implementation of the filter:
@Order(1)
public class DocAccessTokenFilter implements Filter {
private Logger logger = CoreLoggerFactory.getLogger(DocAccessTokenFilter.class);
private String tokenValidationApiUrl;
public DocAccessTokenFilter(String dlApiBaseUrl) {
this.tokenValidationApiUrl = String.format("%s/%s", dlApiBaseUrl, "doctoken/validate");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
final HttpServletRequest req = (HttpServletRequest) request;
final HttpServletResponse res = (HttpServletResponse) response;
final String docToken = req.getParameter("token");
if (StringUtils.isNullOrEmpty(docToken)) {
sendError(res, "Missing Document Access Token");
} else {
RestTemplate restTemplate = new RestTemplate();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
restTemplate.setRequestFactory(requestFactory);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
TokenValidateRequest tknValidateReq = new TokenValidateRequest();
tknValidateReq.setToken(docToken);
ResponseEntity<ApiResult> tknValidateResp = null;
HttpEntity<TokenValidateRequest> tknValidateReqEntity = new HttpEntity<>(tknValidateReq, headers);
try {
tknValidateResp = restTemplate.postForEntity(tokenValidationApiUrl, tknValidateReqEntity, ApiResult.class);
if (tknValidateResp.getStatusCode() == HttpStatus.OK) {
logger.warn("Token validation successful");
chain.doFilter(request, response);
} else {
sendError(res, "Invalid Token");
}
} catch (Exception ex) {
logger.warn(String.format("Exception while validating token %s", ex.getMessage()));
sendError(res, "Invalid Token");
}
}
}
private void sendError(HttpServletResponse response, String errorMsg) throws IOException {
response.resetBuffer();
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setHeader("Content-Type", "application/json");
ApiResult result = new ApiResult();
result.setStatus(HttpStatus.UNAUTHORIZED);
result.setMessage(errorMsg);
ObjectMapper mapper = new ObjectMapper();
String concatenatedMsg = mapper.writeValueAsString(result);
response.getOutputStream().print(concatenatedMsg);
response.flushBuffer();
}
}
In the filter, we take the token query parameter, pass, and validate that token against the service responsible for managing the tokens, including validating the tokens. If this API returns HTTP status 200, then it's a valid token, and in all other cases, it would be treated as an invalid token. In cases where the token is not passed or token validation fails, the filter throws an HTTP 401 unauthorized error code back to the consuming client application. The filter calls another API responsible for validation and managing all tokens. This filter ensures that every request to access a document or image passes through a security checkpoint, verifying that the requestor possesses a valid, unexpired access token.
We need to configure the filter as part of the Spring Boot application as below:
@EnableWebSecurity
@Configuration
@Order(2)
public class DocSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
Environment env;
@Value("${token.validation.api}")
String tokenValidationApiUrl;;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.requestMatchers()
.antMatchers("/api/v1/document/**").and()
.addFilterBefore(new DocAccessTokenFilter(this.tokenValidationApiUrl),
UsernamePasswordAuthenticationFilter.class)
.authorizeRequests().anyRequest().permitAll();
}
}
We configure security for a Spring web application, specifically applying token validation for requests accessing document resources. We insert a custom filter to validate access tokens, ensuring that document or image access is securely controlled based on token validation rules of attempts and time.
Conclusion
The solution transcends the limitations of pre-signed URLs with an access control system based on time and attempts, enhancing security for cloud-stored images. It simplifies their integration into HTML documents, especially for generating digital documents with HTML content in mobile applications. An application example includes displaying a user's digital wet ink signature, securely stored in cloud storage and seamlessly embedded without relying on JavaScript.
Opinions expressed by DZone contributors are their own.
Comments