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:
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:
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;
});
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
});
});
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.
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
);
Vous pouvez consulter ce lien pour plus de détails: ADAL.js deep dive
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.