Microsoft Graph works with one program, but not another - authentication

I am attempting to create a c# Windows service that periodically captures information from Microsoft Graph. This always fails with a "AADSTS700016: Application not found in the directory of our Microsoft 365 account."
Prior to trying to write this service, I created a test program to do same access. I set up an application in Azure Active Directory with a secret. When I run the exact same code in the this test program with the correct tenant ID, client ID and the secret, the program works fine from whatever computer I run it from.
However, the c# service always fails with the error noted above and detailed below. Can't be an issue with permissions as access IDs and secret are the same. I have even tried creating a separate application in AAD but get the same error.
Common c# statements:
var scopes = new[] { "https://graph.microsoft.com/.default" };
// Multi-tenant apps can use "common",
// single-tenant apps must use the tenant ID from the Azure portal
// using Azure.Identity;
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientID, clientSecret, options); ;
graphClient = new GraphServiceClient(clientSecretCredential, scopes);
if (graphClient == null) throw new Exception("Unable able to obtain a GraphClient for this pass");
var groups = await graphClient.Groups.Request().Select(x => new { x.Id, x.DisplayName }).GetAsync();
Any help appreciated. I am sure it is something simple, but clueless at the moment.
Inner Exception 1:
AuthenticationFailedException: ClientSecretCredential authentication failed: AADSTS700016: Application with identifier 'c62d4eb9-587d-4b7f-a4d8-0640747f0958' was not found in the directory xxxx. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You may have sent your authentication request to the wrong tenant.
Trace ID: c8bfac15-c9d6-407e-89e7-36f21fb18300
Correlation ID: 9c8d25ad-c275-43c0-93c1-d295608e9f92
Timestamp: 2022-08-13 15:52:36Z

And just like that, I found the error. Good old global vs local variable name. I hope no one spent too much time on this.

Related

Adding user to Active Directory Group .net core: A referral was returned from the server

I'm building a .net core 3 website where I'm trying to add a user to an Active Directory security group. The below code works fine in my development environment but once it's deployed to IIS I receive:
System.DirectoryServices.DirectoryServicesCOMException (0x8007202B):
A referral was returned from the server.
The error occurs at "group.Save();"
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, "ad.xxx.com:389",
"DC=ad,DC=xxx,DC=com", svcAccountUsername, svcAccountPw))
{
GroupPrincipal group = GroupPrincipal.FindByIdentity(pc, IdentityType.SamAccountName, groupName);
group.Members.Add(pc, IdentityType.SamAccountName, username);
group.Save();
}
Again, this works locally in my development environment but not once deployed to IIS. Any suggestions on how to fix?
I would suggest looking up the account that you are trying to add to the AD. Other things i can suggest is using the debugger to confirm the account / group exists in the domain that you are running this under.
using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, "domain" ...))
{
GroupPrincipal group = GroupPrincipal.FindByIdentity(pc, IdentityType.SamAccountName, groupName);
// Do some validation / logging to make sure there is a group returned.
var principal = Principal.FindByIdentity(pc, IdentityType.SamAccountName, username);
// Do some validation here to make sure principal is not null
group.Members.Add(principal);
group.Save();
}
Make sure the server running this script has access to the domain you are updating.
A referral means that you aren't talking to the right server, but the server knows who you should be talking to. If you look into the exception object more, you might even find which server it wants to send you to.
This can happen if the group is not on the same domain that you passed to the PrincipalContext.

Keycloak - how to allow linking accounts without registration

I am managing a Keycloak realm with only a single, fully-trusted external IdP added that is intended to be the default authentication mechanism for users.
I do not want to allow user to register, i.e. I want to manually create a local Keycloak user, and that user should then be allowed to link his external IdP account to the pre-existing Keycloak account, having the email address as common identifier. Users with access to the external IdP but without an existing Keycloak account should not be allowed to connect.
I tried the following First Broker Login settings, but whenever a user tries to login, he gets an error message (code: invalid_user_credentials).
Do you have any idea what my mistake might be?
Looks like they integrated this feature in version 4.5.0.
See automatic account link docs.
Basically you need to create a new flow and add 2 alternative executions:
Create User If Unique
Automatically Link Brokered Account
According to the doc: https://www.keycloak.org/docs/latest/server_admin/index.html#detect-existing-user-first-login-flow, you must create a new flow like this:
et voilà :)
As per this discussion:
https://keycloak.discourse.group/t/link-idp-to-existing-user/1094/5
It’s a bug in keycloak and they seem to be a reluctant to fix it for
whatever reason. I have very few users so I solved it by manually
querying the idp for the information keycloak uses and then copying it
into the relevant fields in the UI. So there is no sign up process for
my users I just make them myself. Obviously that’s a poor solution
though, what we really need is someone to take over that PR and
persuade the maintainers to merge it.
This is the PR: https://github.com/keycloak/keycloak/pull/6282
As it is described in this GitHub issue response the solution is to use a JavaScript authenticator that handles this.
In order to do so, you need to do the folowing:
Enable [custom authenticators using JavaScript in your server[(https://www.keycloak.org/docs/latest/server_installation/#profiles) by https://stackoverflow.com/a/63274532/550222creating a file profile.properties in your configuration directory that contains the following:
feature.scripts=enabled
Create the custom authenticator. You have to create a JAR file (essentially a ZIP file) with the following structure:
META-INF/keycloak-scripts.json
auth-user-must-exist.js
The content of the files are in this Gist, but I am including them here as well:
META-INF/keycloak-scripts.json:
{
"authenticators": [
{
"name": "User must exists",
"fileName": "auth-user-must-exists.js",
"description": "User must exists"
}
]
}
auth-user-must-exist.js:
AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError")
ServicesLogger = Java.type("org.keycloak.services.ServicesLogger")
AbstractIdpAuthenticator = Java.type("org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator")
IdpCreateUserIfUniqueAuthenticator = Java.type("org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticator")
var IdpUserMustExists = Java.extend(IdpCreateUserIfUniqueAuthenticator)
function authenticate(context) {
var auth = new IdpUserMustExists() {
authenticateImpl: function(context, serializedCtx, brokerContext) {
var parent = Java.super(auth)
var session = context.getSession()
var realm = context.getRealm()
var authSession = context.getAuthenticationSession()
if (authSession.getAuthNote(AbstractIdpAuthenticator.EXISTING_USER_INFO) != null) {
context.attempted()
return
}
var username = parent.getUsername(context, serializedCtx, brokerContext)
if (username == null) {
ServicesLogger.LOGGER.resetFlow(realm.isRegistrationEmailAsUsername() ? "Email" : "Username")
authSession.setAuthNote(AbstractIdpAuthenticator.ENFORCE_UPDATE_PROFILE, "true")
context.resetFlow()
return
}
var duplication = parent.checkExistingUser(context, username, serializedCtx, brokerContext)
if (duplication == null) {
LOG.info("user not found " + username)
context.failure(AuthenticationFlowError.INVALID_USER)
return
} else {
authSession.setAuthNote(AbstractIdpAuthenticator.EXISTING_USER_INFO, duplication.serialize())
context.attempted()
}
}
}
auth.authenticate(context)
}
Then, you can define as follows:
User Must Exist -> ALTERNATIVE
Automatically Set Existing User -> ALTERNATIVE
Honestly i am surprised by the keycloak auto creating behavior. I tried to add new Authentication flow as descibed here https://www.keycloak.org/docs/latest/server_admin/index.html#automatically-link-existing-first-login-flow
My flow :
1 - Create User If Unique [ALTERNATIVE]
2 - Automatically Link Brokered Account [ALTERNATIVE]
My use case : Authenticating users from Github ( Github as IDP )
Result : when a github user logon with an existing "username" keycloak links the github account to my local user ( based on his username ). I expected using his email instead of username.

.Net Core ReportExecutionServiceSoapClient set credentials

I am using ReportExecutionServiceSoapClient in .Net Core i got the latest version of .net Core and tried to get a report from reporting services to work. after I've used the WCF connection service I was able to add the code with looks like bellow
// Instantiate the Soap client
ReportExecutionServiceSoap rsExec = new ReportExecutionServiceSoapClient(ReportExecutionServiceSoapClient.EndpointConfiguration.ReportExecutionServiceSoap);
// Create a network credential object with the appropriate username and password used
// to access the SSRS web service
string historyID = null;
TrustedUserHeader trustedUserHeader = new TrustedUserHeader();
ExecutionHeader execHeader = new ExecutionHeader();
// Here we call the async LoadReport() method using the "await" keyword, which means any code below this method
// will not execute until the result from the LoadReportAsync task is returned
var taskLoadReport = rsExec.LoadReportAsync(reportPath, historyID);
// By the time the LoadReportAsync task is returned successfully, its "executionInfo" property
// would have already been populated. Now the remaining code in this main thread will resume executing
string deviceInfo = null;
string format = "EXCEL";
// Now, similar to the above task, we will call the RenderAsync() method and await its result
var taskRender = await rsExec.RenderAsync(renderReq);
When it hist renderAsync all falls apart because the credentials for the service are not set anywhere. I've tried to Login async with no success. Also I've tried to set the credentials with SetExecutionCredentialsAsync but I've got and error saying "The HTTP request is unauthorized with client authentication scheme 'Anonymous'. The authentication header received from the server was 'NTLM'." I don't know how to change that for ReportExecutionServiceSoapClient.
I have read some posts in which Microsoft guys says that the authentication with a soap is not resolved but for me it seems so close to be true. I feel like I am missing something.
Technology stack: VS 2017, .net Core web api, ssrs 2016, sql server 2016 standard
How can I authenticate the user for this call?
I know this is an old question but I had the same issue and stumbled onto the answer.
After creating the ReportExecutionServiceSoap object you can specify the username and password in the ClientCredentials. I've had success with this using the Basic client credential type. Be sure you are using HTTPS, otherwise your password is sent in plaintext to the reporting server. I also recommend storing the user/password in a secure place and not code.
BasicHttpBinding rsBinding = new BasicHttpBinding();
rsBinding.Security.Mode = BasicHttpSecurityMode.TransportCredentialOnly;
rsBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;
EndpointAddress rsEndpointAddress = new EndpointAddress("https://servername/ReportServer/ReportExecution2005.asmx");
var rsExec = new ReportExecutionServiceSoapClient(rsBinding, rsEndpointAddress);
rsExec.ClientCredentials.UserName.UserName = "username";
rsExec.ClientCredentials.UserName.Password = "pass";

AspNet Identity UserManager Find with Select (optimal sql query)

I have wcf fulRest service with AspNet Identity. My android app use this web service to communicate with database - I use ssl to make connection secure (my app is a mini game so it doesn't contain so important data, I belive that ssl is enough protection in this case).
I have function LoginUser(string userName, string unHashedPassword), if user exsists it returns user's id. In all other functions this id is used if action need information of curUser - for example I have function addComment(string userId, string msg) (this kind of methods still use ssl to protection of unwanted handle userId).
In LoginUser I get id (and a little more information like e-mail, gameLogin) by use:
using (var userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(_context)))
{
ApplicationUser user = userManager.Find(userName, password);
if (user != null)
{
result = new LogInBaseData()
{
Id = user.Id,
Email = user.Email,
Login = user.ApplicationLogin
};
}
}
But function Find generate huge query with select many unneeded data for me. Is any way to optimalizace it? I prefer do it by context.User.Where().Select() but I can't hash user's password.

Why does WCF OperationContext gets wrong WindowsIdentity

I use the below snippet of code to fetch the client user name in my WCF service. On one of my servers, I am getting the wrong client name. My client is Win7 talking to Server 2008R2 in a workgroup configuration and both machines have users Dave and Dave_Admin. Both are admin on Win7 and only the later is admin on the server. Problem is I start my client as Dave and the server shows the client as Dave_Admin. I have debugged the identities on both sides of the connection as Dave on the client and Dave_Admin on the server. The claim resources also show the Dave_Admin SID.
The only two reasons I can imagine this happens are
the server somehow finds user Dave_Admin looking for Dave which I doubt, or
after setup, I may have renamed administrative user Dave to Dave_Admin and then created a new user Dave as a standard user.
I only have a vague recollection I may have done that but am not sure if I did or not. The c:\users folder looks normal. If I did do this, and this is the reason, is there anyway to correct?
Anyone have another possible explanation or means to fix if this happens after a user rename?
OperationContext lContext = OperationContext.Current;
RemoteEndpointMessageProperty mEndpointMessageProperties = lContext.IncomingMessageProperties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;
mIdentity = lContext.ServiceSecurityContext.WindowsIdentity;
mUserName = mIdentity.Name;
mIPAddress = mEndpointMessageProperties.Address;
mPort = mEndpointMessageProperties.Port;
mConsoleID = string.Format("IP:{0}Port:{1}", mIPAddress, mPort);
mCallbackInterface = lContext.GetCallbackChannel<IConsoleCallbacks>();
mAuthority = TxWcfServer.sSelf.Authorized(mIdentity); // get the user's authority from the WcfServer when they logged on
// show client information
if (AppSupport.IsLogLevel(LogLevel.WCF))
{
// show the various security contexts
var x = lContext.ServiceSecurityContext;
AppSupport.WriteLog(LogLevel.Note, "*** WCF WindowsIdentity is '{0}'.", x.WindowsIdentity.Name);
AppSupport.WriteLog(LogLevel.Note, "*** WCF PrimaryIdentity is '{0}'.", x.PrimaryIdentity.Name);
AppSupport.WriteLog(LogLevel.Note, "*** WCF IsAnonymous is '{0}'.", x.IsAnonymous);
foreach (ClaimSet claimset in ServiceSecurityContext.Current.AuthorizationContext.ClaimSets)
{
foreach (System.IdentityModel.Claims.Claim claim in claimset)
{
// Write out each claim type, claim value, and the right. There are two
// possible values for the right: "identity" and "possessproperty".
AppSupport.WriteLog(LogLevel.Note, "*** WCF Claim Type: {0}, Resource: {1} Right: {2}",
claim.ClaimType, claim.Resource.ToString(), claim.Right);
}
}
}
You need to turn on Impersonation on your WCF service for your code to be able to get the client context, otherwise you'll be getting the service context (Which is probably why you get Dave_Admin instead of Dave, as your service is running as Dave_Admin)
This post has information on how to turn it on:
http://msdn.microsoft.com/en-us/library/ms730088.aspx