09 April 2013 Access Control Service, Azure AD, Login, Security, WAAD Robert Muehsig

Buzzwords?! Nein – am Ende passt es doch alles zusammen und ist sogar ziemlich cool. Ich hatte bereits das kostenlose Buch über Claim-based Authentifizierung empfohlen – heute geht es um folgendes Szenario:

Wie kann ich meine ASP.NET Seite auf “einfachste” und ohne grosse Zauberei mit dem Access Control Service verbinden? 

Achtung: Microsoft selbst empfiehlt eher ein grösseres Tooling (im Falle von ASP.NET ist das auch echt schick!), was am Ende die web.config unglaublich aufbläht – zudem hatte ich Schwierigkeiten weil die Handler da “zu viel automatisch” machen.
Die Idee hab ich von diesem Post – und funktioniert bislang ;) Zudem lernt man einiges über die Protokolle dahinter.

Kurzfassung: Was ist der Access Control Service (ACS)

Dieser Azure Service darf man sich wie einen zentralen Verteiler für Authentifizierung vorstellen – man kann verschiedene Authentifizierungsanbieter einklicken und ACS übernimmt dabei die Transformation der Tokens in das was wir benötigen. Eure Anwendung soll ADFS (für Business Kunden), Microsoft Account und Facebook unterstützen? Puh… Der Access Control Service kann hierfür interessant sein! 

Mehr Informationen gibt es hier.

 image

Was ist ein Microsoft Account?

Der Microsoft Account war früher bekannt unter dem Namen Windows Live ID. Diese Bezeichnung sieht man immer wieder, aber eigentlich find ich Microsoft Account vom Naming her besser, daher bleiben wir dabei.

Was ist ein JSON Web Token (JWT)?

Über dieses Format werden am Ende die Claims ausgetauscht. Mehr dazu hier.

Vorbereitung: ACS Namespace

Für das Sample benötigen wir ein ACS Namespace, der im Management-Portal eingerichtet werden kann:

image

Dort legen wir eine “Relying party application” an – dies repräsentiert unsere Claim-based Anwendung:

(aktuell ist die ACS-Verwaltung immer noch im alten look)

image

Wichtigster Punkte:

- der Realm

- die Return URL

- das Token Format auf JWT umstellen

- Bei Identity Provider “Windows Live ID” auswählen

- Eine Rule Group anlegen (bei mir ist die schon angelegt)

In der Rule Group sicher stellen, dass auch die Claims weitergegeben werden:

image

Welche Daten bekomme ich durch die Windows Live ID/Microsoft Account?

Fast keine – bis auf einen kryptischen Key im “nameidentifier”-Claim. D.h. man kommt aktuell an absolut 0 Profil Informationen und man muss den Nutzer diese Geschichte manuell nochmal eintragen lassen. Der Key ist auch pro ACS Namespace eindeutig – die Realms spielen (anders als es vielleicht in der MSDN steht) keine Rolle für den “nameidentifier”

Schauen wir uns das Sample an:

Standard-MVC App – kein Azure Auth Toolkit etc. in Verwendung! Nur der Link hinter “Log in” verweisst auf einen Controller den ich gleich zeige.

image

Bei “Log in” werde ich sofort auf die Microsoft Login Seite umgeleitet.

image

Kleiner Hinweis: Die Parameter in der URL wie “wa” oder “wtrealm” stammen aus dem WS-Federation Spec.

Oben in der Adresse sieht man auch unseren Access Control Service wieder (bzw. den ich angelegt hab). Danach wird man direkt auf die Startseite umgeleitet und ist unter einem sehr seltsamen Namen “angemeldet”

image

Was passiert im Hintergrund?

 

Hier der komplette Code meines Authentifzierungs-Controllers:

/// <summary>
    /// Everything based on WS-Federation: http://docs.oasis-open.org/wsfed/federation/v1.2/os/ws-federation-1.2-spec-os.html
    /// </summary>
    public class AuthController : Controller
    {
        public ActionResult Index()
        {
            // Microsoft Account Login

            // 2 Option - use hosted Loginpage from ACS or host your own...

            // based on Azure ACS Portal - Login Page Integration - Option 1:
            // Problem: wfresh=0 not supported - logout is :/
            return new RedirectResult("https://codeinside.accesscontrol.windows.net:443/v2/wsfederation?wa=wsignin1.0&wtrealm=urn:sample:wifless");

            // Option 2: Use the link from the JSON - but needed more coding.
            // But wfresh=0 is supported!
            //return new RedirectResult("https://login.live.com/login.srf?wa=wsignin1.0&wtrealm=https%3a%2f%2faccesscontrol.windows.net%2f&wreply=https%3a%2f%2fcodeinside.accesscontrol.windows.net%2fv2%2fwsfederation&wp=MBI_FED_SSL&wctx=cHI9d3NmZWRlcmF0aW9uJnJtPXVybiUzYXNhbXBsZSUzYXdpZmxlc3Mmcnk9aHR0cCUzYSUyZiUyZmxvY2FsaG9zdCUzYTg4MTclMmZBdXRoJTJmQ2FsbGJhY2s1&wfresh=0");
        }

        [ValidateInput(false)]
        public ActionResult Callback(string wresult, string wa)
        {
            if (wa == "wsignin1.0")
            {
                var wrappedToken = XDocument.Parse(wresult);
                var binaryToken = wrappedToken.Root.Descendants("{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd}BinarySecurityToken").First();
                var tokenBytes = Convert.FromBase64String(binaryToken.Value);
                var token = Encoding.UTF8.GetString(tokenBytes);

                var result = AcsAuthorizationTokenValidator.RetrieveClaims(token,
                                                                          "https://codeinside.accesscontrol.windows.net/",
                                                                          new List<string>() { "urn:sample:wifless" });

                var name = result.Claims.Single(x => x.Type == ClaimTypes.NameIdentifier);

                FormsAuthentication.SetAuthCookie(name.Value, false);
            }
            else if (wa == "wsignoutcleanup1.0")
            {
                FormsAuthentication.SignOut();
            }
            return RedirectToAction("Index", "Home");
        }

        public ActionResult Logout()
        {
            FormsAuthentication.SignOut();

            return RedirectToAction("Index", "Home");
        }

    }

Über Auth/Index werd ich direkt zum ACS Login bzw. direkt weiter zum Microsoft Login umgeleitet. Die Links zu dieser Seite findet man in seinem ACS-Namespace unter “Application integration” – in meinem Fall nehm ich die Option 1.

image

Wenn ich mehrere Identity Provider (z.B. Facebook oder Google) ebenfalls auf das Sample zuschalten (was wirklich so einfach geht wie es klingt), dann würde eine etwas spartanische Login-Seite mit einer Auswahl den User begrüssen.

Der Nachteil der Variante 1:

- Sehr spartanisches UI bei mehreren Identity Providern

- Den Login erzwingen ist gar nicht so einfach. Wenn man irgendwie mit irgendeinem Microsoft Account eingeloggt ist, sieht der Nutzer noch nichtmal den Account. Da man selbst auch keinen Zugriff auf den Microsoft Account Namen hat ist es schwer dem User zu vermitteln mit welchem Account er überhaupt gerade arbeitet.

- Logout ist auch schwieriger – aber ich zeig gleich eine Variante wie es gehen könnte (die ich jetzt nicht umgesetzt hab)

Das Gute an der Variante: Es ist ein sehr simpler Link mit einem Callback…

Noch zur Variante 2:

Dort wird ein Link zu einem JSON angeboten was man recht einfach auswerten kann – mit allen Identity Providern und den richtigen Login-URLs sowie der Logout URL.

image

Bei dieser LoginUrl (die direkt auf login.live etc. zeigt) wird auch der Parameter “wfresh=0” unterstützt – d.h. der Nutzer muss sich explizit vorher anmelden – was schon wesentlich besser ist.

Zum Callback:

Nachdem die Authentifizierung durch ist schickt der Access Control Service die Antwort zum eingetragenen Callback URL – darin enthalten ist ein XML:

<t:RequestSecurityTokenResponse xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
	<t:Lifetime>
		<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2013-04-08T23:49:21.754Z</wsu:Created>
		<wsu:Expires xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2013-04-08T23:59:21.754Z</wsu:Expires>
	</t:Lifetime>
	<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
		<EndpointReference xmlns="http://www.w3.org/2005/08/addressing">
			<Address>urn:sample:wifless</Address>
		</EndpointReference>
	</wsp:AppliesTo>
	<t:RequestedSecurityToken>
		<wsse:BinarySecurityToken wsu:Id="_d3b3e3f6-4bd2-424c-89e9-ebbe662c1305-E64ACB699C73604D58CEA265FE69ECA9" ValueType="urn:ietf:params:oauth:token-type:jwt" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">ZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKU1V6STFOaUlzSW5nMWRDSTZJamRUZDFaRmMyNHpVbW93YW5WblNWUjBWRlp4WjJwQ1VVWlJVU0o5LmV5SmhkV1FpT2lKMWNtNDZjMkZ0Y0d4bE9uZHBabXhsYzNNaUxDSnBjM01pT2lKb2RIUndjem92TDJOdlpHVnBibk5wWkdVdVlXTmpaWE56WTI5dWRISnZiQzUzYVc1a2IzZHpMbTVsZEM4aUxDSnVZbVlpT2pFek5qVTBOalE1TmpFc0ltVjRjQ0k2TVRNMk5UUTJOVFUyTVN3aWJtRnRaV2xrSWpvaVMwMVpOR3A1ZUdGSFRWWllZbkl3V2xsRE9DOXJlUzlFTjJabWNXRTBZM1Z0U3pKQ1dFNWxWRUZIV1QwaUxDSnBaR1Z1ZEdsMGVYQnliM1pwWkdWeUlqb2lkWEpwT2xkcGJtUnZkM05NYVhabFNVUWlmUS5VeDFsa0hDaDdBYl85d2lQN1BnR20zNjR4T2tsOHBHUEhNSEF2ZVJSMEVGRTR2VHRybV9wbmF3ajR0STVQZlVFOXB1d1JYNDQzOWRhSkQ4MzM4SG5yOVI1VktmZ3JIYnRuTlM2UVJoeGNIUDREZjBINVFQcFZ2Tm9VU1lJa2ZLWTVFUXE5ei1JVEpiNVZhWEZscF81OENUaDJtMkVYYkIwR3pRLV9BQlF3MWIxZEd1dHV5VHVfdEhWWUdPek9iTDdac0l1dHJXQTdjOEtJYUpoT21NVzNxNEp4RVBLNllnTURERWxkcmR6OFRTVTFZVE8xY3ZqMGZZVHh3d0t3MVlFQmlEamJSLUVxN1FKQzZDZWRsZjJmaDRDbkNBQlFTZzlhbmRVSDZSeTBqU3ctX2tZaHBZOTdkdkpRRDh4eDY2eVMxQmZBNXViQlVMbXF6eWh3U2NYaXc=</wsse:BinarySecurityToken>
	</t:RequestedSecurityToken>
	<t:TokenType>urn:ietf:params:oauth:token-type:jwt</t:TokenType>
	<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
	<t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>
</t:RequestSecurityTokenResponse>

Interessant hier ist der “BinarySecurityToken”, wenn wir diesen base64 kodierten String umwandeln bekommen wird dies:

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjdTd1ZFc24zUmowanVnSVR0VFZxZ2pCUUZRUSJ9.eyJhdWQiOiJ1cm46c2FtcGxlOndpZmxlc3MiLCJpc3MiOiJodHRwczovL2NvZGVpbnNpZGUuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldC8iLCJuYmYiOjEzNjU0NjQ5NjEsImV4cCI6MTM2NTQ2NTU2MSwibmFtZWlkIjoiS01ZNGp5eGFHTVZYYnIwWllDOC9reS9EN2ZmcWE0Y3VtSzJCWE5lVEFHWT0iLCJpZGVudGl0eXByb3ZpZGVyIjoidXJpOldpbmRvd3NMaXZlSUQifQ.Ux1lkHCh7Ab_9wiP7PgGm364xOkl8pGPHMHAveRR0EFE4vTtrm_pnawj4tI5PfUE9puwRX4439daJD8338Hnr9R5VKfgrHbtnNS6QRhxcHP4Df0H5QPpVvNoUSYIkfKY5EQq9z-ITJb5VaXFlp_58CTh2m2EXbB0GzQ-_ABQw1b1dGutuyTu_tHVYGOzObL7ZsIutrWA7c8KIaJhOmMW3q4JxEPK6YgMDDEldrdz8TSU1YTO1cvj0fYTxwwKw1YEBiDjbR-Eq7QJC6Cedlf2fh4CnCABQSg9andUH6Ry0jSw-_kYhpY97dvJQD8xx66yS1BfA5ubBULmqzyhwScXiw

Nun kommt ein NuGet Package ins Spiel – der JWTSecurityTokenHandler über dies und etwas Code (basierend auf diesem Microsoft Sample) bekommen wir am Ende die Claims als ClaimsPrincipal:

Update: In der aktuellen (21.08.2013) Fassung des JWTSecurityTokenHandler gab es ein paar Änderung.
Die erste: Aus JWTSec... wurde JwtSecurity.
Die größere Änderung: Der TokenHandler prüft ob ein X.509 Zertifkat im Zertifikatsstore enthalten ist. Wenn man diesen Check nicht machen möchte muss man dem Handler dies zuweisen: "tokenHandler.CertificateValidator = X509CertificateValidator.None;". Dieser Punkt ist auch hier und hier näher beschrieben.

/// <summary>
    /// Based on this sample: http://code.msdn.microsoft.com/AAL-Native-Application-to-fd648dcf/sourcecode?fileId=62849&pathId=697488104
    /// </summary>
    public class AcsAuthorizationTokenValidator
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="token">= Token</param>
        /// <param name="allowedAudience">= Azure ACS Namespace</param>
        /// <param name="issuers">= RPs in ACS</param>
        /// <returns></returns>
        public static ClaimsPrincipal RetrieveClaims(string token, string issuer, List<string> allowedAudiences)
        {
            JWTSecurityTokenHandler tokenHandler = new JWTSecurityTokenHandler();

            // Set the expected properties of the JWT token in the TokenValidationParameters 
            TokenValidationParameters validationParameters = new TokenValidationParameters()
                                                                 {
                                                                     AllowedAudiences = allowedAudiences,
                                                                     ValidIssuer = issuer,
                                                                     SigningToken = new X509SecurityToken(new X509Certificate2(GetSigningCertificate(issuer + "FederationMetadata/2007-06/FederationMetadata.xml")))
                                                                 };

            return tokenHandler.ValidateToken(token, validationParameters); 
        }

        private static byte[] GetSigningCertificate(string metadataAddress)
        {
            if (metadataAddress == null)
            {
                throw new ArgumentNullException(metadataAddress);
            }

            using (XmlReader metadataReader = XmlReader.Create(metadataAddress))
            {
                MetadataSerializer serializer = new MetadataSerializer()
                                                    {
                                                        CertificateValidationMode = X509CertificateValidationMode.None
                                                    };

                EntityDescriptor metadata = serializer.ReadMetadata(metadataReader) as EntityDescriptor;

                if (metadata != null)
                {
                    SecurityTokenServiceDescriptor stsd = metadata.RoleDescriptors.OfType<SecurityTokenServiceDescriptor>().First();

                    if (stsd != null)
                    {
                        X509RawDataKeyIdentifierClause clause = stsd.Keys.First().KeyInfo.OfType<X509RawDataKeyIdentifierClause>().First();

                        if (clause != null)
                        {
                            return clause.GetX509RawData();
                        }
                        throw new Exception("The SecurityTokenServiceDescriptor in the metadata does not contain the Signing Certificate in the <X509Certificate> element");
                    }
                    throw new Exception("The Federation Metadata document does not contain a SecurityTokenServiceDescriptor");
                }
                throw new Exception("Invalid Federation Metadata document");
            }
        } 
    }

Ergebnis:

Claims…von Microsoft! Leider keine “menschlich-lesbaren” – aber immerhin gibt es dabei kein Datenschutz Risiko ;)

image

ACS to the rescue!

Der Access Control Service ist ein recht interessanter Dienst und steht auch in keiner Konkurrenz zum Azure AD – man kann auch Azure AD und Facebook in seiner Anwendung über den ACS nutzen. Stellt es euch als Plugin-fähigen Identitiy Provider vor.

Ich versuch demnächst noch zum neuen “Azure AD” zu schreiben – bis dahin können interssierte ein Blick hier reinwerfen wenn man ACS und Azure Active Directory zusammen nutzen möchte.

 

Download Sample @ GitHub


Written by Robert Muehsig

Software Developer - from Saxony, Germany - working on primedocs.io. Microsoft MVP & Web Geek.
Other Projects: KnowYourStack.com | ExpensiveMeeting | EinKofferVollerReisen.de