I\'m trying out adal.js with an Angular SPA (Single Page Application) web site that gets data from an external Web API site (different domain). Authentication against the SP
ADAL.js does get the access_token apart from id_token for calling Azure AD protected API running on different domain. Initially, during login, it only takes id_token. This token has the access for accessing resource of the same domain. But, on calling the API running in different domain, adal interceptor checks if the API URL is configured in as endpoint in adal.init().
It is only then that the access token is called for the requested resource. It also necessitates that the SPA is configured in the AAD to access API APP.
The key to achieve this is following: 1. Add endpoints in the 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
);
You may refer this link for details : ADAL.js deep dive
I'm not sure if our setup is exactly the same, but I think it it comparable.
I have a Angular SPA that uses and external Web API through Azure API Management (APIM). My code might not be best practice, but it works for me so far :)
The SPAs Azure AD app has a delegated permission to access the External APIs Azure AD app.
The SPA (is based upon the Adal TodoList SPA sample)
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
);
Snippet from the todoListSvc.js
getWhoAmIBackend: function () {
return $http.get('/api/Employee/GetWhoAmIBackend');
},
Snippets from the 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;
}
Now, in my backend external API, my Startup.Auth.cs looks like this:
The external API Startup.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"
});
}
Please let me know if this helps or if I can be of further assistance.
Ok, I've been bashing my head against the wall to figure this out. Trying to make my ADAL.js SPA app (sans angular) successfully make cross-domain XHR requests over to my precious CORS-enabled Web API.
This sample app, the one all the newbies like me are using, has this problem: it features an API and SPA all served from the same domain - and only requires a single AD Tenant app registration. This only confuses things when it comes time to pull things apart into separate pieces.
So, out of the box, the sample has this Startup.Auth.cs which works OK, as far as the sample goes...
public void ConfigureAuth(IAppBuilder app) {
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Audience = ConfigurationManager.AppSettings["ida:Audience"],
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
});
}
but, you need to modify the above code, drop the Audience
assignment, and go for an array of audiences.. That's right: ValidAudiences
.. So, for every SPA client that is talking to your WebAPI, you'll want to put the ClientID of your SPA registration in this array...
It should look like this...
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"
}
});
}
EDIT
Ok, based on @JonathanRupp's feedback, I was able to reverse out the Web API solution I was using shown above, and was able to modify my client JavaScript as shown below to make everything work.
// 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
});
});
You need to make your Web API aware of your Client application. It's not enough to add delegated permission to API from your Client.
To make the API client aware, go to Azure management portal, download API's manifest and add ClientID of your Client application to the list of "knownClientApplications".
To allow Implicit flow you need to set "oauth2AllowImplicitFlow" to true in the manifest as well.
Upload the manifest back to API application.