Azure AD applications

De Banane Atomic
Aller à la navigationAller à la recherche

Liens

Définitions

Client

Application ID
Client ID
ID de l'application cliente
Redirect URI URL de l'application pour une web app
URL fictive pour un client natif https://wpfclient
Tenant Id de l'AD *.onmicrosoft.com
Authority https://login.microsoftonline.com/*.onmicrosoft.com
Service Resource Id
Resource Url
App ID URI de l'application ADD serveur https://*.onmicrosoft.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
url du service pour les requêtes get

Serveur

Application ID
Client ID
ID de l'application serveur
Redirect URI
PostLogoutRedirectUri
URL du service
App ID URI
Audience
https://*.onmicrosoft.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
https://*.onmicrosoft.com/MyAppName
Tenant Id de l'AD *.onmicrosoft.com
AAD Instance https://login.microsoftonline.com/{0}
Utile pour former authority = aadInstance + tenantId

Informations

Tenant (id) Azure AD → Properties → Directory ID
Tenant (url)
Domain
Mettre la sourie sur le nom de l'utilisateur en haut à droite → attendre le tooltip → Domain

Debug

  1. View → Server Explorer
  2. Server Explorer → Azure → App Service → MyApp → MyApp → cliqu-droit → Attach Debugger

Détail des erreurs

Web.config
<!-- Affiche le détail des erreurs -->
<customErrors mode="Off" />

Permissions

  1. Modifier les permissions de l'applications
  2. Appliquer les permissions avec le bouton Grant Access

Erreurs possibles:

  • Authorization_RequestDenied - Insufficient privileges to complete the operation

Liens:

Application Permissions vs Delegated Permissions

  • Application Permissions: appel de l'application elle-même
  • Delegated Permissions: appel avec l'utilisateur connecté

Multi-tenant

Azure Active Directory → App registrations → MyApp → Settings → Properties → Multi-tenanted = Yes

App_Start\Startup.Auth.cs
public void ConfigureAuth(IAppBuilder app)
{
    app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
    {
        // ...
        TokenValidationParameters = new TokenValidationParameters
        {
            SaveSigninToken = true,
            // turns off the default Issuer validation
            ValidateIssuer = false
        },
        Notifications = new OpenIdConnectAuthenticationNotifications
        {
            // ...
            // assigns to the Redirect_Uri and Post_Logout_Redirect_Uri
            RedirectToIdentityProvider = (context) =>
            {
                string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
                context.ProtocolMessage.RedirectUri = appBaseUrl;
                context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
                return Task.FromResult(0);
            }
        }

Erreurs

AADSTS70001: Application with identifier 'xxx' was not found in the directory yyy

Redéfinir le RedirectToIdentityProvider.

AADSTS50011: The reply address 'http://myapp.azurewebsites.net' does not match the reply addresses configured for the application: 'xxx'.

La reply addess est en http alors que celle enregistrée est en https.
Forcer la webapp en https

AADSTS90094: The grant requires admin permission.

Se connecter avec un compte admin pour donner l'autorisation d'accès à tous les utilisateurs du tenant. lien

Se désabonner d'un directory

En cliquant sur le nom d'utilisateur en haut à droite il est possible de passer d'un directory à un autre.
Pour se désabonner d'un directory, il faut supprimer l'utilisateur dans ce directory.

Connect to Office 365 PowerShell

Powershell.svg
Import-Module MSOnline

Connect-MsolService
Get-MsolUser -UserPrincipalName 'me@domain.fr' | Select LastPasswordChangeTimestamp

Installation:

Application web ASP.NET MVC avec une authentification Azure AD App

Javascript SPA calling Azure AD Secured Web API

Limitation: le client javascript ne peut pas stocker de manière sécurisé les refresh tokens (pas de bon chiffrement pour les web app).
Une fois le client fermé, il faut s'authentifier à nouveau.

Service WebAPI

Packages NuGet:

  • Microsoft.Owin.Cors
  • Microsoft.Owin.Host.SystemWeb
  • Microsoft.Owin.Security.ActiveDirectory
App_Start/Startup.cs
// nécessite Microsoft.Owin.Host.SystemWeb
[assembly: OwinStartup(typeof(AzureAdLocationService.App_Start.Startup))]

namespace AzureAdLocationService.App_Start
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // Cross Origin Resource Sharing - CORS
            // Par défault l'appel depuis un doamine vers un autre n'est pas autorisé.
            app.UseCors(CorsOptions.AllowAll);

            // Authentication
            var azureAdBearerAuthOptions = new WindowsAzureActiveDirectoryBearerAuthenticationOptions
            {
                // Azure AD Tenant
                Tenant = ConfigurationManager.AppSettings["ida:Tenant"]
            };

            azureAdBearerAuthOptions.TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
            {
                // Audience = App ID URI
                // http://mydomain.onmicrosoft.com/MyApp
                ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
            };

            app.UseWindowsAzureActiveDirectoryBearerAuthentication(azureAdBearerAuthOptions);
Web.config
<configuration>
  <appSettings>
    <add key="ida:Tenant" value="mydomain.onmicrosoft.com" />
    <add key="ida:Audience" value="https://mydomain.onmicrosoft.com/MyApp" />

Autoriser OAuth2 implicite flow:
Azure → Azure Active Directory → App registration → MyApp → Manifest

Javascript.svg
"oauth2AllowImplicitFlow": true

Client Javascript SPA

  1. Azure → Azure Active Directory → App registration → New application registration
    • Application type: Native
    • Redirect URI: https://<SSL URL>
  2. Récupérer l'Application ID (= Client ID) et la reporter dans Web.config
  3. Autoriser OAuth2 implicite flow: Azure → Azure Active Directory → App registration → MyApp → Manifest
    • "oauth2AllowImplicitFlow": true
  4. Autoriser le client à accéder au service:
    • Required Permissions → Add → Select an API → MyServiceApp → Select → Access MyServiceApp → Save
Utiliser azure-activedirectory-library-for-js
Télécharger adal.js et adal-angular.js
Scripts/app.js
(function () {

    "use strict";

    var locationApp = angular.module('locationApp', ['AdalAngular']);

    locationApp.config(['$httpProvider', 'adalAuthenticationServiceProvider', '$locationProvider', 
      function ($httpProvider, adalProvider, $locationProvider) {

        // When HTML5 mode is configured, ensure the $locationProvider hashPrefix is set
        $locationProvider.html5Mode(true).hashPrefix('!');

        var endpoints = {
            "https://localhost:port": "https://mydomain.onmicrosoft.com/MyServerApp"
        }

        adalProvider.init(
            {
                instance: 'https://login.microsoftonline.com/',
                tenant: 'mydomain.onmicrosoft.com',
                clientId: '...',
                //localLoginUrl: "/login",  // optional
                //redirectUri : "your site", optional
                endpoints: endpoints
            },
            $httpProvider);
    }]);

    var locationController = locationApp.controller("locationController", [
        '$scope', '$http', 'adalAuthenticationService',
        function ($scope, $http, adalService) {
            $scope.getData = function () {
                $http.get("https://localhost:port/api/data?param=value")
                    .then(function (response) {
                        // success
                        $scope.data = response.data;
                    }, function (error) {
                        // failure
                        $scope.error = error.data;
                    });
            }

            $scope.login = function () {
                adalService.login();
            }

            $scope.logout = function () {
                adalService.logOut();
            }
    }]);

})();
index.html
<!DOCTYPE html>
<html>
<head>
    <!-- évite l'erreur $location in HTML5 mode requires a <base> tag to be present! -->
    <base href="/">

    <meta charset="utf-8" />
    <title></title>

    <script src="Scripts/angular.js"></script>
    <script src="Scripts/adal.js"></script>
    <script src="Scripts/adal-angular.js"></script>
    <script src="Scripts/app.js"></script>
</head>

Native Application Calling Azure AD Secured Web API

Les Native Applications peuvent stocker et utiliser les refresh tokens.

Nuget:

  • Microsoft.IdentityModel.Clients.ActiveDirectory

Références:

  • System.Net.Http
  • System.Web.Extensions
  • System.Security
MainWindows.xaml.cs
private static string aadInstance = "https://login.microsoftonline.com/{0}";
private static string tenant = "xxx.onmicrosoft.com";
private static string clientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";

Uri redirectUri = new Uri("https://wpfclient");

private static string authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant);

private AuthenticationContext authContext = null;
private AuthenticationResult authResult = null;

private static string serviceResourceId = "https://xxx.onmicrosoft.com/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";

private HttpClient httpClient = new HttpClient();
private static string serviceBaseAddress = "https://localhost:ppppp/";

public MainWindow()
{
    InitializeComponent();
    authContext = new AuthenticationContext(authority, new FileCache());
}

private async void CallService_Click(object sender, RoutedEventArgs e)
{
    if (authResult == null)
    {
        ServiceResult.Text = "You need to sign-in first !!!";
    }

    try
    {
        authResult = await authContext.AcquireTokenAsync(serviceResourceId, clientId, redirectUri, new PlatformParameters(PromptBehavior.Never));
    }
    catch (AdalException aex)
    {
        ServiceResult.Text = aex.ToString();
        return;
    }

    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);

    var response = await httpClient.GetAsync(serviceBaseAddress + "api/xxx?arg=vvv");            

    if (response.IsSuccessStatusCode)
    {
        var s = await response.Content.ReadAsStringAsync();
        ServiceResult.Text = s;
    }
    else
    {
        ServiceResult.Text = "An error occured: " + response.ReasonPhrase;
    }
}

private async void SignIn_Click(object sender, RoutedEventArgs e)
{
    try
    {
        authResult = await authContext.AcquireTokenAsync(serviceResourceId, clientId, redirectUri, new PlatformParameters(PromptBehavior.Always));
        ServiceResult.Text = "You signed in!";
    }
    catch (AdalException aex)
    {
        ServiceResult.Text = aex.ToString();
    }
}

private void SignOut_Click(object sender, RoutedEventArgs e)
{
    authContext.TokenCache.Clear();
    ClearCookies();
    ServiceResult.Text = "You signed out!";
}

private void ClearCookies()
{
    const int INTERNET_OPTION_END_BROWSER_SESSION = 42;
    InternetSetOption(IntPtr.Zero, INTERNET_OPTION_END_BROWSER_SESSION, IntPtr.Zero, 0);
}

[DllImport("wininet.dll", SetLastError = true)]
private static extern bool InternetSetOption(IntPtr hInternet, int dwOption, IntPtr lpBuffer, int lpdwBufferLength);
FileCache.cs
class FileCache : TokenCache
{
    private string _cacheFilePath;
    private static readonly object _fileLock = new object();

    public FileCache(string filePath = @".\TokenCache.dat")
    {
        _cacheFilePath = filePath;
        this.AfterAccess = AfterAccessNotification;
        this.BeforeAccess = BeforeAccessNotification;

        lock (_fileLock)
        {
            byte[] data = null;
            if (File.Exists(_cacheFilePath))
            {
                data = ProtectedData.Unprotect(File.ReadAllBytes(_cacheFilePath), null, DataProtectionScope.CurrentUser);
            }

            this.Deserialize(data);
        }
    }

    public override void Clear()
    {
        base.Clear();
        File.Delete(_cacheFilePath);
    }

    void BeforeAccessNotification(TokenCacheNotificationArgs args)
    {
        lock (_fileLock)
        {
            byte[] data = null;
            if (File.Exists(_cacheFilePath))
            {
                data = ProtectedData.Unprotect(File.ReadAllBytes(_cacheFilePath), null, DataProtectionScope.CurrentUser);
            }

            this.Deserialize(data);
        }
    }

    void AfterAccessNotification(TokenCacheNotificationArgs args)
    {
        if (this.HasStateChanged)
        {
            lock (_fileLock)
            {
                File.WriteAllBytes(_cacheFilePath, ProtectedData.Protect(this.Serialize(), null, DataProtectionScope.CurrentUser));
                this.HasStateChanged = false;
            }
        }
    }
}

Web site calling Azure AD Secured Web API

Azure AD Secured Web API calling Graph API on behalf of a Native Application

  1. L'applicative native se logue et obtient un token AAD
  2. Elle utilise ce token dans le header de ses requêtes pour faire des appels à l'API web
  3. L'API web récupère le token AAD de l'application native dans le BootstrapContext
  4. Elle utilise le token AAD de l'application native, son id AAD, la clé secrète AAD pour générer ses propres tokens
  • Nuget → Microsoft.IdentityModel.Clients.ActiveDirectory
  • Assembly → System.IdentityModel
Csharp.svg
string accessToken = null;
var bootstrapContextObject = ClaimsPrincipal.Current.Identities.First().BootstrapContext;
if (bootstrapContextObject is BootstrapContext bootstrapContext)
{
    accessToken = bootstrapContext.Token;
}
else if (bootstrapContextObject is string bootstrapContextString)
{
    accessToken = bootstrapContextString;
}
else { }

var authenticationContext = new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext(authority);
var clientCredential = new ClientCredential(AadAppId, AadAppSecretKey);
string userName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn) != null ?
                    ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value :
                    ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;
var userAssertion = new UserAssertion(accessToken, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);

// génération d'un nouveau token pour l'API web qui permettra de faire des requêtes Graph API
AuthenticationResult result = await authenticationContext.AcquireTokenAsync(audience, clientCredential, userAssertion);
string newFreshToken = result.AccessToken;
App_Start\Startup.cs
public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseWindowsAzureActiveDirectoryBearerAuthentication(new WindowsAzureActiveDirectoryBearerAuthenticationOptions
        {
            Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
            TokenValidationParameters = new TokenValidationParameters
            {
                // forcer SaveSigninToken pour avoir le BootstrapContext.Token
                SaveSigninToken = true,
                ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
            }
        });
Web.config
<configuration>
  <configSections>
    <!--WIF 4.5 sections -->
    <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
    <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
  </configSections>

  <system.identityModel>
    <identityConfiguration saveBootstrapContext="true" />
  </system.identityModel>

BootstrapContext null

Token

Type Description Durée de vie
ID / Access Token authentifie les utilisateurs et les requêtes. Fournit par Azure AD et utilisé seulement par l'application cliente.
Après une authentification valide, Azure AD retourne un Access Token JWT encodé en base64
1h
Refresh Token demande de nouveaux ID / Access Token sans interaction avec l'utilisateur. Fournit et utilisé seulement par Azure AD. 14 jours

JWT Token

aud AUDience https://*.onmicrosoft.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx xxx=AppId
iss ISSuer https://sts.windows.net/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy/ yyy=
iat Issued AT date de la demande. Nombre de secondes depuis Epoch (1970-01-01T00:00:00Z UTC)
nbf Not BeFore date avant laquelle le token ne doit pas être utilisé
exp EXPiration time date à partir de laquelle le token ne sera plus accepté
acr Authentication Context class Reference 0 : l'authentification ne respecte pas la norme ISO/IEC 29115
aio
amr Authentication Method pwd
appid APPlication ID Application ID dans Azure AD Applications
appidacr APPlication Authentication Context class Reference
  • 0 : client publique
  • 1 : si le client ID et le client secret sont utilisés
deviceid
oid Object ID ID unique de l'utilisateur
onprem_sid
scp Scope user_impersonation
sub Subject autre ID unique de l'utilisateur
tid Tenant ID
uti

Erreurs

Application is requesting a token for itself

This scenario is supported only if resource is specified using the GUID based App Identifier.

L'application ne peut demander un token pour elle-même, sauf si elle utilise son App ID au lieu de son App ID URI / Audience.

Csharp.svg
audience = "https://xxx.onmicrosoft.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx";
// remplacer l'audience par l'App ID
audience = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyy";
AuthenticationResult result = await authenticationContext.AcquireTokenAsync(audience, clientCredential, userAssertion);

Could not load type Tokens.TokenValidationParameters from assembly Tokens.Jwt

Could not load type 'System.IdentityModel.Tokens.TokenValidationParameters'
from assembly 'System.IdentityModel.Tokens.Jwt, Version=5.1.5.0

System.IdentityModel.Tokens.Jwt 5.x is not compatible with Katana 3.
It is for .NET Core. For .NET Framework use version 4.x

IAppBuilder does not contain a definition for SetDefaultSignInAsAuthenticationType

Csharp.svg
# ajouter le using
using Microsoft.Owin.Security;

Authorization has been denied for this request

Vérifier les paramètres clientId, tenant, authority, resourceUrl

You can't access this application

MyApp needs permission to access resources in your organization that only an admin can grant.
Please ask an admin to grant permission to this app before you can use it.

L'utilisateur doit autoriser (Azure AD consent framework) l'accès à MyApp. Ici il n'a pas assez de droits pour le faire.

  • Solution 1: autoriser par défaut l'accès à MyApp pour tous les utilisateurs.
  • Solution 2: donner les droits aux utilisateur d'autoriser leur accès à MyApp.
    AAD → User Settings → Enterprise applications → Users can consent to apps accessing company data on their behalf → Yes
  • Solution 3: désactiver la demande d'autorisation à MyApp.