In today’s digital age, cloud-based applications often use storage solutions like AWS S3 or Azure Blob Storage for images, documents, and more. Public URLs provide direct access to publicly available resources.
However, sensitive images require protection and are not easily accessible via public URLs. Accessing such an image involves a JWT-protected API endpoint, which returns the required image. We need to pass the JWT token in the header to retrieve 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 img src
attribute. Although simple, this approach may not always be appropriate, especially when avoiding JavaScript execution.
Can we simplify the process by assigning a direct URL to the img src
attribute to display images without JavaScript and you don’t have 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. Although they increase security by limiting access times, pre-signed URLs do not limit the number of access attempts, allowing potentially unlimited access within a time frame.
Recognizing this, a solution is needed that limits the access time for images within HTML attributes and limits access attempts, ensuring that sensitive images are protected from unauthorized distribution.
Time and attempts limited access to cloud storage resources
To address this challenge, we developed a solution that combines cloud resources and a linked database mapping with unique identifiers (GUIDs) and a token system appended to the URL. We use a GET API to securely render an image that combines a base URL, a document identifier, and a token as query parameters. This method bypasses the limitations of embedding tokens in image headers src
attributes.
A unique identifier for the image
For an image for which we have a request to be displayed in an HTML document through an image src
attribute, we generate a unique identifier for this image and retain the image storage path in the cloud and the associated unique identifier (GUID) in the database; there is a dedicated API that does this function. This API is part of a microservice that is responsible for managing all documents in the cloud for us.
Token management
In the main token table, we define token types with attributes such as description, reusability, expiration, and access restrictions. Using the token type above, we generate a limited-use token for the image identifier. Each image identifier is assigned a token, stored in the image GUID transaction token table, which allows us to track access attempts. We have a microservice to manage those tokens. We will generate this timed token using one of the APIs from that microservice.
We now have both a unique identifier for the image and a time usage token; based on that, we build a URL for the cloud storage resource something like this:
baseUrl/v1/document/imageIdentifier?token=limitedTimeUseToken
Example URL:
- https://api.fabrikam.com/v1/document/e8655967-3d85-4a5c-b1a8-bb885cc4b81b?token=d5c68f04-b674-4df8-8729-081fe7a8f6b7
The URL above is for the API endpoint, which will send the image bytes in response after the token is validated. We will describe this API in detail below.
API for image display
This API is simple. It will fetch the cloud storage path of the image based on the UUID of the document sent and then go to AWS S3 to pull the image. But before it does all that, it goes through a 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 achieve this by using filters in Spring web applications. The above URL is an API endpoint in the document microservice; the call goes through the filter DocAccessTokenFilter
before it reaches the controller. DocAccessTokenFilter
acts as a gatekeeper for incoming requests to our document or image serving APIs. This filter intercepts HTTP requests before they reach the intended controller, performing token validation to ensure that the requester 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 a token query parameter, pass and check that token against the service responsible for managing tokens, including token validation. If this API returns an HTTP status of 200, then it is a valid token, and in all other cases, it will be treated as an invalid token. In cases where the token is not passed or token validation fails, the filter returns an HTTP 401 Unauthorized error code back to the client application using it. The filter calls another API responsible for validating and managing all tokens. This filter ensures that every request to access a document or image passes through a security checkpoint, confirming that the requester 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 the Spring web application, specifically applying token validation for requests accessing document resources. We insert a custom access token check filter, ensuring that access to a document or image is securely controlled based on the attempt and time token check rules.
Conclusion
The solution goes beyond the limitations of pre-signed URLs with a time- and retry-based access control system, increasing security for images stored in the cloud. It simplifies their integration into HTML documents, especially for generating digital documents with HTML content in mobile applications. The example application includes a display of a user’s digital signature in wet ink, securely stored in cloud storage and seamlessly embedded without relying on JavaScript.