Clarity, Technology, and Solving Problems | PracticeThis.com
WP7 App with Key Windows Azure resources – Slides, Videos, How-To’s, and T-shooting – for quick consumption on the go.
Windows Azure AppFabric Access Control Service (ACS) v2 has a feature called Error URL that allows a web site to display friendly message in case of error during the authentication/federation process. For example, during the authentication with Facebook or Google a user asked for consent after successful authentication. If the user denies ACS generates an error that could be presneted to a user in a friendly manner. Another case is when there is a mis-configuration at ACS level, for example, no rules generated for specific identity provider which results in error generated by ACS.
How to show friendly error message for these cases, branded with the look an feel as the rest of my website?
The solution is using Error URL feature available through ACS management portal. ACS generated JSON encoded error message and passes it to your error page. You need to specify the URL of the error page on ACS management portal so that ACS will know where to pass the information. Your error page should pars the JSON encoded error message and render appropriate HTML for the end user. Here is a sample JSON encoded error message:
{"context":null,"httpReturnCode":401,"identityProvider":"Google","timeStamp":"2010-12-17 21:01:36Z","traceId":"16bba464-03b9-48c6-a248-9d16747b1515","errors":[{"errorCode":"ACS30000","errorMessage":"There was an error processing an OpenID sign-in response."},{"errorCode":"ACS50019","errorMessage":"Sign-in was cancelled by the user."}]}
To process error messages from ACS complete these steps:
To enable Error URL feature for your relying party:
This step helps creating Error helpers classes for deserializing JSON encoded error messages.
To create Error Helper Classes:
public class Error { public string errorCode { get; set; } public string errorMessage { get; set; } }
public class ErrorDetails { public string context { get; set; } public int httpReturnCode { get; set; } public string identityProvider { get; set; } public Error[] errors { get; set; } }
To process JSON encoded error message generated by ACS:
<asp:Label ID="lblIdntityProvider" runat="server"></asp:Label>
<asp:Label ID="lblErrorMessage" runat="server"></asp:Label>
using System.Web.Script.Serialization;
JavaScriptSerializer serializer = new JavaScriptSerializer(); ErrorDetails error = serializer.Deserialize<ErrorDetails>( Request["ErrorDetails"] ); lblIdntityProvider.Text = error.identityProvider; lblErrorMessage.Text = string.Format("Error Code {0}: {1}", error.errors[0].errorCode, error.errors[0].errorMessage);
To configure anonymous access to the error page:
<location path="ErrorPage.aspx"> <system.web> <authorization> <allow users="*" /> </authorization> </system.web> </location>
To test Error URL feature:
uri:WindowsLiveID Error Code ACS50000: There was an error issuing a token.
Another way to test it is denying user consent. This is presented when you login using Facebook or Google.
This simple walkthrough shows how to use Management Service of Windows Azure AppFabric Access Control Service (ACS) v2 to programmatically add Facebook as an identity provider. Complete walkthrough can be found here - Windows Azure AppFabric Access Control Service v2 - Adding Identity Provider Using Management Service.
The code related to Facebook is cannibalized from the end-to-end SaaS sample - FabrikamShipping SaaS Demo Source Code.
Code samples with other functionalities is available here - Code Sample: Management Service.
Other ACS code samples available here - Code Samples Index.
Code related to this post can be found here. Next I will call out changes and differences comparing to Windows Azure AppFabric Access Control Service v2 - Adding Identity Provider Using Management Service.
To complete this walkthrough:
var facebookKeys = new[] { new IdentityProviderKey { IdentityProvider = facebook, StartDate = DateTime.UtcNow, EndDate = DateTime.UtcNow.AddYears(1), Type = "ApplicationKey", Usage = "ApplicationId", Value = Encoding.Default.GetBytes(facebookAppId) }, new IdentityProviderKey { IdentityProvider = facebook, StartDate = DateTime.UtcNow, EndDate = DateTime.UtcNow.AddYears(1), Type = "ApplicationKey", Usage = "ApplicationSecret", Value = Encoding.Default.GetBytes(facebookAppSecret) } };
This is quick intro to how to use Windows Azure AppFabric Access Control Service (ACS) v2 Management Service. In this sample I will show you what’s needed to add ADFS as an identity provider.
Summary of steps:
The full sample with other functionalities is available here - Code Sample: Management Service.
The simplified sample I used for this walkthrough is here.
string serviceIdentityUsernameForManagement = "ManagementClient"; string serviceIdentityPasswordForManagement = "QL1nPx/iuX...YOUR PASSWORD FOR MANAGEMENT SERVICE GOES HERE"; string serviceNamespace = "YOUR NAMESPACE GOES HERE"; string acsHostName = "accesscontrol.appfabriclabs.com"; string signingCertificate = "MIIDIDCCAgigA... YOUR SIGNING CERT GOES HERE"; //WILL BE USED TO CACHE AND REUSE OBTAINED TOKEN. string cachedSwtToken;
using System.Web; using System.Net; using System.Data.Services.Client; using System.Collections.Specialized; using System.Web.Script.Serialization;
To create Management Service proxy:
string managementServiceHead = "v2/mgmt/service/"; string managementServiceEndpoint = string.Format("https://{0}.{1}/{2}", serviceNamespace, acsHostName, managementServiceHead); ManagementService managementService = new ManagementService(new Uri(managementServiceEndpoint)); managementService.SendingRequest += GetTokenWithWritePermission;
public static ManagementService CreateManagementServiceClient() { string managementServiceHead = "v2/mgmt/service/"; string managementServiceEndpoint = string.Format("https://{0}.{1}/{2}", serviceNamespace, acsHostName, managementServiceHead); ManagementService managementService = new ManagementService(new Uri(managementServiceEndpoint)); managementService.SendingRequest += GetTokenWithWritePermission; return managementService; } public static void GetTokenWithWritePermission(object sender, SendingRequestEventArgs args) { GetTokenWithWritePermission((HttpWebRequest)args.Request); } /// <summary> /// Helper function for the event handler above, adding the SWT token to the HTTP 'Authorization' header. /// The SWT token is cached so that we don't need to obtain a token on every request. /// </summary> public static void GetTokenWithWritePermission(HttpWebRequest args) { if (cachedSwtToken == null) { cachedSwtToken = GetTokenFromACS(); } args.Headers.Add(HttpRequestHeader.Authorization, string.Format("OAuth {0}", cachedSwtToken)); } /// <summary> /// Obtains a SWT token from ACSv2. /// </summary> private static string GetTokenFromACS() { // request a token from ACS WebClient client = new WebClient(); client.BaseAddress = string.Format("https://{0}.{1}", serviceNamespace, acsHostName); NameValueCollection values = new NameValueCollection(); values.Add("grant_type", "password"); values.Add("client_id", serviceIdentityUsernameForManagement); values.Add("username", serviceIdentityUsernameForManagement); values.Add("client_secret", serviceIdentityPasswordForManagement); values.Add("password", serviceIdentityPasswordForManagement); byte[] responseBytes = client.UploadValues("/v2/OAuth2-10/rp/AccessControlManagement", "POST", values); string response = Encoding.UTF8.GetString(responseBytes); // Parse the JSON response and return the access token JavaScriptSerializer serializer = new JavaScriptSerializer(); Dictionary<string, object> decodedDictionary = serializer.DeserializeObject(response) as Dictionary<string, object>; return decodedDictionary["access_token"] as string; }
To add Identity Provider:
Issuer issuer = new Issuer { Name = identityProviderName }; svc.AddToIssuers(issuer); svc.SaveChanges(SaveChangesOptions.Batch);
IdentityProvider identityProvider = new IdentityProvider() { DisplayName = identityProviderName, Description = identityProviderName, WebSSOProtocolType = "WsFederation", IssuerId = issuer.Id }; svc.AddObject("IdentityProviders", identityProvider);
IdentityProviderKey identityProviderKey = new IdentityProviderKey() { DisplayName = "SampleIdentityProviderKeyDisplayName", Type = "X509Certificate", Usage = "Signing", Value = Convert.FromBase64String(signingCertificate), IdentityProvider = identityProvider, StartDate = startDate, EndDate = endDate, }; svc.AddRelatedObject(identityProvider, "IdentityProviderKeys", identityProviderKey);
IdentityProviderAddress realm = new IdentityProviderAddress() { Address = "http://yourdomain.com/sign-in/", EndpointType = "SignIn", IdentityProvider = identityProvider, }; svc.AddRelatedObject(identityProvider, "IdentityProviderAddresses", realm);svc.SaveChanges(SaveChangesOptions.Batch);
foreach (RelyingParty rp in svc.RelyingParties) { // skip the built-in management RP. if (rp.Name != "AccessControlManagement") { svc.AddToRelyingPartyIdentityProviders(new RelyingPartyIdentityProvider() { IdentityProviderId = identityProvider.Id, RelyingPartyId = rp.Id }); } }svc.SaveChanges(SaveChangesOptions.Batch);
To test your work: