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;
}
Related
I have past net core 5 MVC applcation that I am rewriting in Blazor server.
The code I have used in MVC to get it working includes the following:
public void GetADinfo(out string givenName, out string surname, out string homePhone, out string email)
{
//===========================================================
//Go and get AD info for the current user or equivalent
var components = User.Identity.Name.Split('\\');
var username = components.Last();
// create LDAP connection object
DirectoryEntry myLdapConnection = createDirectoryEntry();
DirectorySearcher search = new DirectorySearcher(myLdapConnection);
search.Filter = "(cn=" + username + ")";
SearchResult result = search.FindOne();
DirectoryEntry dsresult = result.GetDirectoryEntry();
givenName = dsresult.Properties["givenName"][0].ToString();
surname = dsresult.Properties["sn"][0].ToString();
email = dsresult.Properties["mail"][0].ToString();
homePhone = dsresult.Properties["homePhone"][0].ToString();
//=============================================================================
}
public DirectoryEntry createDirectoryEntry()
{
// create and return new LDAP connection with desired settings
string ADconn = _context.ApplicConfs.Select(s => s.Ldapconn).FirstOrDefault();
string LDAPConn = _context.ApplicConfs.Select(s => s.Ldappath).FirstOrDefault();
//string ADconn;
//ADconn = "SERVER.A3HR.local";
//string LDAPConn;
//LDAPConn = "LDAP://SERVER.A3HR.local";
//DirectoryEntry ldapConnection = new DirectoryEntry("SERVER.A3HR.local");
//ldapConnection.Path = "LDAP://OU=staffusers,DC=leeds-art,DC=ac,DC=uk";
//ldapConnection.Path = "LDAP://SERVER.A3HR.local";
DirectoryEntry ldapConnection = new DirectoryEntry(ADconn);
ldapConnection.Path = LDAPConn;
ldapConnection.AuthenticationType = AuthenticationTypes.Secure;
return ldapConnection;
}
Does the code for using Active Directory in Blazor Server require anything different? In other words is the authentication different using Blazor as compared to net core MVC?
My app uses Blazor server windows authentication. So I get current user in the normal Blazor way. When I get that I want to use the current userid to look up the email and telephone number in AD and pre-populate it on a page if it exists. That way - in the application- the user doesn't have to re enter this information all the time.
Does anyone have an example of this using Blazor? Is the approach dramatically different between local AD and Azure AD in the logic/coding used? I see a few examples of Azure AD use in Blazor out there.
Thanks for any information provided...
We have problem like, unable to get the user full name when reading from different domain.
eg: My userName is dom1\jsmith and full name is John Smith. When we deploy the project in dom1 domain, we are able to login and get the full name of the user. When we deploy the project in another domain(dom2) where the user(dom1\jsmith) has login permission, able to access the site but not able to get the full name.
We tried different solutions but didn't work.
//output: dom1\jsmith
User.Identity.Name;
//output: dom1\jsmith
string s = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
//output: dom1\jsmith
string sUserName = System.Environment.UserName;
//output: John Smith in same domain but not able to find identity
using (var context = new PrincipalContext(ContextType.Domain))
{
var principal = UserPrincipal.FindByIdentity(context, User.Identity.Name);
if (principal != null)
var fullName = string.Format("{0} {1}", principal.GivenName, principal.Surname);
Pass the name of the logon domain into the constructor for PrincipalContext. So split the DOMAIN\username that you have, and use just the domain portion:
var split = User.Identity.Name.Split('\\');
using (var context = new PrincipalContext(ContextType.Domain, split[0])) {
...
}
Although I am surprised it doesn't work as you have it, since it works for me and I log into a different domain than what my computer is joined to. Although in my case, the two domains are in the same AD forest. Maybe that's not the case for you.
I have just written an app with Azure Active Directory Single Sign-On.
The consent framework is handled in the AccountController, in the ApplyForConsent action listed below. Until recently, everything has worked seamlessly. I could grant consent as an admin user of an external tenant, then sign out of the app, and sign in again as a non-admin user.
My Azure Active Directory app requires the following delegated permissions:
Read directory data
Enable sign-on and read users' profiles
Access your organisation's directory
Now, after I have gone through the consent framework (by POSTing from a form to ApplyForConsent as an admin user), signing in as a non-admin user fails with the error message AADSTS90093 (This operation can only be performed by an administrator). Unhelpfully, the error message doesn't say what "this operation" actually is, but I suspect it is the third one (Access your organisation's directory).
I stress again, this has only recently stopped working. Nothing has changed in this part of the code, although I grant it is possible that other changes elsewhere in the codebase may have had knock-on effects of which I remain blissfully ignorant.
Looking at the documentation, it seems that this use of the consent framework is already considered "Legacy", but I'm having a difficult time finding a more up-to-date implementation.
The requested permissions in the code sample below is the single string "DirectoryReaders".
I have three questions for helping me debug this code:
What is the difference between "Read directory data" and "Access your organisation's directory"? When would I need one rather than another?
Do I need to request more than just "DirectoryReaders"?
Is there now a better way to implement the Consent Framework?
This is the existing code:
private static readonly string ClientId = ConfigurationManager.AppSettings["ida:ClientID"];
[HttpPost]
public ActionResult ApplyForConsent()
{
string signupToken = Guid.NewGuid().ToString();
string replyUrl = Url.Action("ConsentCallback", "Account", new { signupToken }, Request.Url.Scheme);
DatabaseIssuerNameRegistry.CleanUpExpiredSignupTokens();
DatabaseIssuerNameRegistry.AddSignupToken(signupToken, DateTimeOffset.UtcNow.AddMinutes(5));
return new RedirectResult(CreateConsentUrl(ClientId, "DirectoryReaders", replyUrl));
}
[HttpGet]
public ActionResult ConsentCallback()
{
string tenantId = Request.QueryString["TenantId"];
string signupToken = Request.QueryString["signupToken"];
if (DatabaseIssuerNameRegistry.ContainsTenant(tenantId))
{
return RedirectToAction("Validate");
}
string consent = Request.QueryString["Consent"];
if (!String.IsNullOrEmpty(tenantId) && String.Equals(consent, "Granted", StringComparison.OrdinalIgnoreCase))
{
if (DatabaseIssuerNameRegistry.TryAddTenant(tenantId, signupToken))
{
return RedirectToAction("Validate");
}
}
const string error = "Consent could not be provided to your Active Directory. Please contact SharpCloud for assistance.";
var reply = Request.Url.GetLeftPart(UriPartial.Authority) + Url.Action("Consent", new { error });
var config = FederatedAuthentication.FederationConfiguration.WsFederationConfiguration;
var signoutMessage = new SignOutRequestMessage(new Uri(config.Issuer), reply);
signoutMessage.SetParameter("wtrealm", config.Realm);
FederatedAuthentication.SessionAuthenticationModule.SignOut();
return Redirect(signoutMessage.WriteQueryString());
}
private static string CreateConsentUrl(string clientId, string requestedPermissions, string consentReturnUrl)
{
string consentUrl = String.Format(CultureInfo.InvariantCulture, ConsentUrlFormat, HttpUtility.UrlEncode(clientId));
if (!String.IsNullOrEmpty(requestedPermissions))
{
consentUrl += "&RequestedPermissions=" + HttpUtility.UrlEncode(requestedPermissions);
}
if (!String.IsNullOrEmpty(consentReturnUrl))
{
consentUrl += "&ConsentReturnURL=" + HttpUtility.UrlEncode(consentReturnUrl);
}
return consentUrl;
}
I think this link addresses your issue:
http://blogs.msdn.com/b/aadgraphteam/archive/2015/03/19/update-to-graph-api-consent-permissions.aspx
The quick summary is that now only admins can grant consent to a web app for '•Access your organisation's directory'.
This change was made back in March. Native apps are not affected by the change.
My suspicion was correct. I was using legacy tech in the question. By moving to Owin and Identity 2.0, all issues were solved.
The new approach is summarised by https://github.com/AzureADSamples/WebApp-GroupClaims-DotNet
I'm trying to create a user with the DNN 7 services framework. I've taken my working code from my custom registration module and modified to work within a DNN webapi function.
When I get to the UserController.CreateUser call in the code below I receive a
"\"There was an error generating the XML document.\""
exception. My user makes it into the aspnet_Users table and the DNN users table but does not make it into the DNN userportals table. Any ideas would be appreciated.
private void CreateUser()
{
//Update DisplayName to conform to Format
UpdateDisplayName();
User.Membership.Approved = PortalSettings.UserRegistration == (int)Globals.PortalRegistrationType.PublicRegistration;
var user = User;
CreateStatus = UserController.CreateUser(ref user);
I finally found the issue. I was not setting the portal ID for my new users and DNN was excepting out when it was adding them to a portal. All it took was User.PortalId = 0 before the CreateUser call.
I have found by trial and error that the minimum needed to create a viable DNN user is:
UserInfo uiNewUser = new UserInfo();
uiNewUser.Username = "<myUsername>";
uiNewUser.Displayname = "<myDisplayname>";
uiNewUser.Email = "<myUserEmail>";
UserMembership newMembership = new UserMembership(uiNewUser);
newMembership.Password = "<myUserPassword>";
uiNewUser.Membership = newMembership;
uiNewUser.PortalID = <myPortalID>;
DotNetNuke.Security.Membership.UserCreateStatus uStatus;
uStatus = DotNetNuke.Security.Membership.MembershipProvider.Instance().CreateUser(ref uiNewUser);
RoleInfo newRole = RoleController.Instance.GetRoleByName(uiNewUser.PortalID, "Registered Users");
RoleController.Instance.AddUserRole(uiNewUser.PortalID, uiNewUser.UserID, newRole.RoleID, (RoleStatus)1, false, DateTime.MinValue, DateTime.MaxValue);
If any of these are missed out, parts of the user are created in the database, but the user may not be visible in the Admin list of users, or an Exception may be generated. Other details can be added later.
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)