How to transform JWT token to SAML token for WCF

2.8k views Asked by At

we have successfully authenticated to ADFS 3.0 using OAuth using a custom TokenValidationHandler.

public class TokenValidationHandler : DelegatingHandler
{
    private const string JwtAccessTokenCookieName = "jwt_access_token";

    private static readonly string adfsUrl = ConfigurationManager.AppSettings["oauth2.adfsUrl"];
    private static readonly string clientId = ConfigurationManager.AppSettings["oauth2.clientId"];
    private static readonly string redirectUrl = ConfigurationManager.AppSettings["oauth2.redirectUrl"];
    private static readonly string rptIdentifier = ConfigurationManager.AppSettings["oauth2.relyingPartyTrustIdentifier"];

    private AdfsMetadata adfsMetaData;
    public TokenValidationHandler()
    {
        string stsMetadataAddress = string.Format(CultureInfo.InvariantCulture, $"{adfsUrl}/federationmetadata/2007-06/federationmetadata.xml");
        adfsMetaData = new AdfsMetadata(stsMetadataAddress);
    }

    // SendAsync is used to validate incoming requests contain a valid access token, and sets the current user identity 
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        using (HttpResponseMessage responseMessage = new HttpResponseMessage())
        {
            string jwtToken;
            if (HasNoJWTAccessToken(request, out jwtToken))
            {
                string authorizationCode;
                if (HasNoAuthorizationCode(request, out authorizationCode))
                {
                    return RedirectToADFSLoginScreen(request);
                }

                var responseTokenAsJson = await GetAccessToken(cancellationToken, authorizationCode);
                return RedirectToAppWithAccessTokenInCookie(request, responseTokenAsJson);
            }

            try
            {
                var tokenHandler = new JwtSecurityTokenHandler { TokenLifetimeInMinutes = 60 };
                var validationParameters = new TokenValidationParameters
                {
                    ValidIssuer = adfsMetaData.Issuer,
                    IssuerSigningKeys = adfsMetaData.SigningTokens.Select(token => new X509SecurityKey(token.Certificate)),
                    ValidateAudience = false,
                    SaveSigninToken = true
                };
                try
                {
                    Microsoft.IdentityModel.Tokens.SecurityToken valdidationtoken;
                    // Validate token
                    ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwtToken, validationParameters, out valdidationtoken);
                    //set the ClaimsPrincipal on the current thread.
                    Thread.CurrentPrincipal = claimsPrincipal;
                    if (HttpContext.Current != null)
                    {
                        HttpContext.Current.Items["jwtTokenAsString"] = jwtToken;
                        HttpContext.Current.Items["jwtTokenAsSecurityToken"] = valdidationtoken;
                        HttpContext.Current.User = claimsPrincipal;
                    }
                    return await base.SendAsync(request, cancellationToken);
                }
                catch (Exception exception)
                {
                    responseMessage.StatusCode = HttpStatusCode.Unauthorized;
                    return new HttpResponseMessage(HttpStatusCode.Unauthorized)
                    {
                        Content = new StringContent(exception.Message)
                    };
                }
            }
            catch (Exception w)
            {
                return new HttpResponseMessage(HttpStatusCode.InternalServerError)
                {
                    Content = new StringContent(w.Message)
                };
            }
        }
    }

    private static async Task<JObject> GetAccessToken(CancellationToken cancellationToken, string authorizationCode)
    {
        ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
        HttpClient httpClient = new HttpClient();
        var httpResponseMessage = await httpClient.PostAsync(new Uri($"{adfsUrl}/adfs/oauth2/token"), GenerateTokenRequestContent(authorizationCode), cancellationToken);
        var responseContent = await httpResponseMessage.Content.ReadAsStringAsync();
        JObject responseToken = JObject.Parse(responseContent);
        return responseToken;
    }

    private static HttpResponseMessage RedirectToADFSLoginScreen(HttpRequestMessage request)
    {
        var requestUriAsString = request.RequestUri.ToString();
        var redirectResponse = new HttpResponseMessage(HttpStatusCode.Moved);
        redirectResponse.Headers.Location =
            new Uri($"{adfsUrl}/adfs/oauth2/authorize?response_type=code&client_id={clientId}&redirect_uri={HttpUtility.UrlEncode(redirectUrl)}&resource={HttpUtility.UrlEncode(rptIdentifier)}&state={GZipUtils.Compress(requestUriAsString)}");
        return redirectResponse;
    }

    private static HttpResponseMessage RedirectToAppWithAccessTokenInCookie(HttpRequestMessage request, JObject responseTokenAsJson)
    {
        var cookie = CreateCookieWithAccessToken(request, responseTokenAsJson);

        var urlToRedirectTo = GZipUtils.Decompress(request.GetQueryNameValuePairs().FirstOrDefault(param => param.Key == "state").Value);
        var redirectResponse = new HttpResponseMessage(HttpStatusCode.Redirect);
        redirectResponse.Headers.Location = new Uri(urlToRedirectTo);
        redirectResponse.Headers.AddCookies(new[] { cookie });
        return redirectResponse;
    }

    private static CookieHeaderValue CreateCookieWithAccessToken(HttpRequestMessage request, JObject responseTokenAsJson)
    {
        var compressedToken = GZipUtils.Compress(responseTokenAsJson["access_token"].ToString());
        var cookie = new CookieHeaderValue(JwtAccessTokenCookieName, compressedToken)
        {
            Expires = DateTimeOffset.Now.AddSeconds(Int16.Parse(responseTokenAsJson["expires_in"].ToString())),
            Domain = request.RequestUri.Host,
            Path = "/"
        };
        return cookie;
    }

    private static FormUrlEncodedContent GenerateTokenRequestContent(string authorizationCode)
    {
        return new FormUrlEncodedContent(
            new List<KeyValuePair<string, string>>()
            {
                new KeyValuePair<string, string>("grant_type","authorization_code"),
                new KeyValuePair<string, string>("client_id", clientId),
                new KeyValuePair<string, string>("code", authorizationCode),
                new KeyValuePair<string, string>("redirect_uri", redirectUrl),
            });
    }


    private bool HasNoAuthorizationCode(HttpRequestMessage request, out string authorizationCode)
    {
        authorizationCode = request.GetQueryNameValuePairs().FirstOrDefault(param => param.Key == "code").Value;
        return string.IsNullOrEmpty(authorizationCode);
    }

    // Reads the token from the authorization header on the incoming request
    static bool HasNoJWTAccessToken(HttpRequestMessage request, out string token)
    {
        if (HasNoJWTAccessTokenInAuthorizationHeader(request, out token) && HasNoJWTAccessTokenInSecureCookie(request, out token))
        {
            return true;
        }
        return false;
    }

    private static bool HasNoJWTAccessTokenInSecureCookie(HttpRequestMessage request, out string token)
    {
        token = null;
        if (!request.Headers.GetCookies(JwtAccessTokenCookieName).Any())
        {
            return true;
        }
        var cookieHeaderValue = request.Headers.GetCookies(JwtAccessTokenCookieName).FirstOrDefault();
        if (cookieHeaderValue != null)
        {
            token = GZipUtils.Decompress(cookieHeaderValue[JwtAccessTokenCookieName].Value);
        }
        if (token == null)
        {
            return true;
        }
        return false;
    }

    private static bool HasNoJWTAccessTokenInAuthorizationHeader(HttpRequestMessage request, out string token)
    {
        token = null;
        if (!request.Headers.Contains("Authorization"))
        {
            return true;
        }
        string authHeader = request.Headers.GetValues("Authorization").FirstOrDefault();
        // Verify Authorization header contains 'Bearer' scheme
        token = authHeader.StartsWith("Bearer ", StringComparison.Ordinal) ? authHeader.Split(' ')[1] : null;
        if (token == null)
        {
            return true;
        }
        return false;
    }
}

Note: this is still a work in progress (that's why we disable ssl validation).

Now we need to transform this JWT token to a SAML token for some WCF services. IMPORTANT: we cannot change anything to the WCF services as they are not under our control. This means this solution is not applicable for us: How to use JWT tokens with WCF and WIF?

I have access to the original JWT token via the bootstrapcontext.

ClaimsPrincipal principal = (ClaimsPrincipal) Thread.CurrentPrincipal;
var bootstrapContext = principal.Identities.First().BootstrapContext; //=> contains original JWT token.

System.IdentityModel.Tokens.SecurityToken token;
var rstr = RequestSecurityToken(out token); // => need help here

var channelFactory = new ChannelFactory<T>(endpointConfigurationName);
return channelFactory.CreateChannelWithActAsToken(token);

What would be the best approach to do so?

The current configuration to go to WCF (which we received from the other party and is not under our control) is as follows:

     <security authenticationMode="IssuedTokenOverTransport" messageSecurityVersion="WSSecurity11WSTrust13WSSecureConversation13WSSecurityPolicy12BasicSecurityProfile10">
        <issuedTokenParameters keyType="SymmetricKey" tokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0">
          <issuer address="https://fs.contoso-int.be/adfs/services/trust/13/kerberosmixed" binding="customBinding" bindingConfiguration="Contoso.Federation.Bindings.Http.KerberosMixed">
            <identity>
              <servicePrincipalName value="host/fs.contoso-int.be" />
            </identity>
          </issuer>
          <issuerMetadata address="https://fs.contoso-int.be/adfs/services/trust/mex" />
          <claimTypeRequirements>
            <add claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" />
            <add claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" isOptional="true" />
          </claimTypeRequirements>
          <additionalRequestParameters>
            <trust:TokenType xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0</trust:TokenType>
            <trust:KeyType xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://docs.oasis-open.org/ws-sx/ws-trust/200512/SymmetricKey</trust:KeyType>
            <trust:KeySize xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">256</trust:KeySize>
            <trust:KeyWrapAlgorithm xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p</trust:KeyWrapAlgorithm>
            <trust:EncryptWith xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/04/xmlenc#aes256-cbc</trust:EncryptWith>
            <trust:SignWith xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2000/09/xmldsig#hmac-sha1</trust:SignWith>
            <trust:CanonicalizationAlgorithm xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/10/xml-exc-c14n#</trust:CanonicalizationAlgorithm>
            <trust:EncryptionAlgorithm xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/04/xmlenc#aes256-cbc</trust:EncryptionAlgorithm>
          </additionalRequestParameters>
        </issuedTokenParameters>
        <localClientSettings detectReplays="false" />
        <localServiceSettings detectReplays="false" />
      </security>

I already tried to create a SAML token via a RequestSecurityToken but the moment I add the ActAs SecurityTokenElement, I receive an InvalidSecurityToken from ADFS.

The Soap-enveloppe to request the SAML token is as follows:

<?xml version="1.0"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
    <a:Action s:mustUnderstand="1">http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue</a:Action>
    <a:MessageID>urn:uuid:64f34b8a-92bf-4da0-9571-d436ab24d5d1</a:MessageID>
    <a:ReplyTo>
        <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
    </a:ReplyTo>
    <a:To s:mustUnderstand="1">https://fs.contoso-int.be/adfs/services/trust/13/kerberosmixed</a:To>
    <o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">
    <u:Timestamp u:Id="_0">
        <u:Created>2016-12-23T15:11:28.885Z</u:Created>
        <u:Expires>2016-12-23T15:16:28.885Z</u:Expires>
    </u:Timestamp>
    <o:BinarySecurityToken u:Id="uuid-abcb8b3a-61e0-4c9d-a6f3-71ad407b838d-1" ValueType="http://docs.oasis-open.org/wss/oasis-wss-kerberos-token-profile-1.1#GSS_Kerberosv5_AP_REQ" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">YIIGmgYJKoZIhvcSAQICAQB...</o:BinarySecurityToken>
    <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
        <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
        <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"/>
        <Reference URI="#_0">
            <Transforms>
                <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            </Transforms>
            <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
            <DigestValue>1qIxIurrORfpzYMl3AHVmVNGJ9Y=</DigestValue>
        </Reference>
    </SignedInfo>
    <SignatureValue>bCacOSkpjauc+QpMbUqCQ/aQE20=</SignatureValue>
    <KeyInfo>
        <o:SecurityTokenReference>
            <o:Reference URI="#uuid-abcb8b3a-61e0-4c9d-a6f3-71ad407b838d-1"/>
        </o:SecurityTokenReference>
    </KeyInfo>
</Signature>
</o:Security>
</s:Header>
<s:Body>
    <trust:RequestSecurityToken xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
    <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
    <wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
    <wsa:Address>urn:co:feat</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/SymmetricKey</trust:KeyType>
<tr:ActAs xmlns:tr="http://docs.oasis-open.org/ws-sx/ws-trust/200802">
<wsse:BinarySecurityToken xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" ValueType="urn:ietf:params:oauth:token-type:jwt" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">ZXlKMGVYQWlPaUpLVjFRaUxDSmhi...</wsse:BinarySecurityToken>
</tr:ActAs>
<trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>
</trust:RequestSecurityToken>
</s:Body>
</s:Envelope>
1

There are 1 answers

0
Willy Van den Driessche On BEST ANSWER

The key thing is that you can use the security token handlers to transform a token into a claimsprincipal and back. So you need to convert your jwt token to a claims principal. You would typically do this u

var handler = new JwtSecurityTokenHandler();
SecurityToken token;
var principal = handler.ValidateToken("your.jwt.part3", new TokenValidationParameters
            {
                ValidateAudience = false,
                /* be creative with the parameters here */
            }, out token);

var identity = principal.Identity as ClaimsIdentity;

Once you have an identity, you create a SecurityTokenDescriptor. This goes like this:

SecurityTokenDescriptor descriptor = new SecurityTokenDescriptor
            {
                AppliesToAddress = "realm",
                TokenIssuerName = "DoNotTrustThisIssuer",
                EncryptingCredentials = null,
                Subject = identity,
                Lifetime = new Lifetime(DateTime.UtcNow, DateTime.UtcNow.AddDays(1))
            };

The problematic part is the SigninKey that you need to get. Normally you do not have it since that belongs to the STS. Finally, you can now convert this descriptor into any token you want using any securitytokenhandler you want :

var handler2 = new Saml2SecurityTokenHandler();
var saml2Token = handler2.CreateToken(descriptor);

This converts the jwt to a saml2. However, as I said you can only generate a valid signature if you have the private key used by your adfs.