Microsoft Graph API userInfo endpoint UnknownError: Token must contain sub claim - .net-core

I'm trying to execute the userinfo endpoint at https://graph.microsoft.com/oidc/userinfo using an access token received through Open ID Connect.
The response received is:
400 Bad Request
{
"error": {
"code": "UnknownError",
"message": "Token must contain sub claim.",
"innerError": {
"date": "2021-02-22T07:14:37",
"request-id": "650a2928-b0e7-49ae-9e6d-ecb569ee69e6",
"client-request-id": "650a2928-b0e7-49ae-9e6d-ecb569ee69e6"
}
}
}
The access token is valid and does contain a sub claim.
If I sign-in to https://developer.microsoft.com/en-us/graph/graph-explorer, and use the access token it automatically retrieves, it works - for the same user. The sub claim is different though and there are two of them.
It seems the token from OIDC doesn't have a correct sub claim - how come might this be?
Access token from directly from the /authorize endpoint [WORKING]:
Access token from OIDC [NOT WORKING]:
OIDC configuration:
options.Authority = authority;
options.ClientId = Configuration[ConfigKeys.IdentityProvider.ClientID];
options.ClientSecret = Configuration[ConfigKeys.IdentityProvider.ClientSecret];
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.CallbackPath = Configuration[ConfigKeys.IdentityProvider.CallbackPath];
options.SignOutScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.CorrelationCookie.Expiration
= options.NonceCookie.Expiration
= options.ProtocolValidator.NonceLifetime
= options.RemoteAuthenticationTimeout
= TimeSpan.FromHours(8);
options.Resource = "https://graph.microsoft.com";
options.GetClaimsFromUserInfoEndpoint = false;
options.UseTokenLifetime = true;
options.RequireHttpsMetadata = true;
options.SaveTokens = true;
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("offline_access");
options.Scope.Add("groups");
options.RemoteAuthenticationTimeout = TimeSpan.FromHours(10);
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = authority,
//NameClaimType = "name"
};

The access token is valid and does contain a sub claim.
I suppose you didn't get the token correctly, please follow the steps below.
1.Register an application with Azure AD
2.In the API permissions of the AD App, add the following permission in Microsoft Graph
3.In the Authentication, choose the options below.
4.Hit the URL below in the browser, replace the <tenant-id>, <client-id> of yours, login your user account, then you will get an access_token and an id_token.
https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize?client_id=<client-id>&response_type=token+id_token&redirect_uri=http://localhost&scope=user.read+openid+profile+email&response_mode=fragment&state=12345&nonce=678910
5.Use the access_token to call the https://graph.microsoft.com/oidc/userinfo endpoint, it works fine, the sub value is EY4uO7uc1IG2n8EboEalB4LDxJ1NU8nuc2JXZgkisN4 in my sample.
6.Decode the id_token got in step 4 in https://jwt.io/, the sub is also EY4uO7uc1IG2n8EboEalB4LDxJ1NU8nuc2JXZgkisN4, so it means the sub got from https://graph.microsoft.com/oidc/userinfo endpoint is correct.
If I sign-in to https://developer.microsoft.com/en-us/graph/graph-explorer, and use the access token it automatically retrieves, it works - for the same user. The sub claim is different though and there are two of them.
The token you got from Microsoft Graph Explorer is an access_token, the first sub is the value for access_token, the second one is that you want i.e. sub of id_token.
It seems the token from OIDC doesn't have a correct sub claim - how come might this be?
It is correct, as I mentioned above, the sub you got from the OIDC is the same as the sub got from the id_token.
Reference - https://learn.microsoft.com/en-us/azure/active-directory/develop/userinfo#userinfo-response
These are the same values that the app would see in the ID token issued to the app.
Note: You may find the sub got manually is different from the second sub got from the MS Graph Explorer, this is because your user account logged in two different clients, one is the client of Graph Explorer, another one is your custom AD App.
Reference - https://learn.microsoft.com/en-us/azure/active-directory/develop/id-tokens
Update:
OIDC does not use the v2.0 endpoint, to solve this issue, we need to configure OIDC to make it use the v2.0 endpoint, just add v2.0 in the authority of the configuration.

Related

How to get external login profile picture from Microsoft account in asp.net core

how can I get profile picture from Microsoft account using Microsoft.AspNetCore.Authentication.Facebook library? I tried using Claims, but they don't have profile picture value... I also tried looking in account's source control by checking image url, but I noticed that the url is made of some parameters that I can't get with claims, so I can't construct url like I can with facebook... Can someone can help me?
You can obtain the profile picture from Microsoft accounts by using Microsoft Graph:
https://developer.microsoft.com/en-us/graph/quick-start
Specific instructions on how to request the profile picture:
https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/profilephoto_get
If you follow the quick start (select asp.net, click "Get an app ID and secret" and download the sample code), it's easy to obtain the data like so:
GraphServiceClient graphClient = SDKHelper.GetAuthenticatedClient();
var photoStream = await graphService.GetCurrentUserPhotoStreamAsync(graphClient);
EDIT: Sorry, forgot the asp.net core part (it doesn't seem that Microsoft.Identity.Client is available for asp.net core).
In ExternalLoginCallback you can obtain the access token from the ExternalLoginInfo object returned by var info = await _signInManager.GetExternalLoginInfoAsync();
Remember to set SaveTokens to true when configuring authentication (otherwise the access token won't be available):
services.AddAuthentication()
.AddMicrosoftAccount(options =>
{
options.ClientId = Configuration["ExternalProviders:Microsoft:ClientId"];
options.ClientSecret = Configuration["ExternalProviders:Microsoft:ClientSecret"];
options.SaveTokens = true;
...
Then it's just a matter of making a http request - something like this:
var httpClient = new HttpClient();
httpClient.SetBearerToken(info.AuthenticationTokens.Where(t => t.Name.Equals("access_token")).First().Value);
var pictureResult = httpClient.GetAsync("https://graph.microsoft.com/v1.0/me/photo/$value").Result;

How do I issue the corresponding Bearer and Cookie identity in ASP.NET with multiple Authorization schemes?

This documentation describes in part how to use more than one authentication scheme:
In some scenarios, such as Single Page Applications it is possible to end up with multiple authentication methods. For example, your application may use cookie-based authentication to log in and bearer authentication for JavaScript requests. In some cases you may have multiple instances of an authentication middleware. For example, two cookie middlewares where one contains a basic identity and one is created when a multi-factor authentication has triggered because the user requested an operation that requires extra security.
Example:
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "Cookie",
LoginPath = new PathString("/Account/Unauthorized/"),
AccessDeniedPath = new PathString("/Account/Forbidden/"),
AutomaticAuthenticate = false
});
app.UseBearerAuthentication(options =>
{
options.AuthenticationScheme = "Bearer";
options.AutomaticAuthenticate = false;
});
However it only describes how to use Bearer or Cookie auth. What isn't clear is what other combinations are valid, or how to properly issue bearer or cookies to the client.
How can that be accomplished?
One common use case for this which large sites like Facebook, Google etc. use is to use multiple cookie authentication middleware's and set one of them to be the default using AutomaticAuthenticate
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "InsecureLongLived",
LoginPath = new PathString("/Account/Unauthorized/"),
AccessDeniedPath = new PathString("/Account/Forbidden/"),
AutomaticAuthenticate = true
});
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "SecureAndShortLived",
LoginPath = new PathString("/Account/Unauthorized/"),
AccessDeniedPath = new PathString("/Account/Forbidden/"),
AutomaticAuthenticate = false
});
The default one is long lived and used for non-critical auth scenarios e.g. on Facebook, this may be to view your profile page.
The more secure and short lived on is used for security critical user actions like changing your password or profile information.
This gives you the convenience of not having to login all the time with a long lived cookie but as soon as you need to do something potentially dangerous, you switch to doing auth with a much shorter lived and thus more secure cookie which requires the user to login again.

Error:"invalid_grant", Description:"Invalid JWT: Token must be a short-lived token and in a reasonable timeframe", Uri:""

I am trying to access google-calendar with the help of google service account
but i got belloing error
An exception of type 'Google.Apis.Auth.OAuth2.Responses.TokenResponseException' occurred in Google.Apis.dll but was not handled in user code
The error I am getting: "invalid_grant", Description:"Invalid JWT: Token must be a short-lived token and in a reasonable timeframe", Uri:""
string credPath = "key path";
String serviceAccountEmail = xxxx#developer.gserviceaccount.com";
var certificate = new X509Certificate2(credPath, "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = new[] { CalendarService.Scope.CalendarReadonly,
CalendarService.Scope.Calendar}
}.FromCertificate(certificate));
// Create the service.
var service = new CalendarService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "HRTool",
});
var events = service.Events.List("my calaender id").Execute();
Invalid grant
When you try to use a refresh token, the following returns you an invalid_grant error:
Your server's clock is not in sync with network time protocol - NTP.
The refresh token limit has been exceeded.
First, kindly check the synchronization problem with the server clock, see the poor synchronization of the computer's clock answer for additional information. Second, check handling of refresh token and the old tokens. Some flows include additional steps, such as using refresh tokens to acquire new access tokens. For detailed information about flows for various types of applications, see Google's OAuth 2.0 documentation.
Hope this helps!
If you are getting the error of "Invalid JWT Signature." and if you are using the P12 certificate then confirm the P12 is correct or not for the Client Key you have used.
If you are getting the error of "Invalid JWT Signature." This is the error caused by some other plugin which you just installed check. I solved by removing rank math plugin as after this plugin install the elementor update was not working.

Google OpenId Connect migration: getting the openid_id in ASP.NET app

I've gone through plenty of Google documentation and SO Q/A's but with no luck. I wonder if anyone has yet succesfully used the OpenId to OpenId Connect migration as advised by Google.
This is what we used to do:
IAuthenticationResponse response = _openid.GetResponse();
if (response != null) {
//omitted for brevity
} else {
IAuthenticationRequest req = _openid.CreateRequest("https://www.google.com/accounts/o8/id");
req.AddExtension(new ClaimsRequest
{
Country = DemandLevel.Request,
Email = DemandLevel.Request,
Gender = DemandLevel.Require,
PostalCode = DemandLevel.Require,
TimeZone = DemandLevel.Require
});
req.RedirectToProvider();
}
That was done using a version of DotNetOpenAuth that dates back a few years. Because Google has deprecated OpenId authentication we are trying to move over to OpenID Connect. The key question here is: can I somehow get my hands on the OpenId identifier (in the form of https://www.google.com/accounts/o8/id?id=xyz) using the latest version of DotNetOpenAuth library or by any other means?
I have tried the latest DotNetOpenAuth and I can get it to work but it gives me a new Id (this was expected). I have also tried the Javascript way by using this URL (line breaks for readibility):
https://accounts.google.com/o/oauth2/auth?
scope=openid%20profile%20email
&openid.realm=http://localhost/palkkac/
&client_id=//here is the client id I created in google developer console
&redirect_uri=http://localhost/palkkac/someaspxpagehere
&response_type=id_token%20token
I checked (using Fiddler) the realm value that we currently send using the old DotNetOpenAuth code and it is http://localhost/palkkac/. I've put the same realm in the url above. The redirect url starts with the realm value but it is not entirely the same.
When I redirect to a simple page that parses the id_token and decrypts it (using the https://www.googleapis.com/oauth2/v1/tokeninfo?id_token=zyx endpoint) I get this:
audience "client id is here"
email "mikkark#gmail.com"
expires_in 3597
issued_at //some numbers here
issued_to "client id is here"
issuer "accounts.google.com"
user_id "here is a sequence of numbers, my id in the OpenID Connect format that is"
verified_email true
So there is no sign of the openid_id field that you would expect to find here, though the whole structure of the message seems different from the Google docs, there is no field titled sub, for example. I wonder if I'm actually using the wrong endpoint, parameters or something?
What I have been reading is the migration guide: https://developers.google.com/accounts/docs/OpenID. I skipped step 2 because it seemed like an optional step. In step 3 the field openid_id is discussed and I would like to get that to work as a proof-of-concept first.
We registered the app on Google in order to create the client id etc. There are now also numerous allowed redirect url's as well as javascript origins listed in the Google dev console. Let me know if those might mess up the system and I'll post them here for review.
Side note: we are supposed to be moving our app behind a strictly firewalled environment where we would need to open ports in order to do this on the server side. Therefore, a client-side Javascript solution to access Google combined with HTTPS and redirecting the result to the server would be prefered (unless there are other issues that speak against this).
There are other resources on SO regarding this same issue, although all of these seem to use different libraries on the server side to do the job and nobody seems to have made any attempts at using Javascript:
Here (https://stackoverflow.com/questions/22842475/migrating-google-openid-to-openid-connect-openid-id-does-not-match) I think the problem was resolved by setting the realm to be the same as in the old OpenId2.0 flow. This does not seem to work in my case.
over here the openid_id field is also missing, but the problem here is more about how to request the id_token from Google using libraries other than DotNetOpenAuth.
and in here there seem to be similar problems getting Google to return the openid_id field.
You can use the GoogleAuthentication owin middleware.
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions
{
SignInAsAuthenticationType = signAs,
AuthenticationType = "Google",
ClientId = "xxx.apps.googleusercontent.com",
ClientSecret = "xx",
CallbackPath = PathString.FromUriComponent("/oauth2callback"),
Provider = new GoogleOAuth2AuthenticationProvider
{
OnApplyRedirect = context =>
{
context.Response.Redirect(context.RedirectUri + "&openid.realm=https://mydomain.com/"); // DotNetOpenAuth by default add a trailing slash, it must be exactly the same as before
}
},
BackchannelHttpHandler = new MyWebRequestHandler()
}
Then, add a new class called MyWebRequestHandler:
public class MyWebRequestHandler : WebRequestHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var httpResponse = await base.SendAsync(request, cancellationToken);
if (request.RequestUri == new Uri("https://www.googleapis.com/plus/v1/people/me")) return httpResponse;
var configuration = await OpenIdConnectConfigurationRetriever.GetAsync("https://accounts.google.com/.well-known/openid-configuration", cancellationToken); // read the configuration to get the signing tokens (todo should be cached or hard coded)
// google is unclear as the openid_id is not in the access_token but in the id_token
// as the middleware dot not expose the id_token we need to parse it again
var jwt = httpResponse.Content.ReadAsStringAsync().Result;
JObject response = JObject.Parse(jwt);
string idToken = response.Value<string>((object)"id_token");
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
try
{
SecurityToken token;
var claims = tokenHandler.ValidateToken(idToken, new TokenValidationParameters()
{
ValidAudience = "xxx.apps.googleusercontent.com",
ValidIssuer = "accounts.google.com",
IssuerSigningTokens = configuration.SigningTokens
}, out token);
var claim = claims.FindFirst("openid_id");
// claim.Value will contain the old openid identifier
if (claim != null) Debug.WriteLine(claim.Value);
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
return httpResponse;
}
}
If like me you found this not really straightforward, please help by upvoting this issue https://katanaproject.codeplex.com/workitem/359

Service account authentication

I'm trying to create calendar event via PHP for one particular user - say developement#example.com.
I've created Service Account in Google Developers Console, got ClientID, E-mail address and private key. The authentication is done with code:
$client = new Google_Client();
if (isset($_SESSION['service_token'])) {
$client->setAccessToken($_SESSION['service_token']);
}
$cred = new Google_Auth_AssertionCredentials(
'somelongstring#developer.gserviceaccount.com.',
array('https://www.googleapis.com/auth/calendar','https://www.googleapis.com/auth/calendar.readonly'),
file_get_contents('p12 file'));
$client->setAssertionCredentials($cred);
if ($client->getAuth()->isAccessTokenExpired()) {
$client->getAuth()->refreshTokenWithAssertion($cred);
}
$_SESSION['service_token'] = $client->getAccessToken();
This type of authentication seems pretty OK. But all events are created as user with E-mail address somelongstring#developer.gserviceaccount.com instead of developement#example.com.
I've tried setting sub parameter:
$cred = new Google_Auth_AssertionCredentials(
....
$cred->sub = 'developement#example.com';
$client->setAssertionCredentials($cred);
But this piece of code throws exception:
Google_Auth_Exception: Error refreshing the OAuth2 token, message: '{ "error" : "access_denied", "error_description" : "Requested client not authorized." }'
And now I'm lost. Any advice?
OK, resolved ;-)
Problem was with developement on own domain.
As mentioned in other question and in Google SDK Guide I have to grant access for service account to all scopes I request access. I forgot to add read-only scope.

Resources