How to Create Asynchronous HttpWebRequest in Silverlight( F#) - silverlight

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.

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.

Session Id (sid) is not assigned during automatic login via IdentityServer4, what gives?

Questions
First question, what determines if an sid claim is emitted from identityserver?
Second question, do I even need an sid? I currently have it included because it was in the sample..
Backstory
I have one website that uses IdentityServer4 for authentication and one website that doesn't. I've cobbled together a solution that allows a user to log into the non-identityserver4 site and click a link that uses one-time-access codes to automatically log into the identityserver4 site. Everything appears to work except the sid claim isn't passed along from identityserver to the site secured by identityserver when transiting from the non-identityserver site. If I log directly into the identityserver4 secured site the sid is included in the claims. Code is adapted from examples of automatically logging in after registration and/or impersonation work flows.
Here is the code:
One time code login process in identityserver4
public class CustomAuthorizeInteractionResponseGenerator : AuthorizeInteractionResponseGenerator
{
...
//https://stackoverflow.com/a/51466043/391994
public override async Task<InteractionResponse> ProcessInteractionAsync(ValidatedAuthorizeRequest request,
ConsentResponse consent = null)
{
string oneTimeAccessToken = request.GetAcrValues().FirstOrDefault(x => x.Split(':')[0] == "otac");
string clientId = request.ClientId;
//handle auto login handoff
if (!string.IsNullOrWhiteSpace(oneTimeAccessToken))
{
//https://benfoster.io/blog/identity-server-post-registration-sign-in/
oneTimeAccessToken = oneTimeAccessToken.Split(':')[1];
OneTimeCodeContract details = await GetOTACFromDatabase(oneTimeAccessToken);
if (details.IsValid)
{
UserFormContract user = await GetPersonUserFromDatabase(details.PersonId);
if (user != null)
{
string subjectId = await GetClientSubjectIdAsync(clientId, user.AdUsername);
var iduser = new IdentityServerUser(subjectId)
{
DisplayName = user.AdUsername,
AuthenticationTime = DateTime.Now,
IdentityProvider = "local",
};
request.Subject = iduser.CreatePrincipal();
//revoke token
bool? success = await InvalidateTokenInDatabase(oneTimeAccessToken);
if (success.HasValue && !success.Value)
{
Log.Debug($"Revoke failed for {oneTimeAccessToken} it should expire at {details.ExpirationDate}");
}
//https://stackoverflow.com/a/56237859/391994
//sign them in
await _httpContextAccessor.HttpContext.SignInAsync(IdentityServerConstants.DefaultCookieAuthenticationScheme, request.Subject, null);
return new InteractionResponse
{
IsLogin = false,
IsConsent = false,
};
}
}
}
return await base.ProcessInteractionAsync(request, consent);
}
}
Normal Login flow when logging directly into identityserver4 secured site (from sample)
public class AccountController : Controller
{
/// <summary>
/// Handle postback from username/password login
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model)
{
Log.Information($"login request from: {Request.HttpContext.Connection.RemoteIpAddress.ToString()}");
if (ModelState.IsValid)
{
// validate username/password against in-memory store
if (await _userRepository.ValidateCredentialsAsync(model.Username, model.Password))
{
AuthenticationProperties props = null;
// only set explicit expiration here if persistent.
// otherwise we reply upon expiration configured in cookie middleware.
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
};
var clientId = await _account.GetClientIdAsync(model.ReturnUrl);
// issue authentication cookie with subject ID and username
var user = await _userRepository.FindByUsernameAsync(model.Username, clientId);
var iduser = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.UserName
};
await HttpContext.SignInAsync(iduser, props);
// make sure the returnUrl is still valid, and if yes - redirect back to authorize endpoint
if (_interaction.IsValidReturnUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
return Redirect("~/");
}
ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage);
}
// something went wrong, show form with error
var vm = await _account.BuildLoginViewModelAsync(model);
return View(vm);
}
}
AuthorizationCodeReceived in identityserver4 secured site
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// use the code to get the access and refresh token
var tokenClient = new TokenClient(
tokenEndpoint,
electionClientId,
electionClientSecret);
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(
n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(
new Uri(userInfoEndpoint).ToString());
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
Claim subject = userInfoResponse.Claims.Where(x => x.Type == "sub").FirstOrDefault();
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaims(GetRoles(subject.Value, tokenClient, apiResourceScope, apiBasePath));
var transformedClaims = StartupHelper.TransformClaims(userInfoResponse.Claims);
id.AddClaims(transformedClaims);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
THIS FAILS -> id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
n.AuthenticationTicket.Properties);
},
}
});
}
}
Questions again if you don't want to scroll back up
First question, what determines if an sid claim is emitted from identityserver?
Second question, do I even need an sid? I currently have it included because it was in the sample..

Manually create and validate a JWT token

I'm using IdentityServer4 Tools to manually create a token:
var token = await _tools.IssueClientJwtAsync(
clientId: "client_id",
lifetime: lifetimeInSeconds,
audiences: new[] { TokenHelper.Audience },
additionalClaims:new [] { new Claim("some_id", "1234") }
);
I wonder if there is a way (using what IdentityServer4 already have) to manually decode and validate the token.
To decode the token right now I'm using JwtSecurityTokenHandler (System.IdentityModel.Tokens.Jwt):
var handler = new JwtSecurityTokenHandler();
var tokenDecoded = handler.ReadJwtToken(token);
It is quite simple so I'm happy to keep this if IdentityServer4 doesn't have an equivalent.
What is more important is the validation of the token. I found and adapt this example that does the job. Here the code from Github:
const string auth0Domain = "https://jerrie.auth0.com/"; // Your Auth0 domain
const string auth0Audience = "https://rs256.test.api"; // Your API Identifier
const string testToken = ""; // Obtain a JWT to validate and put it in here
// Download the OIDC configuration which contains the JWKS
// NB!!: Downloading this takes time, so do not do it very time you need to validate a token, Try and do it only once in the lifetime
// of your application!!
IConfigurationManager<OpenIdConnectConfiguration> configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{auth0Domain}.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration openIdConfig = AsyncHelper.RunSync(async () => await configurationManager.GetConfigurationAsync(CancellationToken.None));
// Configure the TokenValidationParameters. Assign the SigningKeys which were downloaded from Auth0.
// Also set the Issuer and Audience(s) to validate
TokenValidationParameters validationParameters =
new TokenValidationParameters
{
ValidIssuer = auth0Domain,
ValidAudiences = new[] { auth0Audience },
IssuerSigningKeys = openIdConfig.SigningKeys
};
// Now validate the token. If the token is not valid for any reason, an exception will be thrown by the method
SecurityToken validatedToken;
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
var user = handler.ValidateToken(testToken, validationParameters, out validatedToken);
// The ValidateToken method above will return a ClaimsPrincipal. Get the user ID from the NameIdentifier claim
// (The sub claim from the JWT will be translated to the NameIdentifier claim)
Console.WriteLine($"Token is validated. User Id {user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value}");
The code above is doing the job. I just wonder if IdentityServer4 has already something "simpler" that just does the token validation as the code above does.
What you are trying to do is called token delegation,
you can implement it using Extension Grants on IDS. Here is sample code from docs
public class DelegationGrantValidator : IExtensionGrantValidator
{
private readonly ITokenValidator _validator;
public DelegationGrantValidator(ITokenValidator validator)
{
_validator = validator;
}
public string GrantType => "delegation";
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var userToken = context.Request.Raw.Get("token");
if (string.IsNullOrEmpty(userToken))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
var result = await _validator.ValidateAccessTokenAsync(userToken);
if (result.IsError)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
// get user's identity
var sub = result.Claims.FirstOrDefault(c => c.Type == "sub").Value;
//Generate a new token manually if needed
//Call another API is needed
context.Result = new GrantValidationResult(sub, GrantType);
return;
}
}
Token validation is done using ITokenValidator in above code, you can use this validator in manual validation as well.
Here is another example.

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);
}
}
}

Swagger documentation for BeanParam

I'm trying to document one of my java APIs (implemented in Apache CXF) using Swagger, that receives it's parameters using a Bean Param. Something like:
#GET
#Produces({SemanticMediaType.JSON_LD, MediaType.APPLICATION_JSON_VALUE})
#ApiOperation(value = "Retrieves Themes", position = 0)
#ApiResponses(value = {#ApiResponse(code = 200,
message = "Retrieval was successful"), #ApiResponse(code = 403,
message = "Missing or invalid x-business-group-id header"), #ApiResponse(code = 500,
message = "Internal server error")})
public Response get(#QueryParam(URI_PARAM_NAME) String uri,
final #ApiParam #Valid #BeanParam ThemeParams themeParams) { ... }
I read that Swagger already implements support for BeanParams, but when I try to run it, in swagger-ui, I only see one parameter called "body" and a text field, nothing related to the contents of my BeanParam.
Can somebody provide some assistance with this?
This is a bit old, but for those who are having the same issues, here is what I found helped.
If you are using the DefaultJaxrsConfig, change it to JerseyJaxrsConfig.
If you are linking to swagger-jersey-jaxrs_..., change it to swagger-jersey2-jxsrs_...
You can refer to.
#POST
#Path("/users")
#ApiOperation(value = "vdc", position = 1, notes = "vdc")
#ApiResponses(value = {
#ApiResponse(code = 200, message = "OK",response=UserCreateResponse.class),
#ApiResponse(code = 30601, message = "'httpcode': 400 'Errormsg': Request Params Not Valid"),
#ApiResponse(code = 30602, message = "'httpcode':404 'Errormsg': Data Required Not Found"),
#ApiResponse(code = 30603, message = "'httpcode':405 'Errormsg': Method Not Allowed"),
#ApiResponse(code = 30604, message = "'httpcode':408 'Errormsg': Request Time Expires Timeout"),
#ApiResponse(code = 30605, message = "'httpcode':500 'Errormsg': Internal Server Error") })
public Response createUsersWithArrayInput(
#ApiParam(value = "ID", name = "platform_id", required = true) #QueryParam(value = "platform_id") String platformId,
#ApiParam(value="body",name="user",required=true)UserCreate userCreate) {}
UserCreate.java
#ApiModel("UserCreate")
public class UserCreate {
#ApiModelProperty(value="VDC Id",required=false)
#JsonStringSchema(optional=true,description="VDC Id")
private String vdcId;
#ApiModelProperty(value="description",required=true)
private String name;
#ApiModelProperty(value="description",required=false)
private String password;
}

Resources