2016-12-23 3 views
1

Nous nous sommes authentifiés avec succès à ADFS 3.0 en utilisant OAuth en utilisant un TokenValidationHandler personnalisé.Comment transformer le jeton JWT en jeton SAML pour WCF

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

Remarque: il s'agit toujours d'un travail en cours (c'est pourquoi nous désactivons la validation ssl).

Maintenant, nous devons transformer ce jeton JWT en un jeton SAML pour certains services WCF. IMPORTANT: nous ne pouvons rien changer aux services de la WCF car ils ne sont pas sous notre contrôle. Cela signifie que cette solution ne s'applique pas à nous: How to use JWT tokens with WCF and WIF?

J'ai accès au jeton JWT d'origine via le 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); 

Quelle serait la meilleure approche pour le faire?

La configuration actuelle pour aller à WCF (que nous avons reçu de l'autre partie et n'est pas sous notre contrôle) est la suivante:

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

Je l'ai déjà essayé de créer un jeton SAML via un RequestSecurityToken mais moment où j'ajoute l'ActAs SecurityTokenElement, je reçois un InvalidSecurityToken d'ADFS.

Le savon pour demander le enveloppe jeton SAML est la suivante:

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

Répondre

1

L'essentiel est que vous pouvez utiliser les gestionnaires de jeton de sécurité pour transformer un jeton en claimsprincipal et le dos. Vous devez donc convertir votre jeton jwt en principal de réclamation. Généralement, vous le faites

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; 

Une fois que vous avez une identité, vous créez un SecurityTokenDescriptor. Cela se passe comme ceci:

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

La partie problématique est le SigninKey que vous devez obtenir. Normalement, vous ne l'avez pas depuis qu'il appartient à la STS. Enfin, vous pouvez maintenant convertir ce descripteur en tout jeton que vous voulez utiliser un securitytokenhandler que vous voulez:

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

Ce convertit le JWT à un SAML2. Cependant, comme je l'ai dit, vous ne pouvez générer une signature valide que si vous avez la clé privée utilisée par vos adfs.