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…
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");
}
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:
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:
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");
}
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
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
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…
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…
Anyway, thanks for all your help! I truly appreciate it! Maybe I’ll start learning Angular tonight