The endpoint uses an authentication scheme that requires an HTTP request that has the following header format.
GET /OData.svc/Products(1)
Authorization: WRAP access_token "123456789"
You add the following method to your DataService implementation.
protected override void OnStartProcessing Request(ProcessRequestArgs args)
{
...
}
You need to ensure that the method retrieves the authentication token.
Which line of code should you use?
string token = args.OperationContext.RequestHeaders["Authorization"]
http://blogs.msdn.com/b/astoriateam/archive/2010/08/19/odata-and-authentication-part-8-oauth-wrap.aspx
OData and Authentication – Part 8 – OAuth WRAP
19 Aug 2010 11:07 AM
OAuth WRAP is a claims based authentication protocol supported by the AppFabric Access
Control (ACS) which is part of Windows Azure.
But most importantly it is REST (and thus OData) friendly too.
The idea is that you authenticate against an ACS server and acquire a Simple Web Token or SWT – which contains signed claims about identity / roles / rights etc – and then embed the SWT in requests to a resource server that trusts the ACS server.
The resource server then looks for and verifies the SWT by checking it is correctly signed, before allowing access based on the claims made in the SWT.
If you want to learn more about OAuth WRAP itself here’s the spec.
Our goal is simple. We want an OData service that uses OAuth WRAP for authorization and a client to test it end to end.
OAuth 2.0 essentially combines the best features of OAuth 1.0 and OAuth WRAP.
Unfortunately OAuth 2.0 is not yet a ratified standard, so ACS doesn’t support it yet. On the other hand OAuth 1.0 is cumbersome for RESTful protocols like OData. So that leaves OAuth WRAP.
However once it is ratified OAuth 2.0 will essentially depreciate OAuth WRAP and ACS will rev to support it. When that happens you can expect to see a new post in this Authentication Series.
Next we’ll configure our identity server with appropriate roles, scopes and claim transformation rules etc.
Then we’ll create a HttpModule (see part 5) to intercept all requests to the server, which will crack open the SWT, convert it into an IPrincipal and store it in HttpContext.Current.Request.User. This way it can be accessed later for authorization purposes inside the Data Service.
Then we’ll create a simple OData service using WCF Data Services and protect it with a custom HttpModule.
Finally we’ll write client code to authenticate against the ACS server and acquire a SWT token. We’ll use the techniques you saw in part 3 to send the SWT as part of every request to our OData services.
Once your namespace is running you also have a running ACS server.
ACM.exe is a command line tool that ships as part of the AppFabric SDK, and that allows you to create Issuers, TokenPolicies, Scopes and Rules.
For an introduction to ACM.exe and ACS look no further than this excellent guide by Keith Brown.
To simplify our acm commands you should edit your ACM.exe.config file to include information about your ACS like this:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="host" value="accesscontrol.windows.net"/>
<add key="service" value="{Your service namespace goes here}"/>
<add key="mgmtkey" value="{Your Windows Azure Management Key goes here}"/>
</appSettings>
</configuration>
Doing this saves you from having to re-enter this information every time you run ACM.
Very handy.
Generally claims authentication is used to translate a set of input claims into a signed set of output claims.
Sometimes this extends to Federation, which allows trust relationships to be established between identity providers, such that a user on one system can gain access to resources on another system.
However in this blog post we are going to keep it simple and skip federation.
Don’t worry though we’ll add federation in the next post.
> acm create issuer
-name: partner -issuername: partner
-autogeneratekey
This will generate a key which you can retrieve by issuing this command:
> acm getall issuer
Count: 1
id: iss_89f12a7ed023c3b7b0a85f32dff96fed2014ad0a
name: odata-issuer
issuername: odata-issuer
key: 9QKoZgtxxU4ABv8uiuvaR+k0cOmUxfEOE0qfPK2lCJY=
previouskey: 9QKoZgtxxU4ABv8uiuvaR+k0cOmUxfEOE0qfPK2lCJY=
algorithm: Symmetric256BitKey
Our clients are going to need to know this key, so make a note of it for later.
When creating a token policy you need to balance security versus ease of use and convenience. The shorter the timeout the more likely it is to be based on up to date Identity and Role information, but that comes at the cost of frequent refreshes, which have performance and convenience implications.
For our purposes a timeout of 1 hour is probably about right. So we create a new policy like this:
> acm create tokenpolicy
-name: odata-service-policy
-timeout: 3600
-autogeneratekey
Where 3600 is the number of seconds in an hour. To see what you created issue this command:
> acm getall tokenpolicy Count: 1
id: tp_aaf3fd9ca64d4471a5c7b5c572c087fb
name: odata-service-policy
timeout: 3600
key: WRwJkQ9PgbhnIUgKuuovw/6yVAo/Dh0qrb7rqQWnsBk=
We’ll need both the id and key later.
This key is what we share with our resource servers, so that they can check SWTs are correctly signed. We’ll come back to that later.
Scopes are linked to a token policy, telling ACS how long SWTs should remain valid, how to sign the SWT, and scopes contain a set of rules which tell ACS how to translate incoming claims into claims embedded in the SWT.
When requesting a SWT a client must include an ‘applies_to’ parameter, which tells ACS for which scope they need a SWT, and consequently which token policy and rules should apply when constructing the SWT.
Here are just some of the reasons you might need multiple scopes:
> acm create scope
-name: odata-service-scope
-appliesto:http://odata.mydomain.com
-tokenpolicyid:tp_aaf3fd9ca64d4471a5c7b5c572c087fb
For ‘appliesto’ I chose the url for our planned OData service. Notice too that we bind the scope to the token policy we just created via it’s id.
You can retrieve this scope by executing this:
> acm getall scope
Count: 1
id: scp_c028015be790fb5d3ead59307bb3e537d586eac0
name: odata-service
appliesto: http://odata.mydomain.com
tokenpolicyid: tp_d8c65f770fb14a90bc707e958a722df9
You’ll need to know the scopeid to add Rules to the scope.
The rules are associated with a scope, and tell ACS how to transform input claims for the target scope (via applies_to) into signed output claims.
In our simple example, all we really want to do is this: ‘If you know the key of my issuer, we’ll sign a claim that you are a ‘User’.
To do that we need this rule:
> acm create rule
-name:partner-is-user
-scopeid:scp_c028015be790fb5d3ead59307bb3e537d586eac0
-inclaimissuerid:iss_89f12a7ed023c3b7b0a85f32dff96fed2014ad0a
-inclaimtype:Issuer
-inclaimvalue:partner
-outclaimtype:Roles
-outclaimvalue:User
"Issuer" is a special type of input claim type (normally input claim type is just a string that needs to be found in an incoming SWT) that says anyone who demonstrates direct knowledge of the issuer key will receive a SWT that includes that output claim specified in the rule*.
So this particular rule means anyone who issues an OAuth WRAP request with the Issuer name as the wrap_name and the Issuer key as the wrap_password will receive a signed SWT that claims their "Roles=User".
*NOTE: there are other ways that this rule particular can match, but they are outside the scope of this blog post, check out this excellent guide by Keith Brown for more.
To test that our rule is working try this:
WebClient client = new WebClient();
client.BaseAddress = "https://{your-namespace-goes-here}.accesscontrol.windows.net";
NameValueCollection values = new NameValueCollection();
values.Add("wrap_name", "partner");
values.Add("wrap_password", "9QKoZgtxxU4ABv8uiuvaR+k0cOmUxfEOE0qfPK2lCJY=");
values.Add("wrap_scope", "http://odata.mydomain.com");
byte[] responseBytes = client.UploadValues("WRAPv0.9", "POST", values);
string response = Encoding.UTF8.GetString(responseBytes);
string token = response.Split('&')
.Single(value => value.StartsWith("wrap_access_token="))
.Split('=')[1];
Console.WriteLine(token);
When I run that code get this:
Roles%3dUser%26Issuer%3dhttps%253a%252f%252ffabrikamjets.accesscontrol.windows.net%252f%26Audience%3dhttp%253a%252f%252fodata.mydomain.com%26ExpiresOn%3d1282071821%26HMACSHA256%3d%252bc2ZiBpm74Etw%252bAkXY1jNwme8acHfIYd9AAtGMckoss%253d
As you can see the Roles%#dUser is simply a UrlEncoded version of Roles=User, so assuming this is a correctly signed SWT (more on that in Step 3) our rule appears to be working.
Lets just take the code we wrote in parts 4 & 5 and rework it for OAuth WRAP, firstly by creating a OAuthWrapHttpModule that looks like this:
public class OAuthWrapAuthenticationModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.AuthenticateRequest +=
new EventHandler(context_AuthenticateRequest);
}
void context_AuthenticateRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
if (!OAuthWrapAuthenticationProvider.Authenticate(application.Context))
{
Unauthenticated(application);
}
}
void Unauthenticated(HttpApplication application)
{
// you could ignore this and rely on authorization logic to
// intercept requests etc. But in this example we fail early.
application.Context.Response.Status = "401 Unauthorized";
application.Context.Response.StatusCode = 401;
application.Context.Response.AddHeader("WWW-Authenticate", "WRAP");
application.CompleteRequest();
}
public void Dispose() { }
}
As you can see this relies on an OAuthWrapAuthenticationProvider which looks like this:
public class OAuthWrapAuthenticationProvider
{
static TokenValidator _validator = CreateValidator();
static TokenValidator CreateValidator()
{
string acsHostname =
ConfigurationManager.AppSettings["acsHostname"];
string serviceNamespace =
ConfigurationManager.AppSettings["serviceNamespace"];
string trustedAudience =
ConfigurationManager.AppSettings["trustedAudience"];
string trustedSigningKey =
ConfigurationManager.AppSettings["trustedSigningKey"];
return new TokenValidator(
acsHostname,
serviceNamespace,
trustedAudience,
trustedSigningKey
);
}
public static TokenValidator Validator
{
get { return _validator; }
}
public static bool Authenticate(HttpContext context)
{
if (!HttpContext.Current.Request.IsSecureConnection)
return false;
if (!HttpContext.Current.Request.Headers.AllKeys.Contains("Authorization"))
return false;
string authHeader = HttpContext.Current.Request.Headers["Authorization"];
// check that it starts with 'WRAP'
if (!authHeader.StartsWith("WRAP "))
{
return false;
}
// the header should be in the form 'WRAP access_token="{token}"'
// so lets get the {token}
string[] nameValuePair = authHeader
.Substring("WRAP ".Length)
.Split(new char[] { '=' }, 2);
if (nameValuePair.Length != 2 ||
nameValuePair[0] != "access_token" ||
!nameValuePair[1].StartsWith("\"") ||
!nameValuePair[1].EndsWith("\""))
{
return false;
}
// trim off the leading and trailing double-quotes
string token = nameValuePair[1].Substring(1, nameValuePair[1].Length - 2);
if (!Validator.Validate(token))
return false;
var roles = GetRoles(Validator.GetNameValues(token));
HttpContext.Current.User = new GenericPrincipal(
new GenericIdentity("partner"),
roles
);
return true;
}
static string[] GetRoles(Dictionary<string, string> nameValues)
{
if (!nameValues.ContainsKey("Roles"))
return new string[] { };
else
return nameValues["Roles"].Split(',');
}
}
As you can see the Authenticate method does a number of things:
The TokenValidator used in the code above is lifted from Windows Azure AppFabric v1.0 C# samples, which you can find here. If you download and unzip these samples you’ll find the TokenValidator here:
~\AccessControl\GettingStarted\ASPNETStringReverser\CS35\Service\App_Code\TokenValidator.cs
Our create CreateValidator() method creates a shared instance of the TokenValidator, and as you can see we are pulling these settings from web.config:
<configuration>
…
<appSettings>
<add key="acsHostName" value="accesscontrol.windows.net"/>
<add key="serviceNamespace" value="{your namespace goes here}"/>
<add key="trustedAudience" value="http://odata.mydomain.com"/>
<add key="trustedSigningKey" value="{your token policy key goes here}>
</appSettings>
…
</configuration>
The most interesting one is the trustedSigningKey.
This is a key shared between ACS and the resource server (in our case our HttpModule). It is the key from the token policy we created in step 2.
The ACS server uses the token policy key to create a hash of the claims (or HMACSHA256) which gets appended to the claims to complete the SWT. Then to verify that the SWT and its claims are valid the resource server simply re-computes the hash and compares.
Now that we’ve got our module we simply need to register it with IIS via the web.config like this:
<configuration>
…
<system.webServer>
<modules>
<add name="OAuthWrapAuthenticationModule"
type="SimpleService.OAuthWrapAuthenticationModule"/>
</modules>
</system.webServer>
…
</configuration>
There are lots of ways to create an OData Service using WCF Data Services. But by far the easiest way to create a read/write service is using the Entity Framework like this.
Now because we’ve converted the OAuth WRAP SWT into a GenericPrincipal by the time requests hit our Data Service all the authorization techniques we already know using QueryInterceptors and ChangeIntercepts are still applicable.
So you could easily write code like this:
[QueryInterceptor("Orders")]
public Expression<Func<Order, bool>> OrdersFilter()
{
if (!HttpContext.Current.Request.IsAuthenticated)
return (Order o) => false;
var user = HttpContext.Current.User;
if (user.IsInRole("User"))
return (Order o) => true;
else
return (Order o) => false; }
And of course you can rework the HttpModule and interceptors as needed if your claims get more involved.
In part 3 we explored the available client-side hooks. So we know that we can hook up to the DataServiceContext.SendingRequest like this:
ctx.SendingRequest +=new EventHandler<SendingRequestEventArgs>(OnSendingRequest);
And in our event hander we can add headers to the outgoing request. For OAuth WRAP we need to add a authorization header in the form:
Authorization:WRAP access_token="{YOUR SWT GOES HERE}"
NOTE: the double quotes (") are actually part of the format, but the curly bracked ({) are not. See the string.Format call below if you have any doubts.
So our OnSendingRequest event handler looks like this:
static void OnSendingRequest(object sender, SendingRequestEventArgs e)
{
e.RequestHeaders.Add(
"Authorization",
string.Format("WRAP access_token=\"{0}\"", GetToken())
);
}
As you can see this uses GetToken() to acquire the actual SWT:
static string GetToken()
{
if (_token == null){
WebClient client = new WebClient();
client.BaseAddress =
"https://{your-namespace-goes-here}.accesscontrol.windows.net";
NameValueCollection values = new NameValueCollection();
values.Add("wrap_name", "partner");
values.Add("wrap_password", "{Issuer Key goes here}");
values.Add("wrap_scope", "http://odata.mydomain.com");
byte[] responseBytes = client.UploadValues("WRAPv0.9", "POST", values);
string response = Encoding.UTF8.GetString(responseBytes);
string token = response.Split('&')
.Single(value => value.StartsWith("wrap_access_token="))
.Split('=')[1];
_token = HttpUtility.UrlDecode(token);
}
return _token;
}
static string _token = null;
As you can see we acquire the SWT once (by demonstrating knowledge of the Issuer key)and assuming that is successful we cache it for later reuse.
Finally if we issue queries like say this:
try
{
foreach (Order order in ctx.Orders)
Console.WriteLine(order.Number);
}
catch (DataServiceQueryException ex)
{
//var scheme = ex.Response.Headers["WWW-Authenticate"];
var code = ex.Response.StatusCode;
if (code == 401)
_token = null;
}
And our token has expired, as it will after 60 minutes, an exception will occur and we can just null out the cached SWT and any retries will force our code to acquire a new SWT.
It is a good foundation to build upon. But there are a few things we can do to make it better.
We could:
Alex James Program Manager
Microsoft
But most importantly it is REST (and thus OData) friendly too.
The idea is that you authenticate against an ACS server and acquire a Simple Web Token or SWT – which contains signed claims about identity / roles / rights etc – and then embed the SWT in requests to a resource server that trusts the ACS server.
The resource server then looks for and verifies the SWT by checking it is correctly signed, before allowing access based on the claims made in the SWT.
If you want to learn more about OAuth WRAP itself here’s the spec.
Goal
Now we know the principles behind OAuth WRAP it’s time to map those into the OData world.Our goal is simple. We want an OData service that uses OAuth WRAP for authorization and a client to test it end to end.
Why OAuth WRAP?
You might be wondering why this post covers OAuth WRAP and not OAuth 2.0.OAuth 2.0 essentially combines the best features of OAuth 1.0 and OAuth WRAP.
Unfortunately OAuth 2.0 is not yet a ratified standard, so ACS doesn’t support it yet. On the other hand OAuth 1.0 is cumbersome for RESTful protocols like OData. So that leaves OAuth WRAP.
However once it is ratified OAuth 2.0 will essentially depreciate OAuth WRAP and ACS will rev to support it. When that happens you can expect to see a new post in this Authentication Series.
Strategy
First we’ll provision an ACS server to act as our identity server.Next we’ll configure our identity server with appropriate roles, scopes and claim transformation rules etc.
Then we’ll create a HttpModule (see part 5) to intercept all requests to the server, which will crack open the SWT, convert it into an IPrincipal and store it in HttpContext.Current.Request.User. This way it can be accessed later for authorization purposes inside the Data Service.
Then we’ll create a simple OData service using WCF Data Services and protect it with a custom HttpModule.
Finally we’ll write client code to authenticate against the ACS server and acquire a SWT token. We’ll use the techniques you saw in part 3 to send the SWT as part of every request to our OData services.
Step 1 – Provisioning an ACS server
First you’ll need an Windows Azure account and a running AppFabric namespace.Once your namespace is running you also have a running ACS server.
Step 2 – Configuring the ACS server
To correctly configure the ACS server you’ll need to Install the Windows Azure Platform AppFabric SDK which you can find here.ACM.exe is a command line tool that ships as part of the AppFabric SDK, and that allows you to create Issuers, TokenPolicies, Scopes and Rules.
For an introduction to ACM.exe and ACS look no further than this excellent guide by Keith Brown.
To simplify our acm commands you should edit your ACM.exe.config file to include information about your ACS like this:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="host" value="accesscontrol.windows.net"/>
<add key="service" value="{Your service namespace goes here}"/>
<add key="mgmtkey" value="{Your Windows Azure Management Key goes here}"/>
</appSettings>
</configuration>
Doing this saves you from having to re-enter this information every time you run ACM.
Very handy.
Claims Transformation
Before we start configuring our ACS we need to know a few principles…Generally claims authentication is used to translate a set of input claims into a signed set of output claims.
Sometimes this extends to Federation, which allows trust relationships to be established between identity providers, such that a user on one system can gain access to resources on another system.
However in this blog post we are going to keep it simple and skip federation.
Don’t worry though we’ll add federation in the next post.
Issuers
In ACS terms an Issuer represents a security principal. And whether we want federation or not our first step is to create a new issuer like this:> acm create issuer
-name: partner -issuername: partner
-autogeneratekey
This will generate a key which you can retrieve by issuing this command:
> acm getall issuer
Count: 1
id: iss_89f12a7ed023c3b7b0a85f32dff96fed2014ad0a
name: odata-issuer
issuername: odata-issuer
key: 9QKoZgtxxU4ABv8uiuvaR+k0cOmUxfEOE0qfPK2lCJY=
previouskey: 9QKoZgtxxU4ABv8uiuvaR+k0cOmUxfEOE0qfPK2lCJY=
algorithm: Symmetric256BitKey
Our clients are going to need to know this key, so make a note of it for later.
Token Policy
Next we need a token policy. Token Policies specify a timeout indicating how long a new Simple Web Token (or SWT) should be valid, or put another way, how long before the SWT expires.When creating a token policy you need to balance security versus ease of use and convenience. The shorter the timeout the more likely it is to be based on up to date Identity and Role information, but that comes at the cost of frequent refreshes, which have performance and convenience implications.
For our purposes a timeout of 1 hour is probably about right. So we create a new policy like this:
> acm create tokenpolicy
-name: odata-service-policy
-timeout: 3600
-autogeneratekey
Where 3600 is the number of seconds in an hour. To see what you created issue this command:
> acm getall tokenpolicy Count: 1
id: tp_aaf3fd9ca64d4471a5c7b5c572c087fb
name: odata-service-policy
timeout: 3600
key: WRwJkQ9PgbhnIUgKuuovw/6yVAo/Dh0qrb7rqQWnsBk=
We’ll need both the id and key later.
This key is what we share with our resource servers, so that they can check SWTs are correctly signed. We’ll come back to that later.
Scope
A service may have multiple ‘scopes’ each with a different set of access rules and rights.Scopes are linked to a token policy, telling ACS how long SWTs should remain valid, how to sign the SWT, and scopes contain a set of rules which tell ACS how to translate incoming claims into claims embedded in the SWT.
When requesting a SWT a client must include an ‘applies_to’ parameter, which tells ACS for which scope they need a SWT, and consequently which token policy and rules should apply when constructing the SWT.
Here are just some of the reasons you might need multiple scopes:
- A multi-tenant resource server would probably need different rules per tenant.
- A single-tenant resource server with distinct sets of independently protected resources.
> acm create scope
-name: odata-service-scope
-appliesto:http://odata.mydomain.com
-tokenpolicyid:tp_aaf3fd9ca64d4471a5c7b5c572c087fb
For ‘appliesto’ I chose the url for our planned OData service. Notice too that we bind the scope to the token policy we just created via it’s id.
You can retrieve this scope by executing this:
> acm getall scope
Count: 1
id: scp_c028015be790fb5d3ead59307bb3e537d586eac0
name: odata-service
appliesto: http://odata.mydomain.com
tokenpolicyid: tp_d8c65f770fb14a90bc707e958a722df9
You’ll need to know the scopeid to add Rules to the scope.
Rules
ACS has one real job, which you could sum up with these four words: “Claims in, claims out”. Essentially ACS is just a claims transformation engine, and the transformation is achieved by applying a series of rules.The rules are associated with a scope, and tell ACS how to transform input claims for the target scope (via applies_to) into signed output claims.
In our simple example, all we really want to do is this: ‘If you know the key of my issuer, we’ll sign a claim that you are a ‘User’.
To do that we need this rule:
> acm create rule
-name:partner-is-user
-scopeid:scp_c028015be790fb5d3ead59307bb3e537d586eac0
-inclaimissuerid:iss_89f12a7ed023c3b7b0a85f32dff96fed2014ad0a
-inclaimtype:Issuer
-inclaimvalue:partner
-outclaimtype:Roles
-outclaimvalue:User
"Issuer" is a special type of input claim type (normally input claim type is just a string that needs to be found in an incoming SWT) that says anyone who demonstrates direct knowledge of the issuer key will receive a SWT that includes that output claim specified in the rule*.
So this particular rule means anyone who issues an OAuth WRAP request with the Issuer name as the wrap_name and the Issuer key as the wrap_password will receive a signed SWT that claims their "Roles=User".
*NOTE: there are other ways that this rule particular can match, but they are outside the scope of this blog post, check out this excellent guide by Keith Brown for more.
To test that our rule is working try this:
WebClient client = new WebClient();
client.BaseAddress = "https://{your-namespace-goes-here}.accesscontrol.windows.net";
NameValueCollection values = new NameValueCollection();
values.Add("wrap_name", "partner");
values.Add("wrap_password", "9QKoZgtxxU4ABv8uiuvaR+k0cOmUxfEOE0qfPK2lCJY=");
values.Add("wrap_scope", "http://odata.mydomain.com");
byte[] responseBytes = client.UploadValues("WRAPv0.9", "POST", values);
string response = Encoding.UTF8.GetString(responseBytes);
string token = response.Split('&')
.Single(value => value.StartsWith("wrap_access_token="))
.Split('=')[1];
Console.WriteLine(token);
When I run that code get this:
Roles%3dUser%26Issuer%3dhttps%253a%252f%252ffabrikamjets.accesscontrol.windows.net%252f%26Audience%3dhttp%253a%252f%252fodata.mydomain.com%26ExpiresOn%3d1282071821%26HMACSHA256%3d%252bc2ZiBpm74Etw%252bAkXY1jNwme8acHfIYd9AAtGMckoss%253d
As you can see the Roles%#dUser is simply a UrlEncoded version of Roles=User, so assuming this is a correctly signed SWT (more on that in Step 3) our rule appears to be working.
Step 3 – Creating the OAuth WRAP HttpModule
Now we have our ACS server correctly configured the next step is to create a HttpModule to crack open SWTs and map them into principles for use inside Data services.Lets just take the code we wrote in parts 4 & 5 and rework it for OAuth WRAP, firstly by creating a OAuthWrapHttpModule that looks like this:
public class OAuthWrapAuthenticationModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.AuthenticateRequest +=
new EventHandler(context_AuthenticateRequest);
}
void context_AuthenticateRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
if (!OAuthWrapAuthenticationProvider.Authenticate(application.Context))
{
Unauthenticated(application);
}
}
void Unauthenticated(HttpApplication application)
{
// you could ignore this and rely on authorization logic to
// intercept requests etc. But in this example we fail early.
application.Context.Response.Status = "401 Unauthorized";
application.Context.Response.StatusCode = 401;
application.Context.Response.AddHeader("WWW-Authenticate", "WRAP");
application.CompleteRequest();
}
public void Dispose() { }
}
As you can see this relies on an OAuthWrapAuthenticationProvider which looks like this:
public class OAuthWrapAuthenticationProvider
{
static TokenValidator _validator = CreateValidator();
static TokenValidator CreateValidator()
{
string acsHostname =
ConfigurationManager.AppSettings["acsHostname"];
string serviceNamespace =
ConfigurationManager.AppSettings["serviceNamespace"];
string trustedAudience =
ConfigurationManager.AppSettings["trustedAudience"];
string trustedSigningKey =
ConfigurationManager.AppSettings["trustedSigningKey"];
return new TokenValidator(
acsHostname,
serviceNamespace,
trustedAudience,
trustedSigningKey
);
}
public static TokenValidator Validator
{
get { return _validator; }
}
public static bool Authenticate(HttpContext context)
{
if (!HttpContext.Current.Request.IsSecureConnection)
return false;
if (!HttpContext.Current.Request.Headers.AllKeys.Contains("Authorization"))
return false;
string authHeader = HttpContext.Current.Request.Headers["Authorization"];
// check that it starts with 'WRAP'
if (!authHeader.StartsWith("WRAP "))
{
return false;
}
// the header should be in the form 'WRAP access_token="{token}"'
// so lets get the {token}
string[] nameValuePair = authHeader
.Substring("WRAP ".Length)
.Split(new char[] { '=' }, 2);
if (nameValuePair.Length != 2 ||
nameValuePair[0] != "access_token" ||
!nameValuePair[1].StartsWith("\"") ||
!nameValuePair[1].EndsWith("\""))
{
return false;
}
// trim off the leading and trailing double-quotes
string token = nameValuePair[1].Substring(1, nameValuePair[1].Length - 2);
if (!Validator.Validate(token))
return false;
var roles = GetRoles(Validator.GetNameValues(token));
HttpContext.Current.User = new GenericPrincipal(
new GenericIdentity("partner"),
roles
);
return true;
}
static string[] GetRoles(Dictionary<string, string> nameValues)
{
if (!nameValues.ContainsKey("Roles"))
return new string[] { };
else
return nameValues["Roles"].Split(',');
}
}
As you can see the Authenticate method does a number of things:
- Verifies we are using HTTPS because it would be insecure to pass SWT tokens around over straight HTTP.
- Verifies that the authorization header exists and it is a WRAP header.
- Extracts the SWT token from the authorization header.
- Asks a TokenValidator to validate the token. More on this in a second.
- Then extracts the Roles claims from the token (it assumes there is a Roles claim that contains a ',' delimited list of roles).
- Finally if every check passes it constructs a GenericPrincipal, with a hard coded identity set to ‘partner’, and the list of roles found in the SWT and assigns it to HttpContext.Current.User.
The TokenValidator used in the code above is lifted from Windows Azure AppFabric v1.0 C# samples, which you can find here. If you download and unzip these samples you’ll find the TokenValidator here:
~\AccessControl\GettingStarted\ASPNETStringReverser\CS35\Service\App_Code\TokenValidator.cs
Our create CreateValidator() method creates a shared instance of the TokenValidator, and as you can see we are pulling these settings from web.config:
<configuration>
…
<appSettings>
<add key="acsHostName" value="accesscontrol.windows.net"/>
<add key="serviceNamespace" value="{your namespace goes here}"/>
<add key="trustedAudience" value="http://odata.mydomain.com"/>
<add key="trustedSigningKey" value="{your token policy key goes here}>
</appSettings>
…
</configuration>
The most interesting one is the trustedSigningKey.
This is a key shared between ACS and the resource server (in our case our HttpModule). It is the key from the token policy we created in step 2.
The ACS server uses the token policy key to create a hash of the claims (or HMACSHA256) which gets appended to the claims to complete the SWT. Then to verify that the SWT and its claims are valid the resource server simply re-computes the hash and compares.
Now that we’ve got our module we simply need to register it with IIS via the web.config like this:
<configuration>
…
<system.webServer>
<modules>
<add name="OAuthWrapAuthenticationModule"
type="SimpleService.OAuthWrapAuthenticationModule"/>
</modules>
</system.webServer>
…
</configuration>
Step 4 – Creating an OData Service
Next we need to add (if you haven’t already) an OData Service.There are lots of ways to create an OData Service using WCF Data Services. But by far the easiest way to create a read/write service is using the Entity Framework like this.
Now because we’ve converted the OAuth WRAP SWT into a GenericPrincipal by the time requests hit our Data Service all the authorization techniques we already know using QueryInterceptors and ChangeIntercepts are still applicable.
So you could easily write code like this:
[QueryInterceptor("Orders")]
public Expression<Func<Order, bool>> OrdersFilter()
{
if (!HttpContext.Current.Request.IsAuthenticated)
return (Order o) => false;
var user = HttpContext.Current.User;
if (user.IsInRole("User"))
return (Order o) => true;
else
return (Order o) => false; }
And of course you can rework the HttpModule and interceptors as needed if your claims get more involved.
Step 5 – Acquiring and using a SWT Token
The final step is to write a client that will send a valid SWT with each OData request.In part 3 we explored the available client-side hooks. So we know that we can hook up to the DataServiceContext.SendingRequest like this:
ctx.SendingRequest +=new EventHandler<SendingRequestEventArgs>(OnSendingRequest);
And in our event hander we can add headers to the outgoing request. For OAuth WRAP we need to add a authorization header in the form:
Authorization:WRAP access_token="{YOUR SWT GOES HERE}"
NOTE: the double quotes (") are actually part of the format, but the curly bracked ({) are not. See the string.Format call below if you have any doubts.
So our OnSendingRequest event handler looks like this:
static void OnSendingRequest(object sender, SendingRequestEventArgs e)
{
e.RequestHeaders.Add(
"Authorization",
string.Format("WRAP access_token=\"{0}\"", GetToken())
);
}
As you can see this uses GetToken() to acquire the actual SWT:
static string GetToken()
{
if (_token == null){
WebClient client = new WebClient();
client.BaseAddress =
"https://{your-namespace-goes-here}.accesscontrol.windows.net";
NameValueCollection values = new NameValueCollection();
values.Add("wrap_name", "partner");
values.Add("wrap_password", "{Issuer Key goes here}");
values.Add("wrap_scope", "http://odata.mydomain.com");
byte[] responseBytes = client.UploadValues("WRAPv0.9", "POST", values);
string response = Encoding.UTF8.GetString(responseBytes);
string token = response.Split('&')
.Single(value => value.StartsWith("wrap_access_token="))
.Split('=')[1];
_token = HttpUtility.UrlDecode(token);
}
return _token;
}
static string _token = null;
As you can see we acquire the SWT once (by demonstrating knowledge of the Issuer key)and assuming that is successful we cache it for later reuse.
Finally if we issue queries like say this:
try
{
foreach (Order order in ctx.Orders)
Console.WriteLine(order.Number);
}
catch (DataServiceQueryException ex)
{
//var scheme = ex.Response.Headers["WWW-Authenticate"];
var code = ex.Response.StatusCode;
if (code == 401)
_token = null;
}
And our token has expired, as it will after 60 minutes, an exception will occur and we can just null out the cached SWT and any retries will force our code to acquire a new SWT.
Summary
In this post we’ve come a long way. We’ve now got a simple OData and OAuth WRAP authentication scenario working end to end.It is a good foundation to build upon. But there are a few things we can do to make it better.
We could:
- Configure our ACS to federate identities across domains, and configure our client code to do SWT exchange to go from one domain to another.
- Create an expiring cache of Principals so that we don’t need to re-validate everytime a new request is received.
- Upgrade our Principal object so it can handle more general claims rather than just User/Roles.
Alex James Program Manager
Microsoft
댓글 없음:
댓글 쓰기
국정원의 댓글 공작을 지탄합니다.