IdentityServer / IdentityServer2

[deprecated] Thinktecture IdentityServer is a light-weight security token service built with .NET 4.5, MVC 4, Web API and WCF.
Other
410 stars 291 forks source link

ADFS integration fails on certificate validation in IdSrv #791

Closed cotepatrice closed 10 years ago

cotepatrice commented 10 years ago

I have setup the ADFS integration inside IdSrv. I have the ADFS RP encryption certificate installed on the idSrv with the private key access given to the user of the app pool running idSrv. I tripled check that. In the UI, I added the protocol, and put the values for ADFS UserName Endpoint, ADFS Issuer URI and ADFS Signing Certificate Thumbprint. Also double checked that it is the correct values in the AD FS 2.0 management tool.

When I use OAuth2Client to make the call, I can see in the ADFS server security log that the token has been succesfully issued for the correct Relying Party.

private static TokenResponse RequestToken(string scope) { "Requesting token.".ConsoleYellow(); var client = new OAuth2Client( "https://my-iis-server/idsrv/issue/adfs", "clientId_defined_in_idsrv", "jnfviuhriw8687wedcbedjnwiecv987y6vibrfviuhfv876=");

        var response = client.RequestResourceOwnerPasswordAsync(
            USERNAME,
            PASSWORD,
            "https://my-rp-server.com/MyAPI"
            ).Result;

        if (response.IsError)
            throw new HttpRequestException(response.HttpErrorReason);

        Console.WriteLine(" access token");
        response.AccessToken.ConsoleGreen();

        Console.WriteLine();
        return response;
    }

But the response I get is in error with an InternalServerError. Trace is activated on my idSrv instance, so I checked it. The message I get from the systemIdentityModel.svclog file is this one :

ID4036 : la clé nécessaire pour déchiffrer le jeton de sécurité chiffré n'a pas pu être résolue à partir de l'identificateur de clé de sécurité suivant « <e:EncryptedKey xmlns:e="http://www.w3.org/2001/04/xmlenc#"&gt;&lt;e:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"&gt;&lt;DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" xmlns="http://www.w3.org/2000/09/xmldsig#" /></e:EncryptionMethod><KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"&gt;&lt;o:SecurityTokenReference xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"&gt;&lt;X509Data&gt;&lt;X509IssuerSerial&gt;&lt;X509IssuerName&gt;CN=GeoTrust SSL CA - G2, O=GeoTrust Inc., C=US</X509IssuerName><X509SerialNumber>23205537042980196506765197579588450542</X509SerialNumber></X509IssuerSerial></X509Data></o:SecurityTokenReference></KeyInfo><e:CipherData><e:CipherValue>Pqyz5xgkTphI0/PzqMethuV0bKJUBg5bjksTSEaGDQ+GZOVEv3Fr1X5B9r4cRh+4UszhUL3HRHEeWYMtxiTNRxQ0nU0Xp31WU4Gu5loPXIlLMLZsvo1NwCh3DXvaQfWl9nXmvkNQGZFNq7qAPMC4YPWfvvxbWDUgqfCN6qVZqrAE53Gifs9wWYwkyT9NZNR0PXziIVLgWqfEp6xXSUkQlhL448FjuzBDXcjqcrLpwaSJbINW2W1c8nAXdDyFYuJg7fXKpm3t/3Burvg0gitveREPRSqC15ADbCT/yY0u9vIyzd... ». Vérifiez que le SecurityTokenResolver est renseigné avec la clé requise.

Which would be like this in english : ID4036: The key needed to decrypt the encrypted security token could not be resolved from the following security key identifier 'X'. Ensure that the SecurityTokenResolver is populated with the required key.

So it's quite easy to understand, the X509 certificate needed to decrypt the incoming SAML token has the IssuerName value of "CN=GeoTrust SSL CA - G2, O=GeoTrust Inc" is missing on the IdSrv hosting machine. BUT... the certificate is present in "My" certificate store and in the "Trusted root authority" store. When I check the "Manage privete key" window, I see that the user of the app pool running idSrv (NetworkService) has Read/Write access. Can anyone help me about where to look to get it working ? I'm totally stuck for 2 days now...

The only thing that seems strange is that the serial number in the trace file's message does not seem to exists. Either on the ADFS machine nor on the idSrv server...

leastprivilege commented 10 years ago

Do you need to encrypt the token from ADFS to IdSrv? Try without first.

cotepatrice commented 10 years ago

Well, we do encrypt all our tokens from every RP in ADFS. It's a company standard. But when I remove encryption, the JWT token gets created and returned by IdSrv. I know this is a certificate problem more than an IdSrv problem, but I must admit I'm totally lost on this one. Like I said, everything seems to be set correctly as for access to the private key. This is the correct certificate (same thumprint, IssuerName, etc...).

leastprivilege commented 10 years ago

Have you also configured the decryption key on the ADFS integration config page.

cotepatrice commented 10 years ago

OUpsss... I didn't upload the cer. Now it's done but still same error. Do I need the private key in a pfx ?

cotepatrice commented 10 years ago

And in the Key configuration section of IdSrv, do I need to select the certificate there too or is it just for assertion flow scenarios ?

cotepatrice commented 10 years ago

Even with encryption certificate loaded in IdSrv, private key access to the user set to "Full control" + "Read", still no luck at all. The certificates are in "My" store and I copied it to "Trusted Root CA" too. It is always the same error in the log file.

The error happens in the "ToSecurityToken()" extension method in the Thinktecture.IdentityModel.Extensions.SecurityTokensExtensions class, called from the Thinktecture.IdentityServer.Protocols.AdfsIntegration.AdfsController.CreateTokenResponse() method. I didn't manage to debug it inside, but that's where it happens.

cotepatrice commented 10 years ago

I've found this thread to be exactly the same problem that I have. So it means this doesn't work with encrypted SAML token I guess. Do you have plans to fix this with v3 ?

https://github.com/thinktecture/Thinktecture.IdentityServer.v2/issues/420

leastprivilege commented 10 years ago

I don't think this is related -

just to make sure - are you using the "ADFS integration" feature or just ADFS as an IdP ?

cotepatrice commented 10 years ago

Yes, it's "ADFS integration", not ADFS as an idP. The issue #420 is the exact same case from what I read. Works withour encryption, but get the same error when adding a certificate.

Here's a batch of print screens that shows my configuration and the debug values in Identity Server source code :

ADFS :

adfs

adfs_signin_cert

Identity Server machine :

iis_idsrv

adfs_integration

Visual Studio debug infos :

identity_server

leastprivilege commented 10 years ago

Hm - I am pretty sure we implemented decryption - otherwise there would be no point in having the config UI and setting. I'd need to debug through the code - which I don't have the time right now.

I'll see what I can find out ASAP

leastprivilege commented 10 years ago

OK I see the issue - this cert is not for decrypting incoming tokens - it is rather for encryping outgoing ones.

Seems we don't have decryption support right now - I can see two possible solutions:

https://github.com/thinktecture/Thinktecture.IdentityServer.v2/blob/master/src/Libraries/Thinktecture.IdentityServer.Protocols/WSFederation/HrdController.cs#L100

The token handler has a Configuration property where you can set the resolver in a similar manner IIRC

leastprivilege commented 10 years ago

Actually -

check this here:

https://github.com/thinktecture/Thinktecture.IdentityServer.v2/commit/e1ff271eae1be2aa798b8a0c13f4d4d60e1d7c67

This is in the AdfsBridgeDecryption branch - I haven't tested it, but give it a try.

You need to configure the decryption cert on the admin/keys page for that.

cotepatrice commented 10 years ago

Good ! I'll give it a try today (it's 8:00 AM here) and let you know if it works ASAP. Thanks for your help !

cotepatrice commented 10 years ago

OK. The problem is that the AdfsBridge.ConvertSamlToJwt() takes a SecurityToken as parameter and we have a GenerixXmlSecurityToken. So the code inside the controller method CreateTokenResponse is like this right now :

private HttpResponseMessage CreateTokenResponse(GenericXmlSecurityToken token, string scope) { var response = new TokenResponse();

        if (ConfigurationRepository.AdfsIntegration.PassThruAuthenticationToken)
        {
            response.AccessToken = token.TokenXml.OuterXml;
            response.ExpiresIn = (int)(token.ValidTo.Subtract(DateTime.UtcNow).TotalSeconds);
        }
        else
        {
            var bridge = new AdfsBridge(ConfigurationRepository);
            response = bridge.ConvertSamlToJwt(token.ToSecurityToken(), scope);
        }

        return Request.CreateResponse<TokenResponse>(HttpStatusCode.OK, response);
    }

As you can see, it uses the extension method ToSecurityToken() to convert it to security token before calling ConvertSamlToJwt(). This extension method fails when we use an encrypted token since it uses a default security token handler if we don't give it one. But since there is an overload where we can pass it an handler, here is the modified code that I put inside the AdfsController :

private HttpResponseMessage CreateTokenResponse(GenericXmlSecurityToken token, string scope) { var response = new TokenResponse();

        if (ConfigurationRepository.AdfsIntegration.PassThruAuthenticationToken)
        {
            response.AccessToken = token.TokenXml.OuterXml;
            response.ExpiresIn = (int)(token.ValidTo.Subtract(DateTime.UtcNow).TotalSeconds);
        }
        else
        {
            var bridge = new AdfsBridge(ConfigurationRepository);
            if (ConfigurationRepository.Keys.DecryptionCertificate != null)
            {
                var configuration = new SecurityTokenHandlerConfiguration
                    {
                        AudienceRestriction = {AudienceMode = AudienceUriMode.Never},
                        CertificateValidationMode = X509CertificateValidationMode.None,
                        RevocationMode = X509RevocationMode.NoCheck,
                        CertificateValidator = X509CertificateValidator.None,
                        ServiceTokenResolver = SecurityTokenResolver.CreateDefaultSecurityTokenResolver(
                            new ReadOnlyCollection<SecurityToken>(new SecurityToken[]
                                {new X509SecurityToken(ConfigurationRepository.Keys.DecryptionCertificate)}), false)
                    };
                var handler = SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection(configuration);
                response = bridge.ConvertSamlToJwt(token.ToSecurityToken(handler), scope);
            }
            else
            {
                response = bridge.ConvertSamlToJwt(token.ToSecurityToken(), scope);
            }
        }

        return Request.CreateResponse<TokenResponse>(HttpStatusCode.OK, response);
    }

Tested with these scenarios :

I don't know if this logic shoud go there, but that's the fastest way I found so that we don't break any compatibility. I'll wait for your comments on this.

leastprivilege commented 10 years ago

Cool. If it is tested and works for you - send me a PR.

(maybe check is the decryption cert in IdSrv is set or not / if it is null.

cotepatrice commented 10 years ago

For the null check, I thougt that's what this line was doing ?

if (ConfigurationRepository.Keys.DecryptionCertificate != null)