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.
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.
- Dim ClaimsClient As New ClaimsAuth.Client
- 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
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
Go to Details and click Copy to file
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
- <microsoft.identityModel>
- <service saveBootstrapTokens="true">
- <certificateValidation certificateValidationMode="None" />
- <serviceCertificate>
- <certificateReference x509FindType="FindByThumbprint" findValue="7A41CF269D6BCDED80DDD9B6FD517E37891453B5" storeLocation="LocalMachine" storeName="My" />
- </serviceCertificate>
- </service>
- </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 )
- <microsoft.identityModel>
- <service saveBootstrapTokens="true">
- <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
- <trustedIssuers>
- <add thumbprint="86AC2E62900DF9451B0596562D52F7212AC31065" name="http://adfs.wingu.dk/adfs/services/trust" />
- </trustedIssuers>
- </issuerNameRegistry>
- </service>
- </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
- Dim ClaimsClient As New ClaimsAuth.Client
- ClaimsClient.TokenEncryption = Microsoft.IdentityModel.SecurityTokenService.KeyTypes.Symmetric
- ClaimsClient.IdentityProvider = "https://adfs.wingu.dk/"
- ClaimsClient.Realm = "https://admin.wingu.dk/ssi2/"
- ClaimsClient.authenticateBy = MessageCredentialType.Windows
- 'ClaimsClient.username = txtUsername.Text
- 'ClaimsClient.Password = txtPassword.Text
- 'ClaimsClient.ClientCertificate = MyPersonalCertificate
- 'ClaimsClient.IssuedToken = OtherIssuedToken
- 'Dim ClaimsID As Microsoft.IdentityModel.Claims.ClaimsIdentity = _
- 'DirectCast(HttpContext.Current.User.Identity, Microsoft.IdentityModel.Claims.ClaimsIdentity)
- 'Dim BootstrapToken As System.IdentityModel.Tokens.SecurityToken = ClaimsID.BootstrapToken
- 'ClaimsClient.ActAsToken = BootstrapToken
- 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
Back to the code, so inside the client class I have my authenticate function.
- Function authenticate() As System.IdentityModel.Tokens.SecurityToken
- Select Case _authenticateBy
- Case MessageCredentialType.UserName : _IssuedToken = GetADFSTokenUsernamemixed()
- Case MessageCredentialType.Windows : _IssuedToken = GetADFSTokenKerberos()
- Case MessageCredentialType.IssuedToken
- If _IssuedToken Is Nothing Then Throw New Exception("No token found to issue new token with")
- _IssuedToken = GetADFSTokenIssuedToken()
- Case Else : Throw New Exception("unknown authentication schema")
- End Select
- Return _IssuedToken
- 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
- Private Function GetADFSTokenUsernamemixed() As System.IdentityModel.Tokens.SecurityToken
- Dim Token As System.IdentityModel.Tokens.SecurityToken
- Dim UserNameMixed As String = _IdentityProvider & "adfs/services/trust/13/usernamemixed"
- Dim STSbinding = New Microsoft.IdentityModel.Protocols.WSTrust.Bindings.UserNameWSTrustBinding
- STSbinding.SecurityMode = SecurityMode.TransportWithMessageCredential
- Dim trustChannelFactory As New WSTrustChannelFactory(STSbinding, New EndpointAddress(UserNameMixed))
- trustChannelFactory.TrustVersion = System.ServiceModel.Security.TrustVersion.WSTrust13
- trustChannelFactory.Credentials.SupportInteractive = False
- trustChannelFactory.Credentials.UserName.UserName = _Username
- trustChannelFactory.Credentials.UserName.Password = _Password
- Try
- Dim rst As New RequestSecurityToken()
- rst.RequestType = WSTrust13Constants.RequestTypes.Issue
- rst.AppliesTo = New EndpointAddress(_Realm)
- rst.KeyType = _TokenEncryption
- rst.TokenType = _TokenType
- If _ClientCertificate IsNot Nothing Then
- Dim clause As System.IdentityModel.Tokens.SecurityKeyIdentifierClause = _
- New System.IdentityModel.Tokens.X509RawDataKeyIdentifierClause(_ClientCertificate)
- rst.UseKey = New UseKey(New System.IdentityModel.Tokens.SecurityKeyIdentifier(clause), _
- New System.IdentityModel.Tokens.X509SecurityToken(_ClientCertificate))
- End If
- 'This part will give you identity of logged in user
- If _ActAs IsNot Nothing Then rst.ActAs = _ActAs
- If _requestClaims.Count > 0 Then
- For Each claim In _requestClaims
- rst.Claims.Add(claim)
- Next
- End If
- Dim channel = trustChannelFactory.CreateChannel()
- Dim rstr As RequestSecurityTokenResponse = Nothing
- Token = channel.Issue(rst, rstr)
- Catch ex As Exception
- Throw New Exception(ex.Message, ex)
- Finally
- Try
- If trustChannelFactory.State = CommunicationState.Faulted Then
- trustChannelFactory.Abort()
- Else
- trustChannelFactory.Close()
- End If
- Catch generatedExceptionName As Exception
- End Try
- End Try
- Return Token
- End Function
- Private Function GetADFSTokenKerberos() As System.IdentityModel.Tokens.SecurityToken
- Dim Token As System.IdentityModel.Tokens.SecurityToken
- Dim KerberosMixed As String = _IdentityProvider & "adfs/services/trust/13/kerberosmixed"
- Dim STSbinding = New Microsoft.IdentityModel.Protocols.WSTrust.Bindings.KerberosWSTrustBinding
- STSbinding.SecurityMode = SecurityMode.TransportWithMessageCredential
- Dim trustChannelFactory As New WSTrustChannelFactory(STSbinding, New EndpointAddress(KerberosMixed))
- trustChannelFactory.TrustVersion = System.ServiceModel.Security.TrustVersion.WSTrust13
- trustChannelFactory.Credentials.SupportInteractive = False
- trustChannelFactory.Credentials.Windows.AllowedImpersonationLevel = TokenImpersonationLevel.Impersonation
- trustChannelFactory.Credentials.Windows.ClientCredential = System.Net.CredentialCache.DefaultNetworkCredentials
- Try
- Dim rst As New RequestSecurityToken()
- rst.RequestType = WSTrust13Constants.RequestTypes.Issue
- rst.AppliesTo = New EndpointAddress(_Realm)
- rst.KeyType = _TokenEncryption
- rst.TokenType = _TokenType
- If _ActAs IsNot Nothing Then rst.ActAs = _ActAs
- If _requestClaims.Count > 0 Then
- For Each claim In _requestClaims
- rst.Claims.Add(claim)
- Next
- End If
- Dim channel = trustChannelFactory.CreateChannel()
- Dim rstr As RequestSecurityTokenResponse = Nothing
- Token = channel.Issue(rst, rstr)
- Dim t = rstr.RequestedSecurityToken.SecurityToken
- Dim s As String = ""
- Catch ex As Exception
- Throw New Exception(ex.Message, ex)
- Finally
- Try
- If trustChannelFactory.State = CommunicationState.Faulted Then
- trustChannelFactory.Abort()
- Else
- trustChannelFactory.Close()
- End If
- Catch generatedExceptionName As Exception
- End Try
- End Try
- Return Token
- End Function
- Private Function GetADFSTokenIssuedToken() As System.IdentityModel.Tokens.SecurityToken
- Try
- Dim Token As System.IdentityModel.Tokens.SecurityToken
- Dim IssuedtokenMixed As String = _IdentityProvider & "adfs/services/trust/13/issuedtokenmixedsymmetricbasic256"
- Dim binding = New Microsoft.IdentityModel.Protocols.WSTrust.Bindings.IssuedTokenWSTrustBinding()
- binding.SecurityMode = SecurityMode.TransportWithMessageCredential
- Dim factory = New WSTrustChannelFactory(binding, New EndpointAddress(IssuedtokenMixed))
- factory.TrustVersion = TrustVersion.WSTrust13
- factory.Credentials.SupportInteractive = False
- Dim rst = New RequestSecurityToken() With { _
- .RequestType = WSTrust13Constants.RequestTypes.Issue, _
- .AppliesTo = New EndpointAddress(_Realm), _
- .KeyType = WSTrust13Constants.KeyTypes.Symmetric _
- }
- rst.TokenType = _TokenType
- rst.KeyType = _TokenEncryption
- factory.ConfigureChannelFactory()
- If _requestClaims.Count > 0 Then
- For Each claim In _requestClaims
- rst.Claims.Add(claim)
- Next
- End If
- Dim channel = factory.CreateChannelWithIssuedToken(_IssuedToken)
- Token = channel.Issue(rst)
- Return Token
- Catch ex As Exception
- Throw ex
- End Try
- 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
This is must useful article.:-)
SvarSletI 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
hey Duniya.
SvarSletNo, 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.
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.
SvarSletSpecificaly, 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!
While reading your comment, my first though was your token wasn’t encrypted (Bearer) but you then finish off saying you tried changing it.
SvarSletSometimes 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
Hi,
SvarSlettrustChannelFactory.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.
something like ?
SvarSletDim 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