Microsoft Graph Api returns forbidden response when trying to use /me/memberOf in ASP.NET Core MVC

前端 未结 4 1155
旧巷少年郎
旧巷少年郎 2021-01-07 10:18

Here is what I have. (ApiVersion is v1.0)

private async Task GetUsersRoles(string accessToken, ClaimsIdentity identity, string userId)
         


        
4条回答
  •  隐瞒了意图╮
    2021-01-07 10:28

    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
            /// 
            /// Returns user's roles and groups as a ClaimsIdentity
            /// 
            /// accessToken retrieved using the client credentials and the resource (Hint: NOT the accessToken from the signin event)
            /// The user's unique identifier from the signin event
            /// ClaimsIdentity
            private async Task 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();
    
                    var responseClaims = serializer.DeserializeObject(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(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();
                }
            }
        }
    }
    

提交回复
热议问题