Token cache serialization in MSAL.NET is not working - c#

I am facing some issues when trying to serialize the tokencache, returned from authenticating with MSAL.
I would appreciate any help, since i don't really understand what i am doing wrong.
Here is our situation/problem:
We are currently using ADAL to allow users to authenticate to their SharePoint Online accounts from our desktop application, but want to switch to MSAL.
We have implemented two possible authentication flows. A publicClientApplication, to allow the user authenticating with its currently active Microsoft credentials and a ConfidentialClientApplication, that allows to authenticate with the use of a certificat as you can see in the catch block of the code below:
try
{
Debugger.Launch();
if (certificate != null)
{
IConfidentialClientApplication m_authContext = ConfidentialClientApplicationBuilder.Create(pClientID)
.WithTenantId(m_tenant).WithCertificate(certificate).WithRedirectUri(pClientRedirectURI).Build();
var accounts = await m_authContext.GetAccountsAsync();
authResult = await m_authContext.AcquireTokenSilent(m_scope, accounts.FirstOrDefault()).ExecuteAsync();
}
else
{
IPublicClientApplication m_authContext = PublicClientApplicationBuilder.Create(pClientID).WithTenantId(m_tenant).WithRedirectUri(pClientRedirectURI).Build();
var accounts = await m_authContext.GetAccountsAsync();
authResult = await m_authContext.AcquireTokenSilent(m_scope, accounts.FirstOrDefault()).ExecuteAsync();
}
}
catch
{
if (certificate != null)
{
IConfidentialClientApplication m_authContext = ConfidentialClientApplicationBuilder.Create(pClientID)
.WithTenantId(m_tenant).WithCertificate(certificate).WithRedirectUri(pClientRedirectURI).Build();
TokenCacheHelper.EnableSerialization(m_authContext.AppTokenCache);
authResult = await m_authContext.AcquireTokenForClient(m_scope).WithForceRefresh(true).ExecuteAsync();
}
else
{
IPublicClientApplication m_authContext = PublicClientApplicationBuilder.Create(pClientID)
.WithTenantId(m_tenant).WithRedirectUri(pClientRedirectURI).Build();
TokenCacheHelper.EnableSerialization(m_authContext.UserTokenCache);
authResult = await m_authContext.AcquireTokenInteractive(m_scope).ExecuteAsync();
}
}
So far, the code works fine and the initial authentication is successful in both cases.
The problem now is that we would like to persist the acquired tokens, so that the next time the user starts our program and tries to access SharePoint he does not have to authenticate again. But trying to authenticate silent with the use of a prior stored token does not work, neither for the public nor the confidential application.
The serialization of the AfterAccessNotification however does seem to work, as at least something gets written into the cache file. But reading this data back does not. One thing to point out is that GetAccountsAsync() will always return 0.
What i have read so far, from the Microsoft documentation and other questions here, is that of course the inmemory cache of the applications will get lost when recreating it and the solution seems to be the implementation of the TokenCacheHelper. Our helper class is implemented as its suggested in the documentation:
static class TokenCacheHelper
{
public static readonly string CacheFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location + "msalcache.txt";
private static readonly object FileLock = new object();
public static void EnableSerialization(ITokenCache tokenCache)
{
Debugger.Launch();
tokenCache.SetBeforeAccess(BeforeAccessNotification);
tokenCache.SetAfterAccess(AfterAccessNotification);
}
private static void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
args.TokenCache.DeserializeMsalV3(File.Exists(CacheFilePath)
? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath),
null,
DataProtectionScope.CurrentUser)
: null);
}
private static void AfterAccessNotification(TokenCacheNotificationArgs args)
{
Debugger.Launch();
if (args.HasStateChanged)
{
lock (FileLock)
{
// reflect changesgs in the persistent store
File.WriteAllBytes(CacheFilePath,
ProtectedData.Protect(args.TokenCache.SerializeMsalV3(),
null,
DataProtectionScope.CurrentUser)
);
}
}
}
}
Am i having a major misunderstanding on the use of the TokenCacheHelper or something else here?
Or is there a more simple way on persisting the tokencache?
This seemed to be far less complicated with the use of ADAL.
Thank you very much for your help.

We found out what was causing the problem. There was simply a call of TokenCacheHelper.EnableSerialization(m_authContext.UserTokenCache);
missing before trying to acquire the token silent.

There's no simpler way. I'm using this Mircosoft provided class (TokenCacheHelper) myself and it works very nicely. I'm persisting the token now for several weeks. No problems reading it back and using it.
Generally as far I tested out you must first authenticate interactively for many kinds of data you want to read then read the saved token for future actions. You can check in your Azure security center for which data you have authenticate interactively or may authenticate non-interactively.
Re-check your security settings for reading accounts in Azure security center.
Re-check scopes like: "User.Read", "User.ReadBasic.All"
Check out the example under https://github.com/Azure-Samples/active-directory-dotnet-desktop-msgraph-v2 which helped me
Give it try!
PS: I would consider the whole MS-graph library in a "beta state" and still buggy, error prone and not finished. So expect different behavior now and in the future.
Regards

Related

MSAL - PublicClientApplication - GetAccountsAsync() doesn't return any Accounts

I'm developing a little WPF-App that is supposed to query some data from the MS Graph API. I want to use SSO, so the user doesn't have to login to the app seperatly.
The app is run on a Azure AD joined device. The user is an AADC synchronized AD user. The AAD tenant is federated with ADFS. The user authenticates with Hello for Business (PIN) or via Password. The resulting problem is the same.
I can confirm that the user got a PRT via:
dsregcmd /status
AzureAdPrt: YES
In case it matters: The app registration in Azure AD is set to "Treat application as public client". And the following redirect URIs are configured:
https://login.microsoftonline.com/common/oauth2/nativeclient
https://login.live.com/oauth20_desktop.srf
msalxxxxxxx(appId)://auth
urn:ietf:wg:oauth:2.0:oob
Based on the examples I found, I'm using the following code to try to get an access token. However the GetAccountsAsync() method doesn't return any users nor does it throw any error or exception.
Can anyone tell me, what I'm missing here?
Any help would be much appreciated!
PS: When I try this using "Interactive Authentication" it works fine.
public GraphAuthProvider(string appId, string tenantId, string[] scopes)
{
_scopes = scopes;
try
{
_msalClient = PublicClientApplicationBuilder
.Create(appId)
.WithAuthority(AadAuthorityAudience.AzureAdMyOrg, true)
.WithTenantId(tenantId)
.Build();
}
catch (Exception exception)
{
_log.Error(exception.Message);
_log.Error(exception.StackTrace);
throw;
}
}
public async Task<string> GetAccessToken()
{
_log.Info("Starting 'GetAccessToken'...");
var accounts = await _msalClient.GetAccountsAsync();
_userAccount = accounts.FirstOrDefault();
// If there is no saved user account, the user must sign-in
if (_userAccount == null)
{
_log.Info("No cached accounts found. Trying integrated authentication...");
[...]
}
else
{
// If there is an account, call AcquireTokenSilent
// By doing this, MSAL will refresh the token automatically if
// it is expired. Otherwise it returns the cached token.
var userAccountJson = await Task.Factory.StartNew(() => JsonConvert.SerializeObject(_userAccount));
_log.Info($"Found cached accounts. _userAccount is: {userAccountJson}");
var result = await _msalClient
.AcquireTokenSilent(_scopes, _userAccount)
.ExecuteAsync();
return result.AccessToken;
}
}
To be able to have IAccounts returned from MSAL (which access the cache), it must have the cache bootstrapped at some point. You are missing the starting point, which in your case is AcquireTokenInteractive.
It is recommended to use the following try/catch pattern on MSAL:
try
{
var accounts = await _msalClient.GetAccountsAsync();
// Try to acquire an access token from the cache. If an interaction is required, MsalUiRequiredException will be thrown.
result = await _msalClient.AcquireTokenSilent(scopes, accounts.FirstOrDefault())
.ExecuteAsync();
}
catch (MsalUiRequiredException)
{
// Acquiring an access token interactively. MSAL will cache it so you can use AcquireTokenSilent on future calls.
result = await _msalClient.AcquireTokenInteractive(scopes)
.ExecuteAsync();
}
Use this try/catch pattern instead of you if/else logic and you will be good to go.
For further reference, there is this msal desktop samples which covers a bunch of common scenarios.
Update
If you are instantiating a new _msalClient on every action, then this explains why the other calls are not working. You can either have _msalClient as a static/singleton instance or implement a serialized token cache. Here is a cache example
As there are some questions regarding non-interactive authentication in the comments, this is how I finally got this working:
Use WAM and configure the builder like this:
var builder = PublicClientApplicationBuilder.Create(ClientId)
.WithAuthority($"{Instance}{TenantId}")
.WithDefaultRedirectUri()
.WithWindowsBroker(true);
Configure this redirect URI in the Azure App registration:
ms-appx-web://microsoft.aad.brokerplugin/{client_id}
A code example is available here
In-case anyone has a similar problem, I had an issue where both GetAccountAsync and GetAccountsAsync (the latter being now deprecated), were both sometimes returning null. All I needed to do was make sure all my authentication adjacent libraries were up to date.
I think there was an issue where the in-memory token caching wasn't always working as intended, which seems to be fixed by a simple update.

How do I get accounts from Azure AD?

I have a nice Azure Active Directory set up with a dozen users. (All me!) So I have a Tenant ID, client ID and Client Secret.
I am also working on a simple console application that will function as a public client for this directory. This client also holds a list of usernames and passwords as this is just meant as a simple experiment. Not secure, I know. But I first need to understand how it works...
I do this:
IConfidentialClientApplication client = ConfidentialClientApplicationBuilder
.CreateWithApplicationOptions(options).Build();
And this creates my client app. Works fine.
I also get a token using "https://graph.microsoft.com/.default" and can use this to get all users as JSON:
string result = await GetHttpContentWithToken("https://graph.microsoft.com/v1.0/users",
token.AccessToken);
Although I might want it to be more user-friendly, JSON is fine for now.
How can I check if user is an authorized user?
And no, I don't want complex solutions that require various nuget packages. Just a plain and simple step-by-step explanation. I could probably Google this but I ended up with thousands of results and none were helpful... This should be easy, right?
[EDIT] I first wanted to get a list of users nut that failed because of a typo... (There's a dot before 'default'...)
It took some fooling around but it's not too difficult after all. There are a lot of libraries around Azure but it is all basically just a bunch of HTTP requests and responses. Even in a console application...
I started with making a PublicClientApplicationBuilder first:
var options = new PublicClientApplicationOptions()
{
ClientId = <**clientid**>,
TenantId = <**tenantid**>,
AzureCloudInstance = AzureCloudInstance.AzurePublic,
};
var client = PublicClientApplicationBuilder.CreateWithApplicationOptions(options).Build();
I can also create a ConfidentialClientApplication instead, but this allows me to log in interactively, if need be.
Next, set up the scopes:
var scopes = new List<string>() { "https://graph.microsoft.com/.default" };
As I wanted to log in using username and password, I have to use this:
var token = await client.AcquireTokenInteractive(scopes).ExecuteAsync();
But if I want to log in using code, I can also use this:
var password = new SecureString();
foreach (var c in <**password**>) { password.AppendChar(c); }
var token = await client.AcquireTokenByUsernamePassword(scopes, <**account**>, password).ExecuteAsync();
At this point, I'm authorized as the specified user. So, now all I need is to get whatever data I like, in JSON strings...
public static async Task<string> ExecCmd(string name, string url, string token)
{
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
string result = await GetHttpContentWithToken(url, token);
JObject json = JsonConvert.DeserializeObject(result) as JObject;
File.WriteAllText(name, json.ToString());
return result;
}
As I just want to read the data as text files, I just execute the action in using a specific and write it as formatted JSON to the file . So, using this simple method I can now use this:
await ExecCmd("Profile.txt", "https://graph.microsoft.com/v1.0/me/", token.AccessToken);
await ExecCmd("Groups.txt", "https://graph.microsoft.com/v1.0/groups", token.AccessToken);
await ExecCmd("Users.txt", "https://graph.microsoft.com/v1.0/users", token.AccessToken);
These will provide me with (1) the profile of the current user, (2) the AD groups and (3) the AD users. And probably a bit more...
I can use this ExecCmd to retrieve a lot more data, if I want to. But there's something else to keep in mind! For it all to work, you also need to configure the Azure application and make sure all access rights are assigned and approved!
So, in Azure AD you have to add an "App registration" and fiddle around with the settings... (The Azure experts are horribly shocked now, but when you want to learn, you'd just have to try and fail until you succeed...)
Also set "Default client type" to "public client" for the registered app.
In Azure, with the registered app, you also need to set the proper API permissions! Otherwise, you won't have access. And as I want access to Active Directory, I need to add permissions to "Azure Active Directory Graph". I can do this inside Azure or by using the scope when I call AcquireTokenInteractive(). For example, by using "https://graph.windows.net/Directory.Read.All" instead of "https://graph.windows.net/.default".
Once you've accessed a token interactively, you can also get more tokens using client.AcquireTokenSilent(). It gets a bit tricky from here, especially if you want to access a lot of different items. Fortunately, Active Directory is mostly the directory itself, groups, users and members.
Personally, I prefer to grant access from the Azure website but this is quite interesting.
Anyways, I wanted to authenticate users with Azure and now I know how to do this. It still leaves a lot more questions but this all basically answers my question...
I'll use this as answer, as others might find it useful...

Instasharp in a Console Application

I have a .net console application that I want to use to pull Instagram posts via the Instasharp wrapper using a hashtag search.
I use C# .net web forms extensively and am not very familiar with MVC nor how to use the await keyword. The code sample below seems to run, but never provides any output.
This line:
var tagInfo = await tagApi.Get("soccer");
Returns me to the calling method with no indication of retrieved data.
Can anyone provide insights as to what I am doing wrong here?
public static async void GetInstagram(String tag, InstagramConfig config)
{
var instagramPosts = await LoadInstagramPosts(tag, config);
dynamic dyn = JsonConvert.DeserializeObject(instagramPosts.ToString());
foreach (var data in dyn.data)
{
Console.WriteLine("{0} - {1}", data.filter, data.images.standard_resolution.url);
}
}
public static async Task<TagResponse> LoadInstagramPosts(String hashTagTerm, InstagramConfig config)
{
var tagApi = new InstaSharp.Endpoints.Tags(config);
var tagInfo = await tagApi.Get("soccer");
}
EDITED code after first comment which solved my initial problem.
I feel like I'm close but something is still missing.
See specific questions below...
I've based the code on the documentation from InstaSharp GitHub (https://github.com/InstaSharp/InstaSharp). GitHubs example is based on an MVC application, mine is not an MVC project, but a console application.
I feel like I am very close and maybe others will benefit from helping me solve this.
My specific questions...
1) Not sure where the 'code' parameter in the OAuth method originate??
2) How to perform the needed call backs with Instagram??
var config = new InstaSharp.InstagramConfig(location.InstagramClientId, location.InstagramClientSecret, "http://localhost");
string instagramLoginLink = InstagramLogin(config);
GetInstagram("soccer", config, instagramLoginLink);
public static async void GetInstagram(String tag, InstagramConfig config, string code)
{
OAuthResponse oAuthResponse = await OAuth(code, config);
var instagramPosts = await LoadInstagramPosts(tag, config, oAuthResponse);
if(instagramPosts.Data != null)
{
dynamic dyn = JsonConvert.DeserializeObject(instagramPosts.Data.ToString());
foreach (var data in dyn.data)
{
Console.WriteLine("{0} - {1}", data.filter, data.images.standard_resolution.url);
}
}
}
public static string InstagramLogin(InstagramConfig config)
{
var scopes = new List<OAuth.Scope>();
scopes.Add(InstaSharp.OAuth.Scope.Likes);
scopes.Add(InstaSharp.OAuth.Scope.Comments);
string link = InstaSharp.OAuth.AuthLink(config.OAuthUri + "authorize", config.ClientId, config.RedirectUri, scopes, InstaSharp.OAuth.ResponseType.Code);
return link;
}
public static async Task<OAuthResponse> OAuth(string code, InstagramConfig config)
{
// add this code to the auth object
var auth = new OAuth(config);
// now we have to call back to instagram and include the code they gave us
// along with our client secret
return await auth.RequestToken(code);
}
public static async Task<TagResponse> LoadInstagramPosts(String hashTagTerm, InstagramConfig config, OAuthResponse OAuth)
{
var tagApi = new InstaSharp.Endpoints.Tags(config, OAuth);
return await tagApi.Get("soccer");
}
I'm a bit late to the show, yet probably my answer will help someone who find this question when googling, someone like me.
The main problem with your approach is that Instagram is using OAuth authentication. I suggest you to google on OAuth to understand the principles, but I will try to explain the practical points of it below.
OAuth approach means that the result of the InstagramLogin method in the snippet above is not the code. It's the link where you need to send you user (yes, using a browser or a web-view) so that they can sign into their Instagram account and then authorize your application to access their data (so-called user consent screen).
In the end, after user consent, Instagram will redirect browser to the URL of your choice (it should be previously added in the list of allowed redirect urls in Instagram API -> Manage Clients (top-right corner) -> Create/Select client to Manage -> Security tab)
You can try set a breakpoint and copy the value of instagramLoginLink into your browser's address box. You will be able to see the whole flow of authentication and consent - and finally the redirect url that will most probably produce 404 in your browser.
This final link will contain the code in a get parameter. It's the so-called grant code that allows you to get an access token. This code is to be extracted from url and then used in your call to OAuth.RequestToken).
PS: Yes, everything I say above means that you need either a web app running that will redirect user to Instagram or a client-side app that will show the user a web view and somehow handle the moment when Instagram sends the user back to your redirect url - to grab the code and proceed.

CredRead() not working across login sessions

I am using the Credential Manager API as per this answer. Quoting relevant code snippet:
public static Credential ReadCredential(string applicationName)
{
IntPtr nCredPtr;
bool read = CredRead(applicationName, CredentialType.Generic, 0, out nCredPtr);
if (read)
{
using (CriticalCredentialHandle critCred = new CriticalCredentialHandle(nCredPtr))
{
CREDENTIAL cred = critCred.GetCredential();
return ReadCredential(cred);
}
}
return null;
}
It works great, except that when I log off my Windows account and then login again, CredRead() returns false and Marshal.GetLastWin32Error() gives me 1168, or ERROR_NOT_FOUND.
Why this behaviour? Does the Credential Management API work only for the current session, or am I doing something wrong?
Edit: A comment beneath this question says that:
The documents for the credential management APIs seem to indicate that these credentials are associated with a logon session. Perhaps LogonUser results in a new logon session, so the credentials don't exist there.
However I haven't yet found any evidence that Credential Management is session-specific. I think it would be pretty useless if that were the case.
Edit 2: Just for the record, if you need to get the error number indicating why CredRead() is failing, check ReadCred() method in this article.
You can configure how the credential is persisted by setting the Persist property
From MSDN (http://msdn.microsoft.com/en-us/library/windows/desktop/aa374788(v=vs.85).aspx)
CRED_PERSIST_SESSION
CRED_PERSIST_LOCAL_MACHINE
CRED_PERSIST_ENTERPRISE

Real-Time Subscriptions

Good afternoon,
I'm using version 5.4.1 of the Facebook C# SDK. I should note that I am using the source code directly rather than the DLLs (in case this makes any difference).
So the fact that filter attributes are setup is awesome (thank you dev team :).
My issue is occurring during the initial request (before I get to using verifying the GET response from Facebook)
Here is my initial request:
dynamic result = fb.Post(
string.Format("/{0}/subscriptions",
FacebookApplication.Current.AppId),
new Dictionary<string, object>
{
{"object", "user"},
{"fields", "friends"},
{
"callback_url",
"http://localhost:16917/subscription/verify"
},
{
"verify_token",
"77FB802F-1147-48F0-BB0F-E4E9BC1FBCFC"
}
});
I'm finding that an exception is internally being thrown and via Fiddler I'm seeing that the request is never going out. The exception is:
$exception {"(OAuthException) (#15) This method must be called with an app access_token."} System.Exception {Facebook.FacebookOAuthException}
I initially thought this may be related to Facebook.FacebookClient's PrepareRequest method:
if (httpMethod == HttpMethod.Get)
{
// for GET, all parameters goes as querystrings
input = null;
queryString.Append(FacebookUtils.ToJsonQueryString(parameters));
}
else
{
if (parameters.ContainsKey("access_token"))
{
queryString.AppendFormat("access_token={0}", parameters["access_token"]);
parameters.Remove("access_token");
}
}
but commenting out the line parameters.Remove("access_token"); made no difference.
Any help would be greatly appreciated!
What access_token are you using?
Are you using a User's access token, or an App access token?
If you are using an User's access token take a look here http://developers.facebook.com/docs/authentication/#app-login on how to get an App access token.
You will need to use the app access_token.
You can easily create an app access_token using the following constructor.
var fb = new FacebookClient("appid", "appsecret");
dynamic result = fb.Post( .... );

Categories

Resources