Workflow.Client 5.2 API dll fails to connect when running in a site with OWIN claims identity

  • 29 October 2019
  • 8 replies
  • 129 views

Badge +2

I am converting an ASP.Net site from Windows Authentication to OWIN / Cookie / Claims based authentication. 

We are using K2 5.2 version of the SourceCode.Workflow.Client (and all the other) dlls to make a connection to K2. 
I am making the connection using a connection string *with a user name / password* - i.e. I am NOT trying to do a kerberos style connection to K2.
I have my app all set with all of the OWIN dlls, and a "startup" function, and fully anonymous authentication and my call to: 
var connection = new Connection();
connection.Open(Config.K2.ServerName, Config.K2.K2AdminConnectionString);
...works fine.

As soon as I add the code to actually authenticate (with a 3rd party provider), my HttpContext.Current.User changes from a WindowsPrincipal to a ClaimsPrincipal, and at this point, the connection to K2 fails with the error:

Value cannot be null. Parameter name: BootstrapContext

Stack Trace:
at SourceCode.Workflow.Client.InternalConnection.Call(ArchiveX ar, MessageType msgtype)
at SourceCode.Workflow.Client.InternalConnection.AuthenticateIIdentity()
at SourceCode.Workflow.Client.Connection.Open(String Server)

Doing a bit of detective work with .Net Reflector, I see that it's putting Thread.CurrentPrincipal.Identity into an "ArchiveX" object, and then doing something with it, and it seems like this is where it's breaking down. 

This all seems odd to me, since I'm sending all of the credentials for the connection in with the connection string, so the thread user shouldn't matter. 
Will I need to wrap all of my calls to the API in a windows principal (with the old kernal32.dll impersonation context trick) to get my API calls to work?
 


8 replies

Badge +2

...quick follow up...

 

I was able to make my calls to the API using:

var connection = new Connection();
var savePrincipal = System.Threading.Thread.CurrentPrincipal;
System.Threading.Thread.CurrentPrincipal = new WindowsPrincipal(WindowsIdentity.GetAnonymous());

connection.Open(Config.K2.ServerName, Config.K2.K2AdminConnectionString);
System.Threading.Thread.CurrentPrincipal = savePrincipal;

(note that after this I set the connection to use impersonation - something that was already being done in the Windows Authentication world).

While this works, it seems like a VERY hack-y solution, so I'm wondering if there's a better way!

Good day Chris-L;


 


Perhaps maybe if you could double-check your connection string, sometimes the issue is likely possible to be caused by "Integrated" set to "True" if you can check whether it's and set it to "False" could help solve the problem.


 


Kind regards,


Dumisani

Badge +7

Using the following connection string, the customer was able to connect to K2 successfully:

public bool StartNewProcess(string processName, string userName, IEnumerable dataFields)
{
ProcessInstance instance
using (_impersonationContextFactory.CreateImpersonationContextForK2())
using (var k2Conn = new Connection())
{
SourceCode.Hosting.Client.BaseAPI.SCConnectionStringBuilder builder = new SourceCode.Hosting.Client.BaseAPI.SCConnectionStringBuilder()
builder.Authenticate = true
builder.Host = _configurationProvider.K2Server
builder.Port = 5252 //use port 5252 for SourceCode.Workflow.Client connections
builder.Integrated = false
builder.IsPrimaryLogin = true
builder.SecurityLabelName = "K2"
builder.UserID = "DOMAINUSER"
builder.Password = "xxxxxx"
builder.WindowsDomain = "DOMAIN"
k2Conn.Open(_configurationProvider.K2Server, builder.ConnectionString)
k2Conn.ImpersonateUser(userName)
instance = k2Conn.CreateProcessInstance(processName)
if (dataFields != null andand dataFields.Any())
{
dataFields.ToList().ForEach(field => ApplyDataField(field, instance))
}
k2Conn.StartProcessInstance(instance)
}
return true
}

Have solved this a little different (you might still call it hack-y)

 

WindowsIdentity wi = Thread.CurrentPrincipal.Identity as WindowsIdentity;
(For you : ClaimsIdentity ci = Thread.CurrentPrincipal.Identity as ClaimsIdentity;)

 

WindowsIdentity.Impersonate(ab.Token);

(For you : WindowsIdentity.Impersonate(ci.BootstrapContext);)

connection.CreateConnection();

connection.Connection.Open(getConnectionString);

connection.Connection.Authenticate(getConnectionString);

 

public string getConnectionString(int intPort)

{

SCConnectionStringBuilder k2Connection = new SCConnectionStringBuilder();
k2Connection.Authenticate = true;
k2Connection.Host = myHost;
k2Connection.Integrated = true;
k2Connection.IsPrimaryLogin = true;
k2Connection.Port = intPort; //(5555/5252)
return k2Connection.ConnectionString;

}

 

This solution was used when the smartobject was connected to an sql-server and we needed correct users to be audited when connection to that server was made.

Badge +2

This is the closest to the correct answer, and set me on a path to find out more, and I think I may have solved it...

I think there's may be some confusion on what we're trying to do:
1. We do NOT need to establish a connection to K2 using the current user's credentials - we are OK with connecting with the admin credentials in the connection string, and AFTERWARDS setting the connection to impersonate the current user. 


2. This has been working fine for several years in a live environment - we are able to connect using a connection string with an embedded username/passowrd, and then impersonate with just a domain name with no problems.

3. When we changed the application from Windows Authentication to claims-based authentication, the CurrentThread.Identity changes from a WindowsIdentity to a ClaimsIdentity. At this point, Connection.Open - using the same connectyion string we've always used - throws the error mentioned above. 

I'm just surprised others haven't run into the same issue - since it seems like Connection.Open always fails when the CurrentThread.Identity is not a WindowsIdentity.

...but it turns out the real problem is that BootstrapContext is null, and I thought this was a K2 class, when it's actually an Identity thing. 

Apparently, Microsoft.Identity will set the bootstrap context for you automatically if you add:

<system.identityModel>
<identityConfiguration saveBootstrapContext="true" />
</system.identityModel>

to the web.config.
I've tried that, however, and it didn't work for me - the BootstrapContext was still null. 
It's odd though because BootStrap context is null on my anonymous windows context that I'm using for my work-around, and the Connection.Open doesn't crash.

From your first post I gather that you use Owin Security?

 

If that is the case then you maintain the BootstrapContext by telling the Owin setup that you want it maintained.

public class Startup
{

...

...

public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{

.....

.....

TokenValidationParameters = new TokenValidationParameters()
{
SaveSigninToken = true   /* Saves BootstrapsContext in the ClaimsIdentity*/
},

......

......

}});}

 

This saves your token but I am not sure this will help you since from a quick test it does not seem to be the same type of token used in the Client API. (Example is from a fairly old project so might be a version issue.)

Badge +2

I was optimistioc about this method of getting the claims token stored in the identity. So much so that I forked the Okta API dll we're using for our authentication so that I could set this option on the TokenValidationParameters.

This DOES put the token into the BootstrapContext!

The problem is that we are now getting a new (even more cryptic) exception in the Connection.Open:
Object reference not set to an instance of an object
at SourceCode.Workflow.Client.InternalConnection.Call(ArchiveX ar, MessageType msgtype)
at SourceCode.Workflow.Client.InternalConnection.AuthenticateIIdentity()
at SourceCode.Workflow.Client.Connection.Open(String Server)

Even looking at my decompiled code, I'm having a tough time unraveling where the cause could be and what object in that method is not getting set.

I will just keep using my anonymous identity workaround for the forseeable future...

IIdentity identity = Thread.CurrentPrincipal.Identity;

..

..

ar.AddObject((object) identity);

return this.Call(ar, MessageType.FunctionServer);

 

I am guessing "Thread.CurrentPrincipal.Identity" can't be converted to an "IIdentity" 

....

You will prob be better of using one of the "Impersonation"-solutions people have written earlier

 

Recap:

var userClaims = context.User.Identity as System.Security.Claims.ClaimsIdentity;

string LoginName = userClaims?.FindFirst(System.IdentityModel.Claims.ClaimTypes.Upn)?.Value

 

SourceCode.Hosting.Client.BaseAPI.SCConnectionStringBuilder builder = new SourceCode.Hosting.Client.BaseAPI.SCConnectionStringBuilder();
builder.Authenticate = true;
builder.Host = yourK2Server;
builder.Port = port;
builder.Integrated = false;
builder.IsPrimaryLogin = true;
builder.UserID = K2ServiceAccount;
builder.Password = K2ServiceAccountPassword;

ConnectionSetup connectionSetup = new ConnectionSetup();
connectionSetup.ConnectionString = builder.ConnectionString;

 

_wfConnection = new Connection();
_wfConnection.Open(connectionSetup);
_wfConnection.ImpersonateUser(LabelName + userEmail); //LabelName something you named it in K2 example "K2:", "AAD:"

 

I personaly disslike having my serviceaccount in an application but at least I can keep track of users in and I can use different login methods.

 

/Björn

 

 

Reply