Using Geneva Server's Issuance Language in a Custom STS

| | Comments (1) | TrackBacks (0)
Geneva Server beta 2 included a new issuance language that allows you to specify which claims should be burnt into security tokens.  I am really excited about this specialized claims DSL, not for the ways its creators had envisioned it being used, but because of how it can be used in the implementation of a custom STS.  You see?  The DLL that contains this language, Microsoft.IdentityServer.ClaimsPolicy, is completely isolated from the server proper.  You can use it to help you decide how to issue claims in your custom STS.  To begin leveraging this new DSL, do the following:
  1. Add a reference the aforementioned assembly,
  2. Create a claims policy engine,
  3. Create a policy context that includes the attributes stores you want to use and the IClaimsIdentity with the claims you want to transform, and
  4. Perform the mapping by feeding the policy context to the engine's issue method.
Before diving into the details, it may be helpful to see an example script:

c1:[ Type == "t1" ] =>

    issue(Type = "xxx", Value = c1.Value);

 

c2:[ Type == "t2" ] =>

    issue(Store = "_FooAttributeStore", Types = ("t2"), Query = "Get me {0}", Param = c2.Value);   

 

c3:[ Type == "t3" ] =>

    issue(Type = c3.Type, Issuer = "yyy", Value = c3.Value);

 

c4:[ Value == "v4" ] =>

    issue(Store = "_FooAttributeStore", Types = ("_T_2", "_T_3"), Query = "Get me {0}", Param = "Some Param");

 

c5:[ Value =~ "v\d$" ] =>

    issue(Type = "Regex Transformed", Value = c5.Value + "_XXX");


As Jon Alexander talked about on Channel 9, this language is a set of rules that consist of antecedents and conclusions.  Those antecedents that match an input claim will cause the function in the conclusion to be fired which can insert new claims into the output (if the issue function is called as in this example).

With a script like this, you just need to perform the steps listed above to begin using this awesome new feature of Geneva Server outside of the server itself.  To see how this might work, here is a simple example.

static void Main()

{

    var policyEngine = GetPolicyEngine();

    var originalIdentity = GetIdentity();

    var context = GetPolicyContext(originalIdentity);

    var transformedIdentity = policyEngine.Issue(context);

 

    WriteClaims(policyEngine.Policy, originalIdentity, transformedIdentity);

 

    Console.ReadLine();

}


To create the policy engine, you need to read your script into a string.  This is done by calling the Initialize method defined by the IPolicyEngine interface.

Untitled picture.png

As I said before, you create a PolicyContext to be used during the transformation process.  Here's how that might look:

private static PolicyContext GetPolicyContext(IClaimsIdentity identity)

{

    var attributeStores = GetAttributeStores();

 

    // NOTE: The attribute stores argument can't be null even if your

    // transformation script doens't include the use of any stores.

    Debug.Assert(attributeStores != null);

 

    const string resource = "resource???", action = "issue???";

    var subject = new ClaimsPrincipal(identity);

 

    return new PolicyContext(subject, resource, action, attributeStores);

}

 

private static Dictionary<string, IAttributeStore> GetAttributeStores()

{

    return new Dictionary<string, IAttributeStore>();

}

If you want to use a custom attribute store (a class that implements IAttributeStore), you might do something like this:

private static Dictionary<string, IAttributeStore> GetAttributeStores()

{

    var attributeStores = new Dictionary<string, IAttributeStore>();           

    var store = AttributeStoreFactory.CreateAttributeStore(

        typeof(FooAttributeStore).AssemblyQualifiedName, null);

 

    attributeStores.Add("_FooAttributeStore", store);

 

    return attributeStores;

}


As you can see from the snippet above, I don't really understand what values you should use for the resource and action arguments given to the PolicyContext constructor.  If you know, please leave a comment or contact me.

Once you've created IPolicyEngine and PolicyContext objects, you call the BeginIssue and EndIssue methods on the engine:

var context = GetPolicyContext(originalIdentity);

var transformedIdentity = EndIssue(BeginIssue(context, null, null));


The fictitious attribute store might get values from a Web service, XML file, .NET remoting component, mainframe, or any other source that contains data you would like to include in claims.  The IAttributeStore requires that you use .NET's async programming model.  As a result, you'll end up with something like this:


public class FooAttributeStore : IAttributeStore

{

    public void Initialize(Dictionary<string, string> config) { }

 

    public IAsyncResult BeginExecuteQuery(string query, string[] parameters, AsyncCallback callback, object state)

    {

        Func<string[][]> func = () =>

        {

            var result = new string[2][];

 

            result[0] = new[] { "stuff from attribute store", string.Format(query, parameters) };

            result[1] = new[] { "1", "2" };

 

            return result;

        };

 

        return func.BeginInvoke(callback, state);

    }

 

    public string[][] EndExecuteQuery(IAsyncResult result)

    {

        result.AsyncWaitHandle.WaitOne();

 

        var del = ((AsyncResult)result).AsyncDelegate as Func<string[][]>;

 

        Debug.Assert(del != null);

 

        return del.EndInvoke(result);

    }

}


Note that the Func in BeginExecuteQuery returned two objects.  The cardinality of this array needs to match the number of types given in your script that uses that attribute store:

issue(Store = "_FooAttributeStore", Types = ("_T_2", "_T_3"), Query = "Get me {0}", Param = "Some Param");

As Dominick mentioned recently, you can find more info about creating custom attribute stores on the Connect Web site

Why this DSL is Interesting to Custom STS Developers

Why is this new DSL so interesting to people developing a custom STS?  One fundamental problem in developing a custom STS (that I've encountered at least) is how and where to store the values it uses when issuing claims.  In case you haven't encountered this problem, let me explain.  Imagine you have hundreds of relying parties.  Each one will eventually need to make an authorization decision.  To do this, it needs claims that are very specific to it -- http://iaea.gov/can/launch/nukes, http://eaa.org/ready/for/flight, etc.  Though many Geneva-related samples use claim types such as email, given name, and other common types, some RPs require claim types that are very specific to them (as these examples were intended to show).  Who knows if a certain user is allowed to launch nuclear missiles or is ready to fly experimental planes?  Not the STS (either FP- or RP-STS).  It's the RP that has this data.

Assuming these assertions are correct, what should we do?  Shall we replicate the claim values to the STS from our 100 RPs and store that info in some monstrous, ever-growing STS database?  I don't believe that would scale (though some have disagreed with me on this).  What's the alternative?  Use Geneva Server's issuance language and its attribute stores to fetch the values from the RP itself.  When an RST is sent to the STS, it can figure out which RP the request is for, query its data store for a script specific to that RP, and (presuming it contains rules to query the RP's database) execute it to get data from the RP's database.  This decentralized approach leaves the RP in control of its data, decreases latency related to synchronization, avoids problems when replication goes awry, and scales out the load on the system.  If you're uncomfortable querying the RP's database from your STS (and you should be), implement a custom attribute store that calls a Web service.  If that service's API conforms to a standard, such as WS-Transfer (i.e., "WS-CRUD"), you have yourself a pretty nice system IMO.

The Bad News about the Issuance Language

The DLL that contains this language is included with Geneva Server.  This means that everyone with a Windows Server license is allowed to freely use it.  The types in it are (currently as of beta 2) public, making them generally available; however, this type of usage is not what Microsoft had in mind (IINM).  So, if you link against Microsoft.IdentityServer.ClaimsPolicy, you're swimming in shark infested waters.  They, and I, make no promises that things won't break when you install future versions of Geneva Server.  In no way, do I imply any warranty of the code or information in this blog post.  It is provided AS IS.  If the types in the assembly are made internal in a future release, if the interface changes and breaks a ton of your code, or for any other reason you incur damages by using this information or code, it is not my fault, and I accept no responsibility whatsoever.  Can you tell I just finished a class in business law? ;-)

If you need this language as bad as I do, please tell Microsoft to put it in the Geneva Framework where, IMHO, it belongs.