Blazor TokenAcquisition is throwing Null Reference Exception - microsoft-graph-api

I have a Blazor server app that works fine locally but fails due to Null Reference exception when published to Azure.
The app uses Graph, and when I run it locally, it queries graph fine. But when I run the published version, the ITokenAcquisition object (even though it claims to NOT be null) throws a Null Exception error.
I don't know if there are some "magical" link to values in the appsettings.json that works locally but doesn't work when published to Azure since the Azure configuration is handled different than running locally.
public GraphHelper(ITokenAcquisition tokenAcquisition,
IOptions<WebOptions> webOptionValue)
{
this.tokenAcquisition = tokenAcquisition;
this.webOptions = webOptionValue.Value;
GraphClient = GetGraphServiceClient(new[] { MentorConstants.ScopeUserRead });
try
{
TokenInfo = (tokenAcquisition.GetAccessTokenForUserAsync(new[] { MentorConstants.ScopeUserRead }).GetAwaiter().GetResult(), DateTime.Now);
}
catch(Exception ex)
{
LogError(ex, "Failed to get token info");
}
}
In the constructor, my tokenAcquisition object is NOT null, but when I make the call it fails.
Here's the exception details
Object reference not set to an instance of an object.
StackTrace
at Microsoft.Identity.Web.TokenAcquisition.CreateRedirectUri()
at Microsoft.Identity.Web.TokenAcquisition.BuildConfidentialClientApplicationAsync()
at Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync()
at Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(IEnumerable`1 scopes, String tenant)
at ...d.MoveNext() in C:\project\GraphHelper.cs:line 260

Related

MSAL.NET redirect loop when using graphApi in MVC & blazor with multiple instances

I have created a blazor component that aims to simplify managing users and group of an enterprise application in my ASP.NET MVC website. When I run the code locally, everything works just fine. However, when I deploy my code on the dev environment (in AKS) the code only works if I run one replica.
When I use multiple instances and I try to access the page that calls my blazor component, the page ends up in a redirect loop, and finally shows the Microsoft login interface with an error mentioning that the login was not valid.
This is how my code looks like:
# program.cs
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
var cacheOptions = builder.Configuration.GetSection("AzureTableStorageCacheOptions").Get<AzureTableStorageCacheOptions>();
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddDistributedTokenCaches();
builder.Services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24);
});
builder.Services.AddDistributedAzureTableStorageCache(options =>
{
options.ConnectionString = cacheOptions.ConnectionString;
options.TableName = cacheOptions.TableName;
options.PartitionKey = cacheOptions.PartitionKey;
options.CreateTableIfNotExists = true;
options.ExpiredItemsDeletionInterval = TimeSpan.FromHours(24);
});
builder.Services.AddSession();
...
# The controller that calls the blazor component
[AuthorizeForScopes(Scopes = new[] { "Application.ReadWrite.All", "Directory.Read.All", "Directory.ReadWrite.All" })]
public async Task<IActionResult> UserManagement()
{
string[] scopes = new string[] { "Application.ReadWrite.All", "Directory.Read.All", "Directory.ReadWrite.All" };
try
{
await _tokenAcquisition
.GetAccessTokenForUserAsync(scopes)
.ConfigureAwait(false);
}
catch (Exception ex)
{
_telemetryClient.TrackException(ex);
}
return View();
}
And this is what happens:
If the page loads, I can see this exception in the pod logs:
What am I doing wrong?
The tenant actually needs to provide admin consent to your web API for the scopes you want to use for replicas for the token taken from cache.
Also when AuthorizeForScopes attribute is specified with scopes ,this needs the exact scopes that is required by that api. MsalUiRequiredException gets thrown in case of incorrect scopes for that api and results in a challenge to user.
This error may also occur even when the acquiretokensilent call will not have a valid cookie anymore for authentication in cache .Please check how acquiretokensilent call works from here in msal net acquire token silently | microsoft docs
When valid scopes are given , please make sure the permissions are granted consent by the admin directly from portal or during user login authentication.
Also as a work around try to use use httpContextAccessor to access
token after authentication .
Reference: c# - Error : No account or login hint was passed to the AcquireTokenSilent call - Stack Overflow
So, the culprit was:
#my controller
await _tokenAcquisition
.GetAccessTokenForUserAsync(scopes)
.ConfigureAwait(false);
Which we were using initially to reauthenticate the graph api component when we were using InMemoryCache.
There is no need to get the access token again when using DistributedTokenCache, and actually that was causing the token to get saved / invalidated in an infinite loop.
Also, in my blazor component, I had to do use the consent handler to force a login:
private async Task<ServicePrincipal> GetPrincipal(AzureAdConfiguration addConfiguration)
{
try
{
return await GraphClient.ServicePrincipals[addConfiguration.PrincipalId].Request()
.Select("id,appRoles, appId")
.GetAsync();
}
catch (Exception ex)
{
ConsentHandler.HandleException(ex);
throw;
}
}

webapi odata update savechanges issue - Unable to connect to remote server

In my mvc webapplication, I am using webapi to connect to my database through odata.
Both MVC WebApp and Odata WebApi are on different ports of Azure cloud service webrole endpoints.
MVC WebApp - 80
Odata WebApi - 23900
When I do a odataproxy updateobject and call savechanges like
odataProxy.UpdateObject(xxx);
odataProxy.SaveChanges(System.Data.Services.Client.SaveChangesOptions.PatchOnUpdate);
I am getting a weird exception on savechanges method call - unable to connect to remote server.
When I tried to look into inner exceptions, It says that - No connection could be made as the target machine actively refused it 127.0.0.1:23901
So if you observe the port number in the exception, it shows as 23901 and obviously this error should come as the request is supposed to hit 23900.
I am facing this exception only when running on azure cloud solution. Whenever I do an update request, it fails by hitting a wrong port (added by 1).
Another thing is, apart from this updateobject -> savechanges, rest all works like fetching data and adding data.
FWIW, I've just run across this same thing. Darn near annoying and I really hope it doesn't happen in production. I'm surprised no other people have come across this though.
The idea of creating a new context, attaching the object(s) and calling SaveChanges really repulsed me because not only does it practically break all forms of testing, it causes debug code and production code to be fundamentally different.
I was however able to work around this problem in another way, by intercepting the request just before it goes out and using reflection to poke at some private fields in memory to "fix" the port number.
UPDATE: It's actually easier than this. We can intercept the request generation process with the BuildingRequest event. It goes something like this:
var context = new Context(baseUri);
context.BuildingRequest += (o, e) =>
{
FixPort(e);
};
Then the FixPort method just needs to test the port number and build a new Uri, attaching it back to the event args.
[Conditional("DEBUG")]
private static void FixPort(BuildingRequestEventArgs eventArgs)
{
int localPort = int.Parse(LOCAL_PORT);
if (eventArgs.RequestUri.Port != localPort)
{
var builder = new UriBuilder(eventArgs.RequestUri);
builder.Port = localPort;
eventArgs.RequestUri = builder.Uri;
}
}
Here's the original method using reflection and SendingRequest2, in case anyone is still interested.
First we create a context and attach a handler to the SendingRequest2 event:
var context = new Context(baseUri);
context.SendingRequest2 += (o, e) =>
{
FixPort(e.RequestMessage);
};
The FixPort method then handles rewriting the URL of the internal request, where LOCAL_PORT is the port you expect, in your case 23900:
[Conditional("DEBUG")]
private static void FixPort(IODataRequestMessage requestMessage)
{
var httpWebRequestMessage = requestMessage as HttpWebRequestMessage;
if (httpWebRequestMessage == null) return;
int localPort = int.Parse(LOCAL_PORT);
if (httpWebRequestMessage.HttpWebRequest.RequestUri.Port != localPort)
{
var builder = new UriBuilder(requestMessage.Url);
builder.Port = localPort;
var uriField = typeof (HttpWebRequest).GetField("_Uri",
BindingFlags.Instance | BindingFlags.NonPublic);
uriField.SetValue(httpWebRequestMessage.HttpWebRequest, builder.Uri);
}
}
I have found the root cause and a temporary workaround.
Cause:
When you hit WebApi through some port :23900 in Azure compute emulator and do an update or delete operation, somehow the last request is blocking the port and because of the port walking feature in Azure emulator, it is jumping to next port where there is no service available which is causing the issue.
Even this issue is found only in development emulators.
Temp Workaround:
Use a different proxy to attach to updated context object and then save from the other proxy object.
var odataProxy1 = xxx;
var obj = odataProxy1.xyz.FirstOrDefault();
obj.property1="abcd";
...//Other update assignments
var odataProxy2 = xxx;
odataProxy2.AttachTo("objEntitySet",obj);
odataProxy2.UpdateObject(obj)
odataProxy2.SaveChanges(ReplaceOrUpdate);

facebook login using oauth fails on live server

I'm using OAuthWebSecurity to login with facebook and it is working on localhost. However then deployed to the live server I get the following error message:
The remote server returned an error: (400) Bad Request.
I have checked domain details are correct on facebook.
The sandbox mode is disabled.
I have disabled windows firewall - still get same error.
Responses from facebook have the identical format whether in live or localhost environments.
The date and time of live server is correct.
I've test locally also changing the host file to the live domain - still works locally.
Here's the stack trace:
[WebException: The remote server returned an error: (400) Bad Request.]
System.Net.WebClient.DownloadDataInternal(Uri address, WebRequest& request) +3291120
System.Net.WebClient.DownloadString(Uri address) +207
DotNetOpenAuth.AspNet.Clients.FacebookClient.QueryAccessToken(Uri returnUrl, String authorizationCode) +293
DotNetOpenAuth.AspNet.Clients.OAuth2Client.VerifyAuthentication(HttpContextBase context, Uri returnPageUrl) +167
DotNetOpenAuth.AspNet.OpenAuthSecurityManager.VerifyAuthentication(String returnUrl) +502
Microsoft.Web.WebPages.OAuth.OAuthWebSecurity.VerifyAuthenticationCore(HttpContextBase context, String returnUrl) +231
Any suggestions?
There was a defect open related to this:
https://github.com/DotNetOpenAuth/DotNetOpenAuth/issues/203
but that library is apparently no longer being maintained:
https://github.com/DotNetOpenAuth/DotNetOpenAuth/issues/317#issuecomment-29580565
... although it is still referenced in many Microsoft documents. Investigating that related to another defect.
See also: The remote server returned an error: (400) Bad Request Microsoft.AspNet.Membership.OpenAuth
This is an old one, but I hit a 400 error from Facebook login using dotnetopenoath as it is integrated into MVC 4 and went on a wild goose chase thinking that it was not working because it was no longer supported, when all I needed to do was catch and log the exception which pointed me to the problem for my particular case (app secret proof setting):
catch (WebException exception)
{
using (WebResponse response = exception.Response)
{
HttpWebResponse httpResponse = (HttpWebResponse) response;
m_log.Debug("Error code: " + httpResponse.StatusCode);
using (Stream data = response.GetResponseStream())
{
if (data != null)
{
using (var reader = new StreamReader(data))
{
string text = reader.ReadToEnd();
m_log.Debug(text);
}
}
}
}
}

On Redirect - Failed to generate a user instance of SQL Server

Hello (this is a long post sorry),
I am writing a application in ASP.NET MVC 2 and I have reached a point where I am receiving this error when I connect remotely to my Server.
Failed to generate a user instance of SQL Server due to failure in retrieving the user's local application data path. Please make sure the user has a local user profile on the computer. The connection will be closed.
I thought I had worked around this problem locally, as I was getting this error in debug when site was redirected to a baseUrl if a subdomain was invalid using this code:
protected override void Initialize(RequestContext requestContext)
{
string[] host = requestContext.HttpContext.Request.Headers["Host"].Split(':');
_siteProvider.Initialise(host, LiveMeet.Properties.Settings.Default["baseUrl"].ToString());
base.Initialize(requestContext);
}
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (Site == null)
{
string[] host = filterContext.HttpContext.Request.Headers["Host"].Split(':');
string newUrl;
if (host.Length == 2)
newUrl = "http://sample.local:" + host[1];
else
newUrl = "http://sample.local";
Response.Redirect(newUrl, true);
}
ViewData["Site"] = Site;
base.OnActionExecuting(filterContext);
}
public Site Site
{
get
{
return _siteProvider.GetCurrentSite();
}
}
The Site object is returned from a Provider named siteProvider, this does two checks, once against a database containing a list of all available subdomains, then if that fails to find a valid subdomain, or valid domain name, searches a memory cache of reserved domains, if that doesn't hit then returns a baseUrl where all invalid domains are redirected.
locally this worked when I added the true to Response.Redirect, assuming a halting of the current execution and restarting the execution on the browser redirect.
What I have found in the stack trace is that the error is thrown on the second attempt to access the database.
#region ISiteProvider Members
public void Initialise(string[] host, string basehost)
{
if (host[0].Contains(basehost))
host = host[0].Split('.');
Site getSite = GetSites().WithDomain(host[0]);
if (getSite == null)
{
sites.TryGetValue(host[0], out getSite);
}
_site = getSite;
}
public Site GetCurrentSite()
{
return _site;
}
public IQueryable<Site> GetSites()
{
return from p in _repository.groupDomains
select new Site
{
Host = p.domainName,
GroupGuid = (Guid)p.groupGuid,
IsSubDomain = p.isSubdomain
};
}
#endregion
The Linq query ^^^ is hit first, with a filter of WithDomain, the error isn't thrown till the WithDomain filter is attempted.
In summary: The error is hit after the page is redirected, so the first iteration is executing as expected (so permissions on the database are correct, user profiles etc) shortly after the redirect when it filters the database query for the possible domain/subdomain of current redirected page, it errors out.
Make sure App Pool is set to NetworkService if under windows 7 and IIS 7.5
I searched long and hard for this one, it was my error all along. It seem that at sometime someone decided to change my applciationpool settings and changed the applicationpool for my application. But for some strange reason it didn't impact me until I tried to use the redirection code.
Before adding the code all databases connected without any issues and data was pulled without error, but afterwords it didn't, so after looking at my appool, I noticed it was not set to networkservice any longer, switched, fixed.
So maybe this question should be, why would database connections work before the new code, but not after while not under networdservice user?

Accessing Elmah.axd with SqlErrorLog in SharePoint without adding user to db

I have installed/configured Elmah on my personal SharePoint dev environment and everything works great since I'm logged in as admin, etc. I am using the MS Sql Server Error Log. (I am also using log4net to handle DEBUG/INFO/etc level logging and log statements are also stored in the db, in the same table as ELMAH's.)
However, on the actual dev server (not my personal environment), when I access http://example/elmah.axd I get the error "Login failed for user 'NT AUTHORITY\ANONYMOUS LOGON'". I understand that this is the traditional error for the "double-hop problem" but I don't even want my credentials to be passed along - I would just like the database access to be made with the credentials of the Application Pool Identity. When using the SP object model the SPSecurity.RunWithElevatedPrivileges is available; however, I do not want to modify the Elmah source.
My production environment precludes the use of SQL Server authentication, changing impersonation to false, or giving myself permissions on the db directly.
How can I get this to work? Am I missing something?
My production environment precludes
the use of SQL Server authentication,
changing impersonation to false, or
giving myself permissions on the db
directly.
Then you have no choice but to modify the Elmah source. Sorry.
This has less to do with the double hop problem and more to do with the poorly documented mechanism of impersonation in ASP.NET. According to this article, apparently <identity impersonate="true" /> causes ASP.NET to impersonate the default IIS anonymous account (IUSR_machinename). SharePoint needs this but it does you no good trying to access a remote database, so there is obviously something you need to do.
Yep, according to this article you must edit the Elmah source code and create a new class deriving from the abstract class ErrorLog. This new class then acts as a wrapper around the original SqlErrorLog class and runs its methods in a RWEP block. Here it is:
public class SqlErrorLogWEP : ErrorLog
{
private SqlErrorLog sqlErrorLog;
public SqlErrorLogWEP(IDictionary config)
{
sqlErrorLog = new SqlErrorLog(config);
}
public SqlErrorLogWEP(string connectionString)
{
sqlErrorLog = new SqlErrorLog(connectionString);
}
public override string Log(Error error)
{
string retVal = String.Empty;
SPSecurity.RunWithElevatedPrivileges(delegate()
{
retVal = sqlErrorLog.Log(error);
});
return retVal;
}
public override ErrorLogEntry GetError(string id)
{
ErrorLogEntry retVal = default(ErrorLogEntry);
SPSecurity.RunWithElevatedPrivileges(delegate()
{
retVal = sqlErrorLog.GetError(id);
});
return retVal;
}
public override int GetErrors(int pageIndex, int pageSize, System.Collections.IList errorEntryList)
{
int retVal = -1;
SPSecurity.RunWithElevatedPrivileges(delegate()
{
retVal = sqlErrorLog.GetErrors(pageIndex, pageSize, errorEntryList);
});
return retVal;
}
}
It goes without saying that you will now need to reference SharePoint in the Elmah project and your Elmah.dll will need to be GACed. I've tested this myself and it works. Good luck.

Resources