0
votes

I'm trying to have integration with AAD for the MVC web app made with IdentityServer4. I wanted to have a code authentication flow [https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow]. After my changes in existing code (current code is show later in the post), all seemed to work properly. However, after some testing I found out that when I'm trying to sign in with an account that does not have any administrator role assigned to it, the popup below is shown. Popup shown for users without admin role

And when I'm trying to sign in to the app with account that has administrator role this popup is shown Popup shown for users with admin role

It seems to be proper behavior to me, because one of the permissions requires the admin consent.

But the problem start when I consent the permissions on behalf of my organization (sing in as user with admin Global admin role). My expected behavior is that after granting consent by one of the admins, regular users can access the app.

The actual result is that the popup number 1 is still presented to users and they can't enter my application.

What is going in my code when user signed in successfully?

Basically, when user sign in with AAD to my app for the very first time, I want to add him to my users table in the DB. If it's not the first time he sign in, I want to update some info based on the profile and the group user is assigned to. To do so I need to read the basic user information and groups names using Microsoft Graph API.

The permissions (type=delegated) I configured for the app:

  • GroupMember.Read.All
  • User.Read

GroupMember.Read.All requires admin consent. What I found out is that I need this 'GroupMember.Read.All' to retrieve groups with properties such as DisplayName filled. And it seems this permission is causing the problem with signing in. (only with User.Read signing in works fine, but obviously I don't have access to groups names etc, there are only groups ids)

The code I use for getting signed in user group info:

        var accessToken = authResult.Properties.GetTokenValue("access_token");

        GraphServiceClient graphClient = GetClient(accessToken);

        var groupsList = (await graphClient.Me.MemberOf.Request().GetAsync()).ToList();

As you can see, the access token is retrieved from properties of 'AuthenticateResult'.

Logic for getting this access_token to use graph resource is here below in Startup.cs file. Having authentication code returned I get access_token for Microsoft Graph and then I pass it to authentication result that I can use further in my services code like show above (getting groups the user is member of).

if (aadSettings.IsValid)
        {
            services.AddAuthentication()
            .AddOpenIdConnect("aad", "Active Directory", options =>
            {
                options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
                options.SignOutScheme = IdentityServerConstants.SignoutScheme;

                options.Authority = aadSettings.ADAuthority;
                options.ClientId = aadSettings.ADClientId;
                options.ClientSecret = aadSettings.ApplicationSecret;

                options.CallbackPath = aadSettings.CallbackPath;
                options.SignedOutCallbackPath = aadSettings.SignedOutCallbackPath;
                options.RemoteSignOutPath = aadSettings.RemoteSignOutPath;
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name",
                    RoleClaimType = "role"
                };
                options.Prompt = "consent";
                options.Scope.Add("https://graph.microsoft.com/User.Read");
                options.Scope.Add("https://graph.microsoft.com/GroupMember.Read.All");

                options.ResponseType = "code id_token";
                options.Resource = "https://graph.microsoft.com/";
                options.SaveTokens = true;

                options.Events.OnAuthorizationCodeReceived = async contex => {
                    var authCode = contex.ProtocolMessage.Code;
                    var credential = new ClientCredential(aadSettings.ADClientId, aadSettings.ApplicationSecret);
                    var currentUri = UriHelper.BuildAbsolute(contex.Request.Scheme, contex.Request.Host, contex.Request.PathBase, contex.Request.Path);
                    var authContext = new AuthenticationContext(aadSettings.ADAuthority);

                    AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(authCode, new Uri(currentUri), credential, options.Resource);
                    contex.HandleCodeRedemption(result.AccessToken, result.IdToken);
                };
            });

URL when sending request for auth code: https://login.microsoftonline.com/{tenant_id}/oauth2/authorize?client_id={client_id}&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fsignin-oidc&resource=https%3A%2F%2Fgraph.microsoft.com%2F&response_type=code%20id_token&prompt=consent&scope=openid%20profile%20https%3A%2F%2Fgraph.microsoft.com%2FUser.Read%20https%3A%2F%2Fgraph.microsoft.com%2FGroupMember.Read.All&response_mode=form_post&nonce=...&state=...

So the question is: How can I solve the issue with signing in for users without admin roles? and how can i get groups the user is assigned to? Am I missing some configuration in AAD? Or maybe I'm using wrong permissions?

1
Could you check in the AAD tenant where you are trying to log into if the admin consent took effect? You can see it from the Permissions tab of the Enterprise application.juunas
@juunas Yes they are stated to be granted by 'An Administrator'. also under User consent tab I can see they were granted through 'User Consent' and with users who did itMarcin Dyrcz
When the IdentityServer redirects the user to authenticate, can you see what the scope/resource parameters are in the URL?juunas
@juunas I edited the post and included there URL with parametersMarcin Dyrcz
I added an answer that hopefully solves the issue :)juunas

1 Answers

1
votes

You have specified options.Prompt = "consent";, which triggers the consent dialog every time regardless if the user has consented or not. Since you are using the "v1" authorization endpoint, you don't need to specify the Graph API scope URIs in the scope parameter (it looks at the permissions in your app registration). Removing the prompt parameter should allow users to login without consenting to permissions when they have already been consented.

One downside of the v1 endpoint is that if there are any permissions that have been consented to, consent is not triggered again, even if you add new permissions to your app registration. This works better with the v2 endpoint, where you can specify the permissions needed in the scope parameter (and leave out the resource parameter). In v2, consent is triggered if not all the requested scopes have been consented to.