Automatic code( authorization code ) redemption using latest version of Katana DLLs in openId authorization code flow - asp.net

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.

Related

Using ResourceOwnerPassword flow for .NET Core 3.1/IdentityModel 5.1

I am playing with the IdentityServer4. Part of that I am trying to build a client using IdentityModel 5.1.0 and trying to use following piece of code available here
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "ro.client", "secret");
var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync("alice", "password", "api1");
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}
Console.WriteLine(tokenResponse.Json);
Console.WriteLine("\n\n");
But this is giving me following error.
error CS1729: 'TokenClient' does not contain a constructor that takes 3 arguments
From the docs, it looks like that page is only applicable to Core 1.0. When I change the documentation to 3.1.0, I get
Sorry This pages does not exist yet
Does this mean that ResourceOwnerPassword flow is not supported for the .NET Core 3.1?
Ctrl + clicking on the method takes to to its signature, where you can find out the specific parameters that the method expects.
Browsing the repo, I've found this snippet on using the password credentials token request:
var response = await _client.RequestPasswordTokenAsync(new PasswordTokenRequest
{
ClientId = "client",
UserName = "user",
Password = "password",
Scope = "scope",
Resource = { "resource1", "resource2" }
});
another overload:
var response = await tokenClient.RequestPasswordTokenAsync(userName: "user", password: "password", scope: "scope");
Or see the actual method definition or another helper.
A useful tip: popular packages usually have a lot of tests. You can check them out to learn how to use the library.

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

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!

Issue with jwt-bearer on-behalf-of grant in Azure AD

So I have an Angular app that uses the adal-angular library to authenticate with an ASP.NET Core 2.0 Web API. The API then uses on-behalf-of flow to authenticate with another API using the users token like this MS article https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-on-behalf-of.
The issue I have is this is working fine in the DEV environment but I have now deployed a TST environment with separate App Registrations and I am receiving the following exception when I try and request the token using on-behalf-of
AADSTS240002: Input id_token cannot be used as 'urn:ietf:params:oauth:grant-type:jwt-bearer' grant.
The code I am using to request the token
public async Task<string> AcquireTokenAsync(string resource)
{
try
{
string accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync(AuthenticationConstants.AccessToken);
var credentials = new ClientCredential(_azureOptions.ClientId, _azureOptions.ClientSecret);
var authContext = new AuthenticationContext($"{_azureOptions.Instance}{_azureOptions.TenantId}")
{
ExtendedLifeTimeEnabled = true
};
// On-behalf-of auth token request call
var authResult = await authContext.AcquireTokenAsync(
resource,
credentials,
new UserAssertion(accessToken));
return authResult.AccessToken;
}
catch (AdalServiceException asex)
{
_logger.LogError(asex, $"Instance: {_azureOptions.Instance} Tenant: {_azureOptions.TenantId} ClientId: {_azureOptions.ClientId}");
throw;
}
catch (System.Exception ex)
{
_logger.LogError(ex, ex.Message);
throw;
}
}
And I have used Fiddler and it looks like all the correct parameters are being passed.
Any help would be very much appreciated. I have set knownClientApplications on the second API and I have granted permissions on the Angular backend API to the second API.
For me, I got it to work by changing BOTH of the following to true:
oauth2AllowImplicitFlow
oauth2AllowIdTokenImplicitFlow
See here for more information.
According to your question and the error, it should be caused by that you angular app is not a Native(public) app.
For using this OBO flow with this Grant type, your client must be a public client not credential client.
If you want to register your client as a WebApp/API, you can refer to this Implementation:
Hope this helps!
Update
According to OP's comment, he/she got it working by changing oauth2AllowImplicitFlow from false to true.
We had this problem last week with one Azure Service Registration and not another. A review found that the token didn't return an AIO being returned. It turns out that the registration had redirects with wildcards (e.g., https://*.ngrok.io) and this is incompatible with the AcquireTokenOnBehalfOf function. I'm posting this here so a future person, probably me, will find it.
I was having problems even when oauth2AllowImplicitFlow and oauth2AllowIdTokenImplicitFlow were set to true. One of my Reply URLs had a wildcard in it. When the wildcard was removed, the issue was resolved.

Is Azure Function to Function authentication with MSI supported

I created 2 Azure Function Apps, both setup with Authentication/Authorization so an AD App was created for both. I would like to setup AD Auth from one Function to the other using MSI. I setup the client Function with Managed Service Identity using an ARM template. I created a simple test function to get the access token and it returns: Microsoft.Azure.Services.AppAuthentication: Token response is not in the expected format.
try {
var azureServiceTokenProvider = new AzureServiceTokenProvider();
string accessToken = await azureServiceTokenProvider.GetAccessTokenAsync("https://myapp-registration-westus-dev.azurewebsites.net/");
log.Info($"Access Token: {accessToken}");
return req.CreateResponse(new {token = accessToken});
}
catch(Exception ex) {
log.Error("Error", ex);
throw;
}
Yes, there is a way to do this. I'll explain at a high level, and then add an item to the MSI documentation backlog to write a proper tutorial for this.
What you want to do is follow this Azure AD authentication sample, but only configure and implement the parts for the TodoListService: https://github.com/Azure-Samples/active-directory-dotnet-daemon.
The role of the TodoListDaemon will be played by a Managed Service Identity instead. So you don't need to register the TodoListDaemon app in Azure AD as instructed in the readme. Just enable MSI on your VM/App Service/Function.
In your code client side code, when you make the call to MSI (on a VM or in a Function or App Service), supply the TodoListService's AppID URI as the resource parameter. MSI will fetch a token for that audience for you.
The code in the TodoListService example will show you how to validate that token when you receive it.
So essentially, what you want to do is register an App in Azure AD, give it an AppID URI, and use that AppID URI as the resource parameter when you make the call to MSI. Then validate the token you receive at your service/receiving side.
Please check that the resource id used "https://myapp-registration-westus-dev.azurewebsites.net/" is accurate. I followed steps here to setup Azure AD authentication, and used the same code as you, and was able to get a token.
https://learn.microsoft.com/en-us/azure/app-service/app-service-mobile-how-to-configure-active-directory-authentication
You can also run this code to check the exact error returned by MSI. Do post the error if it does not help resolve the issue.
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Secret", Environment.GetEnvironmentVariable("MSI_SECRET"));
var response = await client.GetAsync(String.Format("{0}/?resource={1}&api-version={2}", Environment.GetEnvironmentVariable("MSI_ENDPOINT"), "https://myapp-registration-westus-dev.azurewebsites.net/", "2017-09-01"));
string msiResponse = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
log.Info($"MSI Response: {msiResponse}");
Update:-
This project.json file and run.csx file work for me. Note: The project.json refers to .NET 4.6, and as per Azure Functions documentation (link in comments), .NET 4.6 is the only supported version as of now. You do not need to upload the referenced assembly again. Most probably, incorrect manual upload of netstandard assembly, instead of net452 is causing your issue.
Only the .NET Framework 4.6 is supported, so make sure that your
project.json file specifies net46 as shown here. When you upload a
project.json file, the runtime gets the packages and automatically
adds references to the package assemblies. You don't need to add #r
"AssemblyName" directives. To use the types defined in the NuGet
packages, add the required using statements to your run.csx file.
project.json
{
"frameworks": {
"net46":{
"dependencies": {
"Microsoft.Azure.Services.AppAuthentication": "1.0.0-preview"
}
}
}
}
run.csx
using Microsoft.Azure.Services.AppAuthentication;
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
try
{
var azureServiceTokenProvider = new AzureServiceTokenProvider();
string accessToken = await azureServiceTokenProvider.GetAccessTokenAsync("https://vault.azure.net/");
log.Info($"Access Token: {accessToken}");
return req.CreateResponse(new {token = accessToken});
}
catch(Exception ex)
{
log.Error("Error", ex);
throw;
}
}

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