Getting currently logged user in web api

Ok, so I have a web app calling a web api, both written in ASP .Net Core 1.1 MVC, using Auth0 as the authentication server, and using the “authorization code grant flow”.

My problem is that, once the user has successfully logged in, and the web app calls a web api controller method, how can I know on the web api side which user is logged in? So if, for example, the user does something which makes the web app call a web api controller method which creates a new record in a database, and I need to log in the database which user created this new record, how can the web api know who was logged in when this method was invoked?

I’ve looked at User.Identity but this seems to only show information about the client (i.e. the web app) and not the actual user…

1 Like

You can get the ID of the user by querying the value of the NameIdentifier claim type, e.g.

// Inside one of your API controller actions

string userId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value;

For more information, see this sample

2 Likes

Thanks for the quick reply jerrie1. I did try that actually, but it returns the ID of the client (i.e. of my web app) and not the ID of the user?

Can you post the access_token you are passing along to the API here?

Well, to be honest, I’m kinda hard coding the access token for now, because for some reason, the one I am retrieving is too short and doesn’t work… So I got the access token from the Test tab in Auth0. This is my controller in the web app which calls the web api to get a list of records:

// GET: Inspections
[Authorize]
public async Task<IActionResult> Index()
{
    string accessToken = User.Claims.FirstOrDefault(c => c.Type == "access_token")?.Value;
    string idToken = User.Claims.FirstOrDefault(c => c.Type == "id_token")?.Value;
    // accessToken doesn't work - it's too short - iU6_dwh-w5Mo7hst
    // idToken doesn't work either
    // So for now (because I've wasted DAYS trying to figure out why the access token is wrong, I've hard coded it :(
    accessToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1ERXpSRUUwUWpFMk9VTkdOa0kyUlRJMFJEUXdNakExUTBFeE1EUkNRalpDUmtWR1JVTXhOQSJ9.eyJpc3MiOiJodHRwczovL2NvZGV4Y3JlYXRpb25zLmV1LmF1dGgwLmNvbS8iLCJzdWIiOiJnb09GTnZ4WjJ0bHRpMlhpNHBCaXRQNTBCdEd1bmlibEBjbGllbnRzIiwiYXVkIjoiaHR0cDovL2luc3BlY3Rpb25zLnByb3B3b3J4LmNvLnphL2FwaS8iLCJleHAiOjE0OTYzMDE1MjcsImlhdCI6MTQ5NjIxNTEyNywic2NvcGUiOiIifQ.O-IHtThIXWx3neW6wcirrxk0DRZG34V3VfZvKyoLsXpC-bIGwkk5EjFSQu3LPaHTb4u7qpYZMucC2Q0pqb_gNv8FeFjVzWfb-r1YwO_t3whSdM2aDX4OoYZ49F7WPSj33jOrZeSKMBjbW1cHOZ8lPD3T7Vmtoms1Y6wjjiaZ8U9e26eaPOMWNYTbIxNUT1c1OBOb4ppZ0NCGGaU5GQDOTw-_ZstkVf9WYrw8UcpYdsq6aiM-Fgo3H5LygA_2jWQ5UdIeH95DK15TwCn0j-kEiFeshCJhGtnYudJrPybLArIFmD4KTC727rx9GU-nl89hTiDUY11GyidhQ9ie3g4fOg";

    HttpClient client = new HttpClient();
    client.BaseAddress = new Uri(apiUrl + "/inspections/");
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
            
    HttpResponseMessage responseMessage = await client.GetAsync(inspectionsUrl);
    if (responseMessage.IsSuccessStatusCode)
    {
        var responseData = responseMessage.Content.ReadAsStringAsync().Result;
        List<Inspection> inspections = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Inspection>>(responseData);
        return View(inspections);
    }
    return View("Error");
}

So I’m getting this accessToken: iU6_dwh-w5Mo7hst (too short)

and this idToken:

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1ERXpSRUUwUWpFMk9VTkdOa0kyUlRJMFJEUXdNakExUTBFeE1EUkNRalpDUmtWR1JVTXhOQSJ9.eyJuYW1lIjoiRmFicmljaW8gQWxlamFuZHJvIFJvZHJpZ3VleiIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJwaWN0dXJlIjoiaHR0cHM6Ly9zY29udGVudC54eC5mYmNkbi5uZXQvdi90MS4wLTEvYzAuMC41MC41MC9wNTB4NTAvMTA1OTI3NjVfMTAxNTIyMjQyMTEzNDIxMjFfMzgyOTM5ODY1MjQwNTIxNDI4MF9uLmpwZz9vaD01OTAwOGU2NWZlMTIzMTc3ZDY3MmEwNmM5YWM3NGRhNyZvZT01OUIzN0VFNyIsImlzcyI6Imh0dHBzOi8vY29kZXhjcmVhdGlvbnMuZXUuYXV0aDAuY29tLyIsInN1YiI6ImZhY2Vib29rfDEwMTU0MzkxNzY5ODM3MTIxIiwiYXVkIjoiZ29PRk52eFoydGx0aTJYaTRwQml0UDUwQnRHdW5pYmwiLCJleHAiOjE0OTYyNjA2ODAsImlhdCI6MTQ5NjIyNDY4MCwibm9uY2UiOiI2MzYzMTgyMTQ2OTA4MTg2NTcuTXpjeFltSmhORFV0T0RVeE1pMDBPR0l6TFdJeVlqa3RNbUUzT0dZM1pHRTNabUV5T1dFMFlUUTVZall0TTJJNE9TMDBZemhqTFRoaVpqQXRaVEkwWldRNE5XTTJZVGMyIn0.V63l-P_M7ZxO_wYbrV4h7RE046d7wVzDZONESBaW0cTt3Kl-dpPjg8teCoPZrGwuOMvqF4p-UF-0tbx0z3EYkEJjTa6oTlgFOXgGBJLnYiOwQwQForQpZ5hJj7ZXU9W9xzLgq7SzWbl8aurXwVpPZKd2b5nckuYzKgcsazVRb5TdGielZY_FxGAd-YPhVg1AFdGafNdNC7Grc_f14vLp18jZFToKeCz2prsyWVZS2VnqVso3PTaTxSW52A4q7GFzyMeHthHpUcMZOS2e13_NKlGICSEF4EVccFB0js3h9KZ-J3YEl9fUUZkRFMx7O51Hhejr_4Av9gzd5snR-H0xXQ

(doesn’t work)

According to Auth0, the access token should be:

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1ERXpSRUUwUWpFMk9VTkdOa0kyUlRJMFJEUXdNakExUTBFeE1EUkNRalpDUmtWR1JVTXhOQSJ9.eyJpc3MiOiJodHRwczovL2NvZGV4Y3JlYXRpb25zLmV1LmF1dGgwLmNvbS8iLCJzdWIiOiJnb09GTnZ4WjJ0bHRpMlhpNHBCaXRQNTBCdEd1bmlibEBjbGllbnRzIiwiYXVkIjoiaHR0cDovL2luc3BlY3Rpb25zLnByb3B3b3J4LmNvLnphL2FwaS8iLCJleHAiOjE0OTYzMDE1MjcsImlhdCI6MTQ5NjIxNTEyNywic2NvcGUiOiIifQ.O-IHtThIXWx3neW6wcirrxk0DRZG34V3VfZvKyoLsXpC-bIGwkk5EjFSQu3LPaHTb4u7qpYZMucC2Q0pqb_gNv8FeFjVzWfb-r1YwO_t3whSdM2aDX4OoYZ49F7WPSj33jOrZeSKMBjbW1cHOZ8lPD3T7Vmtoms1Y6wjjiaZ8U9e26eaPOMWNYTbIxNUT1c1OBOb4ppZ0NCGGaU5GQDOTw-_ZstkVf9WYrw8UcpYdsq6aiM-Fgo3H5LygA_2jWQ5UdIeH95DK15TwCn0j-kEiFeshCJhGtnYudJrPybLArIFmD4KTC727rx9GU-nl89hTiDUY11GyidhQ9ie3g4fOg

OK, so first of all ASP.NET is returning you the correct value. In this case that access_token you are passing was issued using the Client Credentials Grant flow. So it is not issued to a user, but to another application. So therefore the sub claim of the access_token contains the identifier of the other application (client) and not of the user. That is why ASP.NET is returning you the client id.

You need to obtain an access_token using one of the client flows. See this document:

Secondly, the reason you are getting such a short access_token is because you are not specifying the audience parameter. When you are calling the /authorize endpoint. You need to specify the API Identifier for your API (in your case http://inspections.propworx.co.za/api/) in the audience parameter when calling /authorize

Thank you so much for your help jerrie. I’m starting to slowly figure this thing out.
Yes, I am [at least trying] to use the “Authorization Code Grant Flow”, which I believe is the correct flow for a regular web app calling a web api? However, what I’m not sure of is at which point is the authorization code being exchanged for an access token? Is this something I need to do manually? Or is there middleware I have set up which is doing this for me behind the scenes? In my sample code in my previous post, when I retrieve the access token (which is incorrect because the audience isn’t specified), should that access token already be the final product? I.e. Somewhere the authorization code has already been exchanged for an access token, or somewhere I should be doing that but I guess I’m not… LOL sorry I know… I am quite confused… I’m quite new to all of this (in fact quite new to web development in general)

Regarding the audience not being specified, wel… I’m not sure how good your ASP .Net Core knowledge is, but in my Startup.cs file, under the Configure() method, I have the following, which I thought was doing the trick of specifying the audience:

app.UseJwtBearerAuthentication(new JwtBearerOptions
{
    Audience = auth0Settings.Value.ApiIdentifier,
    Authority = $"https://{auth0Settings.Value.Domain}/"
});

But I just commented it out and it didn’t make a difference, so I guess it’s not working…

P.S. I have confirmed the values:

auth0Settings.Value.ApiIdentifier = "http://inspections.propworx.co.za/api/"
auth0Settings.Value.Domain" = "codexcreations.eu.auth0.com"

although like I think they’re not doing anything…

In case you want to see it, and I apologize for how much code this is, but I have been copying and pasting from various sources, and the file has been bloating up… here is the startup.cs of my web app:

public class Startup
{
    public IConfigurationRoot Configuration { get; }
    public IHostingEnvironment HostingEnvironment { get; }

    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

        if (env.IsDevelopment())
            builder.AddUserSecrets<Startup>();

        builder.AddEnvironmentVariables();
        Configuration = builder.Build();

        HostingEnvironment = env;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(
            options => options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme);
        services.AddMvc();
        services.AddOptions();
        services.Configure<Auth0Settings>(Configuration.GetSection("Auth0"));
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IOptions<Auth0Settings> auth0Settings)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBrowserLink();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseStaticFiles();

        //Set up JWT Bearer authentication first
        app.UseJwtBearerAuthentication(new JwtBearerOptions
        {
            Audience = auth0Settings.Value.ApiIdentifier,
            Authority = $"https://{auth0Settings.Value.Domain}/"
        });

        // Add the cookie middleware
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AutomaticAuthenticate = true,
            AutomaticChallenge = true,

            Events = new CookieAuthenticationEvents()
            {
                OnRedirectToLogin = ctx =>
                {
                    // if it is an ajax / api request, don't redirect to login page.
                    if (!(IsAjaxRequest(ctx.Request) || IsApiRequest(ctx.Request)))
                    {
                        ctx.Response.Redirect(ctx.RedirectUri);
                        return Task.CompletedTask;
                    }
                    ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
                    return ctx.Response.WriteAsync("Unauthorized");
                }
            }
        });

        // Get the client secret used for signing the tokens
        var keyAsBytes = Encoding.UTF8.GetBytes(auth0Settings.Value.ClientSecret);
        var issuerSigningKey = new SymmetricSecurityKey(keyAsBytes);

        // Add the OIDC middleware
        var options = new OpenIdConnectOptions("Auth0")
        {
            // Set the authority to your Auth0 domain
            Authority = $"https://{auth0Settings.Value.Domain}",

            // Configure the Auth0 Client ID and Client Secret
            ClientId = auth0Settings.Value.ClientId,
            ClientSecret = auth0Settings.Value.ClientSecret,

            // Do not automatically authenticate and challenge
            AutomaticAuthenticate = false,
            AutomaticChallenge = false,

            // Set response type to code
            ResponseType = OpenIdConnectResponseType.Code,

            // Set the callback path, so Auth0 will call back to http://localhost:5000/signin-auth0 
            // Also ensure that you have added the URL as an Allowed Callback URL in your Auth0 dashboard 
            CallbackPath = new PathString("/signin-auth0"),

            // Configure the Claims Issuer to be Auth0
            ClaimsIssuer = "Auth0",

            // The UserInfo endpoint does not really return any extra claims which were not returned in the original auth response, so
            // we can save ourselves from making an extra request
            GetClaimsFromUserInfoEndpoint = false,

            // Saves tokens to the AuthenticationProperties
            SaveTokens = true,

            Events = new OpenIdConnectEvents
            {
                OnTicketReceived = context =>
                {
                    // Get the ClaimsIdentity
                    var identity = context.Principal.Identity as ClaimsIdentity;
                    if (identity != null)
                    {
                            // Add the Name ClaimType. This is required if we want User.Identity.Name to actually return something!
                            if (!context.Principal.HasClaim(c => c.Type == ClaimTypes.Name) &&
                            identity.HasClaim(c => c.Type == "name"))
                            identity.AddClaim(new Claim(ClaimTypes.Name, identity.FindFirst("name").Value));

                            // Check if token names are stored in Properties
                            if (context.Properties.Items.ContainsKey(".TokenNames"))
                            {   
                                // Token names a semicolon separated
                                string] tokenNames = context.Properties.Items".TokenNames"].Split(';');

                                // Add each token value as Claim
                                foreach (var tokenName in tokenNames)
                                {
                                    // Tokens are stored in a Dictionary with the Key ".Token.<token name>"
                                    string tokenValue = context.Properties.Items$".Token.{tokenName}"];
                                    identity.AddClaim(new Claim(tokenName, tokenValue));
                                }
                            }
                    }

                    return Task.CompletedTask;
                },

                // handle the logout redirection 
                OnRedirectToIdentityProviderForSignOut = (context) =>
                {
                    var logoutUri = $"https://{auth0Settings.Value.Domain}/v2/logout?client_id={auth0Settings.Value.ClientId}";

                    var postLogoutUri = context.Properties.RedirectUri;
                    if (!string.IsNullOrEmpty(postLogoutUri))
                    {
                        if (postLogoutUri.StartsWith("/"))
                        {
                            // transform to absolute
                            var request = context.Request;
                            postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
                        }
                        logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri)}";
                    }

                    context.Response.Redirect(logoutUri);
                    context.HandleResponse();

                    return Task.CompletedTask;
                }
            },

            // manually setup the signature validation key
            TokenValidationParameters = new TokenValidationParameters
            {
                IssuerSigningKey = issuerSigningKey
            }
        };

        options.Scope.Clear();
        options.Scope.Add("openid");
        options.Scope.Add("name");
        options.Scope.Add("email");
        options.Scope.Add("picture");
        options.Scope.Add("country");
        options.Scope.Add("roles");
        app.UseOpenIdConnectAuthentication(options);

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }

    private static bool IsAjaxRequest(HttpRequest request)
    {
        var query = request.Query;
        if ((query != null) && (query"X-Requested-With"] == "XMLHttpRequest"))
        {
            return true;
        }
        IHeaderDictionary headers = request.Headers;
        return ((headers != null) && (headers"X-Requested-With"] == "XMLHttpRequest"));
    }

    private static bool IsApiRequest(HttpRequest request)
    {

        return request.Path.StartsWithSegments(new PathString("/api"));
    }
}

Yeah, that seems correct. The OIDC middleware will call the /authorize endpoint for you, and also do the code exchange. You do not have to worry about that. I see you are also storing the tokens as claims, so that means you can retrieve the access_token later on to pass to your API.

What you are missing is to request the correct audience. To do that you also need to handle the OnRedirectToIdentityProvider event and append the audience to the list of Parameters:

Events = new OpenIdConnectEvents()
{
    OnRedirectToIdentityProvider = context =>
    {
        context.ProtocolMessage.Parameters.Add("audience", auth0Settings.Value.ApiIdentifier);
        return Task.CompletedTask;
    },
    ...
}

Once you do this you should get a correct access_token

BTW, how do you call your API from your web app?

Damn, I was so sure when I read your reply that was the answer to my problems! Alas, after adding your suggested code I’m still getting the exact same access_token (iU6_dwh-w5Mo7hst) and id_token. I do agree with you though that it probably has to do with a missing audience, as I read elsewhere someone else say the same. Well, I’ll keep at it…

To answer your other question, here is the code where my web app calls my web:

public async Task<IActionResult> Index()
    {
        // Authentication
        string accessToken = User.Claims.FirstOrDefault(c => c.Type == "access_token")?.Value;

        HttpClient client = new HttpClient();
        client.BaseAddress = new Uri(apiUrl + "/inspections/");            
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
            
        HttpResponseMessage responseMessage = await client.GetAsync(inspectionsUrl);
        if (responseMessage.IsSuccessStatusCode)
        {
            var responseData = responseMessage.Content.ReadAsStringAsync().Result;
            List<Inspection> inspections = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Inspection>>(responseData);
            return View(inspections);
        }
        return View("Error");
    }

Quick question… on this line:

client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

would the user (sub) information be included in the access token? I.e. somewhere in the encoded string in access_token, is there information related to the logged in user that I can then read from “the other side” (i.e. the called web api)? Or do I need to send user info another way?

I copied the access_token I get from the Test tab in Auth0 into https://jwt.io/ and I don’t see any user info in it, but I guess if I receive a proper access token in the normal flow of operations it would have user info in it?

Did you update your Client settings in the Auth0 Dashboard to be OIDC conformant?

Als0, could you perhaps capture the traffic when you do an authentication call in your ASP.NET Web Application with Fiddler, and then save it and send it to me at jerrie (at) auth0.com ?

An access_token does not contain any user information. It only contains the user id in the sub claim. User information is contained in the id_token, but you should not pass an id_token to an API. For more on that see this document:

Also remember that an access_token you generate from the “Test” tab will contain the Client ID as the sub claim because that one is generated using Client Credentials Grant

If you want to retrieve the actual user information in the API, see the readme for this sample:
https://github.com/auth0-samples/auth0-aspnetcore-webapi-samples/tree/master/Samples/user-info

Your architecture is a bit concerning to me. Reason being that you call out from our web application to an API via HTTP. But the API is the exact same web application from where you are calling. This is going to be very inefficient.

Typically when you use a web application with an API, the web application will be a Single Page application like Angular or React - not a traditional web application.

Is there a reason your are not using something like Angular instead?

Thanks jerrie - excellent suggestion. I just turned on the OIDC compliance feature. Unfortunately it dint make a difference :frowning:

Regarding your second question, well… yes, the web app and web api will be on the same server, but they are two separate applications (if I understand you correctly)? I.e. two separate projects which could exist on separate servers. Although it probably wouldnt make sense to have them on separate servers…

(carried on in next comment)…

The thing is, we have another developer creating a phone app (android/ios) in IONIC2 who will be consuming this API. However, we require a website too (albeit with more limited functionality), and because I am not familiar with Angular, and unfortunately I don’t really have the time to learn it right now (as much as I’d love to), I decided to go with ASP .Net Core (even though I must admit it is also new to me, but at least I know C#).

I’m beginning to wish I had gone the Angular route though - I see most examples are as such. I’m using outdated technology I guess - well, at least on the front end… :frowning:

Anyway, thanks for all your help! I truly appreciate it! Maybe I’ll start learning Angular tonight :slight_smile:

Thanks jerrie - I must say you’ve been fantastic! I will read those two links now…