01 December 2013 ADAL, ASP.NET, Azure, Azure AD, HowTo, WAAD Robert Muehsig

Wer im neuen Visual Studio ein ASP.NET Projekt erstellt, bekommt den neuen “One ASP.NET” Dialog zu sehen. Dieser hilft die verschiedenen ASP.NET Frameworks zu kombinieren und zudem erlaubt er es direkt einen Authentifizierungsprovider zu wählen. Doch was macht dieses Tooling eigentlich?

Schritt für Schritt:

Die neue Projektvorlage für ASP.NET Projekte ist wesentlich aufgeräumter als früher:

image

Wählt man nun dieses Projekt-Template kommt man zum neuen “One ASP.NET” Dialog:

image

Wählt man das “Web Forms”, “Web API” oder das “MVC” Template kann man auch die Authentifizierung wählen;

Für die “Windows Azure Active Directory” Authentifizierung wählt man die “Organizational Accounts” aus. Dort wählt man ob die Anwendung nur innerhalb des eigenen Azure AD Tenants verfügbar ist, oder ob es auch von anderen genutzt werden kann. Den Azure AD Tenant gibt man über die Domain an.

image

Nach Eingabe der Domain muss man sich mit einem berechtigten Nutzer an dem Tenant anmelden:

image

Durch Eingabe des Nutzers und des Kennworts passieren sowohl in der Applikation als auch im Azure AD Management Portal einige Sachen die wir genauer anschauen werden.

image

Was wird im Azure Management Portal eingestellt?

Wenn nun das Projekt erstellt wird im angegebenen Azure AD Tenant eine neue Applikation hinzugefügt. Egal ob es eine “Single”- oder “Multi”-Tenant App ist, existiert die App in eurem angegebenen Tenant. Im Fall einer Multitenant-App gibt es den Hinweis “Diese App wurde veröffentlicht von …”.

image

Schauen wir uns die Konfiguration der App genauer an:

Die App hat denselben Namen wie das ASP.NET Projekt – wobei der Name keine große Rolle spielt.

image

image

(die restlichen Felder sind nicht befüllt). Wichtig hier ist die eingestellte Reply URL/App Url auf “https://localhost:44304”. Diese wird man später auch in den Projekteigenschaften wiederfinden.

Dies war es auch schon auf Azure Seite. Nun zum Visual Studio Projekt.

Das Visual Studio Projekt – was wurde hier verändert?

Über das NuGet Package “Active Directory Authentication Library” kommt neben den “normalen” ASP.NET Identity Assemblies die ADAL Assembly dazu:

image

Diese wird später noch gebraucht um auf die Graph-API zuzugreifen.

In der Web.config:

In der Config sind zwei neue IdentityModel Sections dazugekommen, es wurde eine Datenbank Verbindung hinterlegt, diverse AppSettings hinzugefügt usw.. Die wichtigsten Sachen gehen wir jetzt Stück für Stück durch:

   1: <appSettings>
   2:   <add key="webpages:Version" value="3.0.0.0" />
   3:   <add key="webpages:Enabled" value="false" />
   4:   <add key="ClientValidationEnabled" value="true" />
   5:   <add key="UnobtrusiveJavaScriptEnabled" value="true" />
   6:   <add key="ida:FederationMetadataLocation" value="https://login.windows.net/codeinsidedev.onmicrosoft.com/FederationMetadata/2007-06/FederationMetadata.xml" />
   7:   <add key="ida:Realm" value="https://codeinsidedev.onmicrosoft.com/WaadBehindTheScenes" />
   8:   <add key="ida:AudienceUri" value="https://codeinsidedev.onmicrosoft.com/WaadBehindTheScenes" />
   9:   <add key="ida:ClientID" value="3a577181-2243-4830-955a-b060155b2e93" />
  10:   <add key="ida:Password" value="wp/NWMxltzAGCVrP4D61X39oWms8QTT3da0012097oU=" />
  11: </appSettings>

Die Daten die mit “ida” anfangen sind alle für die Authentifizierung zuständig. Die Daten finden sich auch alle in dem Azure Management Portal wieder. Dass Passwort ist das Secret, welches man nur einmalig während des Erstellens im Azure Portal sieht und wird später ebenfalls für die Graph-API gebraucht.

   1: <system.identityModel>
   2:     <identityConfiguration>
   3:       <issuerNameRegistry type="WaadBehindTheScenes.Utils.DatabaseIssuerNameRegistry, WaadBehindTheScenes" />
   4:       <audienceUris>
   5:         <add value="https://codeinsidedev.onmicrosoft.com/WaadBehindTheScenes" />
   6:       </audienceUris>
   7:       <securityTokenHandlers>
   8:         <add type="System.IdentityModel.Services.Tokens.MachineKeySessionSecurityTokenHandler, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
   9:         <remove type="System.IdentityModel.Tokens.SessionSecurityTokenHandler, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
  10:       </securityTokenHandlers>
  11:       <certificateValidation certificateValidationMode="None" />
  12:     </identityConfiguration>
  13:   </system.identityModel>

Die IdentityModel Section wurde ebenfalls hinzugefügt. Auch hier ist die audienceUri der Azure AD App hinterlegt und es wurde ein IssuerNameRegistry Eintrag angelegt. Dahin kommen wir später noch einmal. Wichtig ist auch dass der normale SecurityTokenHandler entfernt wurde und mit dem MachineKeySessionSecurityTokenHandler ersetzt. Wer mehrere Instanzen hat muss somit nur den MachineKey bei allen Instanzen gleich setzen – so kann jede Instanz das Token validieren.

Die eigentliche Authentifizierung machen zwei HttpModule:

WSFederationAuthenticationModule & SessionAuthenticationModule

Wie man hier sieht: Die eigentliche Nutzerauthentifizierung geschied über WS-Fed. Je nach Verbindungs- und Verarbeitungsgeschwindigkeit sieht man auch die Umleitung auf den WS-Fed Endpunkt.

Zudem wird weiter unten die FederationConfiguration noch weiter spezifiziert.

   1: <system.webServer>
   2:     <modules>
   3:       <add name="WSFederationAuthenticationModule" type="System.IdentityModel.Services.WSFederationAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
   4:       <add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
   5:     </modules>
   6:   </system.webServer>
   7:   <system.identityModel.services>
   8:     <federationConfiguration>
   9:       <cookieHandler requireSsl="true" />
  10:       <wsFederation passiveRedirectEnabled="true" issuer="https://login.windows.net/codeinsidedev.onmicrosoft.com/wsfed" realm="https://codeinsidedev.onmicrosoft.com/WaadBehindTheScenes" requireHttps="true" />
  11:     </federationConfiguration>
  12:   </system.identityModel.services>

Auch hier findet sich der eigentliche Issuer (also unser Azure AD Tenant wieder) und die Einstellung, dass das Auth-Cookie nur unter einer SSL Verbindung erstellt wird.

Leider ist die web.config durch diesen Prozess für meinen Geschmack sehr sehr aufgebläht und es enthält auch oftmals dieselben Werte mehrfach.

Im Code:

Es wurde eine IdentityConfig angelegt, welche die Werte aus den AppSettings ausliesst und ebenfalls in die FederationConfig registriert:

   1: // For more information on ASP.NET Identity, please visit http://go.microsoft.com/fwlink/?LinkId=301863
   2:     public static class IdentityConfig
   3:     {
   4:         public static string AudienceUri { get; private set; }
   5:         public static string Realm { get; private set; }
   6:  
   7:         public static void ConfigureIdentity()
   8:         {
   9:             RefreshValidationSettings();
  10:             // Set the realm for the application
  11:             Realm = ConfigurationManager.AppSettings["ida:realm"];
  12:  
  13:             // Set the audienceUri for the application
  14:             AudienceUri = ConfigurationManager.AppSettings["ida:AudienceUri"];
  15:             if (!String.IsNullOrEmpty(AudienceUri))
  16:             {
  17:                 UpdateAudienceUri();
  18:             }
  19:  
  20:             AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;
  21:         }
  22:  
  23:         public static void RefreshValidationSettings()
  24:         {
  25:             string metadataLocation = ConfigurationManager.AppSettings["ida:FederationMetadataLocation"];
  26:             DatabaseIssuerNameRegistry.RefreshKeys(metadataLocation);
  27:         }
  28:  
  29:         public static void UpdateAudienceUri()
  30:         {
  31:             int count = FederatedAuthentication.FederationConfiguration.IdentityConfiguration
  32:                 .AudienceRestriction.AllowedAudienceUris.Count(
  33:                     uri => String.Equals(uri.OriginalString, AudienceUri, StringComparison.OrdinalIgnoreCase));
  34:             if (count == 0)
  35:             {
  36:                 FederatedAuthentication.FederationConfiguration.IdentityConfiguration
  37:                     .AudienceRestriction.AllowedAudienceUris.Add(new Uri(IdentityConfig.AudienceUri));
  38:             }
  39:         }
  40:     }

In der Global.asax wurde ebenfalls etwas hinzugefügt:

   1: public class MvcApplication : System.Web.HttpApplication
   2:     {
   3:         protected void Application_Start()
   4:         {
   5:             AreaRegistration.RegisterAllAreas();
   6:             IdentityConfig.ConfigureIdentity();
   7:             FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
   8:             RouteConfig.RegisterRoutes(RouteTable.Routes);
   9:             BundleConfig.RegisterBundles(BundleTable.Bundles);
  10:         }
  11:  
  12:         void WSFederationAuthenticationModule_RedirectingToIdentityProvider(object sender, RedirectingToIdentityProviderEventArgs e)
  13:         {
  14:             if (!String.IsNullOrEmpty(IdentityConfig.Realm))
  15:             {
  16:                 e.SignInRequestMessage.Realm = IdentityConfig.Realm;
  17:             }
  18:         }
  19:     }

Insbesondere die “WSFederationAuthenticationModule_RedirectingToIdentityProvider” Methode ist von Interesse – hier endet der Request wenn der User nicht authentifiziert ist und die Seite eine Authentifizierung benötigt. Dies kann auch wie früher über die Web.config gesteuert werden:

   1: <location path="Account">
   2:   <system.web>
   3:     <authorization>
   4:       <allow users="*" />
   5:     </authorization>
   6:   </system.web>
   7: </location>

In diesem Fall ist “Account” für alle Nutzer freigegeben. Ansonsten wird der Nutzer auf die entsprechende Azure Login Seite umgeleitet.

image

Danach wird der User direkt zur Anmeldung umgeleitet:

image

Hier wird der Nutzer über WS-Federation authentifiziert.

Nach dem Anmelden werden zwei Cookies erstellt, sodass die Methode da nicht mehr anspringt:

image

Wo wir beim Anmelden sind – was macht diese Datenbank und “DatabaseIssuerNameRegistry”?

Die Datenbank enthält nur zwei Tabellen mit jeweils einer ID Spalte:

image

Im Grunde merkt sich die IssuerNameRegistry welche Azure AD Tenants zugreifen dürfen. Die Tenant IDs sind die IDs des jeweiligen Azure ADs. Da dies nur eine “Single Tenant App” ist, werden da auch nicht mehr Daten hinterlegt. Die “IssuingAuthorityKeys” enthalten die Thumbprints der jeweiligen Azure AD Tenants.

Dieses Management übernimmt die generierte DatabaseIssuerNameRegistry.

image

Was hat sich in den Controllern geändert?

Auffallend ist natürlich die “UserProfile” Ansicht:

image

Diese Daten werden direkt aus dem Azure AD über die Graph API gezogen. Dazu wurde im HomeController einige Properties und eine Action hinzugefügt:

   1: [Authorize]
   2: public async Task<ActionResult> UserProfile()
   3: {
   4:     string tenantId = ClaimsPrincipal.Current.FindFirst(TenantIdClaimType).Value;
   5:  
   6:     // Get a token for calling the Windows Azure Active Directory Graph
   7:     AuthenticationContext authContext = new AuthenticationContext(String.Format(CultureInfo.InvariantCulture, LoginUrl, tenantId));
   8:     ClientCredential credential = new ClientCredential(AppPrincipalId, AppKey);
   9:     AuthenticationResult assertionCredential = authContext.AcquireToken(GraphUrl, credential);
  10:     string authHeader = assertionCredential.CreateAuthorizationHeader();
  11:     string requestUrl = String.Format(
  12:         CultureInfo.InvariantCulture,
  13:         GraphUserUrl,
  14:         HttpUtility.UrlEncode(tenantId),
  15:         HttpUtility.UrlEncode(User.Identity.Name));
  16:  
  17:     HttpClient client = new HttpClient();
  18:     HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
  19:     request.Headers.TryAddWithoutValidation("Authorization", authHeader);
  20:     HttpResponseMessage response = await client.SendAsync(request);
  21:     string responseString = await response.Content.ReadAsStringAsync();
  22:     UserProfile profile = JsonConvert.DeserializeObject<UserProfile>(responseString);
  23:  
  24:     return View(profile);
  25: }

Um die Profil-Daten zu beziehen wird am Ende die URL “https://graph.windows.net/3351acfe-7e1b-4e9b-b587-f34bfa2e128a/users/admin%40codeinsidedev.onmicrosoft.com?api-version=2013-04-05” aufgerufen. Über “ADAL” wird der AuthorizationToken abgeholt. Dies ist ein OAuth Token. Die normale Nutzer-Authentifizierung ist jedoch über WS-Federation erfolgt. Das Ergebnis der Graph API Abfrage ist solch ein JSON:

image

Dieses wird ausgelesen und am Ende angezeigt, eigentlich easy, oder?

AccountController – Signout ist natürlich auch möglich

Über die WS-Federation Hilfsklassen kann auch eine Signout URL erzeugt werden. Dort wird der User hingeleitet und ist am Ende “abgemeldet”.

   1: public ActionResult SignOut()
   2: {
   3:     WsFederationConfiguration config = FederatedAuthentication.FederationConfiguration.WsFederationConfiguration;
   4:  
   5:     // Redirect to SignOutCallback after signing out.
   6:     string callbackUrl = Url.Action("SignOutCallback", "Account", routeValues: null, protocol: Request.Url.Scheme);
   7:     SignOutRequestMessage signoutMessage = new SignOutRequestMessage(new Uri(config.Issuer), callbackUrl);
   8:     signoutMessage.SetParameter("wtrealm", IdentityConfig.Realm ?? config.Realm);
   9:     FederatedAuthentication.SessionAuthenticationModule.SignOut();
  10:  
  11:     return new RedirectResult(signoutMessage.WriteQueryString());
  12: }

Wenn man das Projekt startet wird eine SSL-Adresse verwendet, warum?

Das Tooling von Visual Studio stellt das Projekt automatisch so um, dass SSL genutzt wird.

image

Wer eine Multitenant Azure AD Anwendung baut, wird ohnehin an SSL nicht vorbei kommen, da das “Onboarding” von Tenants nur mit SSL Adresse funktioniert. Das Tooling stellt generell das Projekt auf SSL um – beugt spätere Überraschungen vor.

Fazit

Das Tooling erleichtert den Einstieg in die Azure AD Welt. Ziel ist es das Entwickeln damit genauso einfach zu halten wie man heute die “Windows Authentifizierung” aktiviert. Als fader Beigeschmack bleibt bei mir, dass recht viele Informationen doppelt in der web.config drin sind. Dies waren aber die wesentlichen “Punkte” die das Tooling verändert. Der Code ist natürlich wie immer auf GitHub zu erreichen. Meine Azure AD App habe ich aber bereits wieder entfernt, da man die Secrets und co. natürlich nicht verteilen sollte.

Wie man ohne das Tooling zu dem gleichen Ergebnis kommt habe ich bereits hier gebloggt: Windows Azure Active Directory–Authentifizierung “nur Code” & erste Schritte mit der Graph API


Written by Robert Muehsig

Software Developer - from Dresden, Germany, now living & working in Switzerland. Microsoft MVP & Web Geek.
Other Projects: KnowYourStack.com | ExpensiveMeeting | EinKofferVollerReisen.de

If you like the content and want to support me you could buy me a beer or a coffee via Litecoin or Bitcoin - thanks for reading!