I've a WCF service that queues and executes commands on behalf of the user at a later, indeterminate time. I was hoping to store the WindowsIdentity as a byte array and stuff it into a database, and then later deserialize and use that object.
Some of the time, the service performs as expected: it correctly serializes, stores, deserializes, and executes the command as the user. Other times, I get an error "Exception has been thrown by the target of an invocation" when deserializing the WindowsIdentity, and still other times the deserialization works but part way through the command execution, the identity is no longer valid.
My question is this: is it possible in the .NET 4.0 framework using WCF to execute a command on behalf of a user at a later, indeterminate time with no explicit username and password?
The code that I'm using is below:
Serialization:
''' <summary>
''' Serializes a WindowsIdentity as a binary encoded byte array.
''' </summary>
''' <param name="identity">The identity to serialize.</param>
''' <returns>A byte array containing a binary representation of the identity.</returns>
Private Function SerializeWindowsIdentity(ByVal identity As WindowsIdentity) As Byte()
If IsNothing(identity) Then Return Nothing
Try
Dim bf As New BinaryFormatter
Using ms As New MemoryStream()
bf.Serialize(ms, identity)
Return ms.ToArray()
End Using
Catch ex As Exception
Return Nothing
End Try
End Function ' SerializeWindowsIdentity
Deserialization:
''' <summary>
''' Deserializes a WindowsIdentity from a binary encoded byte array.
''' </summary>
''' <param name="identity">A byte array containing a binary representation of a WindowsIdentity</param>
''' <returns>The deserialized WindowsIdentity from the byte array.</returns>
Private Function DeserializeWindowsIdentity(ByVal identity As Byte()) As WindowsIdentity
If IsNothing(identity) OrElse identity.Count = 0 Then Return Nothing
Try
Dim bf As New BinaryFormatter()
Using ms As New MemoryStream(identity)
Dim obj As Object = bf.Deserialize(ms)
Return CType(obj, WindowsIdentity)
End Using
Catch ex As Exception
Return Nothing
End Try
End Function ' DeserializeWindowsIdentity
WindowsIdentity capture:
identity = SerializeWindowsIdentity(ServiceSecurityContext.Current.WindowsIdentity)
Usage:
Dim cxt As WindowsImpersonationContext
Try
Dim wi As WindowsIdentity = DeserializeWindowsIdentity(identity)
If Not IsNothing(wi) Then cxt = wi.Impersonate()
' Do Stuff
Finally
If Not IsNothing(cxt) Then cxt.Dispose()
End If
Here is some pseudo-code:
//When command comes in from the user, queue the work
private void QueueWork()
{
//Authorization Check
if (IsAuthorized(DoWork, ServiceSecurityContext.Current.WindowsIdentity))
throw new SecurityAccessDeniedException("Unauthorized");
//Queue the work for later
Queue.Enqueue(ServiceSecurityContext.Current.WindowsIdentity.Name);
}
//check if
private bool IsAuthorized(Action doWork, WindowsIdentity windowsIdentity)
{
//custom logic
}
//Command executed at a later point in time
private void DoWork()
{
var user = Queue.Dequeue() as string;
Log(user + " is invoking DoWork");
//Perform command on behalf of user
//...
}
It assumes that you have windows authentication enabled to capture the WindowsIdentity.
Also I am not sure how you are queueing the commands, and how you are executing them later.
You can replace the IsAuthorized method with some other way of authorization if you wish.
The service has permissions to perform the operation, and simply logs the user and command being executed.
If this doesn't quite fit your scenario, let me know and provide more information and I can modify the answer. But hope this leads you into the right direction
So, the best solution we have so far is to use the token from the ServiceSecurityContext.Current.WindowsIdentity
and create a new primary token that we serialize and store for later. The downside is that the token is invalid once the service is restarted, but it's a fine temporary work-around until we can revise our service so that we don't require a persistent authorization from the user. We looked at using S4U2Proxy which would do what we want, but the requirement for configuring the domain account the service runs under is a bit onerous for our users. The working code snippet is below (note: we probably don't need to serialize any more, but it was easier to keep in place since we don't have to update our database schema. Also, we could break out the serialize routine from the token duplication to make the code more manageable):
Deserialize code:
''' <summary>
''' Deserializes a user token from a binary encoded byte array.
''' </summary>
''' <param name="identity">A byte array containing an binary representation of a user token.</param>
''' <returns>The deserialized user token from the byte array.</returns>
Private Function DeserializeWindowsIdentityToken(ByVal identity As Byte()) As IntPtr
If IsNothing(identity) Then Return Nothing
Dim stream As New MemoryStream(identity)
Dim serializer As New BinaryFormatter()
Try
Dim obj As Object = serializer.Deserialize(stream)
Return CType(obj, IntPtr)
Catch ex As Exception
Return IntPtr.Zero
End Try
End Function ' DeserializeWindowsIdentityToken
Serialize code:
''' <summary>
''' Serializes a user token as a binary encoded byte array.
''' </summary>
''' <param name="identity">The token to serialize.</param>
''' <returns>A byte array containing a binary representation of the token.</returns>
Private Function SerializeWindowsIdentityToken(ByVal identity As IntPtr) As Byte()
Try
Dim newToken As IntPtr = IntPtr.Zero
Const securityDelegation As Int16 = 3
Const tokenPrimary As Integer = 1
Const maximumAllowed As Integer = &H2000000
Dim sa As New SecurityAttributes()
sa.bInheritHandle = True
sa.Length = Marshal.SizeOf(sa)
sa.lpSecurityDescriptor = IntPtr.Zero
If DuplicateTokenEx(identity, maximumAllowed, sa, securityDelegation, tokenPrimary, newToken) = 0 Then Return Nothing
Dim streamWriter As New MemoryStream()
Dim serializer As New BinaryFormatter
serializer.Serialize(streamWriter, newToken)
Return streamWriter.ToArray()
Catch ex As Exception
Return Nothing
End Try
End Function ' SerializeWindowsIdentityToken
<StructLayout(LayoutKind.Sequential)>
Private Structure SecurityAttributes
Public Length As Integer
Public lpSecurityDescriptor As IntPtr
Public bInheritHandle As Boolean
End Structure ' SecurityAttributes
<DllImport("advapi32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
Private Shared Function DuplicateTokenEx(ByVal existingTokenHandle As IntPtr,
ByVal desiredAccess As UInteger,
ByRef threadAttributes As SecurityAttributes,
ByVal impersonationLevel As Integer,
ByVal tokenType As Integer,
ByRef duplicateTokenHandle As IntPtr) As Integer
End Function ' DuplicateTokenEx
Token capture:
Dim storedToken As Byte() = SerializeWindowsIdentityToken(ServiceSecurityContext.Current.WindowsIdentity.Token)
Usage:
Dim identity As IntPtr = DeserializeWindowsIdentityToken(storedToken)
Dim cxt As WindowsImpersonationContext = Nothing
If Not IsNothing(identity) AndAlso identity <> IntPtr.Zero Then
Try
Dim identity As New WindowsIdentity(identity)
cxt = identity.Impersonate()
Catch ex As Exception
' Perform error handling
End Try
End If
' Perform operations
If Not IsNothing(cxt) Then cxt.Dispose()