尝试在ASP.NET Core MVC中使用/ me / memberOf时,Microsoft Graph Api返回禁止响应

时间:2016-11-21 13:20:50

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

这就是我所拥有的。 (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。我无法使用Application Specific Permissions(返回Forbidden响应)。这是一个问题的原因是,为了同意委派的权限,它需要全局管理员。因此,我正在尝试仅限App,允许我同意整个组织。我意识到这与一些已知问题https://graph.microsoft.io/en-us/docs/overview/release_notes非常接近,但它们也列出了替代权限范围,我已经尝试了所有这些但绝对没有成功。 (注意:身份验证工作正常,其他请求可以正常工作)

有人可以给我一些见解吗?

4 个答案:

答案 0 :(得分:1)

好好看了一堆阅读,有些只是简单的运气,我有这个想通了。所以,我想我会分享我学到的东西,因为它太混乱了。此外,我发现我在azure中遗漏的权限是在Microsoft Graph下:登录和读取用户配置文件....这是在windows azure权限中检查但我想它需要在Microsoft Graph中检查权限也是......那就是那些搞乱清单的人的User.Read权限......密切关注GetUsersRoles任务,它已经被评论过来帮忙,但你不能调用“/ me / memberOf” ,你必须调用“/ users /&lt; userId&gt; / memberOf”。我真的希望这对某些人有所帮助,因为自从我开始将它添加到我的项目中以来,每天都让我头疼。

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

答案 1 :(得分:0)

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

  1. 有两种类型的权限:应用程序和委派权限
  2. 最终用户可以同意某些委派权限(通常在权限限定为请求登录用户的数据时 - 例如他们的个人资料,邮件和文件。
  3. 提供对更多数据的访问权限的其他委派权限通常需要管理员同意。
  4. 应用程序权限始终要求管理员同意。根据定义,这些是租户范围的(因为没有用户上下文)。
  5. 管理员可以代表组织同意委派的权限(从而抑制最终用户的任何同意体验)。此外,还有更多相关主题。
  6. 如果您总是有一个已登录的用户(看起来像),我强烈建议您使用委派权限而不是应用程序权限。

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

    希望这有帮助,

答案 2 :(得分:0)

我们也在使用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"
}

答案 3 :(得分:0)

我有一个类似的问题,只是我的令牌已过期或变得无效。