.net 6 winforms single login for multiple Azure resources

Eric Fitskie 96 Reputation points
2022-09-26T08:31:05.437+00:00

I've created a .net 6 winforms application that needs to connect to an API that is secured by Azure AD, Azure Storage Account and Azure App Configuration.

At this moment the user of the application needs to login twice, once for the API and once for the other Azure resources.

How can i change my application that the user only needs to login once and have access to the API and the Azure resources?

For the API i created an App Registration with User.Read permissions. To login to the API use the code:

In my Program.cs i create a PublicClientApplication

private static IPublicClientApplication clientApp;

clientApp = PublicClientApplicationBuilder.Create(ClientId)
.WithRedirectUri(redirectUri)
.WithAuthority(AzureCloudInstance.AzurePublic, tenantId)
.Build();

In the class where i login to the API i use the following code:

AuthenticationResult authResult = await Program.PublicClientApp.AcquireTokenInteractive(scopes)
.WithUseEmbeddedWebView(false)
.WithPrompt(Prompt.SelectAccount)
.ExecuteAsync();

The user has RBAC access to the Azure Storage account. To login to the Azure Storage Account i use this code:

private DefaultAzureCredential azureCredentials = null;

azureCredentials = new DefaultAzureCredential(true);

BlobServiceClient = new BlobServiceClient(serviceUri: new Uri(defaultStorageAccount), azureCredentials);

Can someone guide me how i can combine this so that the user of the application needs to login once and have access to both, the API and the Azure Storage account (and other Azure services)?

Windows Forms
Windows Forms
A set of .NET Framework managed libraries for developing graphical user interfaces.
1,821 questions
Azure Storage Accounts
Azure Storage Accounts
Globally unique resources that provide access to data management services and serve as the parent namespace for the services.
2,670 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
10,204 questions
Microsoft Entra ID
Microsoft Entra ID
A Microsoft Entra identity service that provides identity management and access control capabilities. Replaces Azure Active Directory.
19,389 questions
0 comments No comments
{count} votes

2 answers

Sort by: Most helpful
  1. Shweta Mathur 27,141 Reputation points Microsoft Employee
    2022-09-28T12:06:55.51+00:00

    Hi @Eric Fitskie ,

    Thanks for reaching out.

    You need to add permission for Azure storage or other resources in your registered application to call Azure resources.

    Once permission has been granted, you can authenticate the application and get the access token for Azure storage. You need to acquire token separately for each Azure resource.

    With the access token, you can authorize requests to call Azure blob storage or queue storage based on the permissions.

    Reference: https://learn.microsoft.com/en-us/azure/storage/common/storage-auth-aad-app?tabs=dotnet

    Hope this will help.

    Thanks,
    Shweta

    ---------------------------------

    Please remember to "Accept Answer" if answer helped you.

    0 comments No comments

  2. Eric Fitskie 96 Reputation points
    2022-09-30T14:02:49.96+00:00

    Hi @Shweta Mathur

    Thank you for your guidance. I've managed to get this working but still have one question.

    I need to supply the scopes to the AcquireTokenSilent (line 39) method and the GetTokenAsync (line 29) has a parameter TokenRequestContext that has a property Scopes. How do i populate that property instead of overriding that via my constructor injected scopes? Please note that i'm building a winforms app.

    See the test code below:

    private const string clientId = "";  
    private const string tenantId = "";  
    private const string blobServiceUrl = "";  
    
    _publicClientApplication = PublicClientApplicationBuilder.Create(clientId)  
     .WithRedirectUri("http://localhost")  
     .WithAuthority(AzureCloudInstance.AzurePublic, tenantId)  
     .Build();  
      
    AccessToken accessTokenApi = await GetApiAccessTokenAsync(new string[] { "user.read" });  
    
    //This class injects the scopes in the constructor, but the GetTokenAsync method has a TokenRequestContext  parmeter with Scopes property. How can i supply the information by that parameter?  
    TokenCredential blobTokenCredential = new PublicClientTokenCredential(_publicClientApplication, new string[] { "https://storage.azure.com/user_impersonation" });  
    BlobServiceClient blobServiceClient = new BlobServiceClient(new Uri(blobServiceUrl), blobTokenCredential);  
      
    public class PublicClientTokenCredential : TokenCredential  
    {  
     private readonly IPublicClientApplication _publicClientApplication;  
     private readonly string[] _scopes;  
     public PublicClientTokenCredential(IPublicClientApplication publicClientApplication, string[] scopes)  
     {  
     _publicClientApplication = publicClientApplication;  
     _scopes = scopes;  
     }  
     public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)  
     {  
     return GetTokenAsync(requestContext, cancellationToken).GetAwaiter().GetResult();  
     }  
     public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)  
     {  
     var accounts = await _publicClientApplication.GetAccountsAsync();  
     var firstAccount = accounts.FirstOrDefault();  
      
     if (firstAccount != null)  
     {  
     try  
     {  
                    //Can i use requestContext.Scopes to supply the scopes and how do i populate that?  
     AuthenticationResult authenticationResultSilent = await _publicClientApplication.AcquireTokenSilent(_scopes, firstAccount)  
     .ExecuteAsync(cancellationToken);  
     return new AccessToken(authenticationResultSilent.AccessToken, authenticationResultSilent.ExpiresOn);  
     }  
     catch (MsalUiRequiredException) { }  
     }  
      
     AuthenticationResult authenticationResult = await _publicClientApplication.AcquireTokenInteractive(_scopes)  
     .WithUseEmbeddedWebView(false)  
     .WithPrompt(Prompt.SelectAccount)  
     .ExecuteAsync(cancellationToken);  
     return new AccessToken(authenticationResult.AccessToken, authenticationResult.ExpiresOn);  
     }  
    }  
    

    Hope to hear from you soon.

    0 comments No comments