Is there limit on loading Azure AD users programatically using Graph API? - azure-active-directory

We have Azure AD with more than 20k users. I am trying to read all those users with properties such as displayName, city, accountName, department. I am not able to read all users. I am using the skiptoken to get all users, still I am getting each time only few thousands not all users.
Here is my sample code for extracting skiptoken:
public static string ExtractSkipToken(string responseString)
{
if (responseString.Contains("skiptoken=X"))
{
var startString = "skiptoken=X'";
var endString = "'";
var startIndex = responseString.IndexOf(startString, StringComparison.Ordinal);
var subText = responseString.Substring(startIndex + startString.Length);
var endIndex = subText.IndexOf(endString, StringComparison.Ordinal);
var skipToken = subText.Substring(0, endIndex);
return skipToken;
}
return string.Empty;
}
Has anyone done that?

The Azure AD Graph API supports paging as described in this document. The idea is that the link to the next page of results is provided in the response from the current page. I would recommend not parsing out the skiptoken manually, but rather using the provided link in the JSON response.
If you are following this pattern and the paging is not behaving as expected, please provide more detail on where the paging goes wrong.

Related

Angular sending access token to Asp.net Web API not working if the email address has a '+' in it

I have security working fine in an application, except when a user tries to login with a '+' in their email.
The access token looks fine (when the email contains a + it looks like this):
Bearer 8BGpt_KkEp-_6U5tUdKqK1xLCQBaWzHcxDT9RRKkbzoF2fHCUNhRL3U-fpLdQIuSXm8RcTOH4ZY3a0UZH6-6IgXxx_ojgyL26179JovRm5xQSZD7ANxLvvdU3ubfcpzSr4tw-sza37UaJh7xDFB8eH0NA9Djt7Ik8Ebxdin7u-n76InCulRAV6xMWgXfF9bwoU8MsV3lrh_zhnxYGnx3O7QUNQ740NUJLHJYH12rBth16CA1AXSF86rA5rUB7vJ7yK09k_FJTifyuldTeFHJHsyscnEIQxGozbf3x1cmZowkiK4Q1r8W0M8uz25m8j_tuMrWawTqYJNZiTuI9afW38WWQ4BRLkQF7TwoMOgZQ-f1K_3W8Zy3x-OsKdQS4i9CapvKe1utCscZVroByvyD9SvpILGiZGTjGD_zCAm8KerMPT5GNOb07kPGV_167PHEXm0TGaJbCelb5gLgXbMXv3GxBQLnYIfPUXCBaKx4UFkY8kFMPs9MxFcGY81p67rfnjeswBZ3PW6fDFTf9U_I8g
However, when I try to send a secure request with this access token, I get the response:
status: 401
"{"Message":"Authorization has been denied for this request."}"
As said above, it works without any issue if I remove the plus. This seems to be a Wep API issue rather than an Angular issue.
I found that the methods encodeUrl and decodeUrl to not stop the space from being change to a plus. I have tried the following in the c# code to switch the space to a plus:
var registerEmail = model.email.Replace(' ', '+');
This is used in both the login and register actions.
Perhaps it is not possible to use a + in an email in OAuth in Web API 2?
It seems to be a bug in asp.net roles. I am not sure of a clear solution. However, for the time being, encoding the username as follows before storing it on register and when logging in:
public static class UsernameEncodingService
{
public static string returnEncodedUsername(string email)
{
var emailAsLower = email.ToLowerInvariant();
var encodedEmail = Base64Encode(emailAsLower);
var encodedEmailWithoutEquals = encodedEmail.Replace("=", "213");
var encodedEmailWithoutPlus = encodedEmailWithoutEquals.Replace("+", "214");
return encodedEmailWithoutEquals;
}
private static string Base64Encode(string plainText)
{
var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
return System.Convert.ToBase64String(plainTextBytes);
}
}

Authentication request returned unexpected result 404

Following is a code to get google contacts.
It was working fine but since few days I m getting exception of "Authentication request returned unexpected result: 404".
using Google.GData.Client;
using Google.Contacts;
using Google.GData.Extensions;
private void FetchContactList()
{
List<string> lstContacts = new List<string>();
RequestSettings rsLoginInfo = new RequestSettings("my application", "abc#gmail.com", "XXXXXX");
rsLoginInfo.AutoPaging = true;
ContactsRequest cRequest = new ContactsRequest(rsLoginInfo);
Feed<contact> feedContacts = cRequest.GetContacts();
foreach (Contact gmailAddresses in feedContacts.Entries)
{
// Looping to read email addresses
foreach (EMail emailId in gmailAddresses.Emails)
{
lstContacts.Add(emailId.Address);
}
}
GridView1.DataSource = lstContacts;
GridView1.DataBind();
}
Is google change something from their side?
Please suggest me way to solve the problem.
Update your current api with google api to version 3 and then make changes to the code according. Probably this may be the reason for the error.
https://developers.google.com/analytics/devguides/reporting/core/v3/gdataLibraries
i further suggest you use oauth2.0 for authentication as per the current requirements of your application and if you are using the older version of the api then you must use oauth2.0
Here is the link for oauth2.0 support: https://developers.google.com/google-apps/spreadsheets/authorize
though this is basically for spreadsheets but you can see how it is and working and change it to the requirements of your gmail api

Azure Graph service not finding newly created user

I have a web application that uses Azure ACS and Azure AD to handle our authentication.
We have a user management feature in the web application that allows a user to create new users. This takes the details such as username, password, email etc. and uses the graph service to create a user in azure.
var newUser = new Microsoft.WindowsAzure.ActiveDirectory.User
{
userPrincipalName = user.UserName,
mailNickname = user.MailNickname,
accountEnabled = true,
displayName = user.FirstName + " " + user.Surname,
givenName = user.FirstName,
surname = user.Surname
};
newUser.passwordProfile = new PasswordProfile
{
forceChangePasswordNextLogin = false,
password = user.Password
};
var graphService = GetGraphService(tenantName);
graphService.AddTousers(newUser);
graphService.SaveChanges();
We are then required to create a record in the web application database for this user. The record needs the object ID from azure. So we use the graphService to get the newly-created user details. This is where my problem lies. It doesn't find the user.
private string GetObjectIdFromAzure(string userName, string tenantName)
{
var graphService = GetGraphService(tenantName);
var users = graphService.users;
QueryOperationResponse<Microsoft.WindowsAzure.ActiveDirectory.User> response;
response = users.Execute() as QueryOperationResponse<Microsoft.WindowsAzure.ActiveDirectory.User>;
var user = response.FirstOrDefault(x => x.userPrincipalName == userName);
return user != null ? user.objectId : "";
}
My code was working without any issues for a few months and only today I am having issues. What frustrates me more it that I have another deployment of the same code where it works without any issues. Some differences between the two deployments are:
The deployments use different Access control namespaces in Azure
The deployments have separate applications in Azure AD
One is https, one is http
The users for both system are under the same Directory.
I have put in logging in both deployments to get the number of users returned by
users.Execute()
In both systems it reported 100 (they share the same users)
Any ideas of what would cause this to stop working? I didn't change any code relating to this recently, I haven't changed any configuration on Azure and I didn't change the web.config of the application
The problem was caused by the fact that I was filtering the users after retrieving them. The graph API was only returning a maximum of 100 users.
So the process was like so:
User created in Azure
Success message returned
Web App searches Azure for user to get Object ID
Graph Api only returns top 100 users. User was not in top 100 alphabetically so error thrown
The reason it was working on our second deployment was that I was prefixing the user name with demo_ (we use this site to demo new features before releasing). This meant that it was being returned in the top 100 users.
I changed the code as follows so it filters during the retrieval instead of after:
private Microsoft.WindowsAzure.ActiveDirectory.User GetUserFromAzure(string userName, string tenantName, out DirectoryDataService graphService)
{
graphService = GetGraphService(tenantName);
var users = (DataServiceQuery<Microsoft.WindowsAzure.ActiveDirectory.User>)graphService.users.Where(x => x.userPrincipalName == userName);
QueryOperationResponse<Microsoft.WindowsAzure.ActiveDirectory.User> response;
response = users.Execute() as QueryOperationResponse<Microsoft.WindowsAzure.ActiveDirectory.User>;
var user = response.FirstOrDefault();
return user;
}

How to get Google profile info including custom fields from an Apps Domain user?

Using the user.profile and user.email scope and the /oauth2/v2/userinfo feed doesn't seem to return any custom fields (in my case Department) or phone numbers. These fields show up in the Domain Shared Contacts directory.
Is there perhaps an Apps Domain specific feed URL something like /oauth2/{DOMAIN}/v2/userinfo ?
Does the API/Service not support any custom fields yet?
Is there a way to fudge this into working?
Read access to your own Apps Domain Shared Contacts profile that's connected to your account shouldn't be so difficult.
I'd prefer a non-admin solution because my domain uses Common Access Cards w/ SAML authentication so I can't just store admin credentials (user : password) in an App Engine app and access the /m8/ feed. If there's a flow to access Domain Shared Contacts (with custom fields) with a beforehand authorized consumer key and secret I'd be interested in the instructions for getting that to work.
EDIT Jay Lee nailed it "https://www.google.com/m8/feeds/gal/{domain}/full"
Here's the proof of concept script using Google Apps Script (I'll add the final OAuth2 version when I finish it)
function getGal(email, passwd, domain) {
var res = UrlFetchApp.fetch("https://www.google.com/accounts/ClientLogin", {
contentType: "application/x-www-form-urlencoded",
method: "post",
payload: { "Email": email, "Passwd": passwd, "accountType": "HOSTED", "service":"cp" }
});
var auth = res.getContentText().match(/Auth=(.*)/i)[1];
Logger.log("Auth: " + auth);
res = UrlFetchApp.fetch("https://www.google.com/m8/feeds/gal/" + domain + "/full", {
method: "get",
headers: { "Authorization": "GoogleLogin auth=" + auth, "GData-Version": "1.0" }
});
Logger.log(res.getHeaders());
Logger.log(res.getContentText());
}
EDIT 2 OAuth version that returns JSON and only the info for the user accessing the script.
function googleOAuthM8() {
var oAuthConfig = UrlFetchApp.addOAuthService("m8");
oAuthConfig.setRequestTokenUrl('https://www.google.com/accounts/OAuthGetRequestToken?scope=https://www.google.com/m8/feeds/');
oAuthConfig.setAuthorizationUrl('https://www.google.com/accounts/OAuthAuthorizeToken');
oAuthConfig.setAccessTokenUrl('https://www.google.com/accounts/OAuthGetAccessToken');
oAuthConfig.setConsumerKey('anonymous');
oAuthConfig.setConsumerSecret('anonymous');
return {oAuthServiceName:"m8", oAuthUseToken:'always'};
}
function getGal(domain) {
res = UrlFetchApp.fetch("https://www.google.com/m8/feeds/gal/" + domain + "/full?alt=json&q=" + Session.getActiveUser().getEmail(), googleOAuthM8());
Logger.log(res.getHeaders());
Logger.log(res.getContentText());
}
Any non-admin user can access the GAL programmatically, see:
https://github.com/google/gfw-deployments/blob/master/apps/shell/gal/gal_feed.sh
I don't believe this API call is documented or supported officially but it works even with OAuth authentication rather than the example's ClientLogin (tested on the OAuth 2.0 playground with a non-admin user and the standard https://www.google.com/m8/feeds/ Contacts scope).
Note that the Global Address List is a compilation of user profiles, groups and shared contacts. You'll need to parse it out to find the user(s) you wish to get department information for.
I would utilize the Google Apps Profiles API to do this. It'll give you a bunch of meta information, including profile data and even profile photos:
https://developers.google.com/google-apps/profiles/
Even if you're using PIV/CAC/SAML, you will be able to auth using Two-Legged-OAuth.
https://developers.google.com/accounts/docs/OAuth#GoogleAppsOAuth
Two-legged-oauth is the path of least resistance, but you should also take a look at OAuth2, especially the JWT-signed service accounts portion -- however, it can be a little tricky to get working with the older GData xml apis.
As far as fields available go, you'll have to work with the ones on this page. There are extended properties where you add in arbitrary data, but they don't show up in the Contacts browser with Google Mail itself:
https://developers.google.com/gdata/docs/2.0/elements#gdProfileKind
On a sidenote, if you're in an LDAP environment (and since you mentioned CAC, I think you probably are), you should take a look at Google Apps Directory Sync, which can synchronize that profile data with your local AD/LDAP.
source: I deployed Google Apps to large organizations (3000+), public and private.
I have used the following approach with TwoLeggedOAuthHmacToken:
Consumer key and secret can be found in google apps admin dashboard
CONSUMER_KEY = 'domain.com'
CONSUMER_SECRET = 'secret_key'
class ContactClient():
def __init__(self, username):
# Contacts Data API Example ====================================================
self.requestor_id = username + '#' + CONSUMER_KEY
self.two_legged_oauth_token = gdata.gauth.TwoLeggedOAuthHmacToken(
CONSUMER_KEY, CONSUMER_SECRET, self.requestor_id)
self.contacts_client = gdata.contacts.client.ContactsClient(source=SOURCE_APP_NAME)
self.contacts_client.auth_token = self.two_legged_oauth_token
def newuser(self, username):
self.contacts_client.auth_token.requestor_id = username + '#' + CONSUMER_KEY
def getContacts(self, username=None):
if username:
self.newuser(username)
return self.contacts_client.GetContacts()
class MainPage(webapp2.RequestHandler):
def get(self):
contacts = ContactClient(username='username')
feed = contacts.getContacts()
output = ""
if feed:
for entry in feed.entry:
if entry.title and entry.title.text:
output += entry.title.text + "<br/>"
for email in entry.email:
if email.primary and email.primary == 'true':
output += ' %s<br/>' % (email.address)
self.response.headers['Content-Type'] = 'text/html'
self.response.write('''<h1>Contact Access via GData Client</h1>''' + output)

Google Custom Search and Passing along Querystring Variables

I am working on a web app project that has been in development for long time. The app has two sides, the majority of the site is publicly accessible. However, there are sections that require the user to be logged in before they can access certain content.
When the user logs in they get a sessionid (GUID) which is stored in a table in the database which tracks all sort for data about the user and their activity.
Every page of the app was written to look if this session id variable exists or not in the querystring. If a user tries to access one of these protected areas, the app checks to see if this sessiond variable is in the querystring. If i is not, they are redirected to the login screen.
The flow of the site moves has the user moving seamlessly from secured areas to non-secured areas, back and forth, etc.
So we did a test run with the Google Custom Search and it does an awesome job picking up all our dynamic content in these public areas. However, we have not been able to figure out how to pass the sessionid along with the search results IF the user is logged in already.
Is it possible to pas querystring variables that already exist in the url along with the search results?
As far as I know, this is not possible. Google doesn't give you the possibilty to modify the URL's of the Search Results in their Custom Search.
A possible solution would be to store your Session-Key to a Cookie, rather than passing it with every URL.
Use the parseQueryFromUrl function
function parseQueryFromUrl () {
var queryParamName = "q";
var search = window.location.search.substr(1);
var parts = search.split('&');
for (var i = 0; i < parts.length; i++) {
var keyvaluepair = parts[i].split('=');
if (decodeURIComponent(keyvaluepair[0]) == queryParamName) {
return decodeURIComponent(keyvaluepair[1].replace(/\+/g, ' '));
}
}
return '';
}
Select RESULTS ONLY option in the Look & Feel and it will provide you with the code.
www.google.com/cse/

Resources