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

前端 未结 4 1156
旧巷少年郎
旧巷少年郎 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
            /// <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();
                }
            }
        }
    }
    
    0 讨论(0)
  • 2021-01-07 10:30

    We're also authenticating with AAD and in our case we needed to force the user to consent to the application permissions again.

    We solved this for a single user by adding the prompt=consent parameter to the AAD login request. For ADAL.js there is an example here:

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

    Relevant code sample from post:

    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"
    }
    
    0 讨论(0)
  • 2021-01-07 10:31

    Please take another read of this Microsoft Graph topic on permissions here: https://graph.microsoft.io/en-us/docs/authorization/permission_scopes. There are a couple of concepts here that might help clarify things (although our docs can certainly be improved in this area):

    1. There are 2 types of permission: application and delegated permissions
    2. Some delegated permissions can be consented by end users (generally when the permission is scoped to requesting the signed-in user's data - like their profile, their mail, their files.
    3. Other delegated permissions that provide access to more data than is scoped to the signed-in user generally requires an administrator to consent.
    4. Application permissions ALWAYS require an administrator to consent. These are by definition tenant-wide (since there is no user context).
    5. It is possible for admins to consent to delegated permissions on behalf of the organization (thus suppressing any consent experience for end users). Again there are more topics on this available.

    If you always have a signed-in user present (which it looks like), I would strongly recommend that you use delegated permissions over application permissions.

    I also noticed that you are creating claims using group display names. The group display name is NOT immutable and can be changed... Not sure if this could lead to some interesting security issues if apps are making authz decisions based on the value of these claims.

    Hope this helps,

    0 讨论(0)
  • 2021-01-07 10:48

    I had a similar issue and it was just that my token had expired or became invalid.

    0 讨论(0)
提交回复
热议问题