I'm developing a SPA app in React that needs to integrate with AzureAD and the GraphAPI (implicit flow).
My question is very similar to: ADAL.js - Obtaining Microsoft Graph Access Token with id_token ... but the answer doesn't show me enough code to get me on my way.
So far, using just adal.js (v1.0.14), I can login & get an id_token, but I can't figure out how to use it to get access to make Graph API calls.
UPDATE: I know I'm correctly registered with the Azure portal, because I was able to login and get recent docs without adal.js or any lib ... just using home-made ajax calls.
Here's my code, which does the login/redirect, and then tries to get my recent docs:
// Initial setup
var adalCfg = {
instance : 'https://login.microsoftonline.com/',
tenant : 'common',
clientId : 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
postLogoutRedirectUri : window.location.origin,
extraQueryParameter : 'scope=Mail.ReadWrite+Files.Read+Files.ReadWrite+User.ReadBasic.All' // Is this the proper way to specify what resources I need access to?
};
var authContext = new Adal(adalCfg);
if(!authContext.getCachedUser()) {
authContext.login(); // redirects MS login page successfully
}
// Check For & Handle Redirect From AAD After Login
var isCallback = authContext.isCallback(window.location.hash); // Checks if the URL fragment contains access token, id token or error_description.
if(isCallback) {
authContext.handleWindowCallback(); // extracts the hash, processes the token or error, saves it in the cache and calls the registered callbacks with the result.
}
if (isCallback && !authContext.getLoginError()) {
window.location = authContext._getItem(authContext.CONSTANTS.STORAGE.LOGIN_REQUEST); // redirects back to /
}
// Try to get my recent docs - FAILS with InvalidAuthenticationToken error
// UDPATED authContext.acquireToken(authContext.config.clientId, function (error, token) {
authContext.acquireToken('https://graph.microsoft.com', function (error, token) {
$.ajax({
url: 'https://graph.microsoft.com/v1.0/me/drive/recent',
headers:{'authorization':'Bearer '+ token},
type:'GET',
dataType:'json'
}).done(function(res) {
console.log(res['value']);
});
});
What have I got wrong?
Update 2: I changed acquireToken per Fei's answer, but now when adal silently gets an access token for my resource, it fails to pass it to my API call.
Updated code:
adalCfg.endpoints.graphApiUri = "https://graph.microsoft.com";
authContext.acquireToken(adalCfg.endpoints.graphApiUri, function (errorDesc, token, error) {
console.log('errorDesc = ' + errorDesc)
console.log('token = ' + token)
console.log('error = ' + error)
$.ajax({
url: adalCfg.endpoints.graphApiUri + '/v1.0/me/drive/recent',
headers:{'authorization':'Bearer '+ token},
type:'GET',
dataType:'json'
}).done(function(res) {
console.log(res['value']);
});
});
And console output:
Token not being captured
The image shows the req for a token, which appears to succeed, because the next GET contains the access_token in the hash. However, acquireToken passes a null token to my Graph API call.
However, if I manually grab the access token out of the hash, I can successfully make the Graph API call.
Why doesn't adal pass the access token to my API call? It came back and is valid.
To call the Microsoft Graph, we need to get the specific token for this resource. Based on the code you were acquire the token using the authContext.config.clientId.
If you register the app on Azure portal, to get the access token for the Microsoft Graph, you need to replace authContext.config.clientId with https://graph.microsoft.com.
And to call the REST sucessfully, we need to make sure having the enough permission. For example, to list recent files, one of the following scopes is required:Files.Read,Files.ReadWrite,Files.Read.All,Files.ReadWrite.All,Sites.Read.All,Sites.ReadWrite.All(refer here).
Update
<html>
<head>
<script src="\node_modules\jquery\dist\jquery.js"></script>
<script src="node_modules\adal-angular\lib\adal.js"></script>
</head>
<body>
<button id="login"> login</button>
<button id="clickMe">click me</button>
<script>
$(function () {
var endpoints = {
"https://graph.microsoft.com": "https://graph.microsoft.com"
};
window.config = {
tenant: 'xxxx.onmicrosoft.com',
clientId: 'xxxxxxxxxxxxxxxxx',
endpoints: endpoints
};
window.authContext = new AuthenticationContext(config);
$("#login").click(function () {
window.authContext.login();
});
$("#clickMe").click(function () {
var user = window.authContext.getCachedUser();
console.log(user);
window.authContext.acquireToken('https://graph.microsoft.com', function (error, token) {
console.log(error);
console.log(token);
$.ajax({
url: 'https://graph.microsoft.com/v1.0/me/',
headers:{'authorization':'Bearer '+ token},
type:'GET',
dataType:'json'
}).done(function(res) {
console.log(res['userPrincipalName']);
});
});
}
);
function init(){
if(window.location.hash!="")
window.authContext.handleWindowCallback(window.location.hash);
}
init();
});
</script>
</body>
</html>
Related
I've set up API access for my (Universal) Analytics project and it works fine from Google's "Demos and Tools" via Request Composer. So then I picked up the simple tutorial HTML file from here:
https://developers.google.com/analytics/devguides/reporting/core/v4/quickstart/web-js
But as soon as the page loads, there's an error in the console saying: You have created a new client application that uses libraries for user authentication or authorization that will soon be deprecated... etc etc.
But the library doesn't seem to have been deprecated yet (this seems to be coming in March 2023).
Anyway, I attempted to keep in pace with the times and use the new library, following the guide here:
https://developers.google.com/identity/gsi/web/guides/migration#popup-mode_1
That led nowhere (403 errors and other problems, as it seems that is not suited for API access). Eventually, though, I found a guide that shows how to use GIS for accessing an API:
https://developers.google.com/identity/oauth2/web/guides/migration-to-gis
Using this, I managed to cobble up together a working query. I'll post it as the answer. This is to help others that are affected by the outdated tutorial.
Enable the Analytics API in the way indicated by this tutorial: https://www.jcchouinard.com/google-api/
You'll want to create credentials for an Internal Web Application, and set "Authorized JavaScript origins" to http://localhost:8080
Save this as .html and serve it on localhost:8080. It should ask you to sign in, you should see the name of your Web Application in the Sign In prompt. And once you press on "Query Reports" you should get a nice JSON back.
<!DOCTYPE html>
<html>
<head>
<script src="https://apis.google.com/js/client:platform.js"></script>
<script src="https://accounts.google.com/gsi/client" onload="initClient()" async defer></script>
</head>
<body>
<script>
var client;
var access_token;
function initClient() {
client = google.accounts.oauth2.initTokenClient({
client_id: 'YOUR-CLIENT-ID',
scope: 'https://www.googleapis.com/auth/analytics.readonly',
callback: (tokenResponse) => {
access_token = tokenResponse.access_token;
},
});
}
function getToken() {
client.requestAccessToken();
}
function revokeToken() {
google.accounts.oauth2.revoke(access_token, () => {console.log('access token revoked')});
}
function loadCalendar() {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://www.googleapis.com/calendar/v3/calendars/primary/events');
xhr.setRequestHeader('Authorization', 'Bearer ' + access_token);
xhr.send();
}
</script>
<script>
// Replace with your view ID.
var VIEW_ID = 'YOUR-VIEW-ID';
// Query the API and print the results to the page.
function queryReports() {
gapi.client.request({
path: '/v4/reports:batchGet',
root: 'https://analyticsreporting.googleapis.com/',
method: 'POST',
body: {
reportRequests: [
{
viewId: VIEW_ID,
dateRanges: [
{
startDate: '7daysAgo',
endDate: 'today'
}
],
metrics: [
{
expression: 'ga:sessions'
}
]
}
]
}
}).then(displayResults, console.error.bind(console));
}
function displayResults(response) {
var formattedJson = JSON.stringify(response.result, null, 2);
document.getElementById('query-output').value = formattedJson;
}
</script>
<h1>Google Identity Services Authorization Token model</h1>
<button onclick="getToken();">Get access token</button><br><br>
<button onclick="queryReports();">Query Reports</button><br><br>
<button onclick="revokeToken();">Revoke token</button>
<!-- The API response will be printed here. -->
<textarea cols="80" rows="20" id="query-output"></textarea>
</body>
</html>
We have experienced the following problem:
We had implemented Azure AD authenticated web API and we have been successfully had been making calls to it with ADAL 1.0.12 (we had our own wrapper around it for processing iframe for silent login).
Now we got rid all together of our wrapper and upgraded to the latest version and started to get the problem described here: token renewal operation timed out.
The Azure AD architecture behind the API is very simple: a registered web app with Read directory data and Sign in and read user profile permissions. We have another native app registered with permissions to the web app (Access to the Web APP NAME HERE). Both applications have oauth2AllowImplicitFlow set to true in their manifests.
Both apps have registered a wildcard redirect URIs for https://ourtenant.sharepoint.com/* (we are making the calls from SharePoint to the web API). In addition, the native client app has registered redirect URI to the unique URI (App ID URI) of the web API app.
Both apps have been granted admin consent before - they were working fine with the old version of ADAL.js
So, here is some code. Please, note that we had tried both without displayCall callback (with page refresh directly on the current page) and with page refresh (check the commented code in initLogin method). Also we had a switch for generating a popup or an iframe with a callback on successful login.
The problem is with authContext.acquireToken. Note that if we call OurNamespace.authContext.getCachedToken(OurNamespace.clientId) we get a stored token for the resource.
Note that also, we are calling properly handleWindowCallback after each page refresh / iframe / popup.
Happens regardless of the browser.
OurNamespace = {
serviceMainUrl: "https://localhost:44339",
webApiAppIdUri: "WEB-API-URI",
tenant: "TENANT",
clientId: "NATIVE-APP-GUID", // Native APP
adalEndPoints: {
get: null
},
adal: null,
authContext: null,
dummyAuthPage: null,
usePopup: true,
init: function () {
this.dummyAuthPage = "DummmyLogin.aspx";
OurNamespace.adalEndPoints.get = OurNamespace.serviceMainUrl + "/api/values";
OurNamespace.authContext = new AuthenticationContext({
tenant: OurNamespace.tenant + '.onmicrosoft.com',
clientId: OurNamespace.clientId,
webApiAppIdUri: OurNamespace.webApiAppIdUri,
endpoints: OurNamespace.adalEndPoints,
popUp: false,
postLogoutRedirectUri: window.location.origin,
cacheLocation: "localStorage",
displayCall: OurNamespace.displayCall,
redirectUri: _spPageContextInfo.siteAbsoluteUrl + "/Pages/" + OurNamespace.dummyAuthPage
});
var user = OurNamespace.authContext.getCachedUser(); // OurNamespace.authContext.getCachedToken(OurNamespace.clientId)
if (user) {
OurNamespace.azureAdAcquireToken();
} else {
OurNamespace.initLogin();
}
},
initLogin: function () {
//OurNamespace.authContext.config.displayCall = null;
//var isCallback = OurNamespace.authContext.isCallback(window.location.hash);
//OurNamespace.authContext.handleWindowCallback();
//if (isCallback && !OurNamespace.authContext.getLoginError()) {
// window.location.href = OurNamespace.authContext._getItem(OurNamespace.authContext.CONSTANTS.STORAGE.LOGIN_REQUEST);
//}
OurNamespace.authContext.login();
},
displayCall: function (url) {
var iframeId = "azure-ad-tenant-login",
popup = null;
if (OurNamespace.usePopup) {
popup = window.open(url, 'auth-popup', 'width=800,height=500');
} else {
var iframe = document.getElementById(iframeId);
if (!iframe) {
iframe = document.createElement("iframe");
iframe.setAttribute('id', iframeId);
document.body.appendChild(iframe);
}
iframe.style.visibility = 'hidden';
iframe.style.position = 'absolute';
iframe.src = url;
popup = iframe.contentDocument;
}
var intervalId = window.setInterval(function () {
try {
// refresh the contnetDocument for iframe
if (!OurNamespace.usePopup)
popup = iframe.contentDocument;
var isCallback = OurNamespace.authContext.isCallback(popup.location.hash);
OurNamespace.authContext.handleWindowCallback(popup.location.hash);
if (isCallback && !OurNamespace.authContext.getLoginError()) {
popup.location.href = OurNamespace.authContext._getItem(OurNamespace.authContext.CONSTANTS.STORAGE.LOGIN_REQUEST);
window.clearInterval(intervalId);
if (OurNamespace.usePopup) {
popup.close();
}
var user = OurNamespace.authContext.getCachedUser();
if (user) {
console.log(user);
} else {
console.error(OurNamespace.authContext.getLoginError());
}
}
} catch (e) { }
}, 400);
},
azureAdAcquireToken: function () {
OurNamespace.authContext.acquireToken(OurNamespace.adalEndPoints.get, function (error, token) {
if (error || !token) {
SP.UI.Status.addStatus("ERROR", ('acquireToken error occured: ' + error), true);
return;
} else {
OurNamespace.processWebApiRequest(token);
}
});
},
processWebApiRequest: function (token) {
// Alternatively, in MVC you can retrieve the logged in user in the web api with HttpContext.Current.User.Identity.Name
$.ajax({
type: "GET",
url: OurNamespace.adalEndPoints.get,
contentType: "application/json; charset=utf-8",
dataType: "json",
data: {},
headers: {
'Authorization': 'Bearer ' + token
},
success: function (results) {
var dataObject = JSON.parse(results);
SP.UI.Status.addStatus("Info", "ADAL GET data success: " + dataObject.webApiUser);
$(".grid-info").html("<h1 class=\"h1\">Current Web API authenticated user: " + dataObject.webApiUser + "</h1>");
},
error: function (xhr, errorThrown, textStatus) {
console.error(xhr);
SP.UI.Status.addStatus("ERROR", ('Service error occured: ' + textStatus), true);
}
});
}
}
I am testing using the 1.0.15 adal.js and get the access token successfully by using the authContext.acquireToken which actually call the this._renewToken(resource, callback) to acquire the access token by the hide iframe. Here is the full test sample code for your reference:
<html>
<head>
<script src="/js/1.0.15/adal.js"></script>
<script>
var config = {
tenant: 'adfei.onmicrosoft.com',
clientId: '7e3b0f81-cf5c-4346-b3df-82638848104f',
redirectUri: 'http://localhost/spa.html',
navigateToLoginRequestUrl:false,
};
var authContext=new AuthenticationContext(config);
var hash = window.location.hash;
if(hash.length!=0){
authContext.handleWindowCallback(hash);
var user=authContext.getCachedUser();
}
function login(){
authContext.login();
}
function acqureToken(){
authContext.acquireToken("https://graph.microsoft.com", function(error, token, message){
console.log(token)
})
}
</script>
</head>
<body>
<button onclick="login()">Login</button>
<button onclick="acqureToken()">AcquireToken</button>
</body>
</html>
Is it helpful? Or would you mind sharing a run-able code sample for this issue?
I'm not sure if what version of adal.js you are using. But there is a loadFrameTimeout setting for the config object that you can set in milliseconds. Check the top of your adal.js file and it should be there.
I have this $http request interceptor
app.config(function($httpProvider) {
$httpProvider.interceptors.push(function() {
return {
request: function(req) {
// Set the `Authorization` header for every outgoing HTTP request
req.headers['cdt_app_header'] = 'tamales';
return req;
}
};
});
});
Is there any way we can add a header or cookie to every $http request, but keep the header value secure / not visible with JavaScript?
We can add an obfuscation layer with this header to prevent easy access to our API endpoints, but I am wondering about a more truly secure solution.
Cookies are used for secure sessions, and these are more secure because they cannot be accessed with JavaScript. Say we have a user who can do this request with front-end code:
GET /api/users
we don't really want them to be able to make a simple request with cURL or a browser without an extra piece of information. The cookie we give them will give them the ability to use the browser address bar to make a GET request to /api/users, but if we add the requirement to have another cookie or header in place, then we can prevent them from accessing endpoints that are authorized for, in a format that we don't really want them to use.
In other words, we want to do our best to give them access, but only in the context of a front-end Angular app.
I can't add a comment because of my rep but what are you doing on the back-end to authorize users? If the cookie is signed and contains user permissions it shouldn't matter that the header is visible in the client as it will also be verified on the back-end API call.
in this sample i used HttpRestService to get RESTful API, read this article
at first we create a service to get our configs in this sample is getConfigs
we use getConfigs in the app.run when application is started, after get the configs we set them all in the header as sample.
after that we can get userProfile with new header and also secure by call it from our controller as you see.
in this sample you need to define apiUrl, it's your api host url, remember after logout you can remove the header, also you can define your configs dynamically to make more secure for your application.
HttpRestService.js github link
app.js
var app = angular.module("app", ["HttpRestApp"]);
app.service
app.service("service", ["$http", "$q", "RestService", function (http, q, restService) {
this.getConfigs = function () {
var deferred = q.defer();
http({
method: "GET",
async: true,
headers: {
"Content-Type": "application/json"
},
url: "you url to get configs"
}).then(function (response) {
deferred.resolve(response.data);
}, function (error) {
deferred.resolve(error);
});
return deferred.promise;
}
var api = {
user: "User" //this mean UserController
}
//get user with new header
//this hint to your api with this model "public Get(int id){ return data; }"
//http://localhost:3000/api/users/123456
this.getUserProfile= function(params, then) {
restService.get(params, api.user, true).then(then);
}
}]);
app.run
app.run(["RestService", "service", function (restService, service) {
var header = {
"Content-Type": "application/json"
}
//get your configs and set all in the header
service.getConfigs().then(function (configs) {
header["systemId"] = configs.systemId;
});
var apiUrl = "http://localhost:3000/";
restService.setBaseUrl(apiUrl, header);
}]);
app.controller
app.controller("ctrl", ["$scope", "service", function ($scope, service) {
$scope.getUserProfile = function () {
//this is just sample
service.getUserProfile({ id: 123456 }, function (data) {
$scope.user = data;
});
}
$scope.getUserProfile();
}]);
I have a basic angular APP that makes a GET request call to a API URL. The data returned is in JSON format. The API documentation states the following:
You must provide your App ID and key with every request that you make to the API. To do this, set an HTTP Authorization header on your requests that consists of your ID, followed by a colon, followed by your key, eg abc123:xyz789.
How do I incorporate this to my basic HTTP request.my code is below.
angular.module('myApp', [])
.controller('MyControler', function($scope, $http) {
$scope.$watch('search', function() {
fetch();
});
$scope.search = "My Search Query";
function fetch() {
$http.get("https://APIURlGoesHere" + $scope.search )
.then(function(response) {
$scope.details = response.data;
});
$http.get("ttps://APIURlGoesHere" + $scope.search)
.then(function(response) {
$scope.related = response.data;
});
}
});
Best way I know so far to implement this is: Interceptors
You can find some useful info about it here and here
And on SO, here: AngularJS $http interceptors
In your case, basically, you need to create a file with the following implementation (or equivalent) and include it into your project:
function myInterceptor() {
function request(req) {
var token = "mytoken" ; //<<--here you need to set the custom header's info (eg: abc123:xyz789)
var myHeader = "myHeader"; //<<--here you need to set the custom header's name (eg: Authorization)
if (token) {
//put custom header for sending the token along with every request
req.headers[myHeader] = token;
}
return req;
}
return {
request: request
};
};
function conf($httpProvider) {
$httpProvider['interceptors'].push('myInterceptor');
};
angular
.module('your_module_name')
.factory('myInterceptor', myInterceptor)
.config(['$httpProvider', conf]);
This will intercept every request made from your frontend app and will include that header on it.
Citing this topic:
How to use Basic Auth with jQuery and AJAX?
So, in Angular, it would be:
$http({
method: "GET",
url: "https://APIURlGoesHere" + $scope.search,
headers: { 'Authorization' : 'Basic '+btoa(username + ":" + password)
})
.then(function(response) {
$scope.details = response.data;
});
I have a problem with my angular app- after a user signs in, if he hits the refresh button, the signin info is lost and the app redirects to the log in page. I found a SO answer for something similar here using $cookieStore but I don't think it can work for me as I'm not using cookies. Can anyone suggest a solution? Here's my authorization service-
var app = angular.module('myApp.services');
app.factory('SignIn', ['$resource', '$q', function($resource, $q) {
var signInUrl = 'https://example.com'
var API = $resource(signInUrl, {}, {
signIn: {
withCredentials: true,
url: signInUrl + '/session',
method: 'POST'
},
signOut: {
url: authApiUrl + '/session',
method: 'DELETE'
},
currentUser: {
url: signInUrl + '/users/#me',
method: 'GET'
}
});
var _currentUser = undefined;
return {
isAuthenticated: function() {
return !!_currentUser;
},
getUser: function(){
var d = $q.defer();
// If _currentUser is undefined then we should get current user
if (_currentUser === undefined) {
API.currentUser(function(userData) {
_currentUser = userData;
d.resolve(userData);
}, function(response) {
if (response.statusCode === 401) {
_currentUser = null;
d.resolve(_currentUser);
} else {
d.reject(response);
}
});
} else {
d.resolve(_currentUser);
}
return d.promise;
},
signIn: function(username, password){
var d = $q.defer();
API.signIn({email: username, password: password}, function(data, headers){
_currentUser = data;
d.resolve(_currentUser);
}, d.reject);
return d.promise;
},
signOut: function(){
var d = $q.defer();
API.signOut(function(){
_currentUser = null;
d.resolve();
}, d.reject);
return d.promise;
}
};
}]);
If you just need to keep track of the _currentUser data past a refresh then you could use sessionStorage within the browser. That extends all the way back to IE 8 and we really shouldn't be supporting any browsers before that anyway.
Usually these things are done with cookies though. When the client first makes a connection to the server (even before the first API call in some cases) a cookie is sent to the client so the server can maintain a session associated with that particular client. That's because the cookie is automatically sent back to the server with each request and the server can check its local session and say, "Oh, I'm talking to this user. Now I can use that additional piece of context to know if I can satisfy their API call or not."
You don't show any of your other API calls here but I'm guessing that you're sending something out of the _currentUser with each API call to identify the user instead? If so, that certainly works, and it avoids the need to synchronize cookies across multiple servers if you're clustering servers, but you're going to have to use something local like sessionStorage or localStorage that won't get dumped like your current in-memory copy of the data does when you refresh the page.