Bereits mit VS 2012 und dem “Identity Toolkit” konnte man sich seine ASP.NET Applikation recht einfach so konfigurieren, dass der Anwender sich beim Windows Azure Active Directory (WAAD) anmelden kann. Mit VS 2013 ist die Sache natürlich noch eleganter geworden, aber trotzdem bin ich bei diesen Tooling Geschichten skeptisch. In dem Blogpost zeige ich was eigentlich hinter den Kulissen passiert und dass da eigentlich gar keine so große Magie dahinter steckt.
Stop… Windows Azure Active Directory? What?
Wer noch nie was vom Windows Azure AD gehört hat, dem empfehle ich dies hier. In Kürze: WAAD ist im Grunde ein Multimandanten Active Directory in der Cloud. Alllerdings ohne LDAP Zugriff, sondern mit einer REST Schnittstelle und verschiedenen Authentifizierungsverfahren (OAuth, WSFed etc.). Technisch gesehen sind es komplett getrennte Sachen – aber vom Konzept her ist es sehr ähnlich.
Die Toolkits – was machen die denn?
Wenn man mit VS 2013 ein neues ASP.NET Projekt startet wird man gefragt welche Authentifizierungsvariante man haben möchte.
Unter “Organizational Accounts” kann man seine WAAD Daten eintragen und das Tool konfiguriert die Anwendung.
Ergebnis davon:
Falls man nicht bereits irgendwo im WAAD-Universum (z.b. bei Office 365) eingeloggt ist, wird man direkt auf die Anmeldeseite umgeleitet:
Die Entmystifizierung oder: Wir machen das jetzt selbst. Ohne Magic.
Als erstes müssen wir eine Anwendung unter userem Azure AD erstellen. Die App URI sollte der URL entsprechen unter dem die Anwendung im IIS Express gehostet wird. Grundsätzlich ist eine HTTPS Adresse von Vorteil und spätestens wenn die Anwendung für andere Azure AD Tenants öffnen möchte, dann wird HTTPS benötigt.
Wichtig für den Demo-Code ist die Reply URL die auf Auth/Callback zeigen muss.
Damit der Demo-Code läuft benötigt man sowohl Daten von dem Azure AD Mandanten als auch von der Azure AD App:
(Achtung – einige dieser Einstellungen sind eigentlich “secret” – ich lösch die Anwendung danach wieder)
1: private string AzureAdAppClientId;
2: private string AzureAdAppClientSecret;
3: private string AzureAdAppUri;
4: private IssuingAuthority AzureAdAuthroAuthority;
5:
6: public AuthController()
7: {
8: AzureAdAppUri = "http://localhost:47828/";
9: AzureAdAppClientId = "515e8337-2a81-421a-bf76-cbc78ff89288";
10: AzureAdAppClientSecret = "sYEVBHHMM4kQNv2NOT6x0c55sogupaknnr3gdX9cptg=";
11:
12: // Fix for ID4175 & WIF10201 http://www.cloudidentity.com/blog/2013/02/08/multitenant-sts-and-token-validation-4/
13: AzureAdAuthroAuthority = new IssuingAuthority("WAAD");
14: // Issuer = Azure Ad Tenant
15: AzureAdAuthroAuthority.Issuers.Add("https://sts.windows.net/3351acfe-7e1b-4e9b-b587-f34bfa2e128a/");
16: AzureAdAuthroAuthority.Thumbprints.Add("3464C5BDD2BE7F2B6112E2F08E9C0024E33D9FE0");
17:
18: // Thumbprint can be read via this code:
19: // ia = ValidatingIssuerNameRegistry.GetIssuingAuthority("https://login.windows.net/TENANTID/FederationMetadata/2007-06/FederationMetadata.xml");
20:
21: }
Zum Code: Redirect zum Logn
Der Login wird über einen simplen Redirect gemacht:
1: public ActionResult Index()
2: {
3: // Develop Multitenant apps http://msdn.microsoft.com/en-us/library/windowsazure/dn151789.aspx - before "https://login.windows.net/3351acfe-7e1b-4e9b-b587-f34bfa2e128a/";
4: string issuer = "https://login.windows.net/common/";
5:
6: // returnUrl can be passed in wctx parameter with rm=0&id=passive&ru=%2fHome%2fAbout
7: var redirectUrl = string.Format(@"{0}wsfed?wa=wsignin1.0&wtrealm={1}", issuer, Url.Encode(AzureAdAppUri));
8:
9: return Redirect(redirectUrl);
10: }
Die RedirectUrl sieht in meinem Beispiel so aus:
https://login.windows.net/common/wsfed?wa=wsignin1.0&wtrealm=http%3a%2f%2flocalhost%3a47828%2f
Wie in den Kommentaren geschrieben kann man auch eine “ReturnUrl” in dem wctx Parameter mit angeben.
Der “Callback”: – Token valdieren & ClaimsPrincipal erstellen
Wenn man sich erfolgreich beim Azure AD angemeldet hat, wird die Callback URL der Anwendung aufgerufen (in meinem Beispiel ist dies auf Auth/Callback im Azure AD so eingestellt). In dem Aufruf kommt ein SAML Token mit:
1: <t:RequestSecurityTokenResponse xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
2: <t:Lifetime>
3: <wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2013-11-10T16:02:31.705Z</wsu:Created>
4: <wsu:Expires xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2013-11-11T04:02:31.705Z</wsu:Expires>
5: </t:Lifetime>
6: <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
7: <EndpointReference xmlns="http://www.w3.org/2005/08/addressing">
8: <Address>http://localhost:47828/</Address>
9: </EndpointReference>
10: </wsp:AppliesTo>
11: <t:RequestedSecurityToken>
12: <Assertion ID="_61ae5e5f-fb9a-482f-9ce8-a34506151e96" IssueInstant="2013-11-10T16:02:32.076Z" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
13: <Issuer>https://sts.windows.net/3351acfe-7e1b-4e9b-b587-f34bfa2e128a/</Issuer>
14: <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
15: <ds:SignedInfo>
16: <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
17: <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
18: <ds:Reference URI="#_61ae5e5f-fb9a-482f-9ce8-a34506151e96">
19: <ds:Transforms>
20: <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
21: <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
22: </ds:Transforms>
23: <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
24: <ds:DigestValue>a1HBgQ8rbt75g09a3G/Yi1jzTsUk7Lu/jjY84ObXjws=</ds:DigestValue>
25: </ds:Reference>
26: </ds:SignedInfo>
27: <ds:SignatureValue>OxoL6jFz0Kc7V5UWNn3jPaXMuEhE/v0u4o5c0W4kCpKd2op95uxJaGDDbtXSatkdPtJMLCXsZ1bQpdp7EfkdPg4KPGhgeCmMD4PPya5cqyxwp7E0gWENEQ3aknruEMizAaFUqq/HCPgdAguKgE+9grD3QANAJN2+sfjdfx3ukoAIKBbo6gkfRqi5mWePu56Zx9/wqxpyAT+gm1mZukFUaqoo3WaCKhaCv27UotJjUy2DaHMXAMDvulpRTCcF9rwsViM5d0NFxKoZykOvRqirdyi8GNYKh5J16E6+KOKE3aJNZljmJY2Ubbs1EhUTwIkYOoeRz/ZVY+VbqIp66BGd/w==</ds:SignatureValue>
28: <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
29: <X509Data>
30: <X509Certificate>MIIDPjCCAiqgAwIBAgIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjA3MDcwMDAwWhcNMTQwNjA3MDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArCz8Sn3GGXmikH2MdTeGY1D711EORX/lVXpr+ecGgqfUWF8MPB07XkYuJ54DAuYT318+2XrzMjOtqkT94VkXmxv6dFGhG8YZ8vNMPd4tdj9c0lpvWQdqXtL1TlFRpD/P6UMEigfN0c9oWDg9U7Ilymgei0UXtf1gtcQbc5sSQU0S4vr9YJp2gLFIGK11Iqg4XSGdcI0QWLLkkC6cBukhVnd6BCYbLjTYy3fNs4DzNdemJlxGl8sLexFytBF6YApvSdus3nFXaMCtBGx16HzkK9ne3lobAwL2o79bP4imEGqg+ibvyNmbrwFGnQrBc1jTF9LyQX9q+louxVfHs6ZiVwIDAQABo2IwYDBeBgNVHQEEVzBVgBCxDDsLd8xkfOLKm4Q/SzjtoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQVWmXY/+9RqFA/OG9kFulHDAJBgUrDgMCHQUAA4IBAQAkJtxxm/ErgySlNk69+1odTMP8Oy6L0H17z7XGG3w4TqvTUSWaxD4hSFJ0e7mHLQLQD7oV/erACXwSZn2pMoZ89MBDjOMQA+e6QzGB7jmSzPTNmQgMLA8fWCfqPrz6zgH+1F1gNp8hJY57kfeVPBiyjuBmlTEBsBlzolY9dd/55qqfQk6cgSeCbHCy/RU/iep0+UsRMlSgPNNmqhj5gmN2AFVCN96zF694LwuPae5CeR2ZcVknexOWHYjFM0MgUSw0ubnGl0h9AJgGyhvNGcjQqu9vd1xkupFgaN+f7P3p3EVN5csBg5H94jEcQZT7EKeTiZ6bTrpDAnrr8tDCy8ng</X509Certificate>
31: </X509Data>
32: </KeyInfo>
33: </ds:Signature>
34: <Subject>
35: <NameID>K_PsyS1F5XHlmjb-RNh1HccLx7yl_kG828eddsz4JEw</NameID>
36: <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer" />
37: </Subject>
38: <Conditions NotBefore="2013-11-10T16:02:31.705Z" NotOnOrAfter="2013-11-11T04:02:31.705Z">
39: <AudienceRestriction>
40: <Audience>http://localhost:47828/</Audience>
41: </AudienceRestriction>
42: </Conditions>
43: <AttributeStatement>
44: <Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname">
45: <AttributeValue>R</AttributeValue>
46: </Attribute>
47: <Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname">
48: <AttributeValue>M</AttributeValue>
49: </Attribute>
50: <Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name">
51: <AttributeValue>[email protected]</AttributeValue>
52: </Attribute>
53: <Attribute Name="http://schemas.microsoft.com/identity/claims/tenantid">
54: <AttributeValue>3351acfe-7e1b-4e9b-b587-f34bfa2e128a</AttributeValue>
55: </Attribute>
56: <Attribute Name="http://schemas.microsoft.com/identity/claims/objectidentifier">
57: <AttributeValue>a1b735e1-4b92-4ff9-bd15-4a915c149ff1</AttributeValue>
58: </Attribute>
59: <Attribute Name="http://schemas.microsoft.com/identity/claims/identityprovider">
60: <AttributeValue>https://sts.windows.net/3351acfe-7e1b-4e9b-b587-f34bfa2e128a/</AttributeValue>
61: </Attribute>
62: </AttributeStatement>
63: <AuthnStatement AuthnInstant="2013-11-03T16:40:15.000Z">
64: <AuthnContext>
65: <AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef>
66: </AuthnContext>
67: </AuthnStatement>
68: </Assertion>
69: </t:RequestedSecurityToken>
70: <t:RequestedAttachedReference>
71: <SecurityTokenReference d3p1:TokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0" xmlns:d3p1="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
72: <KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLID">_61ae5e5f-fb9a-482f-9ce8-a34506151e96</KeyIdentifier>
73: </SecurityTokenReference>
74: </t:RequestedAttachedReference>
75: <t:RequestedUnattachedReference>
76: <SecurityTokenReference d3p1:TokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0" xmlns:d3p1="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
77: <KeyIdentifier ValueType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLID">_61ae5e5f-fb9a-482f-9ce8-a34506151e96</KeyIdentifier>
78: </SecurityTokenReference>
79: </t:RequestedUnattachedReference>
80: <t:TokenType>http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0</t:TokenType>
81: <t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
82: <t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>
83: </t:RequestSecurityTokenResponse>
Über diesen Code lässt sich das Token validieren und in ein ClaimsPrincipal verwandeln. Wichtigste Library ist dieses NuGet Package, welches man für die Validierung benötigt.
1: [ValidateInput(false)]
2: public async Task<ActionResult> Callback(string wresult, string wa, string wctx)
3: {
4: // http://www.tecsupra.com/blog/system-identitymodel-manually-parsing-the-saml-token/
5: var wrappedToken = XDocument.Parse(wresult);
6: var requestedSecurityToken = wrappedToken.Root.Descendants("{http://schemas.xmlsoap.org/ws/2005/02/trust}RequestedSecurityToken").First();
7: var asssertion = requestedSecurityToken.DescendantNodes().First();
8:
9: var xmlTextReader = asssertion.CreateReader();
10:
11: var securityTokenHandlers = SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection();
12:
13: // Fix for ID1032 http://blog.fabse.net/2013/01/10/id1032-at-least-one-audienceuri-must-be-specified-2/
14: securityTokenHandlers.Configuration.AudienceRestriction.AllowedAudienceUris.Add(new Uri(AzureAdAppUri));
15: securityTokenHandlers.Configuration.CertificateValidationMode = X509CertificateValidationMode.None;
16: securityTokenHandlers.Configuration.CertificateValidator = X509CertificateValidator.None;
17:
18: securityTokenHandlers.Configuration.IssuerNameRegistry = new ValidatingIssuerNameRegistry(AzureAdAuthroAuthority);
19:
20: SecurityToken token = securityTokenHandlers.ReadToken(xmlTextReader);
21:
22: var viewModel = new CallbackViewModel();
23:
24: var claimsIdentity = securityTokenHandlers.ValidateToken(token);
25:
26: var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
27: ....
Ergebnis:
Erster Schritt mit der Graph API
Nachdem wir uns angemeldet haben, kommt in den Claims auch die TenantId mit. Da unsere Anwendung das Recht besitzt auch lesend auf das Azure AD zuzugreifen wollen wir das doch machen.
Hierfür sind zwei Schritte notwendig:
Wir benötigen als erstes ein AccessToken – dies holen wir mit dem Standard HttpClient, welcher in .NET Framework 4.5 vorhanden ist über den OAuth Endpunkt.
Danach nutzen wir diese NuGet Library – welche allerdings im Grunde nur aus den Samples von Microsoft stammt und nicht mehr aktualisiert wird. Man kann die Abfrage auch über die pure REST Api und den HttpClient machen, aber das war mir an der Stelle zu mühsam. Eine anderen eleganten Wrapper um die REST Api habe ich noch nicht gefunden. Falls es da was gibt wäre ich für einen Tipp dankbar.
1: ...
2: var tenantId =
3: claimsPrincipal.Claims.Single(x => x.Type == "http://schemas.microsoft.com/identity/claims/tenantid")
4: .Value;
5:
6: var waadRequest = new HttpClient();
7:
8: string postData = "grant_type=client_credentials";
9: postData += "&resource=" + HttpUtility.UrlEncode("https://graph.windows.net");
10: postData += "&client_id=" + HttpUtility.UrlEncode(AzureAdAppClientId);
11: postData += "&client_secret=" + HttpUtility.UrlEncode(AzureAdAppClientSecret);
12: var waadRequestContent = new StringContent(postData, System.Text.Encoding.ASCII, "application/x-www-form-urlencoded");
13:
14: string postUrl = string.Format("https://login.windows.net/{0}/oauth2/token?api-version=1.0", tenantId);
15:
16: var waadResult = await waadRequest.PostAsync(postUrl, waadRequestContent);
17:
18: waadResult.EnsureSuccessStatusCode();
19:
20: var result = await waadResult.Content.ReadAsStringAsync();
21:
22: var jObject = JObject.Parse(result);
23: var accessToken = jObject.SelectToken("access_token");
24:
25: var graph = new DirectoryGraph(tenantId, accessToken.Value<string>());
26:
27: string nextPageUrl;
28:
29: var user = graph.GetUsers(out nextPageUrl);
30: ...
Ergebnis: Ein AccessToken mit dem man die User des Tenants abfragen kann.
Zusammengefasst:
Wir haben damit eine sehr simple Applikation geschrieben, über welche man sich am Azure AD anmelden kann und das Verzeichnis auch auslesen kann. Ohne den Config-Wizard zu benutzen.
Multitenancy?
Mit der Variante ist es nun sehr einfach eine Multimandanten-WAAD-Applikation zu schreiben. Was man dazu machen muss: Die “AzureAdAuthroAuthority” muss alle bekannte Tenants kennen. In meinem Beispiel ist dies nur statisch auf mein Demo Azure Tenant eingestellt, aber die Daten können auch aus einer Datenbank kommen. Wichtig ist hier noch, dass Azure AD generell dann nur mit HTTPS Urls umgehen kann. Der Redirect auf eine Non-HTTPS Url kann in bestimmten Situationen zu Fehlern führen (z.B. wenn ein Fremder Admin eurer App “Vertrauen” will).
Das Toolkit macht im Grunde nix anderes
Soweit ich das sehen konnte ist die Redirect-Geschichte fast identisch gemacht (nur gibt es dazu WSFed-Hilfklassen dazu). Der Callback wird über ein Modul gemacht – was unter umständen cleverer ist, aber mir zu magisch war.
Login mit Twitter oder Microsoft Account?
Ich habe bereits in der Vergangenheit zwei ähnliche Artikel geschrieben – wieder mit “Low-Level” Mitteln.
Authentifizierung mit Microsoft Account
Democode auf GitHub
Der Code vom Tool als auch von meiner “Basic”-Variante ist natürlich auf GitHub. Die Secrets müsstet ihr natürlich mit euren austauschen.