Azure/azure-sdk-for-java

[FEATURE REQ] Sign-in for multi tenant and personal accounts and authorized-client to access resource server in my tenant

Opened this issue ยท 14 comments

Query/Question
Hello,

I have a single page frontend application and a backend serving it with an active directory configured using the azure starter. It is a multi-tenant application and users from my tenant, external tenants with admin consent and personal Microsoft accounts (live, outlook etc) can sign in.

The configuration looks like this (see tenant-id is set to common to be able to log in to personal Microsoft accounts, the same can be achieved by just omitting this profile.tenant-id property):

  cloud:
    azure:
      active-directory:
        enabled: true
        app-id-uri: api://{my-app-id}
        profile:
          tenant-id: common
        credential:
          client-id: {my-app-id}
          client-secret: {my-app-secret}

Everything work as planned, users from multiple tenants are logging in etc (personal accounts work too).

Now I have another resource server (located in my tenant) that I want to access with my app. Let's call it API-B. I have got everything configured properly (access to API-B scope from my app registration). Configuration will now look like:

  cloud:
    azure:
      active-directory:
        enabled: true
        app-id-uri: api://{my-app-id}
        profile:
          tenant-id: common
        credential:
          client-id: {my-app-id}
          client-secret: {my-app-secret}
        authorization-clients:
          api-b-client:
            authorization-grant-type: client_credentials
            scopes: api://{api-b-app-id}/.default

But adding an authorization-clients with scope to this API-B will give me:

Specified tenant identifier '{api-b-app-id}' is neither a valid DNS name nor a valid external domain.

When I set profile.tenant-id property with mine tenant value, authorized-client for API-B will start working (also authentication for mine tenant's users and external tenants), but login for users with personal Microsoft accounts will start failing.

Debugging, I see that AadClientRegistrationRepository will wire up all the clients with https://login.microsoftonline.com/common/oauth2/v2.0/authorize in provider details. And this works fine with personal accounts.

But whenever I define my tenant in my profile, all clients will get https://login.microsoftonline.com/{my-tenant-id}/oauth2/v2.0/authorize value which breaks it for users with personal accounts (due to URI for the token is not correct) while authorized-client named api-b-client works as planned cause to it can find an API-B in the specified {my-tenant-id} tenant.

In a past (before a switch to spring-cloud-azure-starter-active-directory) while we were using raw Spring Security, setting jwk.key-set-uri property to https://login.microsoftonline.com/common/discovery/v2.0/keys was solving this issue for us. Example:

  oauth2:
    resource:
      jwk.key-set-uri: https://login.microsoftonline.com/common/discovery/v2.0/keys
      id: <my-app-reg-id>

Any suggestion on how to achieve it? I want to be able to sign in multi-tenant users and users with personal accounts, while also having authorized-client accessing resource server (another app registration) in my tenant.

Why is this not a Bug or a feature Request?
Not sure if it is a bug, or not supported feature, or misconfiguration on my side.

Setup (please complete the following information if applicable):

  • OS: Windows 10 x64
  • IDE: IntelliJ
  • Library/Libraries: [spring-cloud-azure-starter-active-directory:4.3.0

Information Checklist
Kindly make sure that you have added all the following information above and checkoff the required fields otherwise we will treat the issuer as an incomplete report

  • Query Added
  • Setup information Added

Hi, @dbelyaev

Thanks for reaching out.

Please help me to confirm something:

I have a single page frontend application and a backend serving it with an active directory configured using the azure starter.

IMU, the backend you mentioned is a resource-server, it just validates the access token, it will not be responsible for the login flow. The login flow is implemented in the single page frontend application. Tell me if my understanding is wrong.

Specified tenant identifier '{api-b-app-id}' is neither a valid DNS name nor a valid external domain.

Please debug the application with this error, and confirm these things:

  1. Add a breakpoint in ClientRegistrationCondition.java#L44, the applicationType should be RESOURCE_SERVER_WITH_OBO. Tell me if it's not RESOURCE_SERVER_WITH_OBO.

  2. Add a breakpoint in AadAuthenticationProperties.java#L703, the grantType should be JWT_BEARER, tell me if it's not.

Hei, @chenrujun, thank you for the quick response.

IMU, the backend you mentioned is a resource-server, it just validates the access token, it will not be responsible for the login flow. The login flow is implemented in the single page frontend application. Tell me if my understanding is wrong.

Yes, correct but not fully.

Backend is a resource-server for the frontend and verifies the sign-in tokens (my tenant, external tenants and personal accounts), but it is also a client, because it is accessing another resource-server -- an API (app-registration inside of the same tenant).

Debugging

  1. Add a breakpoint in ClientRegistrationCondition.java#L44, the applicationType should be RESOURCE_SERVER_WITH_OBO. Tell me if it's not RESOURCE_SERVER_WITH_OBO.

Yes, I can confirm -- the applicationType is set to RESOURCE_SERVER_WITH_OBO.

image

  1. Add a breakpoint in AadAuthenticationProperties.java#L703, the grantType should be JWT_BEARER, tell me if it's not.

I am unable to reach AadAuthenticationProperties.java#L703.

The app flow goes into validateAuthorizationClientProperties (see AadAuthenticationProperties.java#L594), receives grantType of client_credentials on line AadAuthenticationProperties.java#L601 and goes into AadAuthenticationProperties.java#L603 due to grantType != null, thus never reaching a call to decideDefaultGrantTypeFromApplicationType on line AadAuthenticationProperties.java#L605.

Hi, @dbelyaev

Thank you for your quick response.

Specified tenant identifier '{api-b-app-id}' is neither a valid DNS name nor a valid external domain.

I reproduced this problem and got to know how to fix this problem.

When set tenant-id=common, please change

scopes: api://{api-b-app-id}/.default

to

scopes: api://{directory-name}/{api-b-app-id}/.default

Where {directory-name} should be the email suffix of the tenant. Here is an example: microsoft.com.

Hi, @chenrujun

Yes, I have tried this one. Currently, our Azure AD does have multiple custom domain names configured.
I have tried all of them:

- scopes: api://{current-email-domain-of-tenant}/{api-b-app-id}/.default
- scopes: api://{primay-domain-configured-in-azure-ad}/{api-b-app-id}/.default
- scopes: api://{each-one-of-the-custom-domain-names}/{api-b-app-id}/.default

The response during the initialization of the authorized client was the same during those attempts:

org.springframework.security.oauth2.client.ClientAuthorizationException: [invalid_resource] AADSTS500011: The resource principal named API://{tenant-primary-domain-name}/{api-b-app-id} was not found in the tenant named {Valid-Name-Of-Our-Azure-AD}. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant.

So, this did not work for me.

It is worth mentioning, that I see in the app registration of the {api-b-app-id} that it is configured with supported account types set to "My organization only" (i.e. AzureADMyOrg) and it is logical because it is an internal organization API supporting my product with some needed data. My app ({my-app-id}) which is used for authentication of users is sitting in the same organization tenant and has all admin consents and grants given to access {api-b-app-id} so should not be a problem.

Hi again, @chenrujun

We just tested changing {api-b-app-id} (app registration) supported account types to internal, external and personal accounts using the AzureADandPersonalMicrosoftAccount property and trying to refer it from my app via:

- scopes: api://{current-email-domain-of-tenant}/{api-b-app-id}/.default
- scopes: api://{primay-domain-configured-in-azure-ad}/{api-b-app-id}/.default
- scopes: api://{each-one-of-the-custom-domain-names}/{api-b-app-id}/.default

As a result, we have got the same error response as above.

Hi, @dbelyaev

scopes: api://{directory-name}/{api-b-app-id}/.default

The {directory-name} should be the one where {api-b-app-id} is created.

This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant.

Could you please try this: https://morgantechspace.com/2021/12/fix-aadsts700016-application-not-found-or-consented-to-by-any-user.html

@chenrujun
I am confused now, should it be done on app-registration for {my-app-id} (the one that is the resource server for login and client to access another {api-b-app-id}) or for the {api-b-app-id} app-registration?

On my side in {my-app-id} it is granted by admin that I can access {api-b-app-id}:

image

Hi, @dbelyaev

Here is my current understanding:

There are 2 app-registrations that belong to their own tenant:

  1. {my-app-id} belong to tenant-a.
  2. {api-b-app-id} belong to tenant-b.

Now {my-app-id} want to get access token which have scopes belong to {api-b-app-id}.

Tell me if my understanding is wrong.
I'll create a sample about this scenario and share it here soon.

FYI:

I reproduced this problem.
Now I'm not sure whether it's valid to get access-token without tenant-id in client credentials grant flow.

I created an issue here: MicrosoftDocs/azure-docs#100978

Hi, @chenrujun

Both applications belong to the same tenant - {my-tenant}.

Users are coming from different tenants:

  • multiple externals tenants (work accounts)
  • my tenant
  • Microsoft personal accounts (I see those have their tenant: 9188040d-6c67-4c5b-b112-36a304b66dad)

Here is a quick sketch to show it:

tenants-and-apps

So when I use {my-tenant-id} in spring.cloud.azure.active-directory.profile.tenant-id -- sign-ins for personal Microsoft accounts stop working:

image

And the opposite when I use common in spring.cloud.azure.active-directory.profile.tenant-id -- client credentials flow stops working (due to inability to resolve scope and app-reg-id for {api-b-app-id}:

image

Hi, @dbelyaev

Thank you for your detailed description.

It's a bug in our side. We are thinking about how to fix it.
Any updates will be added as comment in this issue.

stliu commented

@chenrujun could you please explain the root cause of this bug?

@chenrujun could you please explain the root cause of this bug?

@stliu
Root cause:
Here is current design of properties in application.yml:

spring:
  cloud:
    azure:
      active-directory:
        enabled: true
        profile:
          tenant-id: # This is shared for all "authorization-clients". "common" is a valid value.
        credential:
          client-id:
          client-secret:
        authorization-clients:
          client-1:
            scopes:
          client-2:
            scopes:
          client-3:
            scopes:
            authorization-grant-type: client_credentials # "common" is not valid when authorization-grant-type=client_credentials 
  1. In current design, spring.cloud.azure.active-directory.profile.tenant-id is shared for all authorization-clients.
  2. common is a valid value for spring.cloud.azure.active-directory.profile.tenant-id.
  3. common cannot be used for the client credentials grant. Refer to MicrosoftDocs/azure-docs#100978 (comment)

Hi, @dbelyaev

Conclusion

As described in previous comment (#31838 (comment)), this is a new feature and it's not supported now.

Workaround

Not using client_credentials may avoid this problem.

Next step

@moarychan will try to implement this new feature like this:

spring:
  cloud:
    azure:
      active-directory:
        enabled: true
        profile:
          tenant-id: # This is shared for all "authorization-clients". "common" is a valid value.
        credential:
          client-id:
          client-secret:
        authorization-clients:
          client-1:
            scopes:
          client-2:
            scopes:
          client-3:
            scopes:
            authorization-grant-type: client_credentials # "common" is not valid when auth
            tenant-id:  # New property. Tenant id that specific to client-3. 

Now we do not have ETA, maybe it will not be implemented begore 2023-01-01.