web-dev-qa-db-fra.com

Adal.js ne reçoit pas de jetons pour la ressource de point de terminaison api externe

J'essaie adal.js avec un site Web SPA Angular SPA qui récupère les données d'un site Web API externe (domaine différent). Avec adal.js, l'authentification auprès du SPA était simple, mais sa communication avec l'API ne fonctionne pas du tout lorsque des jetons porteurs sont requis. J'ai utilisé https://github.com/AzureAD/Azure-activedirectory-library-for-js comme modèle en plus d'innombrables blogs. 

Le problème est que lorsque je configure des points de terminaison lors de l'initiation d'adal.js, celui-ci semble rediriger tout le trafic des points de terminaison sortants vers le service de connexion microfils. 

Observations:

  • Le stockage de session Adal.js contient deux entrées adal.access.token.key. Un pour l'ID client de l'application SPA Azure AD et un pour l'API externe. Seul le jeton SPA a une valeur. 
  • Si je n'injecte pas $ httpProvider dans adal.js, les appels sont acheminés vers l'API externe et un 401 me revient. 
  • Si j'ajoute manuellement le jeton SPA à l'en-tête http (autorisation: porteur "valeur du jeton"), je reçois 401 en retour.

Ma théorie est que adal.js est incapable de récupérer des jetons pour les points de terminaison (probablement parce que j'ai configuré quelque chose de mal dans le SPA) et qu'il arrête le trafic sur le point de terminaison car il est incapable d'obtenir un jeton requis. Le jeton SPA ne peut pas être utilisé avec l'API car il ne contient pas les droits requis. Pourquoi adal.js n'obtient-il pas de jetons pour les ordinateurs d'extrémité et comment puis-je résoudre ce problème? 

Information additionnelle:

  • L'application Azure AD du client est configurée pour utiliser des autorisations déléguées par rapport à l'API et à oauth2AllowImplicitFlow = true dans le manifeste de l'application.
  • L'application API Azure AD est configurée pour l'emprunt d'identité et oauth2AllowImplicitFlow = true (vous ne pensez pas que cela est nécessaire, mais vous l'avez essayé). C'est multi-locataire.
  • L'API est configurée pour autoriser toutes les origines CORS et fonctionne correctement lorsqu'elle est utilisée par une autre application Web utilisant l'emprunt d'identité (hybride MVC (Adal.net) + Angular). 

Stockage de session:

key (for the SPA application): adal.access.token.keyxxxxx-b7ab-4d1c-8cc8-xxx value: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik1u...

key (for API application): adal.access.token.keyxxxxx-bae6-4760-b434-xxx
value:

app.js (fichier de configuration angulaire et adal)

(function () {
    'use strict';

    var app = angular.module('app', [
        // Angular modules 
        'ngRoute',

        // Custom modules 

        // 3rd Party Modules
        'AdalAngular'

    ]);

    app.config(['$routeProvider', '$locationProvider',
        function ($routeProvider, $locationProvider) {
        $routeProvider           

            // route for the home page
            .when('/home', {
                templateUrl: 'App/Features/Test1/home.html',
                controller: 'home'
            })

            // route for the about page
            .when('/about', {
                templateUrl: 'App/Features/Test2/about.html',
                controller: 'about',
                requireADLogin: true
            })

            .otherwise({
                redirectTo: '/home'
            })

        //$locationProvider.html5Mode(true).hashPrefix('!');

        }]);

    app.config(['$httpProvider', 'adalAuthenticationServiceProvider',
        function ($httpProvider, adalAuthenticationServiceProvider) {
            // endpoint to resource mapping(optional)
            var endpoints = {
                "https://localhost/Api/": "xxx-bae6-4760-b434-xxx",
            };

            adalAuthenticationServiceProvider.init(
                    {                        
                        // Config to specify endpoints and similar for your app
                        clientId: "xxx-b7ab-4d1c-8cc8-xxx", // Required
                        //localLoginUrl: "/login",  // optional
                        //redirectUri : "your site", optional
                        extraQueryParameter: 'domain_hint=mydomain.com',
                        endpoints: endpoints  // If you need to send CORS api requests.
                    },
                    $httpProvider   // pass http provider to inject request interceptor to attach tokens
                    );
        }]);
})();

Code angulaire d'appel du terminal:

$scope.getItems = function () {
            $http.get("https://localhost/Api/Items")
                .then(function (response) {                        
                    $scope.items = response.Items;
                });
16

Ok, je me suis cogné la tête contre le mur pour comprendre. En essayant de rendre mon application SPA ADAL.js (sans angular) avec succès, envoyer des requêtes XHR entre domaines à ma précieuse API Web compatible CORS.

Cet exemple d'application , celui que tous les débutants comme moi utilisent, présente ce problème: il comporte une API et un SPA desservis à partir du même domaine - et ne nécessite qu'un seul enregistrement d'application AD Tenant. Cela ne fait que brouiller les choses quand vient le temps de séparer les choses en morceaux séparés.

Ainsi, dans la boîte de dialogue, l’échantillon a ce Startup.Auth.cs qui fonctionne correctement, dans la mesure où cet exemple disparaît ...

  public void ConfigureAuth(IAppBuilder app) {

        app.UseWindowsAzureActiveDirectoryBearerAuthentication(
            new WindowsAzureActiveDirectoryBearerAuthenticationOptions
            {
                Audience = ConfigurationManager.AppSettings["ida:Audience"],
                Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
            });
  }

mais vous devez modifier le code ci-dessus, abandonner l'assignation Audience et choisir un éventail d'audiences. En effet: ValidAudiences .. Ainsi, pour chaque client SPA qui communique avec votre WebAPI, vous souhaiterez mettre le ClientID de votre inscription SPA dans ce tableau ...

Ça devrait ressembler à ça...

public void ConfigureAuth(IAppBuilder app)
{
    app.UseWindowsAzureActiveDirectoryBearerAuthentication(
        new WindowsAzureActiveDirectoryBearerAuthenticationOptions
        {
            Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
            TokenValidationParameters = new TokenValidationParameters
            {
                ValidAudiences = new [] { 
                 ConfigurationManager.AppSettings["ida:Audience"],//my swagger SPA needs this 1st one
                 "b2d89382-f4d9-42b6-978b-fabbc8890276",//SPA ClientID 1
                 "e5f9a1d8-0b4b-419c-b7d4-fc5df096d721" //SPA ClientID 2
                 },
                RoleClaimType = "roles" //Req'd only if you're doing RBAC 
                                        //i.e. web api manifest has "appRoles"
            }
        });
}

MODIFIER

D'accord, sur la base des commentaires de @ JonathanRupp, j'ai pu inverser la solution API Web que j'utilisais ci-dessus et modifier mon code JavaScript client, comme indiqué ci-dessous, pour que tout fonctionne correctement.

    // Acquire Token for Backend
    authContext.acquireToken("https://mycorp.net/WebApi.MyCorp.RsrcID_01", function (error, token) {

        // Handle ADAL Error
        if (error || !token) {
            printErrorMessage('ADAL Error Occurred: ' + error);
            return;
        }

        // Get TodoList Data
        $.ajax({
            type: "GET",
            crossDomain: true,
            headers: {
                'Authorization': 'Bearer ' + token
            },
            url: "https://api.mycorp.net/odata/ToDoItems",
        }).done(function (data) {
            // For Each Todo Item Returned, do something
            var output = data.value.reduce(function (rows, todoItem, index, todos) {
                //omitted
            }, '');

            // Update the UI
            //omitted

        }).fail(function () {
            //do something with error
        }).always(function () {
            //final UI cleanup
        });
    });
2
bkwdesign

Vous devez informer votre API Web de votre application client. Il ne suffit pas d'ajouter une autorisation déléguée à l'API de votre client.

Pour mettre le client API en évidence, accédez au portail de gestion Azure, téléchargez le manifeste de l'API et ajoutez le ClientID de votre application client à la liste des "applications connues". 

Pour autoriser le flux implicite, vous devez également définir "oauth2AllowImplicitFlow" sur true dans le manifeste.

Rechargez le manifeste dans l'application API.

1
Pavel Pikat

ADAL.js différencie access_token de id_token pour appeler l'API protégée Azure AD s'exécutant sur un domaine différent. Initialement, lors de la connexion, cela ne prend que id_token. Ce jeton a l'accès pour accéder aux ressources du même domaine . Mais, lors de l'appel de l'API s'exécutant dans un domaine différent, adal interceptor vérifie si l'URL de l'API est configurée comme point de terminaison dans adal.init ().

C'est seulement à ce moment-là que le jeton d'accès est appelé pour la ressource demandée. Il faut également que le SPA soit configuré dans l’AAD pour accéder à l’API APP. 

La clé pour y parvenir est la suivante: 1. Ajouter des points de terminaison dans adal.init ()

var endpoints = {

    // Map the location of a request to an API to a the identifier of the associated resource
    //"Enter the root location of your API app here, e.g. https://contosotogo.azurewebsites.net/":
    //    "Enter the App ID URI of your API app here, e.g. https://contoso.onmicrosoft.com/TestAPI",
    "https://api.powerbi.com": "https://analysis.windows.net/powerbi/api",
    "https://localhost:44300/": "https://testpowerbirm.onmicrosoft.com/PowerBICustomServiceAPIApp"
};

adalProvider.init(
    {
        instance: 'https://login.microsoftonline.com/',
        tenant: 'common',
        clientId: '2313d50b-7ce9-4c0e-a142-ce751a295175',
        extraQueryParameter: 'nux=1',
        endpoints: endpoints,
        requireADLogin: true,

        //cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.  
        // Also, token acquisition for the To Go API will fail in IE when running on localhost, due to IE security restrictions.
    },
    $httpProvider
    );
  1. Autorisez l'application SPA d'Azure AD à accéder à l'application API:  enter image description here

Vous pouvez consulter ce lien pour plus de détails: ADAL.js deep dive

1
Rahul Mohan

Je ne sais pas si notre configuration est exactement la même, mais je pense que c'est comparable.

J'ai un Angular SPA qui utilise une API Web externe via Azure API Management (APIM). Mon code n'est peut-être pas la meilleure pratique, mais cela fonctionne pour moi jusqu'à présent :)

L'application Azur AD de SPA dispose d'une autorisation déléguée pour accéder à l'application Azure AD des API externes.

Le SPA (est basé sur l’échantillon Adal TodoList SPA )

app.js

adalProvider.init(
    {
        instance: 'https://login.microsoftonline.com/', 
        tenant: 'mysecrettenant.onmicrosoft.com',
        clientId: '********-****-****-****-**********',//ClientId of the Azure AD app for my SPA app            
        extraQueryParameter: 'nux=1',
        cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.
    },
    $httpProvider
    );

Extrait du todoListSvc.js

getWhoAmIBackend: function () {
        return $http.get('/api/Employee/GetWhoAmIBackend');
    },

Extraits de code de EmployeeController

public string GetWhoAmIBackend()
    {
        try
        {
            AuthenticationResult result = GetAuthenticated();

            HttpClient client = new HttpClient();
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

            var request = new HttpRequestMessage()
            {
                RequestUri = new Uri(string.Format("{0}", "https://api.mydomain.com/secretapi/api/Employees/GetWhoAmI")),
                Method = HttpMethod.Get, //This is the URL to my APIM endpoint, but you should be able to use a direct link to your external API

            };
            request.Headers.Add("Ocp-Apim-Trace", "true"); //Not needed if you don't use APIM
            request.Headers.Add("Ocp-Apim-Subscription-Key", "******mysecret subscriptionkey****"); //Not needed if you don't use APIM

            var response = client.SendAsync(request).Result;
            if (response.IsSuccessStatusCode)
            {
                var res = response.Content.ReadAsStringAsync().Result;
                return res;
            }
            return "No dice :(";
        }
        catch (Exception e)
        {
            if (e.InnerException != null)
                throw e.InnerException;
            throw e;
        }
    }

        private static AuthenticationResult GetAuthenticated()
    {
        BootstrapContext bootstrapContext = ClaimsPrincipal.Current.Identities.First().BootstrapContext as BootstrapContext;
        var token = bootstrapContext.Token;

        Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext authContext =
            new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext("https://login.microsoftonline.com/mysecrettenant.onmicrosoft.com");

        //The Client here is the SPA in Azure AD. The first param is the ClientId and the second is a key created in the Azure Portal for the AD App
        ClientCredential credential = new ClientCredential("clientid****-****", "secretkey ********-****");

        //Get username from Claims
        string userName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn) != null ? ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value : ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;

        //Creating UserAssertion used for the "On-Behalf-Of" flow
        UserAssertion userAssertion = new UserAssertion(bootstrapContext.Token, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);

        //Getting the token to talk to the external API
        var result = authContext.AcquireToken("https://mysecrettenant.onmicrosoft.com/backendAPI", credential, userAssertion);
        return result;
    }

Maintenant, dans mon API externe principale, mon fichier Startup.Auth.cs se présente comme suit:

L'API externeStartup.Auth.cs

        public void ConfigureAuth(IAppBuilder app)
    {
        app.UseWindowsAzureActiveDirectoryBearerAuthentication(
            new WindowsAzureActiveDirectoryBearerAuthenticationOptions
            {
                Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
                TokenValidationParameters = new TokenValidationParameters
                {
                    ValidAudience = ConfigurationManager.AppSettings["ida:Audience"],
                    SaveSigninToken = true
                },
                AuthenticationType = "OAuth2Bearer"
            });
    }

S'il vous plaît laissez-moi savoir si cela aide ou si je peux être d'une aide supplémentaire.

0
Jonas