1
votes

I'm writing an application using Spring Boot and Java that will be writing files to Azure Blob Storage. How can I use a Service Principal to authenticate? The details of the SP should ideally be read in via some properties or an external file.

I've been wading through the reams of documentation and examples, all of which don't seem to be quite what I'm looking for. Most examples that I've seen use the Storage Account Key which I don't want to do.

Some example code would be really appreciated. As I said, I'm struggling to find a decent example (both of how to use an SP but also generally how to write to Azure BLOB Storage in Java) as there seems to be so many different ways of accessing storage scattered around in the microsoft docs.

3
why do you want to use a principal instead of account key? - Starbax
I don't like the idea of giving my application unlimited access to a storage account by providing it the key, using a service principal gives me more limited permissions - user11890829

3 Answers

2
votes

You can use ADAL4J to acquire a token, and then use the token to write to blobs.

  1. Add role assignment to your principal.

enter image description here

  1. Get token.

    public static String getToken() throws Exception {
        String TENANT_ID = "your tenant id or name, e4c9*-*-*-*-*57fb";
        String AUTHORITY = "https://login.microsoftonline.com/" + TENANT_ID;
        String CLIENT_ID = "your application id, dc17*-*-*-*a5e7";
        String CLIENT_SECRET = "the secret, /pG*32";
        String RESOURCE = "https://storage.azure.com/";
        String ACCESS_TOKEN = null;
        ExecutorService service = Executors.newFixedThreadPool(1);
        AuthenticationContext context = null;
        try {
            context = new AuthenticationContext(AUTHORITY, false, service);
            ClientCredential credential = new ClientCredential(CLIENT_ID, CLIENT_SECRET);
            Future<AuthenticationResult> future = context.acquireToken(RESOURCE, credential, null);
            ACCESS_TOKEN = future.get().getAccessToken();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } finally {
            service.shutdown();
        }
        return ACCESS_TOKEN;
    }
    
  2. Access blob.

    public static void main(String[] args) throws Exception {
        String token = getToken();
        StorageCredentialsToken credentialsToken = new StorageCredentialsToken("storagetest789", token);
        CloudBlobClient blobClient = new CloudBlobClient(new URI("https://storagetest789.blob.core.windows.net/"), credentialsToken);
        CloudBlobContainer blobContainer = blobClient.getContainerReference("pub");
        CloudBlockBlob blockBlob = blobContainer.getBlockBlobReference("test.txt");
        blockBlob.uploadText("Test!");
    }
    

Hope it helps.

0
votes

Another way to get the Access token is using the MS authentication lib

This library used "builders" to build out the confidential client. If you use that class, it handles refreshing the token for you and handles the cache

0
votes

I have created an article on connecting Spring Boot App with Azure Storage account using Serivce Principal you can refer that.

https://medium.com/@iamdeveshkumar/using-azure-blob-storage-with-a-spring-boot-app-6238c137df7

pom.xml

<dependency>
    <groupId>com.azure</groupId>
    <artifactId>azure-storage-blob</artifactId>
    <version>12.12.0</version>
</dependency>

<dependency>
    <groupId>com.azure</groupId>
    <artifactId>azure-identity</artifactId>
    <version>1.3.1</version>
</dependency>

application.properties

app.config.azure.client-id=xxxxxxxxxxx
app.config.azure.client-secret=xxxxxxxxxxx
app.config.azure.tenant-id=xxxxxxxxxxx
app.config.azure.storage-id=xxxxxxxxxxx
app.config.azure.storage-endpoint=https://{STORAGE-ID}.blob.core.windows.net
app.config.azure.storage.container=xxxxxxxxxxx

AzureStorageConfiguration.java

@Data
@Configuration
@Slf4j
public class AzureStorageConfiguration {
  private static final Logger logger = LoggerFactory.getLogger(AzureStorageConfiguration.class);

  @Value("${app.config.azure.client-id}")
  private String clientId;
  @Value("${app.config.azure.client-secret}")
  private String clientSecret;
  @Value("${app.config.azure.tenant-id}")
  private String tenantId;
  @Value("${app.config.azure.storage-id}")
  private String storageId;
  @Value("${app.config.azure.storage-endpoint}")
  private String storageEndpoint;
  @Value("${app.config.azure.storage.container}")
  private String storageContainer;

  /**
   * Blob service client builder blob service client builder.
   *
   * @return the blob service client builder
   */
  @Bean
  public BlobServiceClientBuilder blobServiceClientBuilder() {
    return new BlobServiceClientBuilder()
        .credential(getAzureClientCredentials())
        .endpoint(getStorageEndpoint());
  }

  private ClientSecretCredential getAzureClientCredentials() {
    return new ClientSecretCredentialBuilder()
        .clientId(clientId)
        .clientSecret(clientSecret)
        .tenantId(tenantId)
        .build();
  }

  /**
   * Gets storage endpoint.
   *
   * @return the storage endpoint
   */
  public String getStorageEndpoint() {
    return storageEndpoint.replace("{STORAGE-ID}", storageId);
  }

  /**
   * A util method to upload a file to Azure Storage.
   *
   * @param blobServiceClientBuilder service client builder
   * @return BlobServiceAsyncClient blob service async client
   */
  @Bean(name = "blobServiceAsyncClient")
  public BlobServiceAsyncClient blobServiceAsyncClient(
      BlobServiceClientBuilder blobServiceClientBuilder) {
    /*
    retryDelay is by default 4ms and maxRetryDelay is by default 120ms
     */
    return blobServiceClientBuilder.retryOptions(
        new RequestRetryOptions(
            RetryPolicyType.EXPONENTIAL,
            5,
            Duration.ofSeconds(300L),
            null,
            null,
            null)).buildAsyncClient();
  }
}

Then you can use the BlobServiceAsyncClient to create BlobAsyncClient for various blob operations.

/**
   * Get blob async client blob async client.
   *
   * @param container the container
   * @param blobName  the blob name
   * @return the blob async client
   */
  public BlobAsyncClient getBlobAsyncClient(String container, String blobName) {
    BlobContainerAsyncClient blobContainerAsyncClient =
        blobServiceAsyncClient.getBlobContainerAsyncClient(container);
    return blobContainerAsyncClient.getBlobAsyncClient(blobName);
  }


/**
   * Upload to azure blob.
   *
   * @param container the container
   * @param blobName  the blob name
   * @param data      the data
   */
  public void uploadToAzureBlob(String container, String blobName, byte[] data) {
    BlobAsyncClient blobAsyncClient = getBlobAsyncClient(container, blobName);
    long blockSize = 2L * 1024L * 1024L; //2MB
    blobAsyncClient.upload(covertByteArrayToFlux(data),
                           getTransferOptions(blockSize), true)
        .doOnSuccess(blockBlobItem -> logger.info("Successfully uploaded !!"))
        .doOnError(throwable -> logger.error(
            "Error occurred while uploading !! Exception:{}",
            throwable.getMessage()))
        .subscribe();
  }

/**
   * Covert byte array to flux flux.
   *
   * @param byteArray the byte array
   * @return the flux
   */
  public Flux<ByteBuffer> covertByteArrayToFlux(byte[] byteArray) {
    return Flux.just(ByteBuffer.wrap(byteArray));
  }

  /**
   * Creating TransferOptions.
   *
   * @param blockSize represents block size
   * @return ParallelTransferOptions transfer options
   */
  public ParallelTransferOptions getTransferOptions(long blockSize) {
    return new ParallelTransferOptions()
        .setBlockSizeLong(blockSize)
        .setMaxConcurrency(5)
        .setProgressReceiver(
            bytesTransferred -> logger.info("Uploading bytes:{}", bytesTransferred));
  }

For more details and code you can refer to my github repo

https://github.com/kdevesh/azure-storage-spring-boot-app

P.S. I am using the async flavour of Blob Client there is a sync flavour also available if somebody wants to use that.