Azure Access Control Service, Microsoft Account Login, JSON Web Token und ASP.NET – “ohne WIF-Magic”

image.png

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

Wenn dir der Blogpost gefallen hat, dann hinterlasse doch einen Kommentar. Wenn du auf dem Laufenden bleiben willst, abonniere unseren RSS Feed oder folge uns auf Twitter.

About the author

Written by

Hi, ich bin Robert Mühsig und bin Webentwickler und beschäftige mich mit Web-Frameworks auf dem Microsoft Web Stack und scheue mich auch nicht vor Javascript. Der Blog begann als "Problemsammelstelle und Lösungshilfe" und seitdem schreibe ich hier alles auf. Seit 2008 bin ich Microsoft MVP für ASP.NET. Treffen kann man mich online via Twitter (@robert0muehsig) oder hier.

One Response

Comment on this post

Letzte Posts

  • image.png
    Microsoft Account Login via ASP.NET Identity

    Der Microsoft Account ist die zentrale Identifikationsstelle in der “Consumer-Microsoft-Welt”, allerdings ist das Einbinden eben dieser in die eigene Applikation eher schwierig. Das “Live SDK” ist nun unter dem OneDrive Dev Center zu finden und ganz professionell wurden auch alle Links zum alten Live SDK damit unbrauchbar gemacht. Beim Microsoft Account ist es auch unmöglich […]

  • image.png
    Zeitgesteuerte Azure WebJobs – so einfach kann Azure sein

    Das noch in Entwicklung befindliche Azure WebJob SDK bietet einige coole Features zum Verarbeiten und Bereitstellen von Daten. Bekanntes Beispiel ist das Sample welches auf eine Azure Queue lauscht und sobald ein Item da vorhanden ist anfängt dies zu verarbeiten. Szenario: Zeitgesteuerte Aktivitäten – ohne Queue und co. Mein Szenario war allerdings wesentlich trivialer: Ich […]

  • image.png
    Get Involved in OSS! Ja, aber wie geht das denn mit GitHub?

    Auch im .NET Lager gibt es Bewegung im OSS Bereich und es gibt verschiedene Arten wie man bei einem Open Source Projekt “Contributed”. Was zählt alles zu “Contribution”? Unter “Contribution” läuft eigentlich alles – ob es Fragen/Probleme zu dem Projekt via Issues ist oder Dokumentation nachreicht oder ob man darüber bloggt oder das Projekt vorstellt. […]

  • HowTo: Web.config samt eigener ConfigSection zur Laufzeit ändern

    In dem HowTo geht es darum wie man die Web.config zur Laufzeit ändert und was es dabei zu beachten gilt. Das ganze klappt auch mit komplexeren ConfigSections. Eigene ConfigSection? Vor einer ganzen Weile habe ich mal über das Erstellen einer eigenen ConfigSection geschrieben – im Grunde nutzen wir jetzt fast dieselbe Config. Zur Laufzeit? Startet […]

  • image.png
    HowTo: RSS Feeds mit ASP.NET MVC erstellen

    Ich hatte hier schon öfters geschrieben wie man RSS oder Atom Feeds konsumieren kann – jetzt geht es aber mal um das erstellen bzw. publishen eines eigenen Feeds. Trotz des Alters von den Feed Standards und die vielen Abgesänge von Facebook, Twitter und Google auf RSS/Atom halte ich diese einfach zu konsumierende API für ziemlich […]

Amazon Shop

Facebook