【问题标题】:Microsoft Graph Api returns forbidden response when trying to use /me/memberOf in ASP.NET Core MVC尝试在 ASP.NET Core MVC 中使用 /me/memberOf 时,Microsoft Graph Api 返回禁止响应
【发布时间】:2019-08-16 00:02:46
【问题描述】:

这就是我所拥有的。 (ApiVersion 为 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 的一些已知问题相当接近,但它们也列出了替代权限范围,我已经尝试了所有这些绝对没有成功的问题。 (注意:身份验证工作正常,其他请求正常工作)

有人能给我一些见解吗?

【问题讨论】:

    标签: c# asp.net-core office365 asp.net-core-mvc microsoft-graph-api


    【解决方案1】:

    好的,经过大量阅读和一些简单的运气后,我想通了。所以,我想我会分享我学到的东西,因为它是如此令人困惑。另外,我发现我在 azure 中缺少的权限在 Microsoft Graph 下:登录和读取用户配置文件.... 在 windows azure 权限中进行了检查,但我想它需要在 Microsoft Graph 中进行检查权限也...这是与清单混淆的人的 User.Read 权限...密切注意 GetUsersRoles 任务,它已被评论以提供帮助,但您不能调用“/me/memberOf” ,你必须调用“/users//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;
            }
    
        }
    }
    

    NaiveSessionCache.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();
                }
            }
        }
    }
    

    【讨论】:

      【解决方案2】:

      请在此处再次阅读有关权限的 Microsoft Graph 主题:https://graph.microsoft.io/en-us/docs/authorization/permission_scopes。这里有几个概念可能有助于澄清事情(尽管我们的文档在这方面当然可以改进):

      1. 有两种权限:应用权限和委托权限
      2. 某些委派的权限可以得到最终用户的同意(通常当权限的范围是请求登录用户的数据时,例如他们的个人资料、邮件、文件。
      3. 提供对更多数据的访问权限的其他委派权限通常需要管理员同意。
      4. 应用程序权限始终需要管理员同意。根据定义,这些是租户范围的(因为没有用户上下文)。
      5. 管理员可以代表组织同意委派的权限(从而抑制最终用户的任何同意体验)。同样,还有更多可用的主题。

      如果您始终有一个已登录的用户(看起来很像),我强烈建议您使用委派权限而不是应用程序权限。

      我还注意到您正在使用组显示名称创建声明。组显示名称不是一成不变的,可以更改...如果应用根据这些声明的价值做出授权决策,不确定这是否可能会导致一些有趣的安全问题。

      希望这会有所帮助,

      【讨论】:

        【解决方案3】:

        我们还使用 AAD 进行身份验证,在我们的例子中,我们需要强制用户再次同意应用程序权限。

        我们通过将 prompt=consent 参数添加到 AAD 登录请求来解决单个用户的问题。对于 ADAL.js,这里有一个示例:

        Microsoft Graph API - 403 Forbidden for v1.0/me/events

        帖子中的相关代码示例:

        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"
        }
        

        【讨论】:

          【解决方案4】:

          我遇到了类似的问题,只是我的令牌已过期或无效。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多