I'm really just starting to get into this LinkedIn API as well as ASP.NET MVC so bear with me. I'm trying to authenticate my user, which appears to be working, but when I try to store the accessToken value (which also appears to be valid) I'm getting the error:
"Exception Details: System.InvalidOperationException: UserId not found."
The error occurs at the comment "// ERROR OCCURS HERE"
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
var claimsIdentity = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
if (claimsIdentity != null)
{
var userIdClaim = claimsIdentity.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
if (userIdClaim != null)
{
// userIdClaim.Value here is: 5-0Vfh4Gv_
var accessToken = claimsIdentity.FindAll(loginInfo.Login.LoginProvider + "_AccessToken").First();
if (accessToken != null)
{
// gets to this point, but...
await UserManager.AddClaimAsync(userIdClaim.Value, accessToken); // ERROR OCCURS HERE
}
}
}
... ... ...
}
This code is 99% from the MVC application template in Visual Studio. The only things I've changed are to add the linkedin NuGet package (install-package linkedin) and set up my API key/secret in Startup.Auth.cs. It's entirely possible (likely, even) that I'm just doing some things out of order or getting the user ID incorrectly.
I've looked at every example and video I can find and still can't figure this out.
Can anyone help me with this user error message, and also, am I just missing some general best practices kind of things? Feeling lost and frustrated...
Thank you!
The error message says it all. I don't think you are getting the user Id correctly.
Try
await UserManager.AddClaimAsync(claimsIdentity.GetUserId(), accessToken);
instead of
await UserManager.AddClaimAsync(userIdClaim.Value, accessToken);
The issue is that at this point:
await UserManager.AddClaimAsync(userIdClaim.Value, accessToken);
The first parameter is expected to be the UserId of the user in the identity database table (i.e. aspnetusers).
You are passing in the nameidentifer from LinkedIn - and thus, that user does not exist in your database.
I think you need to check first whether or not the external user is already registered, and if not, then create the account, add the external login and then add the claim.
Related
I tried to solve this for hours now and I can not find anything. Basicly I have a simple controller which roughly looks like this:
[Route("v1/lists")]
public class ListController : Controller
{
...
[HttpPost("{id}/invite")]
public async Task<IActionResult> PostInvite([FromBody] string inviteSecret, [FromRoute] int id, [FromQuery] string userSecret)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
List list = await context.Lists.SingleOrDefaultAsync(l => l.ID == id);
if (list == null)
{
return NotFound();
}
User postingUser = await context.Users.SingleOrDefaultAsync(u => u.ID == list.CreationUserID);
if (postingUser == null || postingUser.Secret != userSecret)
{
return Forbid();
}
await context.ListInvites.AddAsync(new ListInvite{ListID = id, InviteSecret = inviteSecret});
await context.SaveChangesAsync();
return Ok();
}
....
}
The thing is: Whenever this method gets called and it exits through return Forbid();, Kestrel throws an InvalidOperationException afterwards with the message
No authentication handler is configured to handle the scheme: Automatic
(and of course the server returns a 500). What's strange about it is the fact that I am not doing any authentication whatsoever anywhere, and it does not happen e.g. if the method leaves with return Ok();. I'm really lost at this point because if you try to google this problem you get solutions over solutions... for people who actually do auth and have a problem with it. I really hope someone over here knows how to resolve this and/or what I could do to find out why this happens.
Like SignIn, SignOut or Challenge, Forbid relies on the authentication stack to decide what's the right thing to do to return a "forbidden" response: some authentication handlers like the JWT bearer middleware return a 403 response while others - like the cookie middleware - prefer redirecting the user to an "access denied page".
If you don't have any authentication handler in your pipeline, you can't use this method. Instead, use return StatusCode(403).
Working on project using PKCE code flow with IdentityServer4. I found this peace of code in IS4 examples:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model, string button)
{
// check if we are in the context of an authorization request
var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
// the user clicked the "cancel" button
if (button != "login")
{
if (context != null)
{
// if the user cancels, send a result back into IdentityServer as if they
// denied the consent (even if this client does not require consent).
// this will send back an access denied OIDC error response to the client.
await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
if (await _clientStore.IsPkceClientAsync(context.ClientId))
{
// if the client is PKCE then we assume it's native, so this change in how to
// return the response is for better UX for the end user.
return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
}
return Redirect(model.ReturnUrl);
}
else
{
// since we don't have a valid context, then we just go back to the home page
return Redirect("~/");
}
}
So the question is why should we show redirect page and how can it improve UX? The only content on Redirec view is message "You are now being returned to the application". Could you give me any reasons to do that or any cases it's necessary for? Thank you!
This is mostly useful for native clients with custom redirect URIs.. feel free to remove the code if you don't need that.
I have the following function to call users from active directory use graph api.
This function is hit on each keyup of a text box. But i am getting following error
Code: TokenNotFound Message: User not found in token cache. Maybe the
server was restarted.
at the line
var user = await graphClient.Users.Request().GetAsync();
Entire function Below:
public async Task<string> GetUsersJSONAsync(string textValue)
{
// email = email ?? User.Identity.Name ?? User.FindFirst("preferred_username").Value;
var identifier = User.FindFirst(Startup.ObjectIdentifierType)?.Value;
var graphClient = _graphSdkHelper.GetAuthenticatedClient(identifier);
string usersJSON = await GraphService.GetAllUserJson(graphClient, HttpContext, textValue);
return usersJSON;
}
public static async Task<string> GetAllUserJson(GraphServiceClient graphClient, HttpContext httpContext, string textValue)
{
// if (email == null) return JsonConvert.SerializeObject(new { Message = "Email address cannot be null." }, Formatting.Indented);
try
{
// Load user profile.
var user = await graphClient.Users.Request().GetAsync();
return JsonConvert.SerializeObject(user.Where(u => !string.IsNullOrEmpty(u.Surname) && ( u.Surname.ToLower().StartsWith(textValue) || u.Surname.ToUpper().StartsWith(textValue.ToUpper()))), Formatting.Indented);
}
catch (ServiceException e)
{
switch (e.Error.Code)
{
case "Request_ResourceNotFound":
case "ResourceNotFound":
case "ErrorItemNotFound":
//case "itemNotFound":
// return JsonConvert.SerializeObject(new { Message = $"User '{email}' was not found." }, Formatting.Indented);
//case "ErrorInvalidUser":
// return JsonConvert.SerializeObject(new { Message = $"The requested user '{email}' is invalid." }, Formatting.Indented);
case "AuthenticationFailure":
return JsonConvert.SerializeObject(new { e.Error.Message }, Formatting.Indented);
case "TokenNotFound":
await httpContext.ChallengeAsync();
return JsonConvert.SerializeObject(new { e.Error.Message }, Formatting.Indented);
default:
return JsonConvert.SerializeObject(new { Message = "An unknown error has occured." }, Formatting.Indented);
}
}
}
// Gets an access token. First tries to get the access token from the token cache.
// Using password (secret) to authenticate. Production apps should use a certificate.
public async Task<string> GetUserAccessTokenAsync(string userId)
{
_userTokenCache = new SessionTokenCache(userId, _memoryCache).GetCacheInstance();
var cca = new ConfidentialClientApplication(
_appId,
_redirectUri,
_credential,
_userTokenCache,
null);
if (!cca.Users.Any()) throw new ServiceException(new Error
{
Code = "TokenNotFound",
Message = "User not found in token cache. Maybe the server was restarted."
});
try
{
var result = await cca.AcquireTokenSilentAsync(_scopes, cca.Users.First());
return result.AccessToken;
}
// Unable to retrieve the access token silently.
catch (Exception)
{
throw new ServiceException(new Error
{
Code = GraphErrorCode.AuthenticationFailure.ToString(),
Message = "Caller needs to authenticate. Unable to retrieve the access token silently."
});
}
}
Can you help whats going wrong?
I know this is 4 months old - is this still an issue for you?
As the previous respondent pointed out, the error you're seeing is being thrown in the catch block in your code meant to handle an empty users collection.
In case you're stuck on this, or anyone else comes here - if you used this sample (or using ConfidentialClientApplication in any respect) and are throwing this exception, it's because your _userTokenCache has no users*. Of course, it's not because your AD has no users, otherwise you wouldn't be able to authenticate. Most likely, it is because a stale cookie in your browser is being passed as the access token to your authProvider. You can use Fiddler (or just check your localhost browser cookies) to find it (should be called AspNetCore.Cookies, but you may want to clear all of them).
If you're storing the tokencache in session (as the example is), remember that each time you start and stop the application, your working memory will be thrown out so the token provided by your browser will no longer match the new one your application will retrieve upon starting up again (unless, again, you've cleared the browser cookies).
*cca.Users is no longer used or supported by MSAL - you have to use cca.GetAccountsAsync(). If you have a deployed application running with the deprecated IUser implementation, you'll have to change this. Otherwise, in development your compiler will complain and not let you build, so you'll already know about this.
Looking at the code, it seems some chunks of logic are missing. For example, you got the method
public async Task<string> GetUserAccessTokenAsync(string userId)
but I can't see where this is being called. Besides that, I don't see the code for fetching a token from Azure AD either. Lastly, the error message you mention
Code: TokenNotFound Message: User not found in token cache. Maybe the server was restarted.
Seems like the error you're throwing
if (!cca.Users.Any()) throw new ServiceException(new Error
{
Code = "TokenNotFound",
Message = "User not found in token cache. Maybe the server was restarted."
});
Since the code isn't complete, I will try and make an assumption on what might be going wrong.
Firstly, assuming you're using MSAL.Net, a step in the acquisition of a token is missing.
The general flow is (Using GetTokenByAuthorizationCodeAsync())
Client challenges the user
User gets redirected and logs in
Callback is called and the client receives a code from the login process
Pass the code to GetTokenByAuthorizationCodeAsync() to obtain an id_token and depending on the permissions an access token.
GetTokenByAuthorizationCodeAsync() will store the token in the cache
that has been provided to the ConfidentialClientApplication
Retrieve the token from the cache with AcquireTokenSilentAsync()
If we fail to retrieve a token from the cache with AcquireTokenSilentAsync(), we'll request a new one from via
AcquireTokenAsync()
Most of this flow seems to be in place in your code, but it could be you're missing the actual token acquisition. Since no token is retrieved, no user is added to the ConfidentialClientApplication, which means cca.Users.Any() returns false, resulting in an ServiceError
Assuming the whole flow is in place, and you're actually acquiring a token, my second assumption would be that the _memoryCache are different. The _memoryCache in which you saved your token differs from the one you use to acquire a token silently.
I would recommend reading the documentation on token acquisition to determine the type of retrieving is the right fit for your application.
EDIT
Actually, I assume your code is inspired by this example.
What's especially interesting is this part
public GraphServiceClient GetAuthenticatedClient(string userId)
{
_graphClient = new GraphServiceClient(new DelegateAuthenticationProvider(
async requestMessage =>
{
// Passing tenant ID to the sample auth provider to use as a cache key
var accessToken = await _authProvider.GetUserAccessTokenAsync(userId);
...
}
return _graphClient;
}
What seems to be happening is that calling var user = await graphClient.Users.Request().GetAsync(); invokes the delegate that is provided to the GraphServiceClient. This in turn calls _authProvider.GetUserAccessTokenAsync(userId); which brings us to the public async Task<string> GetUserAccessTokenAsync(string userId) method. Our error most likely originates here, due to no Users being present in the ConfidentialClientApplication.Users collection
Hope this helps!
I've been developing an internal ASP.NET web forms application for our business and one of the requirements is to display our Twitter feed on our portal home page to all users.
For this I've decided that it is best to use LinqToTwitter Single User Authorisation to get the statuses for everyone without them having to authenticate their own accounts.
My main problem at the minute is that when we use the auth object to get the TwitterContext, it returns with an error on the TwitterContext saying
Value cannot be null
on every internal context object.
I've gone through our twitter application settings at http://dev.twitter.com and I have our correct consumer key/token and access key/token. The permission for the application is set to Read-Only. There is no callback URL specified on the http://dev.twitter.com website as it is currently on our internal system (so it wouldn't be able to get back anyway). Is this where it is going wrong? Do I need to forward some ports and allow the callback to get through to our development machines?
Here's the code for prudence. As far as I can see, there is nothing wrong with it. I know that it is set to .FirstOrDefault, this was just for seeing whether it is actually getting anything (which it isn't).
Thanks for any help you can give! :)
private async Task GetTweets()
{
var auth = new SingleUserAuthorizer
{
CredentialStore = new SingleUserInMemoryCredentialStore
{
ConsumerKey = ConfigurationManager.AppSettings["consumerKey"],
ConsumerSecret = ConfigurationManager.AppSettings["consumerSecret"],
AccessToken = ConfigurationManager.AppSettings["accessToken"],
AccessTokenSecret = ConfigurationManager.AppSettings["accessTokenSecret"],
}
};
try
{
using (TwitterContext twitterContext = new TwitterContext(auth))
{
var searchResponse = await (from c in twitterContext.Status
where c.Type == StatusType.User
&& c.ScreenName == "my_screenname"
select c).ToListAsync();
Tb_home_news.Text = searchResponse.FirstOrDefault().Text;
}
}
catch (Exception ex)
{
Tb_home_news.Text = ex.Message;
}
}
If you're creating a Web app, you do need to add a URL to your Twitter App page. It isn't used for the callback, but might help avoid 401's in the future if you're using AspNetAuthorizer.
It looks like you have a NullReferenceException somewhere. What does ex.ToString() say?
Double check CredentialStore after initialization to make sure that all 4 credentials are populated. AccessToken and AccessTokenSecret come from your Twitter app page.
Does searchResponse contain any values? Calling FirstOrDefault on an empty collection will return null.
I am using Azure Mobile Services (following the standard Azure TodoItems tutorial), and the most basic GET method that they provide is:
public IQueryable<MyModel> GetAllMyInfo()
{
return Query();
}
This works, but I am trying to extend it so that the method will only return MyModel data for an authenticated user (identified by the X-ZUMO-AUTH authentication header standard for Mobile Service API calls). So I modified the code for:
public IQueryable<MyModel> GetAllMyInfo()
{
// Get the current user
var currentUser = User as ServiceUser;
var ownerId = currentUser.Id;
return Query().Where(s => s.OwnerId == ownerId);
}
This also works when a valid auth token is passed. However, if an invalid auth header is passed, then the currentUser is null, and the query fails (obviously). So I am trying to check for null and return a BadRequest or a 403 HTTP code. Yet a simple `return BadRequest("Invalid authentication") gives a compilation error:
public IQueryable<MyModel> GetAllMyInfo()
{
// Get the current user
var currentUser = User as ServiceUser;
if(currentUser == null) {
return BadRequest("Database has already been created."); // This line gives a compilation error saying I need a cast.
}
var ownerId = currentUser.Id;
return Query().Where(s => s.OwnerId == ownerId);
}
Does anyone know how to check for a valid authentication token and return a 403 on this method (which wants an IQueryable return type?
You can use the [AuthorizeLevel] attribute on this method to indicate that a valid token must be present in order for the method to be invoked. It will return a 401 if not.
So your full method would be:
[AuthorizeLevel(AuthorizationLevel.User)]
public IQueryable<MyModel> GetAllMyInfo()
{
// Get the current user
var currentUser = User as ServiceUser;
var ownerId = currentUser.Id;
return Query().Where(s => s.OwnerId == ownerId);
}
Please note that for the Azure Mobile Apps SDK (not Mobile Services), the above attribute is simply replaced with [Authorize].
I know this is a bit late, but will document here for you and others that may come looking for a similar problem.
(While agreeing with Matt that a 403 could/should be achieved with a [Authorize] attribute, the question is regarding returning a different HttpStatusCode OR IQueryable)
I had a similar scenario where I needed to validate some query parameters and either return my results or a HttpError (in my case I wanted a 404 with content).
I found 2 ways, either keeping the return as IQueryable<T> and throwing a HttpResponseException or changing the return to IHttpActionResult and returning normal with HttpStatusCode or Ok(Data).
I found to prefer the later as throwing an Exception would be breaking the execution while in debug and not a very pleasant development experience.
Option 1 (Preferred)
//Adding Return annotation for API Documentation generation
[ResponseType(typeof(IQueryable<MyModel>))]
public IHttpActionResult GetAllMyInfo()
{
// Get the current user
var currentUser = User as ServiceUser;
if(currentUser == null) {
return BadRequest("Database has already been created.");
}
var ownerId = currentUser.Id;
return Ok(Query().Where(s => s.OwnerId == ownerId));
}
Option 2 (Throwing Exception)
public IQueryable<MyModel> GetAllMyInfo()
{
// Get the current user
var currentUser = User as ServiceUser;
if(currentUser == null) {
throw new HttpResponseException(System.Net.HttpStatusCode.BadRequest)
// Or to add a content message:
throw new HttpResponseException(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.BadRequest) {
Content = new System.Net.Http.StringContent("Database has already been created.")
});
}
var ownerId = currentUser.Id;
return Query().Where(s => s.OwnerId == ownerId);
}