I have an application that suddenly stop to work due to the users list (https://content.googleapis.com/admin/directory/v1/users?orderBy=email&viewType=domain_public&maxResults=200&customer=my_customer&domain=XXX&key=XXX) keeps returning:
{
"error": {
"code": 403,
"message": "Not Authorized to access this resource/api",
"errors": [
{
"message": "Not Authorized to access this resource/api",
"domain": "global",
"reason": "forbidden"
}
]
}
}
This application is provided to 2 distinct domains. It works properly to one but not to other. Same code... just appId, apiKey and clientId changes. I configured wide domain delegation with scopes (https://www.googleapis.com/auth/admin.directory.user.readonly, https://www.googleapis.com/auth/admin.directory.user), also configured API KEY, OAuth 2.0 Client.
I tried to recreate the whole project, all credentials, tried http and https... same error.
Is there something that changed in the API specification? Something that am I missing?
EDITED - Source code requested:
I just copied this whole code, changed "settings" with credentials and executed in a browser console. The code works before or after authentication, but with the 403 response :-(
// This first part I got from https://github.com/lord22shark/google
(function (__window) {
const Google = function (configuration, __callback) {
if ((!configuration) || !(configuration instanceof Object)) {
throw new Error('Google API Wrapper - "configuration" must be defined!');
}
if ((!configuration.apiKey) || !(typeof(configuration.apiKey) === 'string') || (configuration.apiKey === '')) {
throw new Error('Google API Wrapper - "apiKey" must be defined!');
}
if ((!configuration.discoveryDocs) || !(configuration.discoveryDocs instanceof Array) || (configuration.discoveryDocs.length === 0)) {
throw new Error('Google API Wrapper - "discoveryDocs" must be a defined array!');
}
if ((!configuration.clientId) || !(typeof(configuration.clientId) === 'string') || (configuration.clientId === '')) {
throw new Error('Google API Wrapper - "clientId" must be defined!');
}
if ((!configuration.scope) || !(typeof(configuration.scope) === 'string') || (configuration.scope === '')) {
throw new Error('Google API Wrapper - "scope" must be defined!');
}
const thiz = this;
/**
*
*/
this.element = document.createElement('script');
this.element.type = 'text/javascript';
this.element.async = true;
this.element.defer = true;
this.element.src = 'https://apis.google.com/js/api.js';
/**
*
*/
this.element.onload = function () {
gapi.load('client:auth2', function () {
gapi.client.init({
'apiKey': configuration.apiKey,
'discoveryDocs': configuration.discoveryDocs,
'clientId': configuration.clientId,
'scope': configuration.scope
}).then(function () {
thiz.googleAuthInstance = gapi.auth2.getAuthInstance();
// Listen for sign-in state changes.
// The callback function must be a global named function
thiz.googleAuthInstance.isSignedIn.listen(onUpdateGoogleSignInStatus);
thiz.setSigninStatus();
}).catch(function (error) {
__callback(error);
});
}.bind(thiz));
};
/**
*
*/
this.element.onreadystatechange = function () {
if (this.readyState === 'complete') {
this.onload();
}
};
/**
*
*/
this.setSigninStatus = function (isSignedIn) {
if ((isSignedIn === true) || (isSignedIn === undefined)) {
this.user = this.googleAuthInstance.currentUser.get();
this.authorized = this.user.hasGrantedScopes(configuration.scope);
this.authorizationToken = (this.authorized) ? this.user.getAuthResponse().access_token : null;
if (!this.authorized) {
this.googleAuthInstance.signIn().then(function (authenticatedUser) {
__callback(this.user, this.authorized, this.authorizationToken, this.googleAuthInstance);
}.bind(this)).catch(function (error) {
__callback(error);
});
} else {
__callback(this.user, this.authorized, this.authorizationToken, this.googleAuthInstance);
}
}
};
/**
*
*/
this.getAuthInstance = function () {
return this.googleAuthInstance || null;
};
/**
*
*/
this.getUser = function () {
return this.user || null;
};
/**
*
*/
this.getAuthorizationToken = function () {
return this.authorizationToken;
};
/**
*
*/
this.getConfiguration = function () {
return configuration;
};
/**
*
*/
this.hasGrantedScopes = function () {
return this.authorized;
};
/**
*
*/
this.disconnect = function (deAuthenticate, callback) {
if (this.googleAuthInstance.isSignedIn.get()) {
if (deAuthenticate === true) {
this.googleAuthInstance.disconnect().then(function () {
if ((callback) && (callback instanceof Function)) {
callback(true);
}
});
} else {
this.googleAuthInstance.signOut().then(function () {
if ((callback) && (callback instanceof Function)) {
callback(false);
}
});
}
}
};
/**
*
*/
__window.onUpdateGoogleSignInStatus = function onUpdateGoogleSignInStatus (isSignedIn) {
this.setSigninStatus(isSignedIn);
}.bind(this);
document.body.appendChild(this.element);
};
__window.Google = Google;
})(window);
// This second part is part of my project - Settings
var settings = {
appId: 'xxx',
apiKey: 'xxx',
discoveryDocs: [
'https://www.googleapis.com/discovery/v1/apis/admin/directory_v1/rest'
],
clientId: 'xxx',
scope: 'https://www.googleapis.com/auth/admin.directory.user.readonly profile email'
};
var domains = [
'mydomain1.com', 'mydomain2.com'
];
// This is a bootstrap that I make some requests to sync information retrieved from oauth2 and my application, where I make the API call after authorized
function bootstrap (google) {
if (google instanceof Error) {
console.log(google);
} else {
// Here I Start Angular angular.element(document).ready.... get the user domain...
var userDomain = 'mydomain1';
var profile = google.getUser().getBasicProfile();
console.log(profile);
gapi.client.directory.users.list({
'viewType': 'domain_public',
'customer': 'my_customer',
'maxResults': 200,
'orderBy': 'email',
'domain': userDomain
}).then(function (response) {
console.log(response.result.users);
}).catch(function (error) {
console.log(error);
});
}
}
// Invokes Google with settings and if the authenticated user is part of one of my domains, I call "bootstrap" function
var google = new Google(settings, function (user, authorized, authorizationToken, authInstance) {
if (authorized !== undefined && authorizationToken !== undefined) {
var email = user.getBasicProfile().getEmail();
var allowed = domains.reduce(function (previous, current) {
if (email.indexOf(current) !== -1) {
previous += 1;
}
return previous;
}, 0);
if (allowed === 0) {
authInstance.disconnect();
bootstrap(new Error('User does not belong to XXX domain.'));
} else {
bootstrap(google);
}
} else {
bootstrap(new Error(user.details));
}
});
In order to run users.list() you should be an administrator with the right permissions as there are different type of Google Workspace admins. That is why you can run successfully your code when you run it from a super admin account and why if you set viewType
to admin_view
you can run it successfully too.
However, while user accounts can only be modified by administrators, any user on the domain can read user profiles (in your case this would be possible as your method is for listing them). According to the documentation detailing this in order to do this setting viewType
to domain_public
you must first enable contact sharing for the domain (individual users with contact sharing disable will not be retrieved). Also note that the following scope should be enough https://www.googleapis.com/auth/admin.directory.user.readonly
.