Authenticate Vue with WebAPI using AzureAD - vuejs2

I am trying to query the data from AzureAD protected WebAPI from Vue using vue-msal.
WebApi: 58ca819e- is webapi app id registered in AzureAD
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "58ca819e-",
"TenantId": "3a0cf09b-"
},
Msal plugin in main.js: be7e77ba is spa client app id registered in AzureAD
import msal from 'vue-msal'
Vue.use(msal, {
auth: {
clientId: 'be7e77ba-x',
tenantId: '3a0cf09b-x',
redirectUri: 'http://localhost:8080/callback'
},
cache: {
cacheLocation: 'localStorage',
},
});
In the component: $msal.acquireToken is called for access token on 2 scopes Microsoft Graph user.read and WebAPI api://58ca819e-/access_as_user where 58ca819e- is WebAPI app id registed in AzureAD.
if (!this.$msal.isAuthenticated()) {
this.$msal.signIn();
}
else{
this.$msal
.acquireToken({scopes: ["user.read", 'api://58ca819e-x/access_as_user']})
.then((res)=>{
console.log(res.accessToken)
this.token = res
})
.catch(err=>console.error(err))
}
The received access token contains only Microsoft Graph but not WebAPI scopes. The token is invalid for the WebAPI.
"scp": "openid profile User.Read email",
When only WebAPI scope is provided, the generated token containing "scp": "access_as_user", can access the WebAPI:
this.$msal.acquireToken({scopes: ['api://58ca819e-x/access_as_user']})
or
this.$msal.acquireToken({scopes: ['api://58ca819e-x/access_as_user','user.read']})
Looks like only 1 scope is used? Can we ask the access token for >1 scopes?

No!
A token can only correspond to one scope. The access token is issued according to the api audience you want to access, and it is unique! A token can only have one audience, and you cannot use multiple scopes to request access tokens.
You should put the api you want to access in the scope. For example, if you want to access MS graph api, you can put https://graph.microsoft.com/.default. If you want to access a custom api, you can put in api://{back-end app client api}/scope name.

Related

How to pass user information from frontend to backend

I have a Ruby on Rails backend API and frontend application written in Nuxt.js integrated with Auth0 for authentication and Axios for API communication.
The authentication works fine, however the frontend does not pass user information to backend. Only user_id and Authorization Bearer token, but not email, name and other attributes. These attributes are however available after authentication in frontend and saved/available in the Vuex store.
How to tell frontend (Axios) to include with every API request to backend, additional user attributes? Is there any syntax to access Vuex store and define global Axios attribute in nuxt.config.js or is there a simpler way to do it?
Nuxt.config.js
auth: {
redirect: {
login: '/auth/login',
callback: '/auth/callback'
},
strategies: {
auth0: {
domain: config.AUTH.DOMAIN,
client_id: config.AUTH.CLIENT_ID,
audience: config.AUTH.AUDIENCE
}
}
}
example request to backend and saving results to Vuex store
this.$axios.get('/organizations/4/projects')
.then(response => {
this.setProjects(response.data.projects);
})
.catch(error => {
console.log(error);
});
You could add the extra information (in a custom claim) to the access token, then it would be available to the backend when you make the request.
Another strategy would be to make use of the /userinfo endpoint to request the user profile, although this will require an extra call from the backend.
The tradeoffs here are the increased size of the token with custom claims vs the extra API call with the /userinfo endpoint.

Azure AD app grants bearer token with a scope from a different AD app under the same tenant

I have an AD app(client id: xyz, scope: xyz_123) and another AD app(client id: abc, scope: abc_123) under the same tenant.
Suppose my jwt token validation code in the service is something like
.AddJwtBearer(o =>
{
o.Authority = Configuration["Authentication:Authority"];
o.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudiences = new List<string>
{
xyz,
xyz_123
}
};
So Now when I generate a bearer token with grant_type as client credentials and client id: abc and the resource_id: xyz_123 and hit my service, the authentication is successful.
Is this expected behavior. Why is the AD app(client id: abc) granting a bearer token when hit with a resource/scope that it does not have(xyz_123). Do all the AD app instances under the same tenant have access to all its scopes?
Yes.
You must check token permissions to secure your API properly.
It used to be possible to do that over tenant boundaries, but now it's only possible within the tenant: https://joonasw.net/view/cross-tenant-token-attacks-now-harder-in-azure-ad.
You need to check any access token received by your app has at least one valid delegated / application permission: https://joonasw.net/view/always-check-token-permissions-in-aad-protected-api

Azure AD User info with JWT Bearer token and ASP.NET Core 2 WebApi

I found a tutorial where I can sign in to my application with Azure AD credentials.
In my frontend I'm using Xamarin.Forms.
In my backend I'm using ASP.NET Core 2.0 WebApi.
Backend:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseMvc();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = String.Format(Configuration["AzureAd:AadInstance"], Configuration["AzureAD:Tenant"]);
options.Audience = Configuration["AzureAd:Audience"];
});
}
It's pretty simple.
In my frontend I'm filling in my credentials and asking for a access_token.
{
"token_type": "Bearer",
"scope": "user_impersonation",
"expires_in": "3600",
"ext_expires_in": "0",
"expires_on": "1507104075",
"not_before": "1507100175",
"resource": "my_resource",
"access_token": "my_access_token",
"refresh_token": "my_refresh_token"
}
The access_token i'm filling in the headers with Authorization set with bearer my_access_token.
My Api know's all my information because it will automaticly set claims with the info from my access_token. This info is provided by Azure AD. (fullname, firstname, lastname, ...)
But how can I get this information in my frontend?
You might want to check out the active-directory-dotnet-native-desktop sample on GitHub.
I shows how to use ADAL.NET in a desktop app, to get a token for a service. you will need to adapt it for your Xamarin forms client, but the principle is the same as far as authentication is concerned.
Also it contains a service and you would replace it by your own service and get a token for your web API by changing the resource ID to be the one of your application created using the ASP.NET wizard (you'll find it in the Azure portal as described in the readme.md of the sample)
the idea is that you first get a token using ADAL.Net line 92 of TodoListClient/MainWindow.xaml.cs
result = await authContext.AcquireTokenAsync(todoListResourceId, clientId, redirectUri, ...)
and then you use it as a bearer token line 121
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
If all the info you required is include in the access token, you can just decode the access token on the client. The access token is a JWT, it is easy to research code sample to decode the access token like following threads:
How to decode JWT Token?
Decoding and verifying JWT token using System.IdentityModel.Tokens.Jwt
And if you also require more user info, you can refresh the access token for the Microsoft Graph, and call the me endpoint of Microsoft Graph(refer here). And below is the document about how to refresh the access token via refresh token:
Refreshing the access tokens

Auth0 JWT as access token comes in only on second login

I have this issue and I'm not sure whether it is a "bug" or my fault somewhere.
All of this is for a SAP on ASP.NET Core Angular which is accessing Auth0 on a hosted page.
I have updated my hosted page Auth0lock object on the hosted page to inculde a params object with a specified audience
var lock = new Auth0Lock(config.clientID, config.auth0Domain, {
auth: {
redirectUrl: config.callbackURL,
responseType: 'token',
params: {
"audience": "https://api.webatom.com"
}
},
assetsUrl: config.assetsUrl,
allowedConnections: connection ? [connection] : null,
rememberLastLogin: !prompt,
language: language,
languageDictionary: languageDictionary,
theme: {
//logo: 'YOUR LOGO HERE',
//primaryColor: 'green'
},
prefill: loginHint ? { email: loginHint, username: loginHint } : null,
closable: false,
// uncomment if you want small buttons for social providers
// socialButtonStyle: 'small'
});
During the first login I get the usual auth result where I receive the JWT as the id_token and a short string for the access token and I don't get a message in auth0 about account access request.
During the second and other logins I get what I want. I get the message and I get the JWT as access token and id_token as null.
How do I get that second result from the start, right from the first login? Is that a bug or am I doing something wrong?
Thank you.
PS: I don't have any rules or hooks implemented at that moment.
As a first step: Add https://jwt.io as an allowed callback to your Client, revert the Auth0 Hosted Login page back to its default (ie. remove the changes you made), then modify the url below with your own settings, and paste it into a browser URL and hit return.
https://{{YOUR_TENANT}}.auth0.com/login?client={{YOUR_CLIENT_ID}}&redirectUrl=https://jwt.io&responseType=token&connection={{YOUR_CONNECTION_NAME}}&audience=https://api.webatom.com&scope=openid
All going well, it should return a JWT Access Token and auto-populate that into the JWT.io text-area.
Next, try this - using Auth0's authorize URL instead. Again, use Auth0 default hosted login page, not the one you modified.
https://{{YOUR_TENANT}}.auth0.com/authorize?client_id={{YOUR_CLIENT_ID}}&protocol=oauth2&redirect_uri=https://jwt.io&response_type=token&scope=openid profile&audience=https://api.webatom.com&nonce=123&state=xyz
Should be same result. And presumably this is what you want every time?
If you do want an Id Token, then simply modify responseType / response_type to be token id_token.
So I would recommend you do not modify the Auth0 Hosted Login page settings for Lock directly (authentication related params..), but instead just send through the parameters you want with the request as per the /authorize endpoint above. If you have a Client application using auth0.js for example, you can set everything up at the Client and send it through when the user authenticates.
Sample snippet for auth0.js library config might be:
auth0 = new auth0.WebAuth({
domain: AUTH_CONFIG.domain,
clientID: AUTH_CONFIG.clientId,
redirectUri: AUTH_CONFIG.callbackUrl,
audience: "https://webapi.com",
responseType: 'token id_token', // just use token if you don't need id token
scope: 'openid profile read:book' // read:book is a scope defined for API
});
So far I have found an interesting work around...
When an opaque token is returned, you can simply copy its aud hash and paste it into the Audience parameter when creating the JwtBearerOptions object into the startup class.
That fixes the error with the invalid audience when using the [Authorize] annotation in the controller api which was the main reason why I needed the jwt from the start.
I thought the only way to get the audience insde the jwt for the JwtBearer to decode it correctly was to set the audience in on the hosted page so it would be returned with the JWT inside the access token.

Adal.js does not get tokens for external api endpoint resource

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 SPA was easy with adal.js, but getting it to communicate with the API is not working at all when bearer tokens are required. I have used https://github.com/AzureAD/azure-activedirectory-library-for-js as template in addition to countless blogs.
The problem is that when I set up endpoints while initiating adal.js, adal.js seems to redirect all outgoing endpoint traffic to microsofts login service.
Observations:
Adal.js session storage contains two adal.access.token.key entries. One for the client ID of the SPA Azure AD application and one for the external api. Only the SPA token has a value.
If I do not inject $httpProvider into adal.js, then calls go out to the external API and I get a 401 in return.
If I manually add the SPA token to the http header ( authorization: bearer 'token value') I get a 401 in return.
My theory is that adal.js is unable to retrieve tokens for endpoints (probably because I configured something wrong in the SPA) and it stops traffic to the endpoint since it is unable to get a required token. The SPA token cannot be used against the API since it does not contain the required rights. Why is adal.js not getting tokens for endpoints and how can I fix it?
Additional information:
The client Azure AD application is configured to use delegated permissions against the API and oauth2AllowImplicitFlow = true in app manifest.
The API Azure AD application is configured for impersonation and oauth2AllowImplicitFlow = true (do not think that is required, but tried it). It is multi tenant.
The API is configured to allow all CORS origins and it works correctly when used by another web app using impersonation (hybrid MVC (Adal.net) + Angular).
Session storage:
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 (Angular and adal configuration file)
(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
);
}]);
})();
Angular code for calling endpoint:
$scope.getItems = function () {
$http.get("https://localhost/Api/Items")
.then(function (response) {
$scope.items = response.Items;
});
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
});
});
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
);
Give permission to the SPA application in Azure AD to access the API application:
You may refer this link for details : ADAL.js deep dive
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.
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.