Microsoft Graph Api возвращает запрещенный ответ при попытке использовать /me/memberOf в ASP.NET Core MVC

Вот что у меня есть. (Апиверсия v1.0)

private async Task<ClaimsIdentity> GetUsersRoles(string accessToken, ClaimsIdentity identity, string userId)
{
           string resource = GraphResourceId + ApiVersion + "/me/memberOf";

            var client = new HttpClient();

            var request = new HttpRequestMessage(HttpMethod.Get, new Uri(resource));

            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

            var response = await client.SendAsync(request);

        return identity;
    }

По сути, я пытаюсь получить все группы, членом которых является аутентифицированный пользователь, а затем я создаю из них заявки на группы и роли. Я не упомянул кое-что из вышеизложенного, но код есть, и он работает со следующими делегированными разрешениями User.Read.All и Directory.Read.All. Я не могу заставить его работать с разрешениями для конкретных приложений (возвращает запрещенный ответ). Причина, по которой это проблема, заключается в том, что для согласия с делегированными разрешениями требуется глобальный администратор. Итак, я пытаюсь сделать разрешения только для приложений, чтобы позволить мне дать согласие для всей организации. Я понимаю, что это довольно близко к некоторым известным проблемам https://graph.microsoft.io/en-us/docs/overview/release_notes , но они также перечисляют альтернативные области разрешений, и я пробовал все из них, но безуспешно. (Примечание: аутентификация работает нормально, а другие запросы работают как надо)

Может ли кто-нибудь дать мне некоторое представление об этом?


person Patrick Mcvay    schedule 21.11.2016    source источник


Ответы (4)


Хорошо, после кучи чтения и просто удачи, я понял это. Итак, я решил поделиться тем, что я узнал, так как это так запутанно. Кроме того, я обнаружил, что разрешение, которого мне не хватало в Azure, было в Microsoft Graph: профиль входа и чтения пользователя .... который был проверен в разрешениях Windows Azure, но я думаю, что его нужно проверить в Microsoft Graph разрешения также... Это разрешение User.Read для людей, которые возятся с манифестом... Обратите особое внимание на задачу GetUsersRoles, которая была прокомментирована, чтобы помочь, но вы не можете вызвать "/me/memberOf" , вы должны вызвать "/users/‹ userId >/memberOf". Я действительно надеюсь, что это кому-то поможет, потому что этот API доставляет мне головную боль каждый день с тех пор, как я начал добавлять его в свой проект.

Startup.cs

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication;
using MyApp.Utils;
using Microsoft.Graph;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;

namespace MyApp
{
    public class Startup
    {
        public static string ClientId;
        public static string ClientSecret;
        public static string Authority;
        public static string GraphResourceId;
        public static string ApiVersion;
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

            if (env.IsDevelopment())
            {
                // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709
                builder.AddUserSecrets();
            }
            builder.AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; set; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add Session services
            services.AddSession();

            // Add Auth
            services.AddAuthentication(
                SharedOptions => SharedOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme);

            services.AddMvc(config =>
            {
                var policy = new AuthorizationPolicyBuilder()
                                .RequireAuthenticatedUser()
                                .Build();

                config.Filters.Add(new AuthorizeFilter(policy));
            });

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            // Configure session middleware.
            app.UseSession();

            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            // Populate AzureAd Configuration Values 

            ClientId = Configuration["AzureAd:ClientId"];
            ClientSecret = Configuration["AzureAd:ClientSecret"];
            GraphResourceId = Configuration["AzureAd:GraphResourceId"];
            Authority = Configuration["AzureAd:AadInstance"] + Configuration["AzureAd:TenantId"];
            ApiVersion = Configuration["AzureAd:ApiVersion"];

            // Implement Cookie Middleware For OpenId
            app.UseCookieAuthentication();
            // Set up the OpenId options
            app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
            {
                ClientId = Configuration["AzureAd:ClientId"],
                ClientSecret = Configuration["AzureAd:ClientSecret"],
                Authority = Configuration["AzureAd:AadInstance"] + Configuration["AzureAd:TenantId"],
                CallbackPath = Configuration["AzureAd:CallbackPath"],
                ResponseType = OpenIdConnectResponseType.CodeIdToken,
                Events = new OpenIdConnectEvents
                {
                    OnRemoteFailure = OnAuthenticationFailed,
                    OnAuthorizationCodeReceived = OnAuthorizationCodeReceived,
                },

                TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
                {
                    NameClaimType = "name",
                },
                GetClaimsFromUserInfoEndpoint = true,
                SaveTokens = true
            });

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });

        }

        private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
        {
            // Acquire a Token for the Graph API and cache it using ADAL.
            string userObjectId = (context.Ticket.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
            ClientCredential clientCred = new ClientCredential(ClientId, ClientSecret);

            // Gets Authentication Tokens From Azure
            AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session));

            // Gets the Access Token To Graph API
            AuthenticationResult authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(
                context.ProtocolMessage.Code, new Uri(context.Properties.Items[OpenIdConnectDefaults.RedirectUriForCodePropertiesKey]), clientCred, GraphResourceId);

            // Gets the Access Token for Application Only Permissions
            AuthenticationResult clientAuthResult = await authContext.AcquireTokenAsync(GraphResourceId, clientCred);

            // The user's unique identifier from the signin event
            string userId = authResult.UserInfo.UniqueId;

            // Get the users roles and groups from the Graph Api. Then return the roles and groups in a new identity
            ClaimsIdentity identity = await GetUsersRoles(clientAuthResult.AccessToken, userId);

            // Add the roles to the Principal User
            context.Ticket.Principal.AddIdentity(identity);

            // Notify the OIDC middleware that we already took care of code redemption.
            context.HandleCodeRedemption();
        }

        // Handle sign-in errors differently than generic errors.
        private Task OnAuthenticationFailed(FailureContext context)
        {
            context.HandleResponse();

            context.Response.Redirect("/Home/Error?message=" + context.Failure.Message);
            return Task.FromResult(0);
        }

        // Get user's roles as the Application
        /// <summary>
        /// Returns user's roles and groups as a ClaimsIdentity
        /// </summary>
        /// <param name="accessToken">accessToken retrieved using the client credentials and the resource (Hint: NOT the accessToken from the signin event)</param>
        /// <param name="userId">The user's unique identifier from the signin event</param>
        /// <returns>ClaimsIdentity</returns>
        private async Task<ClaimsIdentity> GetUsersRoles(string accessToken, string userId)
        {
            ClaimsIdentity identity = new ClaimsIdentity("LocalIds");

            var serializer = new Serializer();

            string resource = GraphResourceId + ApiVersion + "/users/" + userId + "/memberOf";

            var client = new HttpClient();

            var request = new HttpRequestMessage(HttpMethod.Get, new Uri(resource));

            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                var responseString = await response.Content.ReadAsStringAsync();

                var claims = new List<Claim>();

                var responseClaims = serializer.DeserializeObject<Microsoft.Graph.UserMemberOfCollectionWithReferencesResponse>(responseString);
                if (responseClaims.Value != null)
                {
                    foreach (var item in responseClaims.Value)
                    {
                        if (item.ODataType == "#microsoft.graph.group")
                        {
                            // Serialize the Directory Object
                            var gr = serializer.SerializeObject(item);
                            // Deserialize into a Group
                            var group = serializer.DeserializeObject<Microsoft.Graph.Group>(gr);
                            if (group.SecurityEnabled == true)
                            {
                                claims.Add(new Claim(ClaimTypes.Role, group.DisplayName));
                            }
                            else
                            {
                                claims.Add(new Claim("group", group.DisplayName));
                            }
                        }
                    }
                }
                identity.AddClaims(claims);
            }
            return identity;
        }

    }
}

Наивесессионкаче.cs

// This is actually in a directory named Utils

using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.Clients.ActiveDirectory;

namespace MyApp.Utils
{
    public class NaiveSessionCache : TokenCache
    {
        private static readonly object FileLock = new object();
        string UserObjectId = string.Empty;
        string CacheId = string.Empty;
        ISession Session = null;

        public NaiveSessionCache(string userId, ISession session)
        {
            UserObjectId = userId;
            CacheId = UserObjectId + "_TokenCache";
            Session = session;
            this.AfterAccess = AfterAccessNotification;
            this.BeforeAccess = BeforeAccessNotification;
            Load();
        }

        public void Load()
        {
            lock (FileLock)
            {
                Deserialize(Session.Get(CacheId));

            }
        }

        public void Persist()
        {
            lock (FileLock)
            {
                // reflect changes in the persistent store
                Session.Set(CacheId, this.Serialize());
                // once the write operation took place, restore the HasStateChanged bit to false
                this.HasStateChanged = false;
            }
        }

        // Empties the persistent store.
        public override void Clear()
        {
            base.Clear();
            Session.Remove(CacheId);
        }

        public override void DeleteItem(TokenCacheItem item)
        {
            base.DeleteItem(item);
            Persist();
        }

        // Triggered right before ADAL needs to access the cache.
        // Reload the cache from the persistent store in case it changed since the last access.
        void BeforeAccessNotification(TokenCacheNotificationArgs args)
        {
            Load();
        }

        // Triggered right after ADAL accessed the cache.
        void AfterAccessNotification(TokenCacheNotificationArgs args)
        {
            // if the access operation resulted in a cache update
            if (this.HasStateChanged)
            {
                Persist();
            }
        }
    }
}
person Patrick Mcvay    schedule 21.11.2016

Прочтите еще раз эту тему Microsoft Graph о разрешениях здесь: https://graph.microsoft.io/en-us/docs/authorization/permission_scopes. Здесь есть несколько концепций, которые могут помочь прояснить ситуацию (хотя наши документы, безусловно, могут быть улучшены в этой области):

  1. Существует 2 типа разрешений: приложения и делегированные разрешения.
  2. Некоторые делегированные разрешения могут быть предоставлены конечными пользователями (как правило, когда разрешение ограничивается запросом данных вошедшего пользователя, таких как его профиль, его почта, его файлы.
  3. Для других делегированных разрешений, предоставляющих доступ к большему количеству данных, чем предусмотрено для вошедшего в систему пользователя, обычно требуется согласие администратора.
  4. Разрешения приложения ВСЕГДА требуют согласия администратора. Они по определению являются общими для арендатора (поскольку контекст пользователя отсутствует).
  5. Администраторы могут давать согласие на делегированные разрешения от имени организации (таким образом подавляя любой опыт согласия для конечных пользователей). Опять же есть больше тем по этому вопросу.

Если у вас всегда есть зарегистрированный пользователь (как это выглядит), я настоятельно рекомендую вам использовать делегированные разрешения вместо разрешений приложения.

Я также заметил, что вы создаете утверждения, используя отображаемые имена групп. Отображаемое имя группы НЕ является неизменным и может быть изменено... Не уверен, что это может привести к некоторым интересным проблемам безопасности, если приложения принимают решения об аутентификации на основе значения этих утверждений.

Надеюсь это поможет,

person Dan Kershaw - MSFT    schedule 26.11.2016

Мы также аутентифицируемся с помощью AAD, и в нашем случае нам нужно было заставить пользователя снова дать согласие на разрешения приложения.

Мы решили эту проблему для одного пользователя, добавив параметр prompt=consent в запрос на вход в AAD. Для ADAL.js здесь приведен пример:

API Microsoft Graph - 403 Запрещено для v1.0/me /события

Соответствующий пример кода из поста:

window.config = {
    tenant: variables.azureAD,
    clientId: variables.clientId,
    postLogoutRedirectUri: window.location.origin,
    endpoints: {
        graphApiUri: "https://graph.microsoft.com",
        sharePointUri: "https://" + variables.sharePointTenant + ".sharepoint.com",
    },
    cacheLocation: "localStorage",
    extraQueryParameter: "prompt=consent"
}
person gadeweever    schedule 20.04.2017

У меня была аналогичная проблема, и срок действия моего токена истек или стал недействительным.

person Michael    schedule 15.08.2019