Expand my Community achievements bar.

Azure Blob Storage integration with Adobe Experience Manager throws 403

Avatar

Level 2

I am integrating azure blob storage with Adobe Experience Manager and for that I have created my blob storage and a container put in relevant permissions. Also I'm restricting the access to only IP = 49.25.37.71. In Firewalls and virtual networks I've set the Allow access from to Selected networks and put in the IP in the list. Also in the container, I've put in the IP to be allowed which subsequently created a URL like : https://<endpoint>/<containerName>?sp=racwdl&st=2025-11-19T05:23:57Z&se=2025-11-30T13:38:57Z&sip=49....


Now this keeps on throwing 403 with Service Message: If you are using a StorageSharedKeyCredential, and the server returned an error message that says 'Signature did not match', you can compare the string to sign with the one generated by the SDK. To log the string to sign, pass in the context key value pair 'Azure-Storage-Log-String-To-Sign': true to the appropriate method call._If you are using a SAS token, and the server returned an error message that says 'Signature did not match', you can compare the string to sign with the one generated by the SDK. To log the string to sign, pass in the context key value pair 'Azure-Storage-Log-String-To-Sign': true to the appropriate generateSas method call._Please remember to disable 'Azure-Storage-Log-String-To-Sign' before going to production as this string can potentially contain PII._Status code 403, "<?xml version="1.0" encoding="utf-8"?><Error><Code>AuthorizationFailure</Code><Message>This request is not authorized to perform this operation._RequestId:06c1c2df-e01e-0045-42ad-587603000000_Time:2025-11-18T17:04:26.2169791Z</Message></Error>"


But on the contrary if I allow Public access and have the URL formed without the sip=49.25.37.71, it works perfectly. So what exactly am I doing wrong in the Azure config?

This is the code that is setup in AEM which works fine till the time IP restrictions are setup at azure end.

	package .....

	import com.azure.core.http.rest.Response;
	import com.azure.core.util.Context;
	import com.azure.storage.blob.BlobClient;
	import com.azure.storage.blob.BlobContainerClient;
    import com.azure.storage.blob.BlobContainerClientBuilder;
	import com.azure.storage.blob.options.BlobParallelUploadOptions;
	import com.azure.storage.blob.models.BlobStorageException;
	import org.apache.commons.io.IOUtils;
	import org.apache.commons.lang3.StringUtils;
	import org.apache.sling.api.SlingHttpServletRequest;
	import org.apache.sling.api.SlingHttpServletResponse;
	import org.apache.sling.api.resource.Resource;
	import org.apache.sling.api.resource.ResourceResolver;
	import org.apache.sling.api.servlets.HttpConstants;
	import org.apache.sling.api.servlets.SlingAllMethodsServlet;
	import org.osgi.service.component.annotations.Component;
	import org.slf4j.Logger;
	import org.slf4j.LoggerFactory;
	import java.util.Map;
	import java.util.HashMap;

	import javax.servlet.Servlet;
	import java.io.ByteArrayInputStream;
	import java.io.IOException;
	import java.io.InputStream;

	@SuppressWarnings("CQRules:CQBP-75")
	@Component(service = Servlet.class,
			property = {
					"sling.servlet.methods=" + HttpConstants.METHOD_GET,
					"sling.servlet.paths=/bin/OffloadAssetsToAzure"
			})
	public class OffloadAssetsToAzureServlet extends SlingAllMethodsServlet {

		private static final Logger logger = LoggerFactory.getLogger(OffloadAssetsToAzureServlet.class);

		@Override
		protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response){
			long startTime = System.currentTimeMillis();
			String assetPath = null;

			try{
				assetPath = request.getParameter("assetPath") != null ? request.getParameter("assetPath") : StringUtils.EMPTY;

				logger.info("=== OffloadAssetsToAzureServlet Started ===");
				logger.info("Request Parameters - assetPath: {}", assetPath);

				if(StringUtils.isEmpty(assetPath)){
					logger.warn("Asset path is empty or null. Aborting upload.");
					response.setStatus(400);
					response.setContentType("application/json");
					response.getWriter().write("{\"status\":\"error\",\"message\":\"Asset path is required\"}");
					response.getWriter().close();
					return;
				}

				ResourceResolver resourceResolver = request.getResourceResolver();
				logger.debug("ResourceResolver obtained successfully");

				// Initialize Azure Blob Container Client
				BlobContainerClient blobContainerClient = initializeBlobServiceClient();


				if(blobContainerClient == null){
					logger.error("Failed to get BlobContainerClient. Aborting upload.");
					response.setStatus(500);
					response.setContentType("application/json");
					response.getWriter().write("{\"status\":\"error\",\"message\":\"Container access failed\"}");
					response.getWriter().close();
					return;
				}

				// Upload asset
				boolean uploadSuccess = uploadAssetToAzure(blobContainerClient, assetPath, resourceResolver);

				long endTime = System.currentTimeMillis();
				logger.info("=== OffloadAssetsToAzureServlet Completed in {} ms ===", (endTime - startTime));

				response.setContentType("application/json");
				if(uploadSuccess){
					response.setStatus(200);
					response.getWriter().write("{\"status\":\"success\",\"message\":\"Asset uploaded successfully\"}");
				} else {
					response.setStatus(500);
					response.getWriter().write("{\"status\":\"error\",\"message\":\"Asset upload failed\"}");
				}
				response.getWriter().close();

			} catch (BlobStorageException bse) {
				logger.error("Azure Blob Storage Exception occurred");
				logger.error("Error Code: {}", bse.getErrorCode());
				logger.error("Status Code: {}", bse.getStatusCode());
				logger.error("Service Message: {}", bse.getServiceMessage());
				logger.error("Full Exception: ", bse);
				handleErrorResponse(response, "Azure storage error: " + bse.getErrorCode());

			} catch (Exception e) {
				logger.error("Unexpected error processing asset: {}", assetPath, e);
				logger.error("Exception Type: {}", e.getClass().getName());
				logger.error("Exception Message: {}", e.getMessage());
				handleErrorResponse(response, "Unexpected error occurred");
			}
		}

		/**
		 * Initialize Azure Blob Service Client with comprehensive error handling
		 */
		private BlobContainerClient initializeBlobServiceClient() {
            try {
                    logger.info("Initializing Azure Blob Container Client");

                    String containerEndpoint =
                        "endpoint value over here/container name" +
                        "?sp=racwdl&st=2025-11-19T05:23:57Z&se=2025-11-30T13:38:57Z&sip=172.193.91.67" +
                        "&spr=https&sv=2024-11-04&sr=c&sig=AyKAPP%2F9WK%2Bn1zrnt9DO0lIw3zq0VT7UPvjEH0WfLrA%3D";

                    logger.info("Container Endpoint: {}", containerEndpoint);

                    BlobContainerClient containerClient = new BlobContainerClientBuilder()
                            .endpoint(containerEndpoint)
                            .buildClient();

                    logger.info("BlobContainerClient initialized successfully");
                    logger.info("Container URL: {}", containerClient.getBlobContainerUrl());

                    return containerClient;

                } catch (BlobStorageException bse) {
                    logger.error("Azure Blob Storage Exception during client initialization");
                    logger.error("Error Code: {}", bse.getErrorCode());
                    logger.error("Status Code: {}", bse.getStatusCode());
                    logger.error("Service Message: {}", bse.getServiceMessage(), bse);
                    return null;

                } catch (Exception e) {
                    logger.error("Failed to initialize BlobContainerClient", e);
                    return null;
                }
}




		/**
		 * Upload asset to Azure with detailed logging
		 */
		private boolean uploadAssetToAzure(BlobContainerClient blobContainerClient, String assetPath, ResourceResolver resourceResolver) {

			String blobName = assetPath.substring(1);
			logger.info("Starting asset upload - Asset Path: {}, Blob Name: {}", assetPath, blobName);

			try {
				BlobClient blobClient = blobContainerClient.getBlobClient(blobName);
				logger.debug("BlobClient created for blob: {}", blobName);

				String resourcePath = assetPath + "/jcr:content/renditions/original";
				logger.debug("Looking for resource at: {}", resourcePath);

				Resource assetRes = resourceResolver.getResource(resourcePath);

				if(assetRes == null){
					logger.error("Resource not found at path: {}", resourcePath);
					logger.error("Asset upload aborted - resource does not exist");
					return false;
				}

				logger.info("Resource found successfully");

				// Prepare metadata
				Map<String, String> assetMetadata = new HashMap<String, String>();
				assetMetadata.put("docType1", "text");
				assetMetadata.put("category1", "reference");
				assetMetadata.put("uploadTimestamp", String.valueOf(System.currentTimeMillis()));
				logger.debug("Asset metadata prepared: {}", assetMetadata);

				// Convert resource to input stream
				logger.debug("Converting resource to ByteArrayInputStream");
				ByteArrayInputStream inputDataStream = convertResourceToByteArrayInputStream(assetRes);
				long streamSize = inputDataStream.available();
				logger.info("Asset stream prepared - Size: {} bytes", streamSize);

				if(streamSize == 0){
					logger.warn("Asset stream is empty. Upload may fail.");
				}

				// Prepare upload options
				BlobParallelUploadOptions options = new BlobParallelUploadOptions(inputDataStream)
						.setMetadata(assetMetadata);

				logger.info("Initiating upload to Azure Blob Storage...");
				Response blobResponse = blobClient.uploadWithResponse(options, null, Context.NONE);

				int responseCode = blobResponse.getStatusCode();
				logger.info("Upload response received - Status Code: {}", responseCode);

				if(responseCode == 201 || responseCode == 200){
					logger.info("✓ Asset successfully uploaded to Azure");
					logger.info("Blob URL: {}", blobClient.getBlobUrl());
					return true;
				} else{
					logger.error("✗ Asset upload failed with status code: {}", responseCode);
					logger.error("Response Headers: {}", blobResponse.getHeaders());
					return false;
				}

			} catch (BlobStorageException bse) {
				logger.error("Azure Blob Storage Exception during upload");
				logger.error("Error Code: {}", bse.getErrorCode());
				logger.error("Status Code: {}", bse.getStatusCode());
				logger.error("Service Message: {}", bse.getServiceMessage());

				// Handle specific error codes
				switch(bse.getStatusCode()){
					case 403:
						logger.error("PERMISSION DENIED: Insufficient permissions to upload blob");
						logger.error("Required permissions: Write, Create");
						logger.error("Check SAS token permissions and expiry");
						break;
					case 404:
						logger.error("Container not found during upload");
						break;
					case 409:
						logger.error("Blob already exists or conflict occurred");
						break;
					case 413:
						logger.error("Blob size exceeds maximum allowed size");
						break;
					default:
						logger.error("Unhandled Azure error code");
				}

				logger.error("Full Stack Trace: ", bse);
				return false;

			} catch (IOException e) {
				logger.error("IO Exception while processing asset stream");
				logger.error("Error Message: {}", e.getMessage(), e);
				return false;

			} catch (Exception e) {
				logger.error("Unexpected exception during asset upload");
				logger.error("Exception Type: {}", e.getClass().getName());
				logger.error("Exception Message: {}", e.getMessage(), e);
				return false;
			}
		}

		/**
		 * Convert Resource to ByteArrayInputStream with logging
		 */
		private ByteArrayInputStream convertResourceToByteArrayInputStream(Resource resource) throws IOException {
			InputStream inputStream = null;
			ByteArrayInputStream byteArrayInputStream;

			try {
				logger.debug("Adapting resource to InputStream");
				inputStream = resource.adaptTo(InputStream.class);

				if(inputStream == null){
					logger.error("Failed to adapt resource to InputStream");
					throw new IOException("Unable to get InputStream from resource");
				}

				logger.debug("Converting InputStream to byte array");
				byte[] byteArray = IOUtils.toByteArray(inputStream);
				logger.debug("Byte array created - Size: {} bytes", byteArray.length);

				byteArrayInputStream = new ByteArrayInputStream(byteArray);
				logger.debug("ByteArrayInputStream created successfully");

				return byteArrayInputStream;

			} catch (IOException e) {
				logger.error("IOException during stream conversion: {}", e.getMessage(), e);
				throw e;

			} finally {
				IOUtils.closeQuietly(inputStream);
				logger.debug("InputStream closed");
			}
		}

		/**
		 * Handle error response
		 */
		private void handleErrorResponse(SlingHttpServletResponse response, String message) {
			try {
				response.setStatus(500);
				response.setContentType("application/json");
				response.getWriter().write("{\"status\":\"error\",\"message\":\"" + message + "\"}");
				response.getWriter().close();
			} catch (IOException e) {
				logger.error("Failed to write error response: {}", e.getMessage());
			}
		}
	}



Also note that I have the correct IP address in the allowed IPs - I've written a servlet to get the IP. The code is as follows: It returns 49.25.37.71 as result. So the point of wrong IP being whitelisted can be ruled out.

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.ServletResolverConstants;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletPaths;
import org.osgi.service.component.annotations.Component;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

@Component(
        service = Servlet.class,
        property = {
                ServletResolverConstants.SLING_SERVLET_PATHS + "=/bin/check-ip",
                ServletResolverConstants.SLING_SERVLET_METHODS + "=GET"
        }
)
public class CheckIpServlet extends SlingSafeMethodsServlet {

    @Override
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
            throws ServletException, IOException {

        URL url = new URL("https://api.ipify.org");
        HttpURLConnection con = (HttpURLConnection) url.openConnection();
        con.setRequestMethod("GET");

        BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
        String ip = in.readLine();
        in.close();

        response.setContentType("text/plain");
        response.getWriter().write("Outbound IP from AEM: " + ip);
    }
}

 

1 Reply

Avatar

Employee

Hello @ac4320 

Is this AEMaaCS?

If so, please refer to below Documentation :
https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/security/configu...

As per this :

"Traffic to Azure (*.windows.net) is always routed through the shared cluster IPs, not the Dedicated Egress IP."

These shared outbound IPs are dynamic and cannot be provided or whitelisted.

So you may not be able to restrict Azure Blob Storage by IP.

You can try to use :
- SAS tokens WITHOUT the sip parameter.
- You must allow “All networks” in Azure Storage Firewall and rely on SAS for access control instead.