IdentityServer4 upgrade from v3 to v4 - scope validation error - identityserver4

I have just upgraded IdentityServer4 to v4 including the EF schema updates on ApiResources, ApiScopes and ApiResourceScopes. But after making the necessary changes, I start getting error "Scope customers:read not found in store." invalid_scope... as below
I am not sure what I'm missing here; any idea what's causing this error?
Thanks.
SELECT [a].[Id], [a].[AllowedAccessTokenSigningAlgorithms], [a].[Created], [a].[Description], [a].[DisplayName], [a].[Enabled], [a].[LastAccessed], [a].[Name], [a].[NonEditable], [a].[ShowInDiscoveryDocument], [a].[Updated], [a0].[Id], [a0].[ApiResourceId], [a0].[Created], [a0].[Description], [a0].[Expiration], [a0].[Type], [a0].[Value], [a1].[Id], [a1].[ApiResourceId], [a1].[Scope], [a2].[Id], [a2].[ApiResourceId], [a2].[Type], [a3].[Id], [a3].[ApiResourceId], [a3].[Key], [a3].[Value]
FROM [ApiResources] AS [a]
LEFT JOIN [ApiResourceSecrets] AS [a0] ON [a].[Id] = [a0].[ApiResourceId]
LEFT JOIN [ApiResourceScopes] AS [a1] ON [a].[Id] = [a1].[ApiResourceId]
LEFT JOIN [ApiResourceClaims] AS [a2] ON [a].[Id] = [a2].[ApiResourceId]
LEFT JOIN [ApiResourceProperties] AS [a3] ON [a].[Id] = [a3].[ApiResourceId]
WHERE EXISTS (
SELECT 1
FROM [ApiResourceScopes] AS [a4]
WHERE ([a].[Id] = [a4].[ApiResourceId]) AND [a4].[Scope] IN (N'customers:read'))
ORDER BY [a].[Id], [a0].[Id], [a1].[Id], [a2].[Id], [a3].[Id]
dbug: IdentityServer4.EntityFramework.Stores.ResourceStore[0]
Found customers API resources in database
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (56ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [a].[Id], [a].[Description], [a].[DisplayName], [a].[Emphasize], [a].[Enabled], [a].[Name], [a].[Required], [a].[ShowInDiscoveryDocument], [a0].[Id], [a0].[ScopeId], [a0].[Type], [a1].[Id], [a1].[Key], [a1].[ScopeId], [a1].[Value]
FROM [ApiScopes] AS [a]
LEFT JOIN [ApiScopeClaims] AS [a0] ON [a].[Id] = [a0].[ScopeId]
LEFT JOIN [ApiScopeProperties] AS [a1] ON [a].[Id] = [a1].[ScopeId]
WHERE [a].[Name] IN (N'customers:read')
ORDER BY [a].[Id], [a0].[Id], [a1].[Id]
dbug: IdentityServer4.EntityFramework.Stores.ResourceStore[0]
Found customers:read scopes in database
fail: IdentityServer4.Validation.DefaultResourceValidator[0]
Scope customers:read not found in store.
fail: IdentityServer4.Validation.TokenRequestValidator[0]
Invalid scopes requested, {
"ClientId": "or_cust",
"ClientName": "customers mgt api",
"GrantType": "client_credentials",
"Raw": {
"CustomerId": "ZU9h1qCmyU_VCfrUEvOfsg",
"grant_type": "client_credentials",
"scope": "customers:read",
"client_id": "or_cust",
"client_secret": "***REDACTED***"
}
}
info: System.Net.Http.HttpClient.ITokenProvider.ClientHandler[101]
Received HTTP response after 1725.7136ms - BadRequest
info: System.Net.Http.HttpClient.ITokenProvider.LogicalHandler[101]
End processing HTTP request after 1725.9137ms - BadRequest
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
Request finished in 1697.027ms 400 application/json; charset=UTF-8
fail: Goomtera.Runtime.Auth.TokenProvider[0]
invalid_scope

After looking deeper into the DefaultResourceValidator code; the error is caused because the ApiScope I added was not enabled. And the validator fetches ApiResources by applying filter on the Enabled ones.
public virtual async Task<ResourceValidationResult> ValidateRequestedResourcesAsync(ResourceValidationRequest request)
{
if (request == null) throw new ArgumentNullException(nameof(request));
var parsedScopesResult = _scopeParser.ParseScopeValues(request.Scopes);
var result = new ResourceValidationResult();
if (!parsedScopesResult.Succeeded)
{
foreach (var invalidScope in parsedScopesResult.Errors)
{
_logger.LogError("Invalid parsed scope {scope}, message: {error}", invalidScope.RawValue, invalidScope.Error);
result.InvalidScopes.Add(invalidScope.RawValue);
}
return result;
}
var scopeNames = parsedScopesResult.ParsedScopes.Select(x => x.ParsedName).Distinct().ToArray();
var resourcesFromStore = await _store.FindEnabledResourcesByScopeAsync(scopeNames);
/// <summary>
/// Finds the enabled resources by scope.
/// </summary>
/// <param name="store">The store.</param>
/// <param name="scopeNames">The scope names.</param>
/// <returns></returns>
public static async Task<Resources> FindEnabledResourcesByScopeAsync(this IResourceStore store, IEnumerable<string> scopeNames)
{
return (await store.FindResourcesByScopeAsync(scopeNames)).FilterEnabled();
}

Related

The specified 'redirect_uri' is not valid for this client application

I'm using OIDC client and I'm calling below line to siginin,
await this.userManager.signinRedirect(this.createArguments(state));
return this.redirect();
after this I see in the network tab it is navigated to:
https://localhost:5001/connect/authorize?client_id=WebPriorTrainingAuth&redirect_uri=https%3A%2F%2Flocalhost%3A5001%2Fauthentication%2Flogin-callback&response_type=code&scope=openid%20profile&state=9a061d073a424b76bfee25c9bad535d4&code_challenge=ElP_Qtwl8skk13ZyhkzWbnQqU04Y_xYAQXN09cyLY_E&code_challenge_method=S256&response_mode=query
with an error message:
error:invalid_request
error_description:The specified 'redirect_uri' is not valid for this client application.
error_uri:https://documentation.openiddict.com/errors/ID2043
This should have redirected to /Account/Login page (https://localhost:5001/Account/Login?ReturnUrl=%2Fconnect%2) I guess, but that is not happening.
Can someone pls help on this?
In the Authorizationcontroller, the client parameters will have the below value set.
var result = new Dictionary<string, string>();
var application = await applicationManager.FindByClientIdAsync(clientId, cancellationToken);
if (application != null)
{
result.Add("authority", httpContext.GetBaseUrl());
result.Add("client_id", application.ClientId);
result.Add("redirect_uri", "https://localhost:5001/authentication/login-callback");
result.Add("post_logout_redirect_uri", "https://localhost:5001/authentication/logout-callback");
result.Add("response_type", "code");
result.Add("scope", $"openid profile");
//result.Add("response_mode", "query");
}
return result;
In the startup.cs, the below code for OpenIddict settings,
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.LoginPath = "/Identity/Account/Login";
options.LogoutPath = "/Identity/Account/Logout";
})
.AddOpenIdConnect(options =>
{
options.SignInScheme = "Cookies";
options.ForwardSignIn = "Cookies";
options.Authority = baseUrl;
options.SignedOutRedirectUri = baseUrl;
options.ClientId = AuthenticationClient.WebClientId;
options.RequireHttpsMetadata = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.UsePkce = true;
/// Use the authorization code flow.
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
options.Scope.Add(Scopes.OpenId);
options.Scope.Add(Scopes.Profile);
options.Scope.Add(AuthenticationClient.WebClientApiScope);
options.SecurityTokenValidator = new JwtSecurityTokenHandler
{
/// Disable the built-in JWT claims mapping feature.
InboundClaimTypeMap = new Dictionary<string, string>()
};
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "role";
options.Events = new OpenIdConnectEvents
{
/// Add Code Challange
OnRedirectToIdentityProvider = context =>
{
/// Set ProjectId
context.ProtocolMessage.SetParameter("project_id", context.HttpContext.User.Identity.Name);
/// Only modify requests to the authorization endpoint
if (context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
{
/// Generate code_verifier
var codeVerifier = CryptoRandom.CreateUniqueId(32);
/// Store codeVerifier for later use
context.Properties.Items.Add("code_verifier", codeVerifier);
/// Create code_challenge
string codeChallenge;
using (var sha256 = SHA256.Create())
{
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
codeChallenge = Base64Url.Encode(challengeBytes);
}
/// Add code_challenge and code_challenge_method to request
context.ProtocolMessage.Parameters.Add("code_challenge", codeChallenge);
context.ProtocolMessage.Parameters.Add("code_challenge_method", "S256");
}
return Task.CompletedTask;
},
Can some one pls tell me why the signinredirect call is not redirecting to /Account/Login page?
This error is returned when the specified redirect_uri is not recognized by OpenIddict.
Are you sure you added https://localhost:5001/authentication/login-callback to the list of allowed redirect_uris for your WebPriorTrainingAuth client?
I think the redirect URL should be to the Callbackpath of the OpenIDConnect handler in the ASP.NET core client. This path is by default set to:
CallbackPath = new PathString("/signin-oidc");
This is the path where the autorization code is sent to after a successfull authentication in IdentityServer.
See the source code here:
I know this is an old question and already answered .. and this answer not for this case.
But you are a new user getting this error message and you are working on 127.0.0.1 .... please make sure that your OpenIddictApplication has localhost AND 127.0.0.1 as valid rediect urls in RedirectUris list.

Azure AD Microsoft Identity Web OpenIdConnectEvents - How to access optional claims from the user token during sign out

Using Net Core 3.1 with Microsoft Identity Web and Azure AD.
I'm trying to setup some logging for when a user signs in and out of my web app project. The logging needs to include details of the user as well as the IP Address of the client endpoint they used during sign in and sign out. I then pass the IP Address through an extension method for capturing Geo Location info that is added to the log event for that user authentication.
In startup.cs I have configured some extended options for the OpenIdConnectOptions, they are:
OnTokenValidated
OnRedirectToIdentityProviderForSignOut
OnSignedOutCallbackRedirect
The OpenIdEvents class I created is just simply to move away the methods from the startup.cs file for cleanliness.
Extract from startup.cs below:
// Create a new instance of the class that stores the methods called
// by OpenIdConnectEvents(); i.e. when a user logs in or out the app.
// See section below :- 'services.Configure'
OpenIdEvents openIdEvents = new OpenIdEvents();
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
// The claim in the Jwt token where App roles are available.
options.TokenValidationParameters.RoleClaimType = "roles";
// Advanced config - capturing user events. See OpenIdEvents class.
options.Events ??= new OpenIdConnectEvents();
options.Events.OnTokenValidated += openIdEvents.OnTokenValidatedFunc;
// This is event is fired when the user is redirected to the MS Signout Page (before they've physically signed out)
options.Events.OnRedirectToIdentityProviderForSignOut += openIdEvents.OnRedirectToIdentityProviderForSignOutFunc;
// DO NOT DELETE - May use in the future.
// OnSignedOutCallbackRedirect doesn't produce any claims to read for the user after they have signed out.
options.Events.OnSignedOutCallbackRedirect += openIdEvents.OnSignedOutCallbackRedirectFunc;
});
So far I have found a solution to capture the required claims of the user for when they sign in, the 'TokenValidatedContext' passed to the first method 'OnTokenValidatedFunc' contains details of the security token which in itself shows the optional claims that I had configured including the IP Address (referred to as "ipaddr")
Some of these optional claims were configured in the App manifest file in Azure, they are present in the security token in this first method so pretty sure Azure is setup correctly.
Extract from Azure App Manifest File:
"optionalClaims": {
"idToken": [
{
"name": "family_name",
"source": null,
"essential": false,
"additionalProperties": []
},
{
"name": "given_name",
"source": null,
"essential": false,
"additionalProperties": []
},
{
"name": "ipaddr",
"source": null,
"essential": false,
"additionalProperties": []
}
],
"accessToken": [],
"saml2Token": []
},
'OnTokenValidatedFunc' method shown below:
/// <summary>
/// Invoked when an IdToken has been validated and produced an AuthenticationTicket.
/// See weblink: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.ontokenvalidated?view=aspnetcore-3.0
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task OnTokenValidatedFunc(TokenValidatedContext context)
{
var token = context.SecurityToken;
var userId = token.Claims.First(claim => claim.Type == "oid").Value;
var givenName = token.Claims.First(claim => claim.Type == "given_name").Value;
var familyName = token.Claims.First(claim => claim.Type == "family_name").Value;
var userName = token.Claims.First(claim => claim.Type == "preferred_username").Value;
string ipAddress = token.Claims.First(claim => claim.Type == "ipaddr").Value;
GeoHelper geoHelper = new GeoHelper();
var geoInfo = await geoHelper.GetGeoInfo(ipAddress);
string logEventCategory = "Open Id Connect";
string logEventType = "User Login";
string logEventSource = "WebApp_RAZOR";
string logCountry = "";
string logRegionName = "";
string logCity = "";
string logZip = "";
string logLatitude = "";
string logLongitude = "";
string logIsp = "";
string logMobile = "";
string logUserId = userId;
string logUserName = userName;
string logForename = givenName;
string logSurname = familyName;
string logData = "User login";
if (geoInfo != null)
{
logCountry = geoInfo.Country;
logRegionName = geoInfo.RegionName;
logCity = geoInfo.City;
logZip = geoInfo.Zip;
logLatitude = geoInfo.Latitude.ToString();
logLongitude = geoInfo.Longitude.ToString();
logIsp = geoInfo.Isp;
logMobile = geoInfo.Mobile.ToString();
}
// Tested on 31/08/2020
Log.Information(
"{#LogEventCategory}" +
"{#LogEventType}" +
"{#LogEventSource}" +
"{#LogCountry}" +
"{#LogRegion}" +
"{#LogCity}" +
"{#LogZip}" +
"{#LogLatitude}" +
"{#LogLongitude}" +
"{#LogIsp}" +
"{#LogMobile}" +
"{#LogUserId}" +
"{#LogUsername}" +
"{#LogForename}" +
"{#LogSurname}" +
"{#LogData}",
logEventCategory,
logEventType,
logEventSource,
logCountry,
logRegionName,
logCity,
logZip,
logLatitude,
logLongitude,
logIsp,
logMobile,
logUserId,
logUserName,
logForename,
logSurname,
logData);
await Task.CompletedTask.ConfigureAwait(false);
}
See Debug shots below:
When expanding the claims, you can see the claim for "ipaddr" is shown:
MY ISSUE:
The other event types fired from OpenIdConnectEvents for when the user signs out, does not function in the same way and this is where I am stuck!
There are two different event types I have tried testing with:
OnRedirectToIdentityProviderForSignOut
OnSignedOutCallbackRedirect
Each one is fired at a slightly different point during the user sign out process i.e. the 'OnRedirectToIdentityProviderForSignOutFunc' is fired when the user is being re-directed to the Microsoft Sign Out page, just before they actually hit the button and sign out.
This is not an ideal event type to work with given the user could abort signing out of the application and the log generated would not reflect this, however I have so far found that I could at least access most of the claims of the user, BUT the "ipaddr" claim is not listed and I simply don't know why or how to get it.
When I look at the Debug info I find the security token is not shown at all and the only way to access the user claims was to read another part of the context by navigating to context.HttpContext.User.Claims
Debug Screenshot:
The method for this shown below:
public async Task OnRedirectToIdentityProviderForSignOutFunc(RedirectContext context)
{
var user = context.HttpContext.User;
string ipAddress = user.Claims.FirstOrDefault(claim => claim.Type == "ipaddr").Value;
var userId = user.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
var givenName = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value;
var familyName = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value;
var userName = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
// The IP Address claim is missing!
//string ipAddress = claims.First(claim => claim.Type == "ipaddr").Value;
await Task.CompletedTask.ConfigureAwait(false);
}
The above method only gives a partial solution given I still need the IP Address claim but is not present at all, but the choice in using this event type as explained above is not ideal anyway.
AND FINALLY:
Trying to subscribe to the final option 'OnSignedOutCallbackRedirect' has been a complete waste of time so far given none of the user claims are present at all in the context. It seems that Microsoft dumps them once the user has hit the Sign out button and returned back to the 'Signed Out' page in the web app.
Really I want a solution for when the user has actually signed out, not half way through the process of signing out, BUT I must be able access the user claims including the IP Address which is not present in either of the above two events fired during this process.
All I want is to simply capture the details (claims) of the user and the IP Address of the client session they are connecting from and log this when they sign in and sign out of the web application. Is this really too much to ask!
Documentation on this is very sparse, I would much appreciate some clues from anyone out there who understands well how MS Identity Web and OpenIDConnect Events function behind the scenes.
Solution 1 = Being able to access the IP Address claim from the context during 'OnRedirectToIdentityProviderForSignOut' but it is currently missing...
Solution 2 (Preferred) = Being able to access the user claims during 'OnSignedOutCallbackRedirect' but currently none of them are listed at all.
Thanks in advance...
I need to be able to access the claims from the user once they have signed out of the application using one of two possible events that are generated from OpenIdConnect
The user signed out at that point. He/She is no longer there, so it makes sense it has no claims, it's going back to the default, empty anonymous user because it's signed out.
As mentioned in my comments above, and also taking on board Jean-Marc Prieur's comments above on the fact that no claims will ever be present once the user has completely signed out, I ended up just grabbing the details of the user context through OnRedirectToIdentityProviderForSignOutFunc method and then used a separate GeoHelper class to fetch the IP Address of the destination where the person was when singing out (or should we say was about to sign out!
Yes appreciate this isn't the most ideal cause & affect, but to be honest it's not going to present an big issue for me and may not for others given in most cases, logging when someone signs out is not business critical, its more to get a idea of system usage. By the time someone has reached the MS popup page to logout, we should likely assume 99% of cases that they will proceed and actually logout.
So below is the code I used to achieve the above scenario:
startup.cs class (extract from startup.cs code bloat)
// Create a new instance of the class that stores the methods called
// by OpenIdConnectEvents(); i.e. when a user logs in or out the app.
// See section below :- 'services.Configure'
OpenIdEvents openIdEvents = new OpenIdEvents();
// The following lines code instruct the asp.net core middleware to use the data in the "roles" claim in the Authorize attribute and User.IsInrole()
// See https://learn.microsoft.com/aspnet/core/security/authorization/roles?view=aspnetcore-2.2 for more info.
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
// The claim in the Jwt token where App roles are available.
options.TokenValidationParameters.RoleClaimType = "roles";
// Advanced config - capturing user events. See OpenIdEvents class.
options.Events ??= new OpenIdConnectEvents();
options.Events.OnTokenValidated += openIdEvents.OnTokenValidatedFunc;
// This is event is fired when the user is redirected to the MS Signout Page (before they've physically signed out)
options.Events.OnRedirectToIdentityProviderForSignOut += openIdEvents.OnRedirectToIdentityProviderForSignOutFunc;
// DO NOT DELETE - May use in the future.
// OnSignedOutCallbackRedirect doesn't produce any user claims to read from for the user after they have signed out.
options.Events.OnSignedOutCallbackRedirect += openIdEvents.OnSignedOutCallbackRedirectFunc;
});
My custom class for the Geolocation:
namespace MyProject.Classes.GeoLocation
{
/// <summary>
/// See weblink for API documentation: https://ip-api.com/docs or https://ip-api.com/docs/api:json
/// Note: Not free for commercial use - fee plan during development only!
/// Sample query: http://ip-api.com/json/{ip_address}?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,isp,mobile,query
/// </summary>
public class GeoHelper
{
private readonly HttpClient _httpClient;
public GeoHelper()
{
_httpClient = new HttpClient()
{
Timeout = TimeSpan.FromSeconds(5)
};
}
public async Task<GeoInfo> GetGeoInfo(string ip)
{
try
{
var response = await _httpClient.GetAsync($"http://ip-api.com/json/{ip}?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,isp,mobile,query");
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<GeoInfo>(json);
}
}
catch (Exception)
{
// Do nothing, just return null.
}
return null;
}
}
}
OpenIdEvents.cs class:
namespace MyProject.Classes.Security
{
public class OpenIdEvents
{
// Create the concurrent dictionary to store the user's IP Addresss when they sign in, the value is fetched
// from the dictionary when they sing out. given this information is not present within the contect passed through the event.
private readonly ConcurrentDictionary<string, string> IpAddressDictionary = new ConcurrentDictionary<string, string>();
/// <summary>
/// Invoked when an IdToken has been validated and produced an AuthenticationTicket.
/// See weblink: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.ontokenvalidated?view=aspnetcore-3.0
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task OnTokenValidatedFunc(TokenValidatedContext context)
{
var token = context.SecurityToken;
var userId = token.Claims.First(claim => claim.Type == "oid").Value;
var givenName = token.Claims.First(claim => claim.Type == "given_name").Value;
var familyName = token.Claims.First(claim => claim.Type == "family_name").Value;
var username = token.Claims.First(claim => claim.Type == "preferred_username").Value;
var ipAddress = token.Claims.First(claim => claim.Type == "ipaddr").Value;
// Add the IP Address from the user's ID Token to the dictionary, we will remove
// it from the dictionary when the user requests a sign out through OpenIDConnect.
IpAddressDictionary.TryAdd(userId, ipAddress);
GeoHelper geoHelper = new GeoHelper();
var geoInfo = await geoHelper.GetGeoInfo(ipAddress);
string logEventCategory = "Open Id Connect";
string logEventType = "User Sign In";
string logEventSource = "MyProject";
string logCountry = "";
string logRegionName = "";
string logCity = "";
string logZip = "";
string logLatitude = "";
string logLongitude = "";
string logIsp = "";
string logMobile = "";
string logUserId = userId;
string logUserName = username;
string logForename = givenName;
string logSurname = familyName;
string logData = "User with username [" + username + "] forename [" + givenName + "] surname [" + familyName + "] from IP Address [" + ipAddress + "] signed into the application [MyProject] Succesfully";
if (geoInfo != null)
{
logCountry = geoInfo.Country;
logRegionName = geoInfo.RegionName;
logCity = geoInfo.City;
logZip = geoInfo.Zip;
logLatitude = geoInfo.Latitude.ToString();
logLongitude = geoInfo.Longitude.ToString();
logIsp = geoInfo.Isp;
logMobile = geoInfo.Mobile.ToString();
}
// Tested on 31/08/2020
Log.Information(
"{#LogEventCategory}" +
"{#LogEventType}" +
"{#LogEventSource}" +
"{#LogCountry}" +
"{#LogRegion}" +
"{#LogCity}" +
"{#LogZip}" +
"{#LogLatitude}" +
"{#LogLongitude}" +
"{#LogIsp}" +
"{#LogMobile}" +
"{#LogUserId}" +
"{#LogUsername}" +
"{#LogForename}" +
"{#LogSurname}" +
"{#LogData}",
logEventCategory,
logEventType,
logEventSource,
logCountry,
logRegionName,
logCity,
logZip,
logLatitude,
logLongitude,
logIsp,
logMobile,
logUserId,
logUserName,
logForename,
logSurname,
logData);
await Task.CompletedTask.ConfigureAwait(false);
}
/// <summary>
/// Invoked before redirecting to the identity provider to sign out.
/// See weblink: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.onredirecttoidentityproviderforsignout?view=aspnetcore-3.0&viewFallbackFrom=aspnetcore-3.1
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task OnRedirectToIdentityProviderForSignOutFunc(RedirectContext context)
{
var user = context.HttpContext.User;
var userId = user.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
var givenName = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value;
var familyName = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value;
var username = user.Identity.Name;
string logEventCategory = "Open Id Connect";
string logEventType = "User Sign Out";
string logEventSource = "MyProject";
string logCountry = "";
string logRegionName = "";
string logCity = "";
string logZip = "";
string logLatitude = "";
string logLongitude = "";
string logIsp = "";
string logMobile = "";
string logUserId = userId;
string logUserName = username;
string logForename = givenName;
string logSurname = familyName;
IpAddressDictionary.TryRemove(userId, out string ipAddress);
if (ipAddress != null)
{
// Re-fetch the geo-location details which may be different than the login session
// given the user might have been signed in using a cell phone and move locations.
GeoHelper geoHelper = new GeoHelper();
var geoInfo = await geoHelper.GetGeoInfo(ipAddress);
if (geoInfo != null)
{
logCountry = geoInfo.Country;
logRegionName = geoInfo.RegionName;
logCity = geoInfo.City;
logZip = geoInfo.Zip;
logLatitude = geoInfo.Latitude.ToString();
logLongitude = geoInfo.Longitude.ToString();
logIsp = geoInfo.Isp;
logMobile = geoInfo.Mobile.ToString();
}
}
string logData = "User with username [" + username + "] forename [" + givenName + "] surname [" + familyName + "] from IP Address [" + ipAddress + "] signed out the application [MyProject] Succesfully";
// Tested on 31/08/2020
Log.Information(
"{#LogEventCategory}" +
"{#LogEventType}" +
"{#LogEventSource}" +
"{#LogCountry}" +
"{#LogRegion}" +
"{#LogCity}" +
"{#LogZip}" +
"{#LogLatitude}" +
"{#LogLongitude}" +
"{#LogIsp}" +
"{#LogMobile}" +
"{#LogUserId}" +
"{#LogUsername}" +
"{#LogForename}" +
"{#LogSurname}" +
"{#LogData}",
logEventCategory,
logEventType,
logEventSource,
logCountry,
logRegionName,
logCity,
logZip,
logLatitude,
logLongitude,
logIsp,
logMobile,
logUserId,
logUserName,
logForename,
logSurname,
logData);
await Task.CompletedTask.ConfigureAwait(false);
}
/// <summary>
/// Invoked before redirecting to the SignedOutRedirectUri at the end of a remote sign-out flow.
/// See weblink: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.onsignedoutcallbackredirect?view=aspnetcore-3.0
/// Not currently in use becuase neither the user's ID Token or claims were present. We had to use the above method instead.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task OnSignedOutCallbackRedirectFunc(RemoteSignOutContext context)
{
await Task.CompletedTask.ConfigureAwait(false);
}
}
}

SAML2.0 Access token using 'itfoxtec-identity-saml2'

I am trying to use your Nuget package for dotnet core and I get little bit success also I can login to SAML identity providers like Onelogin,Okta and I got loggin user information also But I am confuse while generating access token(Bearer token to call APIs of SAML identity providers). How will I get that token?
I can see securitytoken object in saml2AuthnResponse but don’t know how to that token and in that object security key and singin key is null.
I am totally new to this so may be I misunderstand something.
Please help me.
[Route("AssertionConsumerService")]
public async Task<IActionResult> AssertionConsumerService()
{
var binding = new Saml2PostBinding();
var saml2AuthnResponse = new Saml2AuthnResponse(config);
binding.ReadSamlResponse(Request.ToGenericHttpRequest(), saml2AuthnResponse);
if (saml2AuthnResponse.Status != Saml2StatusCodes.Success)
{
throw new AuthenticationException($"SAML Response status: {saml2AuthnResponse.Status}");
}
binding.Unbind(Request.ToGenericHttpRequest(), saml2AuthnResponse);
await saml2AuthnResponse.CreateSession(HttpContext, claimsTransform: (claimsPrincipal) => ClaimsTransform.Transform(claimsPrincipal));
var relayStateQuery = binding.GetRelayStateQuery();
var returnUrl = relayStateQuery.ContainsKey(relayStateReturnUrl) ? relayStateQuery[relayStateReturnUrl] : Url.Content("~/");
return Redirect(returnUrl);
}
You can get access to the SAML 2.0 token as a XML string by setting Saml2Configuration.SaveBootstrapContext = true in appsettings.json:
...
"Saml2": {
"SaveBootstrapContext": true,
"IdPMetadata": "https://localhost:44305/metadata",
"Issuer": "itfoxtec-testwebappcore",
...
}
Alternatively you can set the configuration in code:
config.SaveBootstrapContext = true;
Then you can read the SAML 2.0 token as a XML string in the saml2AuthnResponse.ClaimsIdentity.BootstrapContext:
public async Task<IActionResult> AssertionConsumerService()
{
var binding = new Saml2PostBinding();
var saml2AuthnResponse = new Saml2AuthnResponse(config);
binding.ReadSamlResponse(Request.ToGenericHttpRequest(), saml2AuthnResponse);
if (saml2AuthnResponse.Status != Saml2StatusCodes.Success)
{
throw new AuthenticationException($"SAML Response status: {saml2AuthnResponse.Status}");
}
binding.Unbind(Request.ToGenericHttpRequest(), saml2AuthnResponse);
await saml2AuthnResponse.CreateSession(HttpContext, claimsTransform: (claimsPrincipal) => ClaimsTransform.Transform(claimsPrincipal));
var samlTokenXml = saml2AuthnResponse.ClaimsIdentity.BootstrapContext as string;
var relayStateQuery = binding.GetRelayStateQuery();
var returnUrl = relayStateQuery.ContainsKey(relayStateReturnUrl) ? relayStateQuery[relayStateReturnUrl] : Url.Content("~/");
return Redirect(returnUrl);
}

500 Error - Unable to select and perform a post action

I am not good with Web API. Here is my problem. I send an Json serialized object from my Windows Form Application. The object is an Entity table. When I do a get response it returns a 500 server error. Basically I plan to have multiple post methods in one controller which I may not be doing right. So I need you guys to guide me on what I have been doing wrong.
Here is my Controller:
[ResponseType(typeof(HttpWebResponse)), HttpPost, ActionName("MerchandiseApi")]
public HttpResponseMessage PostMain(IList<IMF_Main> mainFromConsolidator)
{
if (!ModelState.IsValid)
return Request.CreateResponse(HttpStatusCode.BadRequest, 2);
using (var anthill = new AnthillConsolidatorEntities())
{
var main = new IMF_Main();
foreach (var item in mainFromConsolidator)
{
main.BrandID = item.BrandID;
main.ItemID = item.ItemID;
main.CategoryID = item.CategoryID;
main.SubCategoryID = item.SubCategoryID;
main.ClassID = item.ClassID;
main.GenderID = item.GenderID;
main.CoaID = item.CoaID;
main.SubCoaID = item.SubCoaID;
main.First_SRP = item.First_SRP;
main.Current_SRP = item.Current_SRP;
main.Previous_SRP = item.Previous_SRP;
main.isSenior = item.isSenior;
main.isActive = item.isActive;
main.DateCreated = item.DateCreated;
anthill.IMF_Main.Add(main);
anthill.SaveChanges();
}
}
return Request.CreateResponse(HttpStatusCode.OK, 1);
}
Here's my WebApiConfig:
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
name: "MerchandiseApi",
routeTemplate: "api/{controller}/{action}"
);
}
Here is where the Uri gets built: I have 2 more tables to send but I will start with this. This goes to my first Post method to the server
var jsonMain = JsonConvert.SerializeObject(consolidatorEntities.IMF_Main, Formatting.None);
HttpPost("http://localhost:50826/api/Merchandise/PostMain", jsonMain) == 1.ToString()
public string HttpPost(string uri, string json)
{
string content = "";
try
{
var request = (HttpWebRequest)WebRequest.Create(uri);
request.Method = "POST";
request.Accept = "application/json";
request.ContentType = "application/json";
byte[] bodyBytes = Encoding.UTF8.GetBytes(json);
request.GetRequestStream().Write(bodyBytes, 0, bodyBytes.Length);
request.GetRequestStream().Close();
var response = (HttpWebResponse)request.GetResponse();
var sr = new StreamReader(response.GetResponseStream(), Encoding.GetEncod
ing("UTF-8"));
content = sr.ReadToEnd();
sr.Close();
}
catch (Exception ex)
{
MessageBox.Show("Error sending data to Anthill \nException: " + ex, "Monytron - Consolidator", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
return content;
}
Problem
The main problem is with your routing. Routes will check in order so when you post a request to http://localhost:50826/api/Merchandise/PostMain and you have these routes in order:
"api/{controller}/{id}"
"api/{controller}/{action}"
So the first route will match:
If your PostMain method is the only action with [HttpPost], then mainFromConsolidator will be null in your foreach loop you will receive a NullReferenceException that result in a 500 error.
If you have multiple method decorated with [HttpPost], then the call is ambiguous between those actions and you will receive an InvalidOperationExpception with "Multiple actions were found that match the request" message that result in a 500 error.
The other problem is you are using an ActionName("MerchandiseApi") but didn't post to that action.
Solution
You can use multiple solutions. As an option you can define only one route:
"api/{controller}/{action}/{id}"
This way you can create a controller that contains actions like these:
public class SomeController
{
// matches GET /api/some/action1
[HttpGet]
public HttpResponseMessage Action1()
// matches GET /api/some/action2/5
[HttpGet]
public HttpResponseMessage Action2(int id)
// matches POST /api/some/action3
[HttpPost]
public HttpResponseMessage Action3(SomeType someParameter)
// matches POST /api/some/action4
[HttpPost]
public HttpResponseMessage Action4(SomeType someParameter)
}
Anyway if you decide to define multiple routes, pay attention that routes will match in order and also if you used ActionName attribute, then use that name in url to call that action.

How to Create Asynchronous HttpWebRequest in Silverlight( F#)

As I mentioned, because Silverlight HttpWebRequest.Create hangs inside async block, I just created a bundle of callback functions to implement the same async block.
The login process requires two steps :
1) Get request to a page that returns a cookie
2) Form Post to a second page that passes that cookie w/ it and performs the authentication
The following is the src. Any suggestions and discussions are welcome and appreciated no matter about Asynchronous HttpWebRequest or about the F# code style.
module File1
open System
open System.IO
open System.Net
open System.Text
open System.Security
open System.Runtime.Serialization
open System.Collections.Generic
open JsonData
open System.Net.Browser
open System.Threading
module rpc =
let mutable BASE_DNS = ""
let mutable requestId : int = 0
let getId() =
requestId <- requestId + 1
requestId.ToString()
module internal Helper =
///<Summary>
///Transfer data from Security.loginToRpc to Helper.FetchCookieCallback
///</Summary>
type LoginRequestRecord = {
Request : HttpWebRequest;
UserName : string;
Password : string;
AuthenticationUrl : string;
CallbackUI : (bool -> unit)
}
///<Summary>
///Transfer data from Helper.FetchCookieCallback to Helper.requestAuthenticationCallback
///</Summary>
type AuthenticationRecord = {
Request : HttpWebRequest;
UserName : string;
Password : string;
CallbackUI : (bool -> unit)
}
///<Summary>
///Transfer data from Helper.requestAuthenticationCallback to Helper.responseAuthenticationCallback
///</Summary>
type ResponseAuthenticationRecord = {
Request : HttpWebRequest;
CallbackUI : (bool -> unit)
}
///<Summary>
///The cookieContainer for all the requests in the session
///</Summary>
let mutable cookieJar = new CookieContainer()
///<summary>
///Function: Create HttpRequest
///Param: string
///Return: HttpWebRequest
///</summary>
let internal createHttpRequest (queryUrl : string) =
let uri = new Uri(queryUrl)
let request : HttpWebRequest =
downcast WebRequestCreator.ClientHttp.Create(
new Uri(queryUrl, UriKind.Absolute))
request
///<summary>
///Function: set request whose method is "GET".
///Attention: no contentType for "GET" request~!!!!!!!!!!!!!!!!
///Param: HttpWebRequest
///Return: unit
///</summary>
let internal requestGetSet (request : HttpWebRequest) =
request.Method <- "GET"
///<summary>
///Function: set request whose method is "POST" and its contentType
///Param: HttpWebRequest and contentType string
///Return: unit
///</summary>
let internal requestPostSet (request : HttpWebRequest) contentType =
request.Method <- "POST"
request.ContentType <- contentType
///<summary>
///Function: Callback function inluding EndGetResponse method of request
///Param: IAsyncResult includes the information of HttpWebRequest
///Return: unit
///</summary>
let internal responseAuthenticationCallback (ar : IAsyncResult) =
let responseAuthentication : ResponseAuthenticationRecord
= downcast ar.AsyncState
try
let response = responseAuthentication.Request.EndGetResponse(ar)
//check whether the authentication is successful,
//which may be changed later into other methods
match response.ContentLength with
| -1L -> responseAuthentication.CallbackUI true
| _ -> responseAuthentication.CallbackUI false
()
with
| Ex -> responseAuthentication.CallbackUI false
///<summary>
///Function: Callback function for user to log into the website
///Param: IAsyncResult includes the information of
///HttpWebRequest and user's identity
///Return: unit
///</summary>
let internal requestAuthenticationCallback (ar : IAsyncResult) =
let authentication : AuthenticationRecord = downcast ar.AsyncState
try
let requestStream = authentication.Request.EndGetRequestStream(ar)
let streamWriter = new StreamWriter(requestStream)
streamWriter.Write(
String.Format(
"j_username={0}&j_password={1}&login={2}",
authentication.UserName,
authentication.Password,
"Login"))
streamWriter.Close()
let responseAuthentication = {
ResponseAuthenticationRecord.Request = authentication.Request
ResponseAuthenticationRecord.CallbackUI = authentication.CallbackUI
}
authentication.Request.BeginGetResponse(
new AsyncCallback(responseAuthenticationCallback),
responseAuthentication)
|> ignore
with
| Ex -> authentication.CallbackUI false
()
///<summary>
///This is a magic number to check
///whether the first request have got the cookie from the server-side,
///which should be changed later
///</summary>
let countHeadersAfterGetCookie = 8
///<summary>
///Function: Callback function to get the cookie and
///Param: IAsyncResult includes the information of
///login request, username, password and callbackUI
///Return:
///</summary>
let internal FetchCookieCallback (ar : IAsyncResult) =
let loginRequest : LoginRequestRecord = downcast ar.AsyncState
try
let response = loginRequest.Request.EndGetResponse(ar)
let request : HttpWebRequest
= createHttpRequest loginRequest.AuthenticationUrl
requestPostSet request "application/x-www-form-urlencoded"
request.CookieContainer <- cookieJar
//if the cookie is got, call the callback function; or else, return to UI
match response.Headers.Count with
| countHeadersAfterGetCookie ->
let authentication = {
AuthenticationRecord.Request = request;
AuthenticationRecord.UserName = loginRequest.UserName;
AuthenticationRecord.Password = loginRequest.Password;
AuthenticationRecord.CallbackUI = loginRequest.CallbackUI
}
request.BeginGetRequestStream(
new AsyncCallback(requestAuthenticationCallback),
authentication)
|> ignore
()
| _ ->
loginRequest.CallbackUI false
()
with
| Ex -> loginRequest.CallbackUI false
module Security =
///<summary>
///Function: Use the async workflow around 2 we calls:
/// 1. get the cookie; 2. log into the website
///Param: UserName and password
///Return: unit
///</summary>
let loginToRpc (userName : string)
(password : string)
(callbackUI : (bool-> unit)) =
let sessionIdUrl = BASE_DNS
let authenticationUrl = BASE_DNS + "..................."
let request : HttpWebRequest = Helper.createHttpRequest sessionIdUrl
Helper.requestGetSet(request)
request.CookieContainer <- Helper.cookieJar
let loginRequest = {
Helper.LoginRequestRecord.Request = request
Helper.LoginRequestRecord.UserName = userName
Helper.LoginRequestRecord.Password = password
Helper.LoginRequestRecord.AuthenticationUrl = authenticationUrl
Helper.LoginRequestRecord.CallbackUI = callbackUI
}
request.BeginGetResponse(new
AsyncCallback(Helper.FetchCookieCallback),
loginRequest)
|> ignore
()
Normally when creating instances of a record, there's no need to fully-qualify each property as you're doing.
let authentication = {
AuthenticationRecord.Request = request;
AuthenticationRecord.UserName = loginRequest.UserName;
AuthenticationRecord.Password = loginRequest.Password;
AuthenticationRecord.CallbackUI = loginRequest.CallbackUI
}
As long as the names and types of the properties you're using only match one record type, F# is generally smart enough to figure out what you meant.
let authentication = {
Request = request;
UserName = loginRequest.UserName;
Password = loginRequest.Password;
CallbackUI = loginRequest.CallbackUI
}
Also, I might be inclined to use sprintf over String.Format here:
String.Format(
"j_username={0}&j_password={1}&login={2}",
authentication.UserName,
authentication.Password,
"Login"))
sprintf "j_username=%s&j_password=%s&login=%s"
authentication.UserName authentication.Password "Login"
But since the resulting string is being passed to a StreamWriter, which inherits from TextWriter another option would be to use fprintf which writes directly to a TextWriter.
fprintf streamWriter "j_username=%s&j_password=%s&login=%s"
authentication.UserName authentication.Password "Login"
I usually keep local state very local, hiding it inside a closure. So, unless I missed a reference to requestId, I would move it inside getId:
let mutable requestId : int = 0
let getId() =
requestId <- requestId + 1
requestId.ToString()
// changes to:
let getId =
let mutable requestId : int = 0
(fun () ->
requestId <- requestId + 1
requestId.ToString())
In the second version, getId is actually the fun at the bottom, after the let mutable... line. The fun captures requestId and then is given the name getId. Since requestId then goes out of scope, nobody else can change or even see it.
I answered to the orginal "Silverlight HttpWebRequest.Create hangs inside async block", check that...
In your case you of course need the authentication, but this request.ContentType <- contentType may cause some problems.

Resources