How to validate AWS Cognito JWT in .NET Core Web API using .AddJwtBearer() - .net-core

I was having some trouble figuring out how to go about validating a JWT given to the client by AWS Cognito inside my .NET Core Web API.
Not only could I not figure out what the variables for Microsoft.IdentityModel.Tokens.TokenValidationParameters were supposed to be, but once I finally did, I didn't know how to retrieve the JWT key set from https://cognito-idp.{region}.amazonaws.com/{pool ID}/.well-known/jwks.json
Finally, though a lot of random Googling and trial and error, I found a (seemingly-not-very-efficient solution) solution. However, I spent way too much time doing it. Citing that, plus the fact that AWS documentation on the subject is severely lacking, I decided to post this Q&A to help others find this solution more easily in the future.
If there's a better way to do this, somebody please tell me because I have yet to find a way to do this besides my answer listed below.

The answer lies primarily in correctly defining the TokenValidationParameters.IssuerSigningKeyResolver (parameters, etc. seen here: https://learn.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.tokens.issuersigningkeyresolver?view=azure-dotnet).
This is what tells .NET Core what to verify the JWT sent against. One must also tell it where to find the list of keys. One cannot necessarily hard-code the key set, as it is often rotated by AWS.
One way to do it would be to fetch and serialize the list from the URL inside the IssuerSigningKeyResolver method. The whole .AddJwtBearer() might look something like this:
Startup.cs ConfigureServices() method:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKeyResolver = (s, securityToken, identifier, parameters) =>
{
// get JsonWebKeySet from AWS
var json = new WebClient().DownloadString(parameters.ValidIssuer + "/.well-known/jwks.json");
// serialize the result
var keys = JsonConvert.DeserializeObject<JsonWebKeySet>(json).Keys;
// cast the result to be the type expected by IssuerSigningKeyResolver
return (IEnumerable<SecurityKey>)keys;
},
ValidIssuer = "https://cognito-idp.{region}.amazonaws.com/{pool ID}",
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidAudience = "{Cognito AppClientID}",
ValidateAudience = true
};
});
If you use a JS library such as AWS Amplify, you can see parameters such as the ValidIssuer and ValidAudience in your browser's console by observing the result of Auth.currentSession()
A REST fetch request from a JS client to a .NET Core Web API utilizing the JWT Authentication achieved above as well as using the [Authorize] tag on your controller might look something like this:
JS Client using #aws-amplify/auth node package:
// get the current logged in user's info
Auth.currentSession().then((user) => {
fetch('https://localhost:5001/api/values',
{
method: 'GET',
headers: {
// get the user's JWT token given to it by AWS cognito
'Authorization': `Bearer ${user.signInUserSession.accessToken.jwtToken}`,
'Content-Type': 'application/json'
}
}
).then(response => response.json())
.then(data => console.log(data))
.catch(e => console.error(e))
})

This has been easily the most difficult bit of code I've had to work with in the last year. "Authenticating JWT tokens from AWS Cognito in a .NET Web API app". AWS documentation still leaves much to be desired.
Here's what I used for a new .NET 6 Web API solution (so Startup.cs is now contained within Program.cs. Adjust to fit your version of .NET if needed. Main difference vs .NET 5 and earlier is the Services object is accessed via a variable called builder, so anytime you see code like services.SomeMethod..., you can likely replace it with builder.Services.SomeMethod... to make it .NET 6-compatible):
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = "https://cognito-idp.{aws region here}.amazonaws.com/{Cognito UserPoolId here}",
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidAudience = "{Cognito AppClientId here}",
ValidateAudience = false
};
options.MetadataAddress = "https://cognito-idp.{aws region here}.amazonaws.com/{Cognito UserPoolId here}/.well-known/openid-configuration";
});
Note that I have ValidateAudience set to false. I was getting 401 Unauthorized responses from the .NET app otherwise. Someone else on SO has said they had to do this to get OAuth's Authentication/Authentication Code grant type to work. Evidently ValidateAudience = true will work just fine for implicit grant, however implicit grant is regarded as deprecated by most and you should try to avoid it if possible.
Also note that I am setting options.MetadataAddress. Per another SO user, this apparently allows for behind the scenes caching of the signing keys from AWS that they rotate from time to time.
I was led astray by some official AWS documentation (boo) that had me using builder.Services.AddCognitoIdentity(); (services.AddCognitoIdentity(); for .NET 5 and earlier). Apparently this is for "ASP.NET" apps where the backend serves up the frontend (e.g. Razor/Blazor). Or maybe it's deprecated, who knows. It is on AWS's website so it could very well be deprecated...
As for the Controllers, a simple [Authorize] attribute at the class level sufficed. No need to specify "Bearer" as the AuthenticationScheme in the [Authorize] attribute, or create middleware.
If you want to skip having to add another using to every controller as well as the [Authorize] attribute, and you want every endpoint in every controller to require a JWT, you can put this in Startup/Program.cs:
builder.Services.AddControllers(opt =>
{
var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
opt.Filters.Add(new AuthorizeFilter(policy));
});
Make sure that in Program.cs (Startup.cs for .NET 5 and earlier) app.UseAuthentication comes before app.UseAuthorization().
Here are the usings in Program.cs/Startup.cs:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.IdentityModel.Tokens;

The provided answer here is only required if you need more fine grained control over validation.
Otherwise the following code is sufficient to validate jwt.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "{yourAuthorizationServerAddress}";
options.Audience = "{yourAudience}";
});
Okta have a good article on this. https://developer.okta.com/blog/2018/03/23/token-authentication-aspnetcore-complete-guide
When the JwtBearer middleware handles a request for the first time, it
tries to retrieve some metadata from the authorization server (also
called an authority or issuer). This metadata, or discovery document
in OpenID Connect terminology, contains the public keys and other
details needed to validate tokens. (Curious what the metadata looks like? Here’s an example discovery document.)
If the JwtBearer middleware finds this metadata document, it
configures itself automatically. Pretty nifty!

Related

Auth setup of B2C Web API accessing confidential client (multitenant) Web API

I have a multi-tenant Web API of tenant A. It has permissions exposed and accepted by a B2C Web API of tenant B. (The API App Services live in the same tenant, but their AD instances are separate due to the one being a B2C tenant).
I have the following code in my B2C Web API authenticating with tenant B to access the multi-tenant Web API of tenant A.
I'm using Microsoft.Identity.Web (v1.25.5) and .NET Core (6), and so I don't have to handle making unnecessary calls to get an access token, I'm using the IDownstreamWebApi helper classes (though I have tried without according to the documentation, but land up with the same error.)
My code:
appsettings.json
program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(options =>
{
builder.Configuration.Bind("AzureAdB2C", options);
},
options => {
builder.Configuration.Bind("AzureAdB2C", options);
})
.EnableTokenAcquisitionToCallDownstreamApi(options =>
{
builder.Configuration.Bind("AzureAdB2C", options);
})
.AddDownstreamWebApi("TenantAApi", options =>
{
builder.Configuration.Bind("TenantAApi", options);
})
.AddInMemoryTokenCaches();
Calling code:
var response = await _downstreamWebApi.CallWebApiForAppAsync(
"TenantAApi",
options =>
{
options.HttpMethod = httpMethod;
options.RelativePath = url;
}, content);
var responseContent = await response.Content.ReadAsStringAsync();
The error I receive:
MSAL.NetCore.4.48.0.0.MsalClientException:
ErrorCode: tenant_override_non_aad
Microsoft.Identity.Client.MsalClientException: WithTenantId can only be used when an AAD authority is specified at the application level.
at Microsoft.Identity.Client.AbstractAcquireTokenParameterBuilder`1.WithTenantId(String tenantId)
at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForAppAsync(String scope, String authenticationScheme, String tenant, TokenAcquisitionOptions tokenAcquisitionOptions)
at Microsoft.Identity.Web.DownstreamWebApi.CallWebApiForAppAsync(String serviceName, String authenticationScheme, Action`1 downstreamWebApiOptionsOverride, StringContent content)
What doesn't make sense is that I'm calling this from a B2C Web API, from what I can see in the existing AbstractAcquireTokenParameterBuilder code (see line 292), B2C authorities are not AAD specific, and even so, adding an Authority or AadAuthorityAudience to my AzureAdB2C config object has no effect.
Am I missing a configuration property somewhere?
It seems that this isn't possible according to the following wiki post -
https://github.com/AzureAD/microsoft-identity-web/wiki/b2c-limitations#azure-ad-b2c-protected-web-apis-cannot-call-downstream-apis
For now I'm going to try a different approach and get an access token with a ConfidentialClientApplication object, and if that doesn't work, create a separate app registration in the other tenant and authenticate with that instead.

Automatic code( authorization code ) redemption using latest version of Katana DLLs in openId authorization code flow

From the recent release and conversation below, it says that now Katana(4.1.0) supports code-flow with automatic code redemption(that meaning we do not have call tokenendpoint explicitly to redeem the code for idtoken, accesstoken etc)
https://github.com/aspnet/AspNetKatana/pull/297
so, I've upgraded Katana dlls and have p
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
//MessageReceived = OnMessageReceived, -- previous I were calling token endpoint in this notification
SecurityTokenReceived = notification => Task.FromResult(0),
SecurityTokenValidated = OnSecurityTokenValidated,
AuthenticationFailed = OnAuthenticationFailed,
AuthorizationCodeReceived = AuthorizationCodeReceived, -- added this notification per latest improvements
TokenResponseReceived = TokenResponseReceived
}
and the implementation here
private Task AuthorizationCodeReceived(AuthorizationCodeReceivedNotification arg)
{
return Task.FromResult(0);
}
and Im expecting middleware to call the token endpoint to redeem the auth code, which doesn't happen.
Am I missing something here? should I add some code here for the middleware to redeem the code? Please advsie..
Update:
I have set below as per other blogs,
args.App.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
//other properties removed for brevity
SaveTokens = true,
RedeemCode = true,
}
still midleware does not redeem codes automatically.
Just a thought, is this supported on in .NET core? Im actually using .NET Framework 4.7.1.
Actually, the above settings were working and making token api call, but failing due to "clientsecret" was missing in my settings, once corrected everything worked just fine.Thank you.

Endpoint belongs to different authority

trying to use Azure AD as OpenID provider with IdentityModel package
However the problem is that it produces wrong endpoint configuration
var client = new HttpClient();
const string identityUrl = "https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/v2.0";
const string restUrl = "https://localhost:44321";
var disco = await client.GetDiscoveryDocumentAsync(identityUrl);
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
returns error
Endpoint belongs to different authority:
https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/oauth2/v2.0/authorize
openid-configuration output is
{"authorization_endpoint":"https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/oauth2/v2.0/authorize",
"token_endpoint":"https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/oauth2/v2.0/token" ... }
oauth2 is added between the tenatID and version. I suppose this is why openid metadata validation fails.
Is it possible to configure AzureAD to return correct metadata for the openid-configuration ?
Regards
could you find a solution for this? The only way I could figure out (far to be the optimal solution) is to add the endpoints to a list of additional endpoint base addresses. Otherwise you have to set the validations to false as stated in the comments above.
var client = httpClientFactory.CreateClient();
var disco = await client.GetDiscoveryDocumentAsync(
new DiscoveryDocumentRequest
{
Address = "https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/v2.0",
Policy =
{
ValidateIssuerName = true,
ValidateEndpoints = true,
AdditionalEndpointBaseAddresses = { "https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/oauth2/v2.0/token",
"https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/oauth2/v2.0/authorize",
"https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/discovery/v2.0/keys",
"https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/oauth2/v2.0/devicecode",
"https://graph.microsoft.com/oidc/userinfo",
"https://login.microsoftonline.com/00edae13-e792-4bc1-92ef-92a02ec1d939/oauth2/v2.0/logout"
}
},
}
);
If you take a look at the code inside IdentityModel repository, you can see that the default validation of the endpoints validates them by doing a "starts with" method. https://github.com/IdentityModel/IdentityModel/blob/1db21e2677de6896bc11227c70b927c502e20898/src/Client/StringComparisonAuthorityValidationStrategy.cs#L46
Then the only two required AdditionalEndpointBaseAddresses inside the DiscoveryDocumentRequest Policy field you need to add are "https://login.microsoftonline.com/<guid>" and "https://graph.microsoft.com/oidc/userinfo".
I had the same problem as well and when i upgraded IdentityModel to version 2.16.1 the problem was solved
Azure AD seems to need Additional Endpoints configuration as #flacid-snake suggested. Setting validate endpoints to False is a security threat and should be avoided.
The best way is to make it configurable, preferable in the UI when you configure the SSO server. Endpoints can change and they should be easy to change. It will also make it easier if you later decide to support Okta or other providers and they require additional endpoints.
As of June 2021 you also need to include Kerberos endpoint like:
https://login.microsoftonline.com/888861fc-dd99-4521-a00f-ad8888e9ecc8bfgh/kerberos (replace with your directory tenant id).

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;

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

Resources