I'm using MSAL for .NET to acquire tokens for my Graph API requests, but out of sudden, I'm getting following error, which I can see a lot of post about, but no solution of reason why Error 80049217 happens? Does anyone know why this error occurs and maybe a solution to avoid the error?
System.InvalidOperationException: CompactToken parsing failed with
error code: 80049217
UPDATE 22-01-10
Example of method to acquire access token (Client is instance of HttpClient reused by all threads using the factory class containing this method. _confidentialClient is an instance of IConfidentialClientApplication in the MSAL .NET library):
private IConfidentialClientApplication _confidentialClient;
public void Initialize()
{
// Construct the ConfidentialClientApplication
_confidentialClient =
ConfidentialClientApplicationBuilder.Create("clientId")
.WithClientSecret("clientSecret")
.WithAuthority("authority")
.Build();
}
// Multiple threads will access this method
private async Task GetAccessToken()
{
try
{
Console.WriteLine("Acquire token....");
// Is the .AcquireTokenForClient method thread safe??
var result = await _confidentialClient.AcquireTokenForClient("https://graph.microsoft.com/.default").ExecuteAsync();
if(Client.DefaultRequestHeaders.Authorization?.Parameter == result.AccessToken)
{
Console.WriteLine("Token havn't changed.");
return;
}
Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
Console.WriteLine("Acquire token successfully!");
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
All threads interacting with Graph API by this factory class will start by calling the GetAccessToken method to make sure the HttpClient has a valid AccessToken in the Authorization header. As far as I have read about IConfidentialClientApplication, the AcquireTokenForClient() will look for valid tokens in the internal cache, and if there isn't any, acquiring a new one, which is why the method is always invoked by any thread.
UPDATE 22-01-13:
Added some logic of how the IConfidentialClientApplication is built.
It seemed that you wrote this code in your asp.net core backend project, and you wanna a method to help generate access token for different scopes without entering user name/password to sign in, so that it can serve different scenario. But you made a mistake here.
See this document first. In a server/daemon application, you can only use client credential flow to generate access token, so the scope for graph api should be https://graph.microsoft.com/.default, and this section provides the sample code to use client credential flow in your asp.net core app. Here's the snippet.
var scopes = new[] { "https://graph.microsoft.com/.default" };
// Multi-tenant apps can use "common",
// single-tenant apps must use the tenant ID from the Azure portal
var tenantId = "common";
// Values from app registration
var clientId = "YOUR_CLIENT_ID";
var clientSecret = "YOUR_CLIENT_SECRET";
// using Azure.Identity;
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
// https://learn.microsoft.com/dotnet/api/azure.identity.clientsecretcredential
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret, options);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
Related
I am using azure graph service client to add/update users and groups in Office365.
I relied on its exceptions which were of type ServiceException and contained status code and message, which I used to determine what to do next with this result.
It worked well, because when I tried to retrieve user I simply used graphClient.Users[email].Request().GetAsync() and I got ServiceException with status of 'NotFound' I knew that user does not exist and what should I do about.
Unfortunately now it has changed and when user does not exist I get System.Exception with following message Object reference not set to an instance of an object.
I'm guessing that it still means that user was not found, but it could also mean something different so my custom logic might break if I catch it and treat it like user does not exist.
Does anybody know why it's like that? Did anything change lately with exceptions from graph client or is it temporar bug on graph service client side?
// UPDATE
Here's the code I used to create graph service client and run the method to retrieve a user:
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create("client_id")
.WithTenantId("tenant_id")
.WithClientSecret("client_secret")
.Build();
var scopes = new[] { "https://graph.microsoft.com/.default" };
AuthenticationResult result = null;
try
{
result = await confidentialClientApplication.AcquireTokenForClient(scopes).ExecuteAsync();
}
catch(Exception)
{
// error handling
}
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", result.AccessToken);
}));
var user = graphClient.Users["email"].Request().GetAsync();
Currently having issues integrating Microsoft Graph API, into my ASP.NET Core 2.2 Web Application(MVC). That uses "Work or Schools Accounts" : “Cloud – Single Organisation” using Two Factor Azure Sign-on Authentication.
Using Code Sample 1 code I'm attempting to GET the graph query: -
https://graph.microsoft.com/v1.0/me/
returning the surname from the response header
The issue that i'm experiencing at the moment is that i'm receiving an error at the line of code: -
var objMessages = objGraphClient.Me.Request().GetAsync().Result;
With the error message : "does not exist or one of its queried reference-property objects are not present".
// #############
// Code Sample 1
// #############
// Graph Api.
string strResource = "https://graph.microsoft.com";
string SecretId = "<Secret Id>";
// Azure Ad.
Uri strInstance = new Uri("https://login.microsoftonline.com/");
string strDomain = "<Domain>.onmicrosoft.com";
string strTenantId = "<Tenant Id>";
string strClientId = "<Client Id>";
string strCallbackPath = "/signin-oidc";
// The authority to ask for a token: your azure active directory.
string strAuthority = new Uri(strInstance, strTenantId).AbsoluteUri;
AuthenticationContext objAuthenticationContext = new AuthenticationContext(strAuthority);
ClientCredential objClientCredential = new ClientCredential(strClientId, SecretId);
// Acquire Token.
AuthenticationResult objAuthenticationResult = objAuthenticationContext.AcquireTokenAsync(strResource, objClientCredential).Result;
// Get bearer token.
GraphServiceClient objGraphClient = new GraphServiceClient(new DelegateAuthenticationProvider(
async request =>
{
// This is adding a bearer token to the httpclient used in the requests.
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", objAuthenticationResult.AccessToken);
}));
// The next line produces an error :: does not exist or one of its queried reference-property objects are not present.
var objResult = objGraphClient.Me.Request().GetAsync().Result;
Debug.WriteLine($"{objResult.Surname}");
If I change Code Sample 1 above to Code Sample 2 below passing in the tokenPlease() requested that’s obtained from Microsoft Graph Explorer after successful login, this works, returning the surname successfully, indicating that their is an issue possible in my Bearer token: -
// #############
// Code Sample 2
// #############
// Get bearer token.
GraphServiceClient objGraphClient = new GraphServiceClient(new DelegateAuthenticationProvider(
async request =>
{
// This is adding a bearer token to the httpclient used in the requests.
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer","ERf54f2f...Etc");
}));
// The next line now works.
var objResult = objGraphClient.Me.Request().GetAsync().Result;
Debug.WriteLine($"{objResult.Surname}");
Any help on this would be much appreciated!
You are using the ADAL library which uses the old Azure AD V1 authentication endpoint. You should be using the MSAL Library which uses the Azure AD V2 authentication endpoint.
I would suggest making your life easy and go grab the Microsoft.Graph.Auth Nuget package and then use this code instead of having to create your own
DelegateAuthenticationProvider
IConfidentialClientApplication clientApplication = AuthorizationCodeProvider.CreateClientApplication(clientId, redirectUri, clientCredential);
AuthorizationCodeProvider authenticationProvider = new AuthorizationCodeProvider(clientApplication, scopes);
I'm executing the following C# magic and read the token obtained in jwt.io. All's looking great.
DiscoveryResponse vasco = DiscoveryClient.GetAsync("http://localhost:5100").Result;
string tokenUri = vasco.TokenEndpoint;
TokenClient client = new TokenClient(vasco.TokenEndpoint, "Blopp", "SuperSecret");
TokenResponse cred = client.RequestClientCredentialsAsync("secured_api").Result;
string token = cred.AccessToken ?? "none!";
However, it seems not to be entirely well functioning one, because when pasted into Postman using key Authorization and value Bearer + token (the prefix daded manually), I get into the service not being reachable (as discussed in this question).
Using the same credentials on the endpoint http://localhost:5100/connect/token and Postman's OAuth 2.0 based wizard, produces a token that works.
My conclusion's that I somehow don't fetch the proper token using my code (and fail to realize it due to ignorance) or that I fetch a token that's missing something.
How do I fetch the proper token, complete and entirely equivalent to the one that Postman obtains at the URL above?
My conclusion's that I somehow don't fetch the proper token using my code (and fail to realize it due to ignorance) or that I fetch a token that's missing something.
From your codes , you are protecting an API using Client Credentials, so firstly please follow the detailed steps in article to config the identity server , web api and the clients .
For testing , i follow the steps in the article , and use same codes as you shown to acquire token :
// discover endpoints from metadata
var disco = await DiscoveryClient.GetAsync("http://localhost:5000");
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret");
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}
Console.WriteLine(tokenResponse.Json);
Console.WriteLine("\n\n");
'http://localhost:5000' is the identity server's host endpoint and clinet/secret is the credential of my client :
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "client",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.ClientCredentials,
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
// scopes that client has access to
AllowedScopes = { "api1" }
}
};
}
Use that token to access the web api in Postman :
You can also compare the acquiring token request when using the OAuth 2.0 based wizard and confirm that you are using the client credential flow .
My goal is to protect a Web API, such that it can only be accessed by a client using an access token issued by IS based on Windows authentication. I worked through this basic sample:
http://docs.identityserver.io/en/release/quickstarts/1_client_credentials.html
Now, I need to extend the basic sample such that the access token returned to the client is issued based on Windows authentication. More specifically, I need to have the user (which is executing the client application) to be authenticated against Active Directory when requesting an access token. How should this be done?
I have already been running the quick start (https://github.com/IdentityServer/IdentityServer4.Templates) successfully, where the login is based on a Windows external provider, but I cannot figure out how to adopt this functionality to my strategy.
I tried using an Extension Grant (http://docs.identityserver.io/en/release/topics/extension_grants.html) and have the ValidateAsync() method be the one to do the authentication against AD, but could not make it work (primarily since HttpContext is not available). Is this even the correct approach?
Update
In this system, the client is a console application (without human interaction), thus the context is the account running the application.
I have been running the QuickstartUI and see how the AccountController logic handles the "Windows" button, but cannot grasp how to combine this with requesting access tokens. My client code goes like this:
static async Task Main(string[] args)
{
var disco = await DiscoveryClient.GetAsync("http://localhost:50010");
var tokenClient = new TokenClient(disco.TokenEndpoint);
var tokenResponse = await tokenClient.RequestCustomGrantAsync("CustomWindows"); // Not sure about this
var client = new HttpClient();
client.SetBearerToken(tokenResponse.AccessToken);
var response = await client.GetAsync("http://localhost:50011/api/identity");
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
Console.ReadLine();
}
I am not sure how to use the TokenClient to get an access token in this case. I would prefer not to store and use passwords, but have IS issue access tokens based on authenciating the client context against AD. If implicit or hybrid flows must be used in this case, how must that be done?
I had the same requirement and implemented it using an extension grant.
This is the code of the extension grant:
public class WinAuthGrantValidator : IExtensionGrantValidator
{
private readonly HttpContext httpContext;
public string GrantType => WinAuthConstants.GrantType;
public WinAuthGrantValidator(IHttpContextAccessor httpContextAccessor)
{
httpContext = httpContextAccessor.HttpContext;
}
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
// see if windows auth has already been requested and succeeded
var result = await httpContext.AuthenticateAsync(WinAuthConstants.WindowsAuthenticationSchemeName);
if (result?.Principal is WindowsPrincipal wp)
{
context.Result = new GrantValidationResult(wp.Identity.Name, GrantType, wp.Claims);
}
else
{
// trigger windows auth
await httpContext.ChallengeAsync(WinAuthConstants.WindowsAuthenticationSchemeName);
context.Result = new GrantValidationResult { IsError = false, Error = null, Subject = null };
}
}
}
And this is the client code:
var httpHandler = new HttpClientHandler
{
UseDefaultCredentials = true,
};
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret", httpHandler, AuthenticationStyle.PostValues);
var tokenResponse = await tokenClient.RequestCustomGrantAsync("windows_auth", "api1");
So I believe my APIservice should be fine since I can return results through Swagger? I am calling from a WPF project. I launch the program and it asks me to login, then it continues and will tell me I don't have permission.
I'm super green to WebAPI2 and think I may just be constructing my call incorrectly. It does seem that I get a token back correctly from my site, the only issue is when I try to actually call on the API for data.
Here is my code:
public static string clientId = "{#Calling App Id}";
public static string commonAuthority = "https://login.windows.net/{#my Azure AD tenant}";
public static Uri returnUri = new Uri("http://MyDirectorySearcherApp");
const string ResourceUri = "https://{#Api App Service}.azurewebsites.net";
public static async Task<List<User>> LoadBands(IPlatformParameters parent)
{
AuthenticationResult authResult = null;
List<User> results = new List<User>();
try {
//get token or use refresh
AuthenticationContext authContext = new AuthenticationContext(commonAuthority);
if (authContext.TokenCache.ReadItems().Count() > 0)
authContext = new AuthenticationContext(authContext.TokenCache.ReadItems().First().Authority);
authResult = await authContext.AcquireTokenAsync(ResourceUri, clientId, returnUri, parent);
} catch (Exception ee) {
throw ex;
}
using (var httpClient = new HttpClient()) {
using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, $"{ResourceUri}/api/Band/")) {
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
using (var response = await httpClient.SendAsync(request)) {
string responseData = await response.Content.ReadAsStringAsync();
//responseData always equals "You do not have permission to view this directory or page"
return results;
}
}
}
Edit: Maybe helpful to note I'm using a DataAPI that is called by a Rest API, the rest API is secured by Azure AD.
Edit: I'm calling from a Portable Class Library.
Edit: Well, I'm getting authenticated but it does not appear to make any difference. If I completely remove the Auth header I get the same result
It seems that the token is incorrect for the web API which protected by Azure AD. Please check the aud claim in the token which should match the Audience you config in the web API project. You can check the aud claim by parse the token from this site.
And if you still have the problem please share the code how you protect the web API.
Update
If you were using the Express mode like below, you need to acquire the access_token using the app which you associate with the web API.
If you were using the Advanced mode, we should also use the that app to acquire the token and the ResourceUri should matched the value you config in ALLOWED TOKEN AUDIENCES like below: