none
WS Trust 1.3 Challenge extensions

    Question

  • Hello

    I'm trying to implement SAML challenge exchange. According to the standard a client sends an RST, an STS issues an RSTR with a challenge, the client sends an RSTR with the challenge answer, and then the STS returns an RSTR with a SAML token. I cannot follow the sequence. Instead I can do RST-RSTR-RST-RSTR. Is it ok to have the difference.

    How do I implement the proper sequence?

    A client uses WSTrustChannelFactory and WSTrustChannel. The latter implements a IWSTrustChannelContract and contains a method:

    public Message Issue(Message message)

    However, Microsoft's RST and RSTR are not of type Message so I cannot pass them to the method. Moreover, SecurityTokenService does not implement the same interface. Does it mean that I have to implement WSTrustServiceHost, WSTrustServiceContract? If so how do I hook it to the service? Are there any samples I can take a look at? Any directions I can start looking at?

    The specs have a notice "is is NOT REQUIRED that exchange elements be symmetric". That  is in Custom Exchanges part. Is it applied in my case?

     

    • Changed type Vicel Friday, March 04, 2011 10:13 PM
    Tuesday, March 01, 2011 10:32 PM

All replies

  • Hello

    Hi Vicel,
    I'm trying to implement SAML challenge exchange.
    I did this about a year ago too. The details are a little fuzzy to me ATM, but IINM I may still have the code somewhere.
    According to the standard a client sends an RST, an STS issues an RSTR with a challenge, the client sends an RSTR with the challenge answer, and then the STS returns an RSTR with a SAML token.
    That's one possible flow. In another, the client may know that it's going to be challenged, so it could skip the first leg by sending an RSTR immediately. Also, there is no limit in the standard on how many legs there are in the interactive challenge process. So, possible flows are these:
    • RST_c → RSTR_s → RSTR_c → RSTRC/RSTR_s
    • RSTR_c → RSTRC/RSTR_s
    • RST_c → RSTR_s → RSTR_c → RSTR_s → RSTR_c → RSTRC/RSTR_s
    I cannot follow the sequence. Instead I can do RST-RSTR-RST-RSTR. Is it ok to have the difference.
    No. That would be a non-standard implementation.

    How do I implement the proper sequence?

    A client uses WSTrustChannelFactory and WSTrustChannel. The latter implements a IWSTrustChannelContract and contains a method:

    public Message Issue(Message message)

    However, Microsoft's RST and RSTR are not of type Message so I cannot pass them to the method. Moreover, SecurityTokenService does not implement the same interface. Does it mean that I have to implement WSTrustServiceHost, WSTrustServiceContract? If so how do I hook it to the service? Are there any samples I can take a look at? Any directions I can start looking at?

    You have to create new RequestSecurityToken and RequestSecurityTokenResponse classes that have properties for dealing w/ the interactive user challenge stuff. You use use on the client and server side. You also need to create a WSTrustRequestSerializer and a WSTrustResponseSerializer that can deal w/ the extra properties on those classes. You'll end up creating a new WSTrustChannelFactory as well w/ these serializers pre-associated. There is stuff you have to do on the service as well of course to cope w/ the requests and generate the responses. I can't remember how exactly this part was done. Also, IIRC I had to override the Issue method and see if the request contained my additional WS-Trust 1.4 stuff or if the user needed to be challenged. If not, I would call the base class's method. Otherwise, I would do my stuff and return my RSTR.

    I will have a look for my code next week and post back here or on my blog w/ more details if I find them.

    The specs have a notice "is is NOT REQUIRED that exchange elements be symmetric". That  is in Custom Exchanges part. Is it applied in my case?

     


    I don't remember that part of the spec off the top of my head, but I can say w/ a high degree of certainty that that quote does not make the flow you described stand compliant.

    HTH!

     


    Regards,

    Travis Spencer
    http://travisspencer.com
    • Edited by Travis Spencer Wednesday, March 02, 2011 7:58 AM your preview != WSIWYG
    Wednesday, March 02, 2011 7:56 AM
  • Travis

    Thanks for your response. If you could post your code it would be awesome.

    Just to clarify my situation:

    I already have custom RST and RSTR classes as well as serializers. I have not implemented WSTrustChannelFactory, I add the serializers in my STS proxy when instantiate it. It works, I can send challenge and challenge response. Also I have Issue method overriden to deal with the challenge.

    I have 2 problems:

    at the client: Microsoft.IdentityModel.Protocols.WSTrust.RequestSecurityToken inherits from WSTrustMessage which is an OpenObject and not a System.ServiceModel.Channels.Message. So even the channel has the Issue(Message):Message method I cannot use it unless I have another implementation of RST and RSTR. Having a convertion operator might work as well though. I'll try it out.

    at the STS:  Microsoft.IdentityModel.SecurityTokenService.SecurityTokenService does not have any methods accepting Message as a parameter. I can implement custom Microsoft.IdentityModel.Protocols.WSTrust.WSTrustServiceHostFactory and Microsoft.IdentityModel.Protocols.WSTrust.WSTrustServiceHost as well as WSTrustServiceContract. However I have no idea how to dispatch imcoming Message to my implementation of Issue(Message):Message (and the opposite how to dispatch outgoing Message properly).

     

    PS: I also wonder how you use WSTrust 1.4. WSTrustServiceHost contains 1.3 I think.  

    Update:

    I have overriden WSTrustChannel, WSTrustChannelFactory as well as WSTrustServiceHostFactory,  WSTrustServiceHost, and WSTrustServiceContract. They are successfuly invoked. I've added a method Issue(IClaimsPrincipal, RequestSecurityTokenResponse):RequestSecurityTokenResponse to my STS and added dispatching logic to the contract. That's fine. But I've got another problem I do not have a clue how to solve it. When I pass an RSTR as a parameter to the Issue(Message):Message method the STS throws an exception:  "No signature message parts were specified for messages with the 'http://schemas.microsoft.com/idfx/requesttype/issue' action." It probably has something to do with how RSTR is converted to Message but what's the difference? 

     

    Wednesday, March 02, 2011 7:38 PM
  • I searched hi and low for my code, Vicel, but I can't find it :-/ I was hoping to use that to refresh my memory on the issues you're hitting. I'll try to find some time to recode it, but it won't be any time soon. If I do get to it, I'll get back to you. Till then, good luck.

     


    Regards,

    Travis Spencer
    http://travisspencer.com
    Monday, March 14, 2011 7:39 AM
  • Thank you Travis. I have managed to make it working. I just wonder how to switch to WSTrust 1.4. Well I guess it will require some more reading :)

    Monday, March 14, 2011 9:40 PM
  • Hi Vicel,

    could you provide some input on how you implemented the DispatchLogic for your Challenge Extension? I need to implement a WS Trust 1.4 Interactive User Challenge. This involves getting an RSTR from the STS including a choice of user profiles. After that the client shows some kind of UI to the end user and sends his choice back to the STS wrapped inside an RSTR. The STS responds with an RSTR containing the actual token...

    Regards,

    Sebastian

    Wednesday, May 23, 2012 5:43 AM
  • Hi Sebastian,

    In short it looks like that:

    Client-STS communication:

    -> RST(username/password)

    <- RSTR (SAMLToken,Challenge)

    -> RSTR (SAMLToken, ChallengeResponse)

    <- RSTR (SAMLToken)

    First, you implement UserInteractiveChallenge & UserInteractiveChallengeResponse - classes that are serialized into xml defined in WS-Trust 1.4 (and classes that can be converted in these e.g. SecurityQuestionChallenge and SecurityQuestionChallengeResponse)

    then RST & RSTR

        public class MyRequestSecurityToken : RequestSecurityToken
        {
            public SerializableDictionary<string, string> ClientInfo { get; set; }
     
            public UserInteractionChallenge Challenge { get; set; }
         
            public UserInteractionChallengeResponse ChallengeResponse { get; set; }
        }
    }
       public class MyRequestSecurityTokenResponse : RequestSecurityTokenResponse
        {
             public UserInteractionChallenge Challenge { get; set; }
          
            public UserInteractionChallengeResponse ChallengeResponse { get; set; }
    
            public AccountInfo AuthenticatedUserAccountInfo { get; set; }
        }

    then extend Microsoft.IdentityModel.Protocols.WSTrust.WSTrust13RequestSerializer and WSTrust13ResponseSerializer to support your RST and RSTR. All of those are shared between clients and your STS.

    Now the STS part:

    you can start with an STS that is generated by the framework. Add a new method 

            public virtual RequestSecurityTokenResponse Issue(IClaimsPrincipal principal, RequestSecurityTokenResponse response)
            {
                 Principal = principal;
    
                    try
                    {
                        ValidateRequestResponse(response);
                        userAuthenticated = validateChallengeResponse(response);
                        if (!userAuthenticated)
                            throw new System.Security.Authentication.AuthenticationException("Challenge is not valid");
    
    
                        Scope = GetScope(principal, response);
    
                        if (Scope == null)
                            throw new InvalidOperationException(
                                "SecurityTokenService.GetScope returned null. Return a suitable Scope instance from SecurityTokenService.GetScope to issue a token.");
    
                        SecurityTokenDescriptor = CreateSecurityTokenDescriptor(response, Scope);
    
                        SecurityTokenHandler securityTokenHandler = GetSecurityTokenHandler(response.TokenType);
    
                        SecurityTokenDescriptor.TokenType = getTokenType(response.TokenType, securityTokenHandler);
                        SecurityTokenDescriptor.TokenIssuerName = GetIssuerName();
                        SecurityTokenDescriptor.Proof = GetProofToken(response, Scope);
                        SecurityTokenDescriptor.Lifetime = GetTokenLifetime(null);
                        SecurityTokenDescriptor.Subject = getOutputClaimsIdentity(principal, response);
                        SecurityTokenDescriptor.Token = securityTokenHandler.CreateToken(SecurityTokenDescriptor);
                        tokenAudited = SecurityTokenDescriptor.Token;
                        SecurityTokenDescriptor.AttachedReference = securityTokenHandler.CreateSecurityTokenReference(SecurityTokenDescriptor.Token, true);
                        SecurityTokenDescriptor.UnattachedReference = securityTokenHandler.CreateSecurityTokenReference(SecurityTokenDescriptor.Token, false);
    
                        challenge = getChallenge(principal);
    
                        RequestSecurityTokenResponse newresponse = getResponse(response, SecurityTokenDescriptor);
                        return newresponse;
                    }
                    catch (Exception ex)
                    {
    ...
                         throw;
                    }
            }

    you configure you STS to use custom WSTrustServiceFactory and SecurityTokenServiceConfiguration. These objects allow you to use a subclass of WSTRustServiceContract. That is the main point here.  In your custom contract add a method like:

            protected override void DispatchRequest(DispatchContext dispatchContext)
            {
                try
                {
                   MyRequestSecurityTokenResponse requestMessage = dispatchContext.RequestMessage as MyRequestSecurityTokenResponse;
                    if (requestMessage != null)
                    {
                        if (requestMessage.RequestType != RequestTypes.Issue)
                            throw new InvalidOperationException(string.Format("Unrecognized RequestType '{0}' specified in the incoming request.",
                                                                              requestMessage.RequestType));
    
                        MySecurityTokenService securityTokenService = (MySecurityTokenService)dispatchContext.SecurityTokenService;
                        dispatchContext.ResponseMessage = securityTokenService.Issue(dispatchContext.Principal, requestMessage);
                    }
                    else
                        base.DispatchRequest(dispatchContext);
                }
                catch (Exception exception)
                {
                    ...
                }
            }
        }

    Well that is it. A little bit about the client:
    I needed to access multiple services and I did not want to reauthenticate the client again and again so all my STS authentication was explicit. I called STS directly passing username and password, received a saml token, and displayed the challenge to the user. Then I called the STS again with the challenge response and the issued token, received a new token, cached it and used it to access other services.

    Here is some implementation details:

        public class MyWSTrustChannel : WSTrustChannel
        {
            private readonly MessageVersion messageVersion;
    
            public MyWSTrustChannel(WSTrustChannelFactory factory,
                                        IWSTrustChannelContract inner,
                                        TrustVersion trustVersion,
                                        WSTrustSerializationContext context,
                                        WSTrustRequestSerializer requestSerializer,
                                        WSTrustResponseSerializer responseSerializer)
                : base(factory, inner, trustVersion, context, requestSerializer, responseSerializer)
            {
                messageVersion = MessageVersion.Default;
                if (((factory.Endpoint != null) && (factory.Endpoint.Binding != null)) && (factory.Endpoint.Binding.MessageVersion != null))
                {
                    messageVersion = factory.Endpoint.Binding.MessageVersion;
                }
    
            }
    
            protected Message CreateResponse(RequestSecurityTokenResponse response)
            {
                return Message.CreateMessage(messageVersion, WSTrust13Constants.Actions.IssueResponse, new WSTrustResponseBodyWriter(response, WSTrustResponseSerializer, WSTrustSerializationContext));
            }
    
            public override SecurityToken Issue(RequestSecurityToken rst, out RequestSecurityTokenResponse rstr)
            {
                Message message = CreateRequest(rst, RequestTypes.Issue);
                Message response = Contract.Issue(message);
                rstr = ReadResponse(response);
                // GetTokenFromResponse throws if IsFinal!=true
                bool isFinal = rstr.IsFinal;
                rstr.IsFinal = true;
                SecurityToken token = GetTokenFromResponse(rst, rstr);
                rstr.IsFinal = isFinal;
                return token;
            }
    
            public virtual SecurityToken Issue(RequestSecurityTokenResponse rstr, out RequestSecurityTokenResponse out_rstr)
            {
                Message message = CreateResponse(rstr);
                Message response = Contract.Issue(message);
                out_rstr = ReadResponse(response);
                return GetTokenFromResponse(out_rstr);
            }
    
    
            protected SecurityToken GetTokenFromResponse(RequestSecurityTokenResponse response)
            {
                if (response == null)
                {
                    throw new ArgumentNullException("response");
                }
                if (response.RequestedSecurityToken == null)
                {
                    return null;
                }
    
                if (response.RequestedSecurityToken.SecurityTokenXml == null)
                {
                    throw new InvalidOperationException("The RequestSecurityTokenResponse that was received did not contain a SecurityToken.");
                }
    
                SecurityToken securityToken = response.RequestedSecurityToken.SecurityToken;
                if (securityToken != null)
                {
                    return securityToken;
                }
                SecurityToken proofKey = GetProofKey(response);
                DateTime? created;
                DateTime? expires;
                if (response.Lifetime != null)
                {
                    created = response.Lifetime.Created;
                    expires = response.Lifetime.Expires;
                    if (!created.HasValue)
                    {
                        created = new DateTime?(DateTime.UtcNow);
                    }
                    if (!expires.HasValue)
                    {
                        expires = new DateTime?(DateTime.UtcNow.AddHours(10.0));
                    }
                }
                else
                {
                    created = new DateTime?(DateTime.UtcNow);
                    expires = new DateTime?(DateTime.UtcNow.AddHours(10.0));
                }
    
                return new GenericXmlSecurityToken(response.RequestedSecurityToken.SecurityTokenXml,
                                                   proofKey,
                                                   created.Value,
                                                   expires.Value,
                                                   response.RequestedAttachedReference,
                                                   response.RequestedUnattachedReference,
                                                   new ReadOnlyCollection<IAuthorizationPolicy>(new List<IAuthorizationPolicy>()));
            }
    
            protected SecurityToken GetProofKey(RequestSecurityTokenResponse response)
            {
                if (response.RequestedProofToken != null && response.RequestedProofToken.ProtectedKey != null)
                {
                    return new BinarySecretSecurityToken(response.RequestedProofToken.ProtectedKey.GetKeyBytes());
                }
                throw new NotSupportedException(string.Format("The WSTrustChannel cannot compute a proof key. The received RequestedSecurityTokenResponse does not contain a RequestedProofToken and the ComputedKeyAlgorithm specified in the response is not supported: '{0}'.", 
                    response.RequestedProofToken == null ? "" : response.RequestedProofToken.ComputedKeyAlgorithm));
            }
    
        }

    and channel factory

            public MyWSTrustChannelFactory(string endpointConfigurationName) : base(endpointConfigurationName)
            {
                 WSTrustResponseSerializer = new MyWSTrust13ResponseSerializer();
                WSTrustRequestSerializer = new MyWSTrust13RequestSerializer();
            }
    
            protected override WSTrustChannel CreateTrustChannel(IWSTrustChannelContract innerChannel, 
                                                                TrustVersion trustVersion, 
                                                                WSTrustSerializationContext context, 
                                                                WSTrustRequestSerializer requestSerializer, 
                                                                WSTrustResponseSerializer responseSerializer)
            {
                return new MyWSTrustChannel(this, innerChannel, trustVersion, context, requestSerializer, responseSerializer);
            }
        }

    and the STS proxy

        public class STSProxy : IDisposable
        {
            private MyWSTrustChannel channel;
            private MyWSTrustChannelFactory factory;
    
             public STSProxy(string configName, string username, string password)
            {
                factory = new MyWSTrustChannelFactory(configName);
    
                if (factory.Credentials != null)
                {
                    factory.Credentials.UserName.UserName = username;
                    factory.Credentials.UserName.Password = password;
                }
    
                channel = (MyWSTrustChannel)factory.CreateChannel();
                
            }
    
            public STSProxy(string configName, SecurityToken issuedToken)
            {
                factory = new MyWSTrustChannelFactory(configName);
    
                factory.ConfigureChannelFactory();
    
                channel = (MyWSTrustChannel)factory.CreateChannelWithIssuedToken(issuedToken);
            }
    
            /// <summary>
            /// 
            /// </summary>
            /// <param name="requestType">use predefined enum: Microsoft.IdentityModel.Protocols.WSTrust.WSTrust13Constants.RequestTypes</param>
            /// <param name="appliesTo"></param>
            /// <returns></returns>
            protected MyRequestSecurityToken GetRequestSecurityToken(string requestType, string appliesTo)
            {
                MyRequestSecurityToken rst = new MyRequestSecurityToken(requestType)
                                                     {
                                                         KeySizeInBits = factory.TokenKeySize(),
                                                         AppliesTo = new EndpointAddress(appliesTo)
                                                     };
                return rst;
            }
    
            /// <summary>
            /// Issues RSTR to the STS
            /// </summary>
            /// <param name="appliesTo">an URI of a webservice which is in the list of allowed audience in the STS config</param>
            /// <param name="accountInfo">additional info to be passed to the STS</param>
            /// <param name="inParams">an object containing Challenge, ChallengeResponse and Proof token</param>
            /// <param name="outParams">set of objects that were returned from STS</param>
            /// <exception cref="ApplicationException" >if the channel is not created</exception>
            /// <exception cref="ArgumentNullException" >if inParams is null</exception>
            /// <returns>issued token</returns>
            public SecurityToken Issue(string appliesTo, AccountInfo accountInfo, STSParameters inParams, out STSParameters outParams)
            {
                if (channel == null)
                    throw new ApplicationException("STS Channel is not initialized.");
    
                if (inParams == null)
                    throw new ArgumentNullException();
    
                MyRequestSecurityTokenResponse response = new MyRequestSecurityTokenResponse(GetRequestSecurityToken(WSTrust13Constants.RequestTypes.Issue, appliesTo))
                                                                 {
                                                                     Challenge = inParams.Challenge,                                                                 ChallengeResponse = inParams.ChallengeResponse,                                                                                                                            IsFinal = false,
                                                                     RequestedProofToken = inParams.ProofToken == null ? null : new RequestedProofToken(inParams.ProofToken.GetKeyBytes()),
                                                                     KeySizeInBits = factory.TokenKeySize()
                                                                 };
    
                RequestSecurityTokenResponse rstr;
    
                SecurityToken token = channel.Issue(response, out rstr);
    
                outParams = null;
                MyRequestSecurityTokenResponse rrstr = rstr as MyRequestSecurityTokenResponse;
                if (rrstr != null)
                    outParams = new STSParameters(rrstr);
    
                return token;
            }
    
            /// <summary>
            /// Issues RST to the STS
            /// </summary>
            /// <param name="appliesTo">an URI of a webservice which is in the list of allowed audience in the STS config</param>
            /// <param name="userInfo">arbitrary client's data to be audited in the STS</param>
            /// <param name="outParams">set of objects that were returned from STS</param>
            /// <exception cref="ApplicationException" >if the channel is not created</exception>
            /// <returns>issued token</returns>
            public SecurityToken Issue(string appliesTo, Dictionary<string, string> userInfo, out STSParameters outParams)
            {
                if (channel == null)
                    throw new ApplicationException("STS Channel is not initialized.");
    
                MyRequestSecurityToken rst = GetRequestSecurityToken(WSTrust13Constants.RequestTypes.Issue, appliesTo);
                RequestSecurityTokenResponse rstr;
                if (userInfo != null && userInfo.Count > 0)
                    rst.ClientInfo = SerializableDictionary<string, string>.FromDictionary(userInfo);
    
                SecurityToken securityToken = channel.Issue(rst, out rstr);
    
                if (!(rstr is MyRequestSecurityTokenResponse))
                    throw new InvalidOperationException("The WSTrustChannel does not support multi-leg issuance protocols.");
    
                if (!rstr.IsFinal && ((MyRequestSecurityTokenResponse)rstr).Challenge == null)
                    throw new InvalidOperationException("The WSTrustChannel does not support multi-leg issuance protocols. The RSTR received from the STS must be enclosed in a RequestSecurityTokenResponseCollection element.");
    
                outParams = new STSParameters((MyRequestSecurityTokenResponse)rstr);
    
    
                return securityToken;
            }
    
             public SecurityToken Renew(string appliesTo, SecurityToken renewTargetToken)
            {
                if (channel == null)
                    throw new ApplicationException("STS Channel is not initialized.");
    
                RequestSecurityToken rst = GetRequestSecurityToken(WSTrust13Constants.RequestTypes.Renew, appliesTo);
                rst.Renewing = new Renewing(true, true);
                rst.RenewTarget = new SecurityTokenElement(renewTargetToken);
    
                RequestSecurityTokenResponse rstr = channel.Renew(rst);
    
                return channel.GetTokenFromResponse(rst, rstr);
            }
    
    }

    now you can use it like:

                using (STSProxy proxy = new STSProxy(Constants.STS, userName, password))
                {
                try
                {
                    SecurityToken issuedToken = proxy.Issue(getServerName(), new Dictionary<string, string>(), out prms);
    
                    session[SessionKeys.UserAccountInfo] = prms == null ? null : prms.AccountInformation;
    
                    UserInteractionChallenge challenge = prms == null ? null : prms.Challenge;
                    if (challenge != null)
                    {
                        session[SessionKeys.SamlToken] = new SerializableGenericSecurityToken((GenericXmlSecurityToken)issuedToken);
                        session[SessionKeys.ProofToken] = (SerializableBinarySecretSecurityToken)prms.ProofToken;
                        session[SessionKeys.StsChallenge] = challenge;
    
                        if (stsChallengeHandler != null)
                            stsChallengeHandler(challenge);
    
                        if (authenticateUserOnChallenge)
                            ...                }
                    else
                    {
                        session[SessionKeys.TokenProvider] = createSecurityTokenProvider(issuedToken);
                        ...
                    }
                }
                catch (FaultException ex)
                {
                    status = GetFailedAuthResult(ex);
                }
                catch (Exception ex)
                {
                    HandleError(ex);
                }
                }

    I have omitted some details but you get the idea.

    Regards



    • Edited by Vicel Wednesday, May 23, 2012 5:16 PM
    Wednesday, May 23, 2012 4:48 PM