尝试在ASP.NET Core MVC中使用/me/memberOf时,Microsoft Graph Api返回禁止的响应 [英] Microsoft Graph Api returns forbidden response when trying to use /me/memberOf in ASP.NET Core MVC

查看:59
本文介绍了尝试在ASP.NET Core MVC中使用/me/memberOf时,Microsoft Graph Api返回禁止的响应的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

这就是我所拥有的. (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 ,但它们还列出了其他权限范围,而我尝试了所有这些方法,但都没有成功. (注意:身份验证可以正常工作,其他请求也可以正常工作)

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

解决方案

好吧,经过一番阅读和一些简单的运气,我明白了.因此,我想我将分享我学到的东西,因为它是如此令人困惑.另外,我发现在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;
        }

    }
}

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

Here is what I have. (ApiVersion is 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;
    }

Essentially what I am attempting to do is get all of the groups that the authenticated user is a member of, then I am creating group and role claims from that. I have left some of that out above, but the code is there and it works with the following delegated permissions User.Read.All and Directory.Read.All. I can not get it to work with Application Specific Permissions (returns Forbidden response). The reason this is a problem is because, in order to consent to the delegated permissions, it requires Global Administrator. So, I am trying to do App Only permissions to allow me to consent for the entire organization. I realize that this is fairly close to some known issues https://graph.microsoft.io/en-us/docs/overview/release_notes , but they also list alternative permission scopes, and I have tried all of those with absolutely no success. (Note: Authentication works just fine and other requests work as they should)

Could someone please give me some insight into this?

解决方案

Ok after a bunch of reading and some just plain Luck, I have this figured out. So, I figured I would share what I have learned since it is so confusing. Also, I found out that the permission that I was missing in azure was under Microsoft Graph : the Sign in and Read User profile.... which was checked in the windows azure permissions but I guess it needs to be checked in the Microsoft Graph permissions also... That is the User.Read permission for the people that mess with the manifest... Pay close attention to the GetUsersRoles Task it has been commented to help out, but you can't call "/me/memberOf", you have to call "/users/< userId >/memberOf". I really hope this helps somebody, because this Api has given me a headache everyday since I started adding it to my project.

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

这篇关于尝试在ASP.NET Core MVC中使用/me/memberOf时,Microsoft Graph Api返回禁止的响应的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆