Craig
Craig

Reputation: 788

IdentityServer4 - user permissions on the API

I'm currently setting up a new project with a Web API and a MVC UI (will eventually have a mobile UI as well which can talk to the same API).

So, I have the following plan:

  1. User navigates to the MVC UI which takes them off to the IdentityServer4 server to log in or sign up
  2. IdentityServer user is then added to the applications own user table in the database
  3. Permissions can then be set on the user to limit their access

This means the identity server is just that, an identity server (and means I allow people to log in through Google, etc. without worrying about their roles and permissions).

So, to achieve the above, I need to check the user's permission on the API, NOT on the client (the client could be anything - web, phone app, JavaScript client, etc. so we can't rely on that to handle user permissions).

On the API, I have implemented a Permissionhandler and PermissionRequirement authorization policy. So, on the API controller or method, I can do something like: [Authorize(Policy = "CreateUser")]. It looks like I'll need to have a policy per system permission.

So in the authorisation handler I need to:

  1. Get the current user's username
  2. If they exist in the app database, check their permissions and auth or deny
  3. If they don't exist in the app database, add them, then we can set their permissions later from the admin panel

This was going well up until I tried to request the user's username from the identity server. I understand I need to use the UserInfoClient to do that, but I can't figure out how to use the user's token/credentials to get their claims from the identity server or to at least get their User.Identity.Name.

Now I could just use the sub ID to add the user to the application database, but then whoever's managing the permissions would have no idea who that person is, so I need to use their email really.

Now in the MVC client, I can see User.Identity.Name without any problems, but in the API that value is null!?

So my question is: how on earth do I get the current user's Identity.Name, username or email from within the API?

Thanks.

Upvotes: 3

Views: 2855

Answers (2)

Craig
Craig

Reputation: 788

Think I've cracked it now.

            var mvcContext = context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext;
        if (mvcContext != null)
        {
            // Use the UserInfo endpoint to get the user's claims
            var discoveryClient = new DiscoveryClient("http://localhost:5000");
            var doc = await discoveryClient.GetAsync();

            var accessToken = await mvcContext.HttpContext.Authentication.GetTokenAsync("access_token");

            var userInfoClient = new UserInfoClient(doc.UserInfoEndpoint);
            var response = await userInfoClient.GetAsync(accessToken);

            var claims = response.Claims;
        }

This is detailed right at the bottom of https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies so according to Microsoft, it's the right way.

You did however spark this idea, thank you Win. I had been staring at this bloody thing for hours. All you need sometimes is someone else to say something, anything, and the curse is broken! :)

I am still open to a better way of achieving this, if anyone has one.

Cheers

Upvotes: 3

Win
Win

Reputation: 62290

I believe you already configure IdentityServerAuthentication in Web API similar like this -

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
   ...
   app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
   {
      Authority = "http://UrlOfIentityServer",
      RequireHttpsMetadata = false,
      ApiName = "exampleapi"
   });
   ...
}

When you make a web service call, you will need to pass the same token received from IdentityServer like this -

using (var httpClient = new HttpClient())
{
   string accessToken = await HttpContext.Authentication.GetTokenAsync("access_token");

   httpClient.BaseAddress = new Uri("http://UrlOfWebAPI");
   httpClient.DefaultRequestHeaders.Clear();
   httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

   return await httpClient.GetStringAsync("api/clock/time");
}

Upvotes: 2

Related Questions