0
votes

I am working on an Web API application that will be secured used Identity Server 4. The user is authenticated using implicit flow using a JavaScript client.

I am having problems reading the IdentityResource scopes in my Web API client. I think I might be missing something in the configuration when I invoke app.UseIdentityServerAuthentication( ) in my Startup.cs. I am not seeing the JSON data in any of these IdentityResource scopes when I examine the User.Claims collection.

In the JavaScript client, I am able to view the scope data using the oidc-client.js library. But, I cannot read the scope data in my Web API application.

In the javascript client, I can see the scope data stored as json blob by doing this in TypeScript:

class MyService {
   private _userManager: Oidc.UserManager;

   private _createUserManager(authority: string, origin: string) {

    // https://github.com/IdentityModel/oidc-client-js/wiki#configuration
    var config : Oidc.UserManagerSettings = {
        authority: authority,
        client_id: "myapi",
        redirect_uri: `${origin}/callback.html`,
        response_type: "id_token token",
        scope: "openid profile user.profile user.organization ...",
        post_logout_redirect_uri: `${origin}/index.html`

    };

     this._userManager = new Oidc.UserManager(config);
   }

   // snip
   user() {
    this._userManager.getUser().then(user => {

            const userProfile = JSON.parse(user.profile["user.profile"]);
            console.log(userProfile);

            const userOrganization = JSON.parse(user.profile["user.organization"]);
            console.log(userOrganization);

        });

   }
}

In my Web API Project, I have:

public class Startup
{
    public IConfigurationRoot Configuration { get; set; }
    public IContainer Container { get; set; }

    public Startup(IHostingEnvironment env)
    {
        var configurationBuilder = new ConfigurationBuilder();

        if (env.IsDevelopment())
        {
            configurationBuilder
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json");

            env.ConfigureNLog("nlog.development.config");
        }
        else
        {
            configurationBuilder.AddEnvironmentVariables();

            env.ConfigureNLog("nlog.config");
        }

        Configuration = configurationBuilder.Build();
    }

    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        services
            .AddMvcCore()
            .AddJsonFormatters()
            .AddAuthorization();

        services.AddCors();

        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        services.AddScoped<IRestService, RestService>();

        var builder = AutoFacConfig.Create(Configuration);
        builder.Populate(services);

        Container = builder.Build();

        return new AutofacServiceProvider(Container);
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        app.UseCors(policy =>
        {
            policy
                .WithOrigins(
                    "http://app.local:5000"
                )
                .AllowAnyHeader()
                .AllowAnyMethod()
                .AllowCredentials();
        });

        ConfigureExceptionHandling(app, env);
        ConfigureOidc(app);

        app.UseMvc();

    }

    private void ConfigureExceptionHandling(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    }

    private void ConfigureOidc(IApplicationBuilder app)
    {

        app.UseCors("default");

        app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
        {
            Authority = Configuration["IdentityServerUrl"],
            AllowedScopes = { "api1", "openid", "profile", "user.profile", "user.organization" ... },
            ApiName = "myapi",

            RequireHttpsMetadata = false
        });
    }
}

The problem is when I try to access the claims to read its scopes. When the user is authenticated, I pass the token in the header of the request to the Web API endpoint. I have a BaseController in which I would like to read the scopes from, but cannot.

public class BaseController : Controller
{
    private ApplicationUser _user;

    public string UserId => CurrentUser?.UserInfo.Id;

    public string OrganizationId => CurrentUser?.Organization.Id;

    [CanBeNull]
    public ApplicationUser CurrentUser
    {
        get
        {           
            var claims = Request.HttpContext.User.Claims.ToList();

            _user = new ApplicationUser
            {
                UserInfo = claims.Where(f => f.Type == "user.profile").Select(s => s.Value).FirstOrDefault(),
                OrganizationInfo = claims.Where(f => f.Type == "user.organization").Select(s => s.Value).FirstOrDefault()
            };

            return _user;
        }
    }
}

If, I look at the claims collection, I see the following for Types and values:

nbf: 1499441027

exp: 1499444627

iss: https://identity-service....

aud: https://identity-service..../resources

aud: myapi

client_id: myapi

sub: the_user

auth_time: 1499441027

idp: local

scope: openid

scope: profile

scope: user.profile

scope: user.organization

...

scope: myapi

amr: pwd

How do I read scope data from C#?

For user.organization scope, I am expecting to read a data structure like:

{
   "Id":"some id",
   "BusinessCategory":"some category",
   "Name":"some name"
}
2

2 Answers

0
votes

Scope is Type and Profile is Value for one of the scopes, that is why it is like that. What value you want to see for profile?? It doesn't have any value as it is a value itself. You are asking for it: AllowedScopes = { "api1", "openid", "profile", "user.profile", ... },

and that is what you are getting in your scope in the claims list.

If you want to fetch a particular value from the claims list, you can also use JwtRegisteredClaimNames using

User.FindFirst(JwtRegisteredClaimNames.Email).Value

0
votes

In case this is useful to anyone. I have implemented a hack. I added extra code in my middleware to call the identity server's user info endpoint. The user info endpoint will return the data that I need. I then manually add claims with this data.

I have added this code to my Configure method:

app.Use(async (context, next) =>
{

    if (context.User.Identity.IsAuthenticated)
    {
        using (var scope = Container.BeginLifetimeScope())
        {
            // fetch resource identity scopes from userinfo endpoint
            var restService = scope.Resolve<IRestService>();
            var token = context.Request.Headers["Authorization"]
              .ToString().Replace("Bearer ", string.Empty);

            restService.SetAuthorizationHeader("Bearer", token);

            // fetch data from my custom RestService class and 
            // serialize into my custom ScopeData object
            var data = await restService.Get<ScopeData>(
              Configuration["IdentityServerUrl"], "connect/userinfo");

            if (data.IsSuccessStatusCode)
            {

                // add identity resource scopes to claims
                var claims = new List<Claim>
                {
                    new Claim("user.organization", data.Result.Organization),
                    new Claim("user.profile", data.Result.UserInfo)
                };

            }
            else
            {
                sLogger.Warn("Failed to retrieve identity scopes from profile service.");
                context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
            }
        }


    }

    await next();
});