1

I want to get the claims for an access token, but where I try an get the UserInfo the response returns an error "Forbidden". Why is this and how do I fix it? The userinfo endpoint is https://localhost:44307/connect/userinfo The code below will be refactored once it works. The field response1 contains the error message;

    var client = new HttpClient();
    var disco = await client.GetDiscoveryDocumentAsync(Settings.AuthorityUrl);
    if (disco.IsError)
    {
        throw new Exception(disco.Error);
    }
    var tokenRequest = new ClientCredentialsTokenRequest
    {
        Address = Settings.AuthorityUrl + "connect/token",
        ClientId = Settings.ClientId,
        ClientSecret = "secret",
        Scope = "SIR"
    };

    var response = await client.RequestClientCredentialsTokenAsync(tokenRequest);
    var token = response.AccessToken;
    var response1 = await client.GetUserInfoAsync(new UserInfoRequest
    {
        Address = disco.UserInfoEndpoint,
        Token = token
    });

    if (response1.IsError) throw new Exception(response1.Error);

    var claims = response1.Claims;

In my IDP my config file is

using IdentityServer4;
using IdentityServer4.Models;
using IdentityServer4.Test;
using System.Collections.Generic;
using System.Security.Claims;

namespace QuickstartIdentityServer
{
    public class Config
    {
        // scopes define the resources in your system
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Address()
            };
        }

        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource("SIR", "Service Inspection Report")
            };
        }

        // clients want to access resources (aka scopes)
        public static IEnumerable<Client> GetClients()
        {
            var baseUri = "http://localhost:53200/";
            // client credentials client
            return new List<Client>
            {

                // OpenID Connect hybrid flow and client credentials client (MVC)
                new Client
                {
                    ClientId = "SIR",
                    ClientName = "SIR",
                    AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,

                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },

                    RedirectUris = { $"{baseUri}signin-oidc" },
                    PostLogoutRedirectUris = { $"{baseUri}signout-callback-oidc" },

                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.Address,
                        "SIR"
                    },
                    AllowOfflineAccess = true,
                    AlwaysIncludeUserClaimsInIdToken = true
                }
            };
        }

        public static List<TestUser> GetUsers()
        {
            return new List<TestUser>
            {
                new TestUser
                {
                    SubjectId = "1",
                    Username = "alice",
                    Password = "password",

                    Claims = new List<Claim>
                    {
                        new Claim("name", "Alice"),
                        new Claim("website", "https://alice.com"),
                        new Claim("address", "1a The Street")
                    }
                },
                new TestUser
                {
                    SubjectId = "2",
                    Username = "bob",
                    Password = "password",

                    Claims = new List<Claim>
                    {
                        new Claim("name", "Bob"),
                        new Claim("website", "https://bob.com"),
                        new Claim("address", "2a The Street")
                    }
                }
            };
        }
    }
}

And the Startup is;

public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        // configure identity server with in-memory stores, keys, clients and scopes
        services.AddIdentityServer()
            .AddSigningCredential(new X509Certificate2(Settings.CertPath, Settings.Password))
            .AddInMemoryIdentityResources(Config.GetIdentityResources())
            .AddInMemoryApiResources(Config.GetApiResources())
            .AddInMemoryClients(Config.GetClients())
            .AddTestUsers(Config.GetUsers());

        services.AddAuthentication()
            .AddGoogle("Google", options =>
            {
                options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;

                // register your IdentityServer with Google at https://console.developers.google.com
                // enable the Google+ API
                // set the redirect URI to http://localhost:port/signin-google
                options.ClientId = "copy client ID from Google here";
                options.ClientSecret = "copy client secret from Google here";
            })
            .AddOpenIdConnect("oidc", "OpenID Connect", options =>
            {
                options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
                options.SignOutScheme = IdentityServerConstants.SignoutScheme;

                options.Authority = "https://demo.identityserver.io/";
                options.ClientId = "implicit";

                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name",
                    RoleClaimType = "role"
                };
            });
    }

    // 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)
    {
        loggerFactory.AddConsole();
        loggerFactory.AddDebug();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseMiddleware<StackifyMiddleware.RequestTracerMiddleware>();
        app.UseIdentityServer();
        app.UseStaticFiles();
        app.UseMvcWithDefaultRoute();
    }
}
arame3333
  • 9,887
  • 26
  • 122
  • 205

1 Answers1

3

You are using client credential grant defined by OAuth 2.0.

In your code, RequestClientCredentialsTokenAsync stands for a token request using this grant type. Note that when you obtain an access token through this grant, there is no end user involvement. It is not OpenID Connect (no end user authenticated) and simply works on client credentials, letting you have an access token only.

Now when you use this access token for UserInfo request, identity server detects it to not have any correlated end user. Hence it return forbidden response to let you know that you do not have permission to access the endpoint.

See the exact information from documentation,

The UserInfo endpoint can be used to retrieve identity information about a user (see spec).

The caller needs to send a valid access token representing the user.

If you want user information, use authorization code grant or hybrid flow with scope value openid to enable OpenID Connect request. You can read more here for different grant types.

Community
  • 1
  • 1
Kavindu Dodanduwa
  • 12,193
  • 3
  • 33
  • 46
  • I guess yoou are right. However the problem I am having is that I am trying to get the access token via the Microsoft.Owin.Security namespace whilst var accessToken = await HttpContext.GetTokenAsync("access_token") requires the Microsoft.AspNetCore.Authentication which is not available in my client application. So how can I fix this? Maybe I can't? – arame3333 Jan 14 '19 at 07:37
  • @arame3333 well this is out of my knowledge. But I checked few of your other questions. If you are worried over logout, then you can simply invoke IdentityServer's end session endpoint. Please read more - http://docs.identityserver.io/en/latest/topics/signout.html#sign-out-initiated-by-a-client-application – Kavindu Dodanduwa Jan 14 '19 at 07:47
  • anyway as a person who has implemented clients in many platforms, my recommendation is to not to worry about framework as most of the involved server calls are HTTP ones which can be easily understood. Only commitment is to read the specification a bit. – Kavindu Dodanduwa Jan 14 '19 at 07:49
  • I read the specification to get the original code. However I did not appreciate that Access Tokens would be different from each other but I guess that is bound to be the case as the grants do different things. When I look at the spec for Token endpoints there is not one for Hybrid tokens – arame3333 Jan 14 '19 at 08:26
  • @arame3333 yes, access token will have different scopes (permissions) set depending on the grant used. At the moment you are using client credential grant which does not give you access to any user information (since there's no user involvement). Hybrid flow is one defined by OpenID Connect (https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowSteps) . You may not require to dig in to that flow. Instead use authoriztion code flow which is supported by many client libraries. – Kavindu Dodanduwa Jan 14 '19 at 08:33
  • Also there's nothing called hybrid tokens :) It is hybrid flow, which provide tokens from authorization code response. But I highly recommend to use authorizartion code flow. – Kavindu Dodanduwa Jan 14 '19 at 08:34
  • As you can tell I am new to this, so I am Googling around to find some example code. Looking at the docs I am wondering if this means I am using the implicit flow, and what value I should set the Nonce? https://identitymodel.readthedocs.io/en/latest/client/authorize.html. I am also looking at getting the token using the authorisation code, see https://identitymodel.readthedocs.io/en/latest/client/token.html and how I connect the 2? – arame3333 Jan 14 '19 at 09:49
  • @arame3333 It is a two stepped processes. You initially obtain authorization code. This occur through the browser. Once auth. code is there, you directly call identity server to obtain tokens (invoking token endpoint). That's how it happens. About nonce, well check my answer - https://stackoverflow.com/questions/46844285/difference-between-oauth-2-0-state-and-openid-nonce-parameter-why-state-cou/46859861#46859861 (explains more) – Kavindu Dodanduwa Jan 14 '19 at 09:55
  • Check this diagram as well - https://learn.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code#oauth-20-authorization-flow . Hope things are clear now – Kavindu Dodanduwa Jan 14 '19 at 09:56
  • To use the authorisation flow, do I need to create a new client to do that? Hybrid includes authorization flow but I cannot get an access token from a hybrid flow. To get an authorisation flow token I need to send a Code and I am not sure where that comes from. – arame3333 Jan 15 '19 at 16:50
  • I do not think you need to create a dedicated client. You obtain authorization code from authorization code requests' response – Kavindu Dodanduwa Jan 16 '19 at 03:14