如何将JWT令牌转换为WCF的SAML令牌

时间:2016-12-23 15:50:47

标签: wcf jwt adfs3.0

我们使用OAuth使用自定义TokenValidationHandler成功验证了ADFS 3.0。

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;
    }
}

注意:这仍在进行中(这就是我们禁用ssl验证的原因)。

现在我们需要将此JWT令牌转换为某些WCF服务的SAML令牌。重要提示:我们无法对WCF服务进行任何更改,因为它们不受我们控制。这意味着此解决方案不适用于我们:How to use JWT tokens with WCF and WIF?

我可以通过bootstrapcontext访问原始的JWT令牌。

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);

这样做的最佳方法是什么?

转到WCF的当前配置(我们从另一方收到并且不受我们控制)如下:

     <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>

我已经尝试通过RequestSecurityToken创建SAML令牌,但是当我添加ActAs SecurityTokenElement时,我从ADFS收到InvalidSecurityToken。

请求SAML令牌的Soap-enveloppe如下:

<?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 个答案:

答案 0 :(得分:2)

关键是您可以使用安全令牌处理程序将令牌转换为claimprincipal并返回。因此,您需要将您的jwt令牌转换为声明主体。你通常会这样做

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;

获得身份后,即可创建SecurityTokenDescriptor。这是这样的:

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

有问题的部分是你需要获得的SigninKey。通常你没有它,因为它属于STS。最后,您现在可以使用任何所需的securitytokenhandler将此描述符转换为您想要的任何标记:

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

这会将jwt转换为saml2。但是,正如我所说,如果您拥有adfs使用的私钥,则只能生成有效签名。