tirsdag den 6. september 2011

Claimsbased authentication and WCF Services

Update 22-09-2011: Code samples posted here
In the last few months I’ve spend a lot of time messing with claimsbased authentication. a lot of it have been toward implementing single sign on though various social providers. But some of it have also been involving working with SharePoint 2010 and CRM 2011 and developing and calling my own WCF services from both applications and websites.

I wanted to document what I learn, as I learned it, on this blog but the time just hasn’t been on my side, but I do how ever spend a little time on a piece of code I wrote that I think a lot people will find useful and can copy’n’paste a bit from.

Most of the actions and information I need in different places are all wrapped up in 1 WCF service, I call this WCF service from both other WCF services and websites and from various applications. I found it was easier for me to just create an Class Library that could talk with this WCF service and then reference that from all my projects.

image

Most of the code is about handling active federation against any kind of claimsbased identity provider, but I also put in a bit of code to handle passive federation that is needed when working with SharePoint 2010 though code.

I have client class that in a simple way make authentication against an identity provider and receiving a claims token back. I want to go though how to use this client to get a token and what can happen, since I have seen A LOT of forum post from people getting these errors and not getting an answer that fits.

We start by creating an instance of the client and choosing what kind of encryption we want to use.

Code Snippet
  1. Dim ClaimsClient As New ClaimsAuth.Client
  2. ClaimsClient.TokenEncryption = Microsoft.IdentityModel.SecurityTokenService.KeyTypes.Symmetric

if we look in Microsoft.IdentityModel.SecurityTokenService.KeyTypes we see we can use Asymmetric, Symmetric or Bearer. Tons of post out there about this.

If you use Asymmetric you as requestor need to supply a key to encrypt the claims with. ( set "UseKey” )

If you use Symmetric the identity provider have all ready been told what certificate to use, to encrypt the claims with.

If you choose Bearer. The token get signed, but claims will not be encrypted. If a token signing certificate have been assigned on the Relying Party, claims will simply not be included at all.

When you request a token, the token gets signed (not encrypted) with a certificate installed on the Identity Provider ( ADFS ). If you add a certificate on a Relying Party Trust (RP) on the ADFS server, the claims inside the token gets encrypted with with that certificate. Only host/applications that have access to the private key of that certificate can now decrypt the token and read the claims. You don’t need to read the claims in order to authenticate your self. For instance if you have a WCF Service you want to call from within an application. You can from within that application still request a token from the ADFS server and then access the WCF service with that Token. As long as the WCF service have access to the private key and can read the claims, your application don’t need it.

If you choose Symmetric but the Relying Party on the ADFS have not been assigned a certificate to encrypt the claims with you will get

ID3037: The specified request failed.

and in the event log on the ADFS server they would also see

ID4007: The symmetric key inside the requested security token must be encrypted. To fix this, either override the SecurityTokenService.GetScope() method to assign appropriate value to Scope.EncryptingCredentials or set Scope.SymmetricKeyEncryptionRequired to false.

Either request a Bearer token or Asymmetric (not sure if you can this?) token, or add a certificate on the RP

imageimage

To make it simple. If you want to make absolutely sure your ADFS server only issues tokens to the hosts you have given the certificate with private key too, sign the tokens with this certificate by taking the public part of the certificate and save to a file and then assign it on this tab.
If you need to authenticate from many places and don’t want to struggle with distributing a certificate including its private key around. or if you don’t care others can read the claim ( you need to successfully authenticate in order to get the claims in the first place so in theory they should have access to it anyway ) leave this field blank.

To add the certificate to the ADFS server, on the computer you have the certificate you want to use for signing claims with, open certificates and add Local computer or user, depending on where you have the certificate installed. Right click it and choose open

image
Go to Details and click Copy to file

image

Accept the defaults and save the file. Then use this file when adding a certificate on the RP on the ADFS server.

If you choose to get an Bearer token you cannot reuse this key to authenticate to other RP’s by authentication with the issued token, That will fail with

The signing token XXXX has no keys. The security token is used in a context that requires it to perform cryptographic operations, but the token contains no cryptographic keys. Either the token type does not support cryptographic operations, or the particular token instance does not contain cryptographic keys. Check your configuration to ensure that cryptographically disabled token types (for example, UserNameSecurityToken) are not specified in a context that requires cryptographic operations (for example, an endorsing supporting token).

When you have a token and you want read the claims inside, you will often see errors like

ID4022: The key needed to decrypt the encrypted security token could not be resolved. Ensure that the SecurityTokenResolver is populated with the required key.

Or from a asp.net website

ID4036: The key needed to decrypt the encrypted security token could not be resolved from the following security key identifier 'XXXXX'. Ensure that the SecurityTokenResolver is populated with the required key.

I think its pretty self explaining but there's a ton of forum post’s out there where people ask for help. Again, you have a token, its valid, you can authenticate your self with it, but when you try to read the token you get the above error. You get it be course a certificate has been added on the RP on the ADFS and you haven't given WIF the certificate including private key, needed to decrypt it. If using my code, just load it and add it on the TokenSigningCertificate Property. If you see this error on a websites you are probably missing the serviceCertificate  in web.config

 

Code Snippet
  1. <microsoft.identityModel>
  2.   <service saveBootstrapTokens="true">
  3.     <certificateValidation certificateValidationMode="None" />
  4.     <serviceCertificate>
  5.       <certificateReference x509FindType="FindByThumbprint" findValue="7A41CF269D6BCDED80DDD9B6FD517E37891453B5" storeLocation="LocalMachine" storeName="My" />
  6.     </serviceCertificate>
  7.   </service>
  8. </microsoft.identityModel>

And while at those errors. if you get something down the line of

ID4175: The issuer of the security token was not recognized by the IssuerNameRegistry. To accept security tokens from this issuer, configure the IssuerNameRegistry to return a valid name for this issuer.

you are missing the certificate from the Identity Provider ( ADFS )

Code Snippet
  1. <microsoft.identityModel>
  2.   <service saveBootstrapTokens="true">
  3.     <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
  4.       <trustedIssuers>
  5.         <add thumbprint="86AC2E62900DF9451B0596562D52F7212AC31065" name="http://adfs.wingu.dk/adfs/services/trust" />
  6.       </trustedIssuers>
  7.     </issuerNameRegistry>
  8.   </service>
  9. </microsoft.identityModel>

Back to the code, Next I choose what I want to access (realm) and who to authenticate me (identity provider / ADFS) and how I want to authenticate

Code Snippet
  1. Dim ClaimsClient As New ClaimsAuth.Client
  2. ClaimsClient.TokenEncryption = Microsoft.IdentityModel.SecurityTokenService.KeyTypes.Symmetric
  3. ClaimsClient.IdentityProvider = "https://adfs.wingu.dk/"
  4. ClaimsClient.Realm = "https://admin.wingu.dk/ssi2/"
  5. ClaimsClient.authenticateBy = MessageCredentialType.Windows
  6. 'ClaimsClient.username = txtUsername.Text
  7. 'ClaimsClient.Password = txtPassword.Text
  8. 'ClaimsClient.ClientCertificate = MyPersonalCertificate
  9. 'ClaimsClient.IssuedToken = OtherIssuedToken
  10. 'Dim ClaimsID As Microsoft.IdentityModel.Claims.ClaimsIdentity = _
  11. 'DirectCast(HttpContext.Current.User.Identity, Microsoft.IdentityModel.Claims.ClaimsIdentity)
  12. 'Dim BootstrapToken As System.IdentityModel.Tokens.SecurityToken = ClaimsID.BootstrapToken
  13. 'ClaimsClient.ActAsToken = BootstrapToken
  14. ClaimsClient.authenticate()

AuthenticateBy can be either Certificate, IssuedToken, UserName or Windows. Just for fun I showed other ways to authenticate in the remarks. On that is particular interesting is the ActAs . This is what you would normally do from within an asp.net application that needs to call an WFC service on behalf of the user. Either Authenticate by Windows or username/password form within the asp.net application and then attach the user’s bootstrap token. you need permission to do this of course. That is what the Delegation Authorization Rules are for on the RP Claims rule dialog

image

Back to the code, so inside the client class I have my authenticate function.

Code Snippet
  1. Function authenticate() As System.IdentityModel.Tokens.SecurityToken
  2.     Select Case _authenticateBy
  3.         Case MessageCredentialType.UserName : _IssuedToken = GetADFSTokenUsernamemixed()
  4.         Case MessageCredentialType.Windows : _IssuedToken = GetADFSTokenKerberos()
  5.         Case MessageCredentialType.IssuedToken
  6.             If _IssuedToken Is Nothing Then Throw New Exception("No token found to issue new token with")
  7.             _IssuedToken = GetADFSTokenIssuedToken()
  8.         Case Else : Throw New Exception("unknown authentication schema")
  9.     End Select
  10.     Return _IssuedToken
  11. End Function

most of this code can be reused against any kind of Claimsbased authentication identity provider but for now I have only done the logic for ADFS and SharePoint.
GetADFSTokenUsernamemixed and GetADFSTokenKerberos is almost the same, GetADFSTokenIssuedToken is a bit more tricky

So here they are

Code Snippet
  1. Private Function GetADFSTokenUsernamemixed() As System.IdentityModel.Tokens.SecurityToken
  2.     Dim Token As System.IdentityModel.Tokens.SecurityToken
  3.     Dim UserNameMixed As String = _IdentityProvider & "adfs/services/trust/13/usernamemixed"
  4.     Dim STSbinding = New Microsoft.IdentityModel.Protocols.WSTrust.Bindings.UserNameWSTrustBinding
  5.     STSbinding.SecurityMode = SecurityMode.TransportWithMessageCredential
  6.     Dim trustChannelFactory As New WSTrustChannelFactory(STSbinding, New EndpointAddress(UserNameMixed))
  7.     trustChannelFactory.TrustVersion = System.ServiceModel.Security.TrustVersion.WSTrust13
  8.  
  9.     trustChannelFactory.Credentials.SupportInteractive = False
  10.     trustChannelFactory.Credentials.UserName.UserName = _Username
  11.     trustChannelFactory.Credentials.UserName.Password = _Password
  12.  
  13.     Try
  14.         Dim rst As New RequestSecurityToken()
  15.         rst.RequestType = WSTrust13Constants.RequestTypes.Issue
  16.         rst.AppliesTo = New EndpointAddress(_Realm)
  17.         rst.KeyType = _TokenEncryption
  18.         rst.TokenType = _TokenType
  19.  
  20.         If _ClientCertificate IsNot Nothing Then
  21.             Dim clause As System.IdentityModel.Tokens.SecurityKeyIdentifierClause = _
  22.                 New System.IdentityModel.Tokens.X509RawDataKeyIdentifierClause(_ClientCertificate)
  23.  
  24.             rst.UseKey = New UseKey(New System.IdentityModel.Tokens.SecurityKeyIdentifier(clause), _
  25.                                      New System.IdentityModel.Tokens.X509SecurityToken(_ClientCertificate))
  26.         End If
  27.  
  28.         'This part will give you identity of logged in user
  29.         If _ActAs IsNot Nothing Then rst.ActAs = _ActAs
  30.  
  31.         If _requestClaims.Count > 0 Then
  32.             For Each claim In _requestClaims
  33.                 rst.Claims.Add(claim)
  34.             Next
  35.         End If
  36.         Dim channel = trustChannelFactory.CreateChannel()
  37.         Dim rstr As RequestSecurityTokenResponse = Nothing
  38.         Token = channel.Issue(rst, rstr)
  39.     Catch ex As Exception
  40.         Throw New Exception(ex.Message, ex)
  41.     Finally
  42.         Try
  43.             If trustChannelFactory.State = CommunicationState.Faulted Then
  44.                 trustChannelFactory.Abort()
  45.             Else
  46.                 trustChannelFactory.Close()
  47.             End If
  48.         Catch generatedExceptionName As Exception
  49.         End Try
  50.     End Try
  51.     Return Token
  52. End Function
  53.  
  54. Private Function GetADFSTokenKerberos() As System.IdentityModel.Tokens.SecurityToken
  55.     Dim Token As System.IdentityModel.Tokens.SecurityToken
  56.     Dim KerberosMixed As String = _IdentityProvider & "adfs/services/trust/13/kerberosmixed"
  57.     Dim STSbinding = New Microsoft.IdentityModel.Protocols.WSTrust.Bindings.KerberosWSTrustBinding
  58.     STSbinding.SecurityMode = SecurityMode.TransportWithMessageCredential
  59.  
  60.     Dim trustChannelFactory As New WSTrustChannelFactory(STSbinding, New EndpointAddress(KerberosMixed))
  61.  
  62.     trustChannelFactory.TrustVersion = System.ServiceModel.Security.TrustVersion.WSTrust13
  63.     trustChannelFactory.Credentials.SupportInteractive = False
  64.     trustChannelFactory.Credentials.Windows.AllowedImpersonationLevel = TokenImpersonationLevel.Impersonation
  65.     trustChannelFactory.Credentials.Windows.ClientCredential = System.Net.CredentialCache.DefaultNetworkCredentials
  66.  
  67.     Try
  68.         Dim rst As New RequestSecurityToken()
  69.         rst.RequestType = WSTrust13Constants.RequestTypes.Issue
  70.         rst.AppliesTo = New EndpointAddress(_Realm)
  71.         rst.KeyType = _TokenEncryption
  72.         rst.TokenType = _TokenType
  73.         If _ActAs IsNot Nothing Then rst.ActAs = _ActAs
  74.  
  75.         If _requestClaims.Count > 0 Then
  76.             For Each claim In _requestClaims
  77.                 rst.Claims.Add(claim)
  78.             Next
  79.         End If
  80.         Dim channel = trustChannelFactory.CreateChannel()
  81.         Dim rstr As RequestSecurityTokenResponse = Nothing
  82.         Token = channel.Issue(rst, rstr)
  83.         Dim t = rstr.RequestedSecurityToken.SecurityToken
  84.         Dim s As String = ""
  85.  
  86.     Catch ex As Exception
  87.         Throw New Exception(ex.Message, ex)
  88.     Finally
  89.         Try
  90.             If trustChannelFactory.State = CommunicationState.Faulted Then
  91.                 trustChannelFactory.Abort()
  92.             Else
  93.                 trustChannelFactory.Close()
  94.             End If
  95.         Catch generatedExceptionName As Exception
  96.         End Try
  97.     End Try
  98.     Return Token
  99. End Function
  100.  
  101. Private Function GetADFSTokenIssuedToken() As System.IdentityModel.Tokens.SecurityToken
  102.     Try
  103.         Dim Token As System.IdentityModel.Tokens.SecurityToken
  104.         Dim IssuedtokenMixed As String = _IdentityProvider & "adfs/services/trust/13/issuedtokenmixedsymmetricbasic256"
  105.  
  106.         Dim binding = New Microsoft.IdentityModel.Protocols.WSTrust.Bindings.IssuedTokenWSTrustBinding()
  107.         binding.SecurityMode = SecurityMode.TransportWithMessageCredential
  108.  
  109.         Dim factory = New WSTrustChannelFactory(binding, New EndpointAddress(IssuedtokenMixed))
  110.         factory.TrustVersion = TrustVersion.WSTrust13
  111.         factory.Credentials.SupportInteractive = False
  112.  
  113.         Dim rst = New RequestSecurityToken() With { _
  114.          .RequestType = WSTrust13Constants.RequestTypes.Issue, _
  115.          .AppliesTo = New EndpointAddress(_Realm), _
  116.          .KeyType = WSTrust13Constants.KeyTypes.Symmetric _
  117.         }
  118.         rst.TokenType = _TokenType
  119.         rst.KeyType = _TokenEncryption  
  120.         factory.ConfigureChannelFactory()
  121.  
  122.         If _requestClaims.Count > 0 Then
  123.             For Each claim In _requestClaims
  124.                 rst.Claims.Add(claim)
  125.             Next
  126.         End If
  127.  
  128.         Dim channel = factory.CreateChannelWithIssuedToken(_IssuedToken)
  129.         Token = channel.Issue(rst)
  130.         Return Token
  131.     Catch ex As Exception
  132.         Throw ex
  133.     End Try
  134. End Function

I need to wrap up a few more loose ends and add a few comments but the class library and a simple test application will be available for download on this blog in a few days

6 kommentarer:

  1. This is must useful article.:-)

    I am working with kinda same solution but got stuck in the middle. I am using asds web services .
    adfs/services/trust/13/issuedtokenmixedasymmetricbasic256

    While posting incoming token i get following exception.
    The supporting token provided for parameters 'System.ServiceModel.Security.Tokens.IssuedSecurityTokenParameters: InclusionMode: AlwaysToRecipient ReferenceStyle: Internal RequireDerivedKeys: False TokenType: null KeyType: AsymmetricKey KeySize: 0 IssuerAddress: null IssuerMetadataAddress: null DefaultMessgeSecurityVersion: null UseStrTransform: False IssuerBinding: null ClaimTypeRequirements: none' did not endorse the primary signature

    Do you have any idea why it is failing? i really appreciate your help.

    Thanks in advance

    SvarSlet
  2. hey Duniya.

    No, sorry that error looks new to me.
    Try installing fiddler and see if the request does ever get sent. If it does first step would be to enable WCF tracing on the server and get a stack trace to see why the server is rejecting the request. If not, make sure your not trying to send a symetric key token to a binding that expect a bear token and vice versa.

    SvarSlet
  3. Hi Allan. Let me start by stating that you are a rock star for putting this terrific example together. I am running into an issue though that I was hoping you could clarify for me. I am receiving one of the errors that you mentioned above.

    Specificaly, it is "The signing token System.IdentityModel.Tokens.SamlSecurityToken has no keys. The security token is used in a context that requires it to perform cryptographic operations, but the token contains no cryptographic keys. Either the token type does not support cryptographic operations, or the particular token instance does not contain cryptographic keys. Check your configuration to ensure that cryptographically disabled token types (for example, UserNameSecurityToken) are not specified in a context that requires cryptographic operations (for example, an endorsing supporting token)."

    My scenario is that I have a web application recieving a token from ADFS indirectly via WS-Federation passive requester profile. I have my config file set to save the bootstrap token. I then try to use this bootstrap token to retrieve another token to access another service via the IssuedTokenWSTrustBinding. Unfortunately I am stuck here. Based upon your comments above I'm wondering if the WS-Federation interaction is obtaining a bearer token from ADFS. You mentioned something about bearer tokens not being encrypted so I took a chance to see if setting the original token to be encrypted would change anything. As you probably guessed it didn't. In any case, I was hoping you might have some ideas. Thanks!

    SvarSlet
  4. While reading your comment, my first though was your token wasn’t encrypted (Bearer) but you then finish off saying you tried changing it.
    Sometimes you can stare blind one something, and I cannot resist asking you to try one more time using Symmetric encryption for the token.
    Remember you need to set this up on both on client side (relaying party) and server side (STS /ADFS) and will ofc. require your client to have a certificate with the private key.
    If this still doesn’t work, drop me a message on snigerosten at chatportal.dk

    SvarSlet
  5. Hi,

    trustChannelFactory.Credentials.UserName.UserName = _Username
    trustChannelFactory.Credentials.UserName.Password = _Password

    In the above code, instead of passing username and password explicitly, is there any way we can use Windows credentials. Currently I am consuming a REST based web service. I am passing the tokens in web request headers. We are not using any binding configurations. Can you please provide any code snippet. Thanks in advance.

    SvarSlet
  6. something like ?

    Dim uri As New Uri("https://" & host)
    Dim creds As System.Net.ICredentials = System.Net.CredentialCache.DefaultCredentials
    Dim cred As System.Net.NetworkCredential = creds.GetCredential(uri, "Basic")
    trustChannelFactory.Credentials.Windows.ClientCredential

    SvarSlet