Implementing support for SAML 2.0 tokens

SAML 2.0 is a major improvement over the OASIS SAML 1.1 specification and is a result of the effort of several individuals, companies, and organizations. It represents a convergence of the SAML 1.1, Liberty Alliance Identity Federation Framework (ID-FF 1.2), and the Shibboleth web SSO attribute exchange mechanism (Shibboleth 1.3). Unlike ADFS 2.0, WIF has support only for SAML 2.0 tokens and does not support SAML 2.0 Profiles. The SAML 2.0 Profiles support a variety of scenarios, including the most commonly encountered Web Browser SSO.

Note

Note that the SAML 2.0 CTP for WIF has extensive guidelines for implementing the SAML 2.0 features which are discussed in Chapter 7, Extension and Future of Windows Identity Foundation. This recipe is built on the assumption that you are not using the CTP.

In this recipe, we will discuss a simple IP initiated service access scenario using the SAML 2.0 Web Browser SSO profile where the IP sends a SAML 2.0 response token using HTTP POST. A response from the IP uses a couple of well-known form variables, SAMLResponse (containing the token) and RelayState (representing the state information maintained at the RP).

Getting ready

You can learn more about the SAML 2.0 tokens, protocols, and profiles in an article at Wikipedia (http://en.wikipedia.org/wiki/SAML_2.0#SP_POST_Request.3B_IdP_POST_Response). The SAML 2.0 Profiles specification for Web Browser SSO will be discussed in this recipe.

How to do it...

Follow these steps:

  1. Open the IdentityManagement solution and create a class named Saml20SecureTokenProvider. Implement the SecureTokenProviderBase abstract class.
  2. Copy all of the abstract method implementations from the Saml11SecureTokenProvider.cs file, except for the SerializeToken and GetTokenHandler methods. Also for simplicity, we will not encrypt the token.
    protected override bool IsEncrypted()
    {
    return false;
    }
    
  3. Return an instance of the Saml2SecurityTokenHandler object in the GetTokenHandler method implementation:
    protected override SecurityTokenHandler GetTokenHandler()
    {
    SecurityTokenHandlerCollection handlers = SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection();
    return handlers[typeof(Saml2SecurityToken)] as Saml2SecurityTokenHandler;
    }
    
  4. Implement the SerializeToken method to generate the SecurityToken XML string.
    protected override string SerializeToken(SecurityToken token)
    {
    XmlWriterSettings settings = new XmlWriterSettings()
    {
    Encoding = Encoding.UTF8,
    Indent = true
    };
    StringBuilder sb = new StringBuilder();
    XmlWriter innerWriter = XmlWriter.Create(sb, settings);
    innerWriter.WriteStartElement("Response", "urn:oasis:names:tc:SAML:2.0:protocol");
    innerWriter.WriteAttributeString("IssueInstant", DateTime.UtcNow.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.ffffZ"));
    innerWriter.WriteAttributeString("ID", "_" + Guid.NewGuid());
    innerWriter.WriteAttributeString("Version", "2.0");
    innerWriter.WriteStartElement("Status");
    innerWriter.WriteStartElement("StatusCode");
    innerWriter.WriteAttributeString("Value", "urn:oasis:names:tc:SAML:2.0:status:Success");
    innerWriter.WriteEndElement();
    innerWriter.WriteEndElement();
    SecurityTokenHandlerCollectionManager mgr = SecurityTokenHandlerCollectionManager.CreateDefaultSecurityTokenHandlerCollectionManager();
    SecurityTokenHandlerCollection sthc = mgr.SecurityTokenHandlerCollections.First();
    SecurityTokenSerializer ser = new SecurityTokenSerializerAdapter(sthc);
    ser.WriteToken(innerWriter, token);
    innerWriter.WriteEndElement();
    innerWriter.Close();
    return sb.ToString();
    }
    

    The handler only generates the Assertion. The rest of the elements needed to wrap the Assertion and create a SAMLResponse structure are injected using the XmlWriter instance methods.

  5. Override the default SecurityTokenDescriptor implementation provided in the base class.
    public override SecurityTokenDescriptor GetSecurityTokenDescriptor()
    {
    return new SecurityTokenDescriptor
    {
    TokenType = Microsoft.IdentityModel.Tokens.SecurityTokenTypes.OasisWssSaml2TokenProfile11,
    AppliesToAddress = GetAppliesToAddress(),
    Lifetime = GetTokenLifeTime(),
    TokenIssuerName = GetIssuerName(),
    SigningCredentials = GetSigningCredentials(),
    Subject = GetOutputClaimsIdentity()
    };
    }
    

    Notice that a new property of the descriptor named TokenType is introduced and is set to the OASIS SAML 2.0 Token Profile (OasisWssSaml2TokenProfile11).

  6. Override the default implementation of the Issue method to create Saml2SecurityToken and return the serialized token string:
    public override string Issue()
    {
    var handler = GetTokenHandler();
    var descriptor = GetSecurityTokenDescriptor();
    var saml2Token = handler.CreateToken(descriptor) as Saml2SecurityToken;
    Saml2SubjectConfirmationData subConfirmData = new Saml2SubjectConfirmationData();
    subConfirmData.Recipient = new Uri(GetAppliesToAddress());
    subConfirmData.NotOnOrAfter = descriptor.Lifetime.Expires;
    Saml2SubjectConfirmation subjConfirm = new Saml2SubjectConfirmation(
    Saml2Constants.ConfirmationMethods.Bearer,
    subConfirmData);
    saml2Token.Assertion.Subject = new Saml2Subject(subjConfirm);
    Saml2AuthenticationContext authCtx = new Saml2AuthenticationContext(new Uri("urn:none"));
    saml2Token.Assertion.Statements.Add(new Saml2AuthenticationStatement(authCtx));
    return SerializeToken(saml2Token);
    }
    

    The Saml2AuthenticationContext class is set to urn:none. In a more real implementation it should be something like urn:oasis:names:tc:SAML:2.0:ac:classes:Password. Also, note that the subject confirmation is set to check for the lifetime of the token using the LifeTime.Expires property on the SecurityTokenDescriptor object.

  7. In the Default.aspx.cs file, replace the Button1_Click event handler with the following code to create an instance of Saml20SecureTokenProvider and assign the generated token to a HttpContext current item dictionary with SAMLResponse as the key:
    protected void Button1_Click(object sender, EventArgs e)
    {
    Dictionary<string, string> claims = new Dictionary<string, string>();
    claims.Add(WSIdentityConstants.ClaimTypes.Name, txtName.Text);
    claims.Add(WSIdentityConstants.ClaimTypes.PrivatePersonalIdentifier, txtUserId.Text);
    claims.Add(WSIdentityConstants.ClaimTypes.Locality, txtLanguageId.Text);
    var provider = new Saml20SecureTokenProvider(claims);
    string token = provider.Issue();
    if (token != null)
    {
    HttpContext.Current.Items.Add("SAMLResponse", token);
    HttpContext.Current.Items.Add("RelayState", ConfigurationManager.AppSettings["AppliesToAddress"]);
    Server.Transfer("~/StsProcessing.aspx");
    }
    }
    
  8. Update the StsProcessing.aspx page to use the SAMLResponse and RelayState hidden variables for capturing the token string and the realm (AppliesToAddress):
    protected void Page_Load(object sender, EventArgs e)
    {
    string encToken = HttpContext.Current.Items["SAMLResponse"] as string;
    string targetRp = HttpContext.Current.Items["RelayState"] as string;
    if (string.IsNullOrEmpty(encToken))
    Response.Write("error");
    else
    {
    txtToken.InnerText = encToken;
    Page.Form.Action = targetRp;
    Page.Form.Method = "POST";
    RelayState.Value = "http://localhost:8002";
    SAMLResponse.Value = encToken;
    }
    }
    
  9. Comment out the timeout script on the StsProcessing.aspx page. Compile the solution and run the WebSTS project. You should see the generated token printed on the page, as shown in the following screenshot, after clicking on the Create Token button:
    How to do it...
  10. Now, we will create a token consumer to validate and parse the incoming token and retrieve back the claims. To do this, right-click on the WebRP project and add reference to the System.Runtime.Serialization assembly.
  11. Create a custom security token handler by creating a class named Saml20SecureTokenHandler and inheriting the SecurityTokenHandler (Microsoft.IdentityModel.Tokens) class:
    public class Saml20SecureTokenHandler : SecurityTokenHandler
    {
    public override string[] GetTokenTypeIdentifiers()
    {
    throw new NotImplementedException();
    }
    public override Type TokenType
    {
    get { throw new NotImplementedException(); }
    }
    }
    
  12. Implement the GetTokenTypeIdentifiers method and the TokenType property to identify the type of the token that the handler is going to parse into, from the XML token string:
    static string[] _tokenTypeIdentifiers = null;
    static Saml20SecureTokenHandler()
    {
    _tokenTypeIdentifiers = new string[]
    {
    "urn:oasis:names:tc:SAML:2.0:protocol", "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-protocol-1.1#SAMLV2.0"
    };
    }
    public override string[] GetTokenTypeIdentifiers()
    {
    return _tokenTypeIdentifiers;
    }
    public override Type TokenType
    {
    get { return typeof(MySaml20SecurityToken); }
    }
    

    Note

    Notice that the TokenType property returns the type of MySaml20SecurityToken instead of Saml2SecurityToken. We need to create a custom SecurityToken class and use the Saml2SecurityToken type under the hood to prevent a collision in the SecurityTokenHandlerCollection object. The handler collection instance will throw an error during the process of reading the token if a custom security token object is not used, as it will find a duplicate instance of Saml2SecurityToken.

    public class MySaml20SecurityToken : SecurityToken
    {
    Saml2SecurityToken _token;
    public MySaml20SecurityToken(Saml2SecurityToken samlToken)
    {
    _token = samlToken;
    }
    public MySaml20SecurityToken()
    {
    }
    public Saml2SecurityToken Token
    {
    get { return _token; }
    }
    public override string Id
    {
    get { throw new NotImplementedException(); }
    }
    public override System.Collections.ObjectModel.ReadOnlyCollection<SecurityKey> SecurityKeys
    {
    get { throw new NotImplementedException(); }
    }
    public override DateTime ValidFrom
    {
    get { throw new NotImplementedException(); }
    }
    public override DateTime ValidTo
    {
    get { throw new NotImplementedException(); }
    }
    }
    
  13. Override the default ReadToken implementation to extract the Assertion from the token XML string and de-serialize into a SecurityToken object:
    public override SecurityToken ReadToken(XmlReader reader)
    {
    string assertionXML = null;
    try
    {
    Saml2SecurityTokenHandler saml2Handler = new Saml2SecurityTokenHandler();
    XmlDictionaryReader reader2 = XmlDictionaryReader.CreateDictionaryReader(reader);
    reader2.ReadToDescendant("Assertion", "urn:oasis:names:tc:SAML:2.0:assertion");
    assertionXML = reader2.ReadOuterXml();
    XmlReader reader3 = XmlReader.Create(new StringReader(assertionXML));
    XmlDocument signedXml = new XmlDocument();
    signedXml.Load(reader3);
    XmlReader reader4 = XmlReader.Create(new StringReader(signedXml.OuterXml));
    return base.ContainingCollection.ReadToken(reader4);
    }
    catch (Exception ex)
    {
    throw new ApplicationException("Can't validate token", ex);
    }
    }
    
  14. Now that we have created the custom handler, we will create a token consumer named Saml20SecureTokenConsumer and provide an implementation for the SecureTokenConsumerBase abstract class. The following code illustrates the implementation of the abstract methods:
    protected override SecurityTokenHandlerCollection GetTokenHandlerCollection()
    {
    SecurityTokenHandlerCollectionManager manager = SecurityTokenHandlerCollectionManager.CreateDefaultSecurityTokenHandlerCollectionManager();
    SecurityTokenHandlerCollection handlers = manager.SecurityTokenHandlerCollections.First();
    handlers.Add(new Saml20SecureTokenHandler());
    return handlers;
    }
    protected override SecurityToken DeserializeToken(SecurityTokenHandlerCollection handlers)
    {
    ServiceConfiguration config = new ServiceConfiguration(_serviceConfig);
    handlers.Configuration.AudienceRestriction = config.AudienceRestriction;
    var txtReader = new StringReader(_token);
    StringBuilder sb = new StringBuilder();
    XmlReader reader = XmlReader.Create(txtReader);
    var token = handlers.ReadToken(reader);
    return token;
    }
    public override Dictionary<string, string> ParseAttributesFromSecureToken()
    {
    Dictionary<string, string> attributes = new Dictionary<string, string>();
    var handlers = GetTokenHandlerCollection();
    var token = DeserializeToken(handlers) as Saml2SecurityToken;
    foreach (var item in token.Assertion.Statements)
    {
    Saml2AttributeStatement attStmt = item as Saml2AttributeStatement;
    if (attStmt != null)
    {
    foreach (var item2 in attStmt.Attributes)
    {
    attributes.Add(item2.Name, item2.Values[0]);
    }
    }
    }
    return attributes;
    }
    

    In the GetTokenHandlerCollection method, we add our Saml20SecureTokenHandler object to the default SecurityTokenHandlerCollection instance. Another important thing to notice here is that the claims are represented as attribute statements in SAML 2.0. In the ParseAttributesFromSecurityToken method we loop through the collection of Saml2AttributeStatement objects to retrieve the claim key/value pairs.

  15. Update the Default.aspx.cs file to use the Saml20SecureTokenConsumer instance for retrieving the list of claims and displaying them on the page:
    protected override void CreateChildControls()
    {
    base.CreateChildControls();
    var rv = Request.Params["SAMLResponse"];
    var tokenConsumer = new Saml20SecureTokenConsumer(Server.HtmlDecode(rv), "IdentityServiceConfig");
    _claimList = tokenConsumer.ParseAttributesFromSecureToken();
    HtmlTable table = new HtmlTable();
    table.Border = 1;
    foreach (var item in _claimList)
    {
    HtmlTableRow row = new HtmlTableRow();
    HtmlTableCell cell1 = new HtmlTableCell();
    cell1.InnerText = item.Key;
    HtmlTableCell cell2 = new HtmlTableCell();
    cell2.InnerText = item.Value;
    row.Controls.Add(cell1);
    row.Controls.Add(cell2);
    table.Controls.Add(row);
    }
    this.form1.Controls.Add(table);
    }
    
  16. Compile the solution and run the WebSTS application. On clicking on the Create Token button, you should get redirected to the Default.aspx page of the WebRP application and the list of retrieved tokens should get displayed on the page.

How it works...

The WIF runtime does not provide any mechanism to generate SAMLResponse, however, the Microsoft.IdentityModel.Tokens.Saml2 namespace exposes classes and methods to help you construct a Saml2SecurityToken assertion for the SAMLResponse message. In our solution, an attempt is made to construct a SAMLResponse message in the SerializeToken method implementation of the Saml20SecureTokenProvider class. The XmlWriter instance constructs the message and the Assertion generated by the Saml2SecureTokenHandler is serialized into an XML string and injected in the message body.

In the RP application, the custom handler extracts the Assertion from the incoming token and de-serializes into the Saml2SecurityToken object.

There's more...

For simplicity, we haven't implemented token validation in our solution. The token can be validated by the RP by checking the token signature. The RP can also verify if the token was issued by a trusted STS. The ValidateToken method of the SecurityTokenHandler class can be used for the purpose.

Bearer and Holder-of-key tokens

In our solution, we have the bearer subject confirmation method. It is the default mode in a passive federation scenario. Active federation using the WS-Trust protocol requires a Holder-of-Key subject confirmation method. You can learn more about the process of signing and encrypting tokens in the Generating SAML Tokens with WIF" by Michèle Leroux Bustamante article at the following URL:

http://www.devproconnections.com/content1/topic/generating-saml-tokens-with-wif-part-2/catpath/federated-security

See also

The complete source code for this recipe can be found in the Chapter 3Recipe 4 folder.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.163.175