AD FS 2.0 Attribute Store Overview

 

Active Directory® Federation Services (AD FS) 2.0 includes built-in attribute stores that you can use to query for claim information from external data stores, such as Enterprise Active Directory, Lightweight Directory Access Protocol (LDAP) directories, and Microsoft SQL Server. You can also define custom attribute stores to query for claim information from other external data stores. This topic shows you how to define a custom attribute store.

Attribute Store Interface

The life cycle of an instance of an attribute store implementation consists of three stages:

  1. The instance is created and configured by AD FS 2.0.

  2. The instance is added to the list of available attribute stores maintained by AD FS 2.0.

  3. The instance is deleted.

This section discusses the interface used in step 2. Steps 1 and 3 are discussed in the section “Attribute Store Management in AD FS 2.0”.

The querying model uses the asynchronous calling pattern because the attribute query might involve communicating across processes or computers. The attribute store implementation is always executed in the same address space as the policy engine, which is an AD FS 2.0 server component that evaluates policies for claims issuance. The implementation is responsible for any invocations that cross process or computer boundaries.

A query is passed from the policy engine to the attribute store using a string query argument. This means that every attribute store implementation must use a query language that can be expressed using strings. Within that constraint, the implementation can use any query language that you want.

You can also pass string arguments to be substituted for placeholders in the query string, similar to the sprintf function in C or the String.Format() method in the .NET Framework. For example, for a SQL Server attribute store, a query might be SELECT employee_name FROM employee WHERE employee_type={0} AND employee_dept={1}. You would then pass the desired employee type to be substituted for {0} and the desired employee department for {1}.

Important

We recommend that the attribute store implementation check these parameters for script injection before each query is run.

The result from an attribute store query is a collection of claim values. Querying an attribute store can be an expensive operation, so we recommend limiting the number of queries that are performed by the policy. For this reason, the claims issuance language allows the policy to query for values for multiple claim types with a single query. Also, each query can return multiple values for each claim type. Therefore, the query result type is defined as a two-dimensional array of strings. The columns in the array represent individual claim types. The rows represent claim values. If the query result contains multiple claim types, the array contains multiple columns, each of which represents a claim type. If a given claim type has multiple values, the column for that claim type contains multiple rows, each of which contains a claim value. Because the result type is a two-dimensional array of strings, it is not possible to return any additional information that might be associated with the claims.

AD FS 2.0 allows multiple attribute stores to be configured for a given STS instance, so the claims issuance language uses the store argument in the issuance statement to identify a particular instance of the attribute store.

The following pseudo-C# defines the IAttributeStore interface.

public interface IAttributeStore  
{  
    void Initialize(Dictionary<string, string> configurationParameters);  
    IAsyncResult BeginExecuteQuery(string query,   
                                  string[] queryParameters,   
                                  AsyncCallback callback,   
                                  object state);  
    string[][] EndExecuteQuery(IAsyncResult result);  
}  
  

The AD FS 2.0 runtime calls Initialize to initialize the attribute store with configuration parameters. These parameters are name-value pairs that are specific to the attribute store definition.

The policy engine calls the BeginExecuteQuery method to start a query request on the attribute store. The callback parameter is a reference to the callback method that the attribute store invokes at the end of the query. The state parameter is made available in the AsyncState property of the IAsyncResult reference returned by this method. It is also made available in the IAsyncResult reference that is passed to the AsyncCallback method that is specified in the callback parameter. The IAsyncResult reference that is returned by this method is passed as the result parameter of the EndExecuteQuery method.

The policy engine calls the EndExecuteQuery method to get the result of the query. This method should block until the query is over and then return the results of the query in the two-dimensional string array. The columns in the array represent claim types, and the rows represent claim values. The result parameter is the IAsyncResult reference that is returned by the BeginExecuteQuery method.

Attribute Store Management in AD FS 2.0

The previous section discussed the attribute store interface and how the policy engine uses it to query an attribute store. This section discusses how to configure and manage attribute store instances in AD FS 2.0.

AD FS 2.0 maintains a global registry of available attribute stores. No attribute store is attached to any particular relying party or identity provider, and any policy executed within a specific STS instance has the same set of attribute stores available for querying.

The attribute store registry contains the following information:

  • The name of the attribute store. This name must be unique within the registry.

  • The type of the attribute store implementation class.

  • An optional list of configuration parameters, which are encoded as string-based name-value pairs. Each parameter name must be unique.

Each registered attribute store has a type that represents the class name that implements the attribute store interface. A single type can be used multiple times within a single STS instance, as long as each attribute store instance has a unique name.

AD FS 2.0 creates a single instance of each attribute store in its registry. This instance is shared among all policy executions that the AD FS 2.0 instance performs. This means that the ExecuteQuery method, in which the attribute store implementation performs queries, must be thread-safe and re-entrant.

AD FS 2.0 also requires that the attribute store implementation provide a public default constructor (one that takes no arguments).

After AD FS 2.0 creates an attribute store instance, it calls the Initialize method and passes in a Dictionary<string, string> that represents the configuration parameters associated with the attribute store instance in the registry. The implementation must parse the configuration parameters and initialize its state (for example, establish a connection to its back end) so that it is ready to accept query requests through the ExecuteQuery method. If the configuration parameters are not valid, or the implementation is otherwise unable to initialize its state, it should throw an exception, as described in the section “Attribute Store Exceptions”.

The attribute store specification does not require any particular configuration parameter name or any particular format for the parameter values (other than the fact that they must be strings). However, when implementing an attribute store, you should keep in mind that the configuration of the attribute store is exposed to the STS administrator as a simple set of text boxes, so you should avoid using a complex syntax for the parameter values.

When AD FS 2.0 is shutting down or otherwise decides to remove the attribute store instance from memory, it calls the attribute store implementation’s Dispose() method if the instance implements IDisposable. If not, AD FS 2.0 simply removes the reference to the attribute store instance and the instance is garbage collected.

Attribute Store Exceptions

Microsoft.IdentityServer.ClaimsPolicy.Engine.AttributeStore defines the following exceptions, which an attribute store implementation should throw when necessary.

  • AttributeStoreException. This is the abstract base class for attribute store exceptions.

  • AttributeStoreInvalidConfigurationException. Throw this exception when any of the configuration parameters passed to the attribute store is not valid.

  • AttributeStoreQueryExceptionException. Throw this exception when there are errors in query processing.

  • AttributeStoreQueryFormatException. Throw this exception when the query string passed to the attribute store is not valid.

When thrown, these exceptions are automatically logged as distinct events in the AD FS 2.0 event log. They also cause AD FS 2.0 to abort security token issuance. For more information about the correct use of these exceptions, see the FileAttributeStore code sample in the “Sample Custom Attribute Store Implementation” section.

Sample Custom Attribute Store Implementation

This sample, which implements a custom attribute store called “FileAttributeStore,” queries claim information from a text file in comma-separated value (CSV) format. The following is an example text file.

EmployeeID,EmployeeName,Age,Department,Role  
1,John,33,HR,Manager  
2,Jane,25,Sales,Vendor  
3,Tim,45,Marketing,Evangelist  
4,Leann,33,IT,Administrator  

You can query these data using the column names that are defined in the first row. FileAttributeStore supports only equality queries. You can also specify which columns to include in the result.

The following is an example query that FileAttributeStore supports.

=> issue(store = "FileAttributeStore", types = ("https://schemas.microsoft.com/ws/2008/06/identity/claims/name", "https://schemas.microsoft.com/ws/2008/06/identity/claims/role"), query = "Age={0};EmployeeName,Role", param = "33");  

The semicolon (;) separates the query columns from the return columns, and commas (,) delineate individual query columns and return columns.

This query gets the employee names and roles for employees whose age is 33 and places them in the name and role claims.

The following code example shows how to implement the FileAttributeStore itself.

  
//-----------------------------------------------------------------------------  
//  
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF  
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO  
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A  
// PARTICULAR PURPOSE.  
//  
// Copyright (c) Microsoft Corporation. All rights reserved.  
//  
//  
//-----------------------------------------------------------------------------  
  
using System;  
using System.Collections.Generic;  
using System.IO;  
using System.Text;  
using Microsoft.IdentityModel.Threading;  
using Microsoft.IdentityServer.ClaimsPolicy.Engine.AttributeStore;  
using RemoteMessagingAsyncResult = System.Runtime.Remoting.Messaging.AsyncResult;  
  
namespace CustomAttributeStores  
{  
  
    public class FileAttributeStore : IAttributeStore  
    {  
        string _fileName;  
  
        delegate string[][] RunQueryDelegate( string formattedQuery );  
  
        // The default constructor is mandatory.  
        public FileAttributeStore()  
        {  
        }  
  
        #region IAttributeStore Members  
  
        /* In this sample, this method executes asynchronously. You may choose to implement this synchronously as well. In the synchronous case, you will need to catch any exceptions and call the Complete method on the AsyncResult object inside this method. */  
  
        public IAsyncResult BeginExecuteQuery( string query, string[] parameters, AsyncCallback callback, object state )  
        {  
            string formattedQuery = string.Format( query, parameters );  
  
            /* Note the usage of TypedAsyncResult class defined in Microsoft.IdentityModel.Threading namespace. */  
            AsyncResult queryResult = new TypedAsyncResult<string[][]>( callback, state );  
  
            /* The asynchronous query is being implemented using a delegate. This is to show the asynchronous way of running the query, even though we are reading from a simple text file, which can be done synchronously. You may also use a data access interface that provides an asynchronous way to access the data. For example, the BeginExecuteReader method of System.Data.SqlClient.SqlCommand class asynchronously accesses data from a SQL Server data store. */  
            RunQueryDelegate queryDelegate = new RunQueryDelegate( RunQuery );  
            queryDelegate.BeginInvoke( formattedQuery, new AsyncCallback(AsyncQueryCallback), queryResult );  
            return queryResult;  
        }  
  
        // Get the query results.  
        public string[][] EndExecuteQuery( IAsyncResult result )  
        {  
            /* Each column of the returned array corresponds to a column requested in the query. Each row in the array corresponds to a row of data within the requested columns. For example, for the query "Age=33;EmployeeName,Role", if there are two rows that match Age=33, then the returned data will be:  
            string[0][0] = "John" string[0][1] = "HR Manager"  
            string[1][0] = "Leann"  string[1][1] = "Admin" */  
            return TypedAsyncResult<string[][]>.End( result );  
        }  
  
        // Initialize state here.  
        public void Initialize( Dictionary<string, string> config )  
        {  
            if ( config == null )  
            {  
                throw new ArgumentNullException( "config" );  
            }  
  
            // FileName must be the full path and name of the text file.  
            if ( !config.TryGetValue( "FileName", out _fileName ) )  
            {  
                throw new AttributeStoreInvalidConfigurationException( "FileName configuration entry not found" );  
            }  
  
            if ( string.IsNullOrEmpty( _fileName ) )  
            {  
                throw new AttributeStoreInvalidConfigurationException( "File name should be valid" );  
            }  
        }  
  
        #endregion  
  
        void AsyncQueryCallback( IAsyncResult result )  
        {  
            TypedAsyncResult<string[][]> queryResult = (TypedAsyncResult<string[][]>)result.AsyncState;  
            RemoteMessagingAsyncResult delegateAsyncResult = (RemoteMessagingAsyncResult)result;  
            RunQueryDelegate runQueryDelegate = (RunQueryDelegate)delegateAsyncResult.AsyncDelegate;  
  
            string[][] values = null;  
            Exception originalException = null;  
            try  
            {  
                values = runQueryDelegate.EndInvoke( result );  
            }  
            /* We don't want exceptions to be thrown from the callback method as these need to be made available to the thread that calls EndExecuteQuery. */  
            catch ( Exception e )  
            {  
                originalException = e;  
            }  
  
            /* Any exception is stored in query Result and re-thrown when EndExecuteQueryMethod calls TypedAsyncResult<string[][]>.End(..) method. */  
            queryResult.Complete( values, false, originalException );  
        }  
  
        string[][] RunQuery( string formattedQuery )  
        {  
            try  
            {  
                using ( StreamReader reader = File.OpenText( _fileName ) )  
                {  
                    List<string[]> data = new List<string[]>();  
  
                    /* We use this to map the column names to the corresponding index. */  
                    Dictionary<string, int> columns = new Dictionary<string, int>();  
  
                    /* The first row in the text file must be the column header row. */  
                    string columnNamesRow = reader.ReadLine();  
                    if ( string.IsNullOrEmpty( columnNamesRow ) )  
                    {  
                        throw new AttributeStoreQueryExecutionException( "Column header row is missing in data file" );  
                    }  
                    string[] columnNames = columnNamesRow.Split( new char[] { ',' } );  
                    string col;  
                    for ( int i = 0; i < columnNames.Length; i++ )  
                    {  
                        col = columnNames[i].Trim();  
                        if ( string.IsNullOrEmpty( col ) )  
                        {  
                            throw new AttributeStoreQueryExecutionException( "One or more column headers missing in data file" );  
                        }  
                        columns.Add( col, i );  
                    }  
  
                    /* Split the query into selection query and return data columns. queryParts[0] has the selection query. queryParts[1] has comma separated column names whose values will be returned. */  
                    string[] queryParts = formattedQuery.Split( new char[] { ';' } );  
                    if ( queryParts.Length != 2 )  
                    {  
                        throw new AttributeStoreQueryFormatException( "Invalid query " + formattedQuery );  
                    }  
                    queryParts[0] = queryParts[0].Trim();  
                    queryParts[1] = queryParts[1].Trim();  
                    if ( queryParts[0].Length == 0 || queryParts[1].Length == 0 )  
                    {  
                        throw new AttributeStoreQueryFormatException( "Invalid query " + formattedQuery );  
                    }  
  
                    /* Split the selection query and return the left and right sides of the query. Note that we only handle the equality (=) query. */  
                    string[] queryData = queryParts[0].Split( new char[] { '=' } );  
                    if ( queryData.Length != 2 )  
                    {  
                        throw new AttributeStoreQueryFormatException( "Invalid query " + queryParts[0] );  
                    }  
                    queryData[0] = queryData[0].Trim();  
                    queryData[1] = queryData[1].Trim();  
                    if ( queryData[0].Length == 0 || queryData[1].Length == 0 )  
                    {  
                        throw new AttributeStoreQueryFormatException( "Invalid query " + queryParts[0] );  
                    }  
  
                    // Get the columns that need to be returned.  
                    List<int> returnColumnList = new List<int>();  
                    string[] returnColumns = queryParts[1].Split( new char[] { ',' } );  
                    int index = 0;  
                    foreach ( string column in returnColumns )  
                    {  
                        if ( columns.TryGetValue( column.Trim(), out index ) )  
                        {  
                            returnColumnList.Add( index );  
                        }  
                        else  
                        {  
                            throw new AttributeStoreQueryExecutionException( "Invalid column requested : " + column );  
                        }  
                    }  
  
                    /* Read the data and select the columns in rows matching the selection query. */  
                    string line = reader.ReadLine();  
                    while ( line != null )  
                    {  
                        string[] row = ApplyQuery( queryData[0], queryData[1], line, columns, returnColumnList );  
                        if ( row != null )  
                        {  
                            data.Add( row );  
                        }  
                        line = reader.ReadLine();  
                    }  
  
                    return data.ToArray();  
                }  
            }  
            catch ( AttributeStoreQueryFormatException )  
            {  
                throw;  
            }  
            catch ( AttributeStoreQueryExecutionException )  
            {  
                throw;  
            }  
            // Wrap any unknown exceptions.  
            catch ( Exception e )  
            {  
                throw new AttributeStoreQueryExecutionException( "Query Execution Failed", e );  
            }  
        }  
  
        string[] ApplyQuery( string lhs, string rhs, string row, Dictionary<string, int> columns, List<int> returnColumnList )  
        {  
            string[] data = row.Split( new char[] { ',' } );  
            if ( data.Length != columns.Count )  
            {  
                throw new AttributeStoreQueryExecutionException( "Invalid data. Count of colum data does not match count of colum headers" );  
            }  
  
            int queryColIndex = 0;  
            if ( columns.TryGetValue( lhs, out queryColIndex ) )  
            {  
            /* If the value corresponding to the queried column matches the requested value, then return the values that correspond to the requested columns. */  
                if ( data[queryColIndex].Trim() == rhs )  
                {  
                    List<string> returnValues = new List<string>();  
                    foreach ( int returnColumn in returnColumnList )  
                    {  
                        returnValues.Add( data[returnColumn] );  
                    }  
                    return returnValues.ToArray();  
                }  
            }  
            else  
            {  
                throw new AttributeStoreQueryExecutionException( "Invalid column in query : " + lhs );  
            }  
            return null;  
        }  
    }  
}  
  

The following code example uses the FileAttributeStore to query the text file shown previously. The code assumes that the text file is stored at C:\temp\data.txt.

  
static void Main( string[] args )  
{  
    FileAttributeStore attributeStore = new FileAttributeStore();  
    Dictionary<string, string> config = new Dictionary<string, string>();  
    config.Add("FileName", @"c:\temp\data.txt");  
  
    attributeStore.Initialize( config );  
  
    IAsyncResult result = attributeStore.BeginExecuteQuery( "EmployeeName={0};EmployeeID,Age", new string[] {"Tim"}, null, null );  
  
    string[][] data = attributeStore.EndExecuteQuery( result );  
    foreach ( string[] row in data )  
    {  
        foreach ( string col in row )  
        {  
            Console.Write( "{0}\t\t", col );  
        }  
    Console.WriteLine();  
    }  
}  
  

After compiling these classes into a class library assembly, copy the assembly to the %Program Files%\Active Directory Federation Services 2.0 folder on the AD FS 2.0 server, or just register it in the global assembly cache (GAC).

The next step is to register the attribute store with AD FS 2.0, which you can do by using the AD FS 2.0 Management console or by using the Windows PowerShell command-line interface.

Registering an Attribute Store Using the AD FS 2.0 Management Console

  1. Open the AD FS 2.0 Management console.

  2. Open the Trust Relationships folder, right-click Attribute Stores, and then click Add Custom Attribute Store ….

  3. In the Add a Custom Attribute Store dialog box, enter a Display name of FileAttributeStore. Set the Custom attribute store class name to CustomAttributeStores.FileAttributeStore, CustomAttributeStores. Note that if the CustomAttributeStores assembly is registered in the GAC, you must provide the full name of the type and assembly, for example: CustomAttributeStores.FileAttributeStore,CustomAttributeStores, Version=1.0.0.0, Culture=neutral, PublicKeyToken=<publickeytoken>.

  4. In the Add a Custom Attribute Store dialog box, click Add. The Add an Initialization Parameter dialog box appears. Enter a Parameter name of FileName and a Parameter value of C:\temp\data.txt.

  5. Save all changes.

You can now use the FileAttributeStore in issuance rules. You identify the attribute store in an issuance rule by using the store’s display name: in this case, “FileAttributeStore”.

Registering an Attribute Store Using Windows PowerShell

In the AD FS 2.0 PowerShell snap-in, use the Add-ADFSAttributeStore cmdlet as follows:

Add-ADFSAttributeStore  -TypeQualifiedName "CustomAttributeStores.FileAttributeStore,CustomAttributeStores" -Configuration @{"FileName"="c:\temp\data.txt"}  -Name FileAttributeStore  

You can use the Set-ADFSAttributeStore cmdlet to change the attribute store information and Get-ADFSAttributeStore cmdlet to get the attribute store information.

Once the custom attribute store is registered, you can author a custom claim rule using the registered attribute store. The following is an example of such a rule.

=> issue(store = "FileAttributeStore",  
    types =   
        ("https://schemas.microsoft.com/ws/2008/06/identity/claims/name",  
        "https://schemas.microsoft.com/ws/2008/06/identity/claims/role"),  
    query = "Age=33;EmployeeName,Role");