Search code examples
azureazure-cosmosdbazure-ad-msal

MSAL SPA + .NET API authenticate cosmos DB via OBO


I have SPA Angular application signing in to Azure AD. I have followed this guide so far.

The application is able to authenticate, generates me a token which is then appended to the requests going back to the .NET API.

Msal module is declared as follows:

MsalModule.forRoot(
            new PublicClientApplication({
                auth: {
                    clientId: "obscured9",
                    authority:
                        "obscured",
                    redirectUri: "http://localhost:4200",
                },
                cache: {
                    cacheLocation: "localStorage",
                    storeAuthStateInCookie: isIE,
                },
            }),
            {
                interactionType: InteractionType.Popup,
                authRequest: {
                    scopes: ["access_as_user"],
                },
            },
            {
                interactionType: InteractionType.Popup, // MSAL Interceptor Configuration
                protectedResourceMap: new Map([
                    ["Enter_the_Graph_Endpoint_Here/v1.0/me", ["user.read"]],
                    ["https://localhost:7162", ["api://obscured/API"]],
                ]),
            }
        ),
    ],

So far this works fine, when I try to authenticate to CosmosClient SDK with the token I'm receiving The user or administrator has not consented to use the application with

The application registered in the azure ad doesn't have admin consent (which is company policy, the user impersonation should be able to grant enough permissions to access the cosmos DB instance)

On the backend I'm getting the token as follows:

            
            var token = _tokenResolver.GetToken(); //Token from SPA
            string[] scopes = { "access_as_user" };

            string appKey = "obscured";
            string clientId = "obscured";

            var app = ConfidentialClientApplicationBuilder.Create(clientId)
                .WithClientSecret(appKey)
                .WithTenantId("obscured")
                .Build();
            UserAssertion userAssertion = new UserAssertion(token, 
                "urn:ietf:params:oauth:grant-type:jwt-bearer");
            var result = app.AcquireTokenOnBehalfOf(scopes, userAssertion).ExecuteAsync().Result;
            // base64 encode
            var token2 = Convert.ToBase64String(Encoding.UTF8.GetBytes(result.AccessToken));
            var credentials = new AzureKeyCredential(Convert.ToBase64String(Encoding.UTF8.GetBytes(token2)));
            _cosmosClient = new CosmosClient("obscured",
                credentials,
                new CosmosClientOptions
                {
                    AllowBulkExecution = true,
                    ApplicationName = "obscured",
                    ConnectionMode = ConnectionMode.Direct,
                    ConsistencyLevel = ConsistencyLevel.Session,
                    RequestTimeout = new TimeSpan(0,
                        0,
                        400),
                    Serializer = serializer,
                    MaxRetryAttemptsOnRateLimitedRequests = 10,
                    MaxRetryWaitTimeOnRateLimitedRequests = new TimeSpan(0,
                        0,
                        120)
                });

            // ensure created
            await CreateDatabaseAndContainerIfNotExistsAsync();

            _isLoaded = true;
        }

This gives me the a new token, however as soon as I try to access the resource I'm getting the error message about not giving the consent. Is there a way to give the user consent either in the BE request or in the SPA for the cosmos DB?

EDIT: After grating the admin consent, the app keeps returning 403, the permissions are added as follows and the token is generated enter image description here

System.AggregateException: One or more errors occurred. (Response status code does not indicate success: Forbidden (403); Substatus: 5301; ActivityId: bc1f49a1-4398-4610-8e51-539cb9a65fa7; Reason: (Request blocked by Auth upodi : Request is blocked because principal [5c4d3d80-546b-471b-bba3-de92008fc398] does not have required RBAC permissions to perform action [Microsoft.DocumentDB/databaseAccounts/readMetadata] on resource [/]. Learn more: https://aka.ms/cosmos-native-rbac.
ActivityId: bc1f49a1-4398-4610-8e51-539cb9a65fa7, Microsoft.Azure.Documents.Common/2.14.0, Windows/10.0.22621 cosmos-netstandard-sdk/3.30.8);)
 ---> Microsoft.Azure.Cosmos.CosmosException : Response status code does not indicate success: Forbidden (403); Substatus: 5301; ActivityId: bc1f49a1-4398-4610-8e51-539cb9a65fa7; Reason: (Request blocked by Auth upodi : Request is blocked because principal [5c4d3d80-546b-471b-bba3-de92008fc398] does not have required RBAC permissions to perform action [Microsoft.DocumentDB/databaseAccounts/readMetadata] on resource [/]. Learn more: https://aka.ms/cosmos-native-rbac.
ActivityId: bc1f49a1-4398-4610-8e51-539cb9a65fa7, Microsoft.Azure.Documents.Common/2.14.0, Windows/10.0.22621 cosmos-netstandard-sdk/3.30.8);
   at Microsoft.Azure.Cosmos.GatewayStoreClient.ParseResponseAsync(HttpResponseMessage responseMessage, JsonSerializerSettings serializerSettings, DocumentServiceRequest request)
   at Microsoft.Azure.Cosmos.GatewayAccountReader.GetDatabaseAccountAsync(Uri serviceEndpoint)
   at Microsoft.Azure.Cosmos.Routing.GlobalEndpointManager.GetAccountPropertiesHelper.GetAndUpdateAccountPropertiesAsync(Uri endpoint)
   at Microsoft.Azure.Cosmos.Routing.GlobalEndpointManager.GetAccountPropertiesHelper.GetAccountPropertiesAsync()
   at Microsoft.Azure.Cosmos.GatewayAccountReader.InitializeReaderAsync()
   at Microsoft.Azure.Cosmos.CosmosAccountServiceConfiguration.InitializeAsync()
   at Microsoft.Azure.Cosmos.DocumentClient.InitializeGatewayConfigurationReaderAsync()
   at Microsoft.Azure.Cosmos.DocumentClient.GetInitializationTaskAsync(IStoreClientFactory storeClientFactory)
   at Microsoft.Azure.Documents.BackoffRetryUtility`1.ExecuteRetryAsync[TParam,TPolicy](Func`1 callbackMethod, Func`3 callbackMethodWithParam, Func`2 callbackMethodWithPolicy, TParam param, IRetryPolicy retryPolicy, IRetryPolicy`1 retryPolicyWithArg, Func`1 inBackoffAlternateCallbackMethod, Func`2 inBackoffAlternateCallbackMethodWithPolicy, TimeSpan minBackoffForInBackoffCallback, CancellationToken cancellationToken, Action`1 preRetryCallback)
   at Microsoft.Azure.Documents.ShouldRetryResult.ThrowIfDoneTrying(ExceptionDispatchInfo capturedException)
   at Microsoft.Azure.Documents.BackoffRetryUtility`1.ExecuteRetryAsync[TParam,TPolicy](Func`1 callbackMethod, Func`3 callbackMethodWithParam, Func`2 callbackMethodWithPolicy, TParam param, IRetryPolicy retryPolicy, IRetryPolicy`1 retryPolicyWithArg, Func`1 inBackoffAlternateCallbackMethod, Func`2 inBackoffAlternateCallbackMethodWithPolicy, TimeSpan minBackoffForInBackoffCallback, CancellationToken cancellationToken, Action`1 preRetryCallback)
   at Microsoft.Azure.Cosmos.AsyncCacheNonBlocking`2.GetAsync(TKey key, Func`2 singleValueInitFunc, Func`2 forceRefresh)
   at Microsoft.Azure.Cosmos.AsyncCacheNonBlocking`2.GetAsync(TKey key, Func`2 singleValueInitFunc, Func`2 forceRefresh)
   at Microsoft.Azure.Cosmos.DocumentClient.EnsureValidClientAsync(ITrace trace)

Solution

  • The "403 forbidden" error usually occurs if the access token doesn't have sufficient permissions to perform the action.

    I created an Azure AD Application and granted API permissions:

    enter image description here

    For sample, I tried to generate access token via Postman using On-Behalf-Of flow:

    https://login.microsoftonline.com/TenantID/oauth2/v2.0/token
    
    client_id:ClientID
    grant_type:authorization_code
    scope:api://ClientID/access_as_user
    code:code
    redirect_uri:https://jwt.ms
    client_secret:-ClientSecret
    

    enter image description here

    Using the above generated access token, I generated token to access Cosmos DB:

    https://login.microsoftonline.com/TenantID/oauth2/v2.0/token
    
    client_id:ClientID
    client_secret:ClientSecret
    scope:https://cosmos.azure.com/user_impersonation
    grant_type:urn:ietf:params:oauth:grant-type:jwt-bearer
    assertion:
    requested_token_use:on_behalf_of
    

    enter image description here

    When I decoded the access token, the Cosmos DB scope is present like below:

    enter image description here

    Note that: To access Cosmos DB resource, you have to pass Cosmos DB API permission that is https://cosmos.azure.com/user_impersonation while generating the access token.

    To resolve the error, check the below:

    • Decode the access token in jwt.ms: Welcome! and check if the aud is https://cosmos.azure.com and scp is user_impersonation.
    • Modify your code and pass scope as https://cosmos.azure.com/user_impersonation while acquiring access token for Cosmos DB like below:
    string[] scopes = { "https://cosmos.azure.com/user_impersonation" }
    

    If still the issue persists, maybe the resource needs RBAC role. Assign role based like Cosmos DB Account Reader Role to the Azure AD Application based on the resource you are trying to access:

    enter image description here