So far, you have learned how to secure the web applications using ACS. In this recipe, we will look at how to secure a WCF REpresentational State Transfer (REST) service with ACS and OAuth.
If you are not familiar with OAuth, you can learn more on this at http://oauth.net/.
To create the REST WCF service, we will use the WCF REST service project template installed with the WCF REST Multi-Project Template Visual Studio extension. To install this extension, go to Tools | Extension Manager in Visual Studio 2010 and search for WCF REST in the online gallery:
You can also go to the online Visual Studio gallery and install the extension from http://visualstudiogallery.msdn.microsoft.com/9272629c-74e2-423b-9841-f20b57f855fe.
If the extension is successfully installed, you will see a WCF REST Service template in Visual Studio. To create the service, perform the following steps:
SampleService
with ReservationService
and the SampleItem
entity in the Entities
project by the Reservation
entity. Once done, go to Global.ascx
of the service host web application and update the RegisterRoutes
method, as shown in the following code snippet:private void RegisterRoutes() { RouteTable.Routes.Add(new ServiceRoute("ReservationService", new WebServiceHostFactory(), typeof(ReservationService.Service.ReservationService))); }
The final changes should look like the following screenshot:
CreateReservation
method from the client. For this, go to Program
class in the ReservationService.Client
project and replace all the code with the following code snippet:class Program { static void Main(string[] args) { // First start the web project, then the client WebClient client = new WebClient(); client.Headers["Content-type"] = "text/xml"; var url = new Uri("http://localhost:2795 /ReservationService/CreateReservation"); var newReservation = new Reservation { GuestName = "Jack Sparrow", ReservationDate = DateTime.Now.AddDays(20) }; var resString = EntitySerializer.GetString <Reservation>(newReservation); var result = client.UploadString (url, "POST", resString); var createdReservation = EntitySerializer. GetObject<Reservation>(result); } }
Note the use of the EntitySerializer
helper class in the preceding code snippet. The class is used to serialize/de-serialize the objects to/from the XML string and the implementation for this class can be found in the source code under the Helpers
folder in the client project. Run the client and verify that the calls to the service are successful to ensure that you have set up the service correctly.
Next, we will create a relying party application for our WCF REST service that we created previously. Perform the following steps:
MyACSNamespace
namespace that you created in the Configuring Access Control Service for an ASP.NET MVC 3 relying party recipe).Click on Add to add a new relying party application and enter the application name and the realm URL in the Name and Realm fields respectively, as shown in the following screenshot:
So far, you have configured the REST service in ACS and you need to configure the service to handle the incoming tokens from the client for authentication. Perform the following steps:
Web.config
of the service host and add the following settings. The IssuerSigningKey
is the token signing key generated while adding your service as a relying party:<appSettings> <add key="ACSHostName" value="accesscontrol.windows.net"/> <add key="ACSNamespace" value="your namespace"/> <add key="IssuerSigningKey" value="your key"/> </appSettings>
SecurityAuthorizationManager
. The source code for this recipe includes one such implementation in the ACSAuthorizationManager
class. The class overrides the CheckAccessCore
method of SecurityAuthorizationManager
and checks the access token of the incoming message in the header. If it is valid, the appropriate service method is called else the Unauthorized
error message is sent in the response:public class ACSAuthorizationManager : ServiceAuthorizationManager { string serviceNamespace = ConfigurationManager.AppSettings.Get("ACSNamespace"); string acsHostName = ConfigurationManager.AppSettings.Get("ACSHostName"); string trustedTokenPolicyKey = ConfigurationManager.AppSettings.Get ("IssuerSigningKey"); string trustedAudience = "http://localhost:2795/ReservationService/"; protected override bool CheckAccessCore(OperationContext operationContext) { string headerValue = WebOperationContext.Current.IncomingRequest. Headers[HttpRequestHeader.Authorization]; // check that a value is there if (string.IsNullOrEmpty(headerValue)) { CreateUnauthorizedResponse(); return false; } // check that it starts with 'WRAP' if (!headerValue.StartsWith("WRAP ")) { CreateUnauthorizedResponse(); return false; } string[] nameValuePair = headerValue.Substring("WRAP ".Length).Split(new char[] { '=' }, 2); if (nameValuePair.Length != 2 || nameValuePair[0] != "access_token" || !nameValuePair[1].StartsWith(""") || !nameValuePair[1].EndsWith(""")) { CreateUnauthorizedResponse(); return false; } // trim off the leading and trailing double-quotes string token = nameValuePair[1].Substring(1, nameValuePair[1].Length - 2); // create a token validator TokenValidator validator = new TokenValidator( this.acsHostName, this.serviceNamespace, this.trustedAudience, this.trustedTokenPolicyKey); // validate the token if (!validator.Validate(token)) { CreateUnauthorizedResponse(); return false; } return true; }
Global.asax
file to enable the use of the previously created authorization manager in the WCF pipeline. For this, first create a new class called SecureWebServiceHostFactory
inherited from WebServiceHostFactory
and override the CreateServiceHost
method to set ServiceAuthorizationManager
of the host to the instance of the custom authorization manager that you created in step 2:public class SecureWebServiceHostFactory : WebServiceHostFactory { protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses) { ServiceHost host = base.CreateServiceHost(serviceType, baseAddresses); host.Authorization.ServiceAuthorizationManager = new ACSAuthorizationManager(); return host; } public override ServiceHostBase CreateServiceHost(string constructorString, Uri[] baseAddresses) { ServiceHostBase host = base.CreateServiceHost(constructorString, baseAddresses); host.Authorization.ServiceAuthorizationManager = new ACSAuthorizationManager(); return host; } }
Update the RegisterRoutes
method in the Global.asax
file to use the new service host factory:
public class Global : HttpApplication { void Application_Start(object sender, EventArgs e) { RegisterRoutes(); } private void RegisterRoutes() { RouteTable.Routes.Add(new ServiceRoute("ReservationService", new SecureWebServiceHostFactory(), typeof(ReservationService.Service.ReservationService))); } }
So far, we are done with the server side. Let's see how we can call the service passing in the required security token so that the call doesn't fail. Perform the following steps:
App.config
file to the client project (your console application) and add the following application settings:<appSettings> <add key="ACSHostName" value="accesscontrol.windows.net"/> <add key="ACSNamespace" value="--your namespace--"/> <add key="ServiceIdentityUserName" value="--your user name- -"/> <add key="ServiceIdentityCredentialPassword" value="--your password--"/> </appSettings>
You can find ServiceIdentityUserName
and ServiceIdentityCredentialPassword
on the Service identities page of ACS Management Portal. You will need to click on Password under the Credentials section to view your password:
Program
class of your console application (client) with the following code snippet and run the application:class Program { static void Main(string[] args) { try { // First start the web project, then the client WebClient client = new WebClient(); var token = RetrieveACSToken(); client.Headers.Add("Authorization", token); client.Headers.Add("Content-type", "text/xml"); var url = new Uri("http://localhost:2795/ReservationService /CreateReservation"); var newReservation = new Reservation { GuestName = "Jack Sparrow", ReservationDate = DateTime.Now.AddDays(20) }; var resString = EntitySerializer.GetString <Reservation>(newReservation); var result = client.UploadString(url, "POST", resString); var createdReservation = EntitySerializer.GetObject <Reservation>(result); } catch (Exception ex) { throw ex; } } private static string RetrieveACSToken() { var acsHostName = ConfigurationManager.AppSettings. Get("ACSHostName"); var acsNamespace = ConfigurationManager.AppSettings. Get("ACSNamespace"); var username = ConfigurationManager.AppSettings. Get("ServiceIdentityUserName"); var password = ConfigurationManager.AppSettings. Get("ServiceIdentityCredentialPassword"); var scope = "http://localhost:2795/ReservationService/"; // request a token from ACS WebClient client = new WebClient(); client.BaseAddress = string.Format("https://{0}.{1}", acsNamespace, acsHostName); NameValueCollection values = new NameValueCollection(); values.Add("wrap_name", username); values.Add("wrap_password", password); values.Add("wrap_scope", scope); 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=", StringComparison.OrdinalIgnoreCase)) .Split('=')[1]; var decodedToken = string.Format("WRAP access_token="{0}"", HttpUtility.UrlDecode(token)); return decodedToken; } }
Note that in the Main
method in the preceding code snippet, the token is retrieved from ACS and is put in the header of the message prior to making the call to the REST service. This is because the authorization implementation is set up at the service to look for the token in the header as described in the previous section. If everything is set up correctly, you will see a successful response from the service. To validate whether the authorization is really working at the service, try changing one of your username/password settings in App.config
to invalid values and run the application. You should get a 404 Unauthorized error from the service.
The client application first sends a web request to ACS to retrieve the ACS token using the service identity configured with ACS, which it can pass to the WCF REST service. Finally, a REST call is made to the service passing on the authorization token retrieved from ACS. The token is validated at the service using the custom authorization manager implementation and, if it is successful, the call is made to the service method else an unauthorized exception is thrown:
18.222.182.66