How to do Active Directory authentication in Razor (cshtml) - authentication

I am doing a simple website with Razor. Currently, I have database-based authentication that works, as follows:
In _AppStart.chtml:
WebSecurity.InitializeDatabaseConnection("db_connection",
"users", "id", "username", true);
In login.cshtml page:
username = Request["username"];
password = Request["password"];
if (WebSecurity.Login(username, password, true))
{
Response.Redirect("/admin");
}
else
{
errorMessage = "Login was not successful.";
}
In protected CSHTML pages, I have the following at the top of a page:
if (!WebSecurity.IsAuthenticated)
{
Response.Redirect("/login.cshtml");
}
Everything is pretty simple and works well. Now I would like to add authentication with AD. I don't know how to do it.
I came from the Java world with many years of experience. For this simple website, I do not need MVC architecture. I need simple things similar to the above (if possible). I need to do authentication just within the login.cshtml file. I googled a lot and am unable to find a tutorial (so that I can copy and paste) for what I need.
Any pointers or help is really appreciated!
Thanks and Regards
Update: This application sits on the internal network.
Update 2: Here is the code I have after successfully implemented X3074861X's code
if (IsPost)
{
username = Request["username"];
password = Request["password"];
var domain = "domain";
var host = "host";
var port = "389";
LdapConnection ldapConnection = new LdapConnection(host + ":" + port);
try
{
// authenticate the username and password
using (ldapConnection)
{
// pass in the network creds, and the domain.
var networkCredential = new NetworkCredential(username, password, domain);
// if we're using unsecured port 389, set to false. If using port 636, set this to true.
ldapConnection.SessionOptions.SecureSocketLayer = false;
// since this is an internal application, just accept the certificate either way
ldapConnection.SessionOptions.VerifyServerCertificate += delegate { return true; };
// to force NTLM\Kerberos use AuthType.Negotiate, for non-TLS and unsecured, just use AuthType.Basic
ldapConnection.AuthType = AuthType.Basic;
// this is where the authentication occurs
ldapConnection.Bind(networkCredential);
//check local database to make sure the user is one of we allowed
if (WebSecurity.Login(username, "fixed-password, just to check whether someone is on the list of allowed people", true))
{
Response.Redirect("/admin");
}
else
{
errorMessage = "Login was not successful.";
}
}
}
catch (LdapException exception)
{
//Authentication failed, exception will dictate why
errorMessage = "Login was not successful.";
}
Some explanation. I dont have control over the AD and so I can only authenticate users against it. I still have a little local database that indicates who can access the app. Everyone with access to the app has the same rights.
Thanks and credit goes to X3074861X.

Since this is an internal application, and you're looking for something simple, I would consider writing a single class to do the Active Directory authentication. You're going to need a couple things though, in order for this to work :
A reference to System.DirectoryServices.Protocols in your project.
The IP or DNS name of your Active Directory server. We'll call it host in the code below.
The port it's running on (LDAPS will be port 636, basic LDAP will be port 389). We'll call it port in the code below.
The Domain to which your users belong. We'll call it domain in the code below.
Now that you have that, you can wire this up to check the credentials from the request against your AD instance. I would try something like this :
// the username and password to authenticate
username = Request["username"];
password = Request["password"];
// define your connection
LdapConnection ldapConnection = new LdapConnection("host:port");
try
{
// authenticate the username and password
using (ldapConnection)
{
// pass in the network creds, and the domain.
var networkCredential = new NetworkCredential(username, password, domain);
// if we're using unsecured port 389, set to false. If using port 636, set this to true.
ldapConnection.SessionOptions.SecureSocketLayer = false;
// since this is an internal application, just accept the certificate either way
ldapConnection.SessionOptions.VerifyServerCertificate += delegate { return true; };
// to force NTLM\Kerberos use AuthType.Negotiate, for non-TLS and unsecured, just use AuthType.Basic
ldapConnection.AuthType = AuthType.Basic;
// authenticate the user
ldapConnection.Bind(networkCredential);
}
catch (LdapException ldapException)
{
//Authentication failed, exception will dictate why
}
}
Also, in the same way you'd communicate an authorization issue before, the ldapException can tell you why the call failed. If you want to display custom messaging, I would check the LdapException.ErrorCode property, and maybe create a case statement of return messages based on the error codes.
Or, you could just output LdapException.Message directly to the page - either way, that will at least dictate to the user why their login didn't work.

Related

Xero API Allows connection but fails to redirect back, has an uncaughtreferenceerror: fbq is not defined

Upon running the program I am redirected to sign in with xero. Once I sign in I am able to choose an organization to allow access to the app
Upon clicking allow access I get redirected to the default "This site can't be reached" error page.
If I look at the console output when I click the button, for a few seconds an "uncaught reference error: fbq is not defined" is shown. Unfortunately it goes away before I can click on it.
Here is some of the relevant code:
void LoginToXero()
{
var xeroLoginUri = XeroService.GetLoginUri();
OpenBrowser(xeroLoginUri);
var listener = new HttpListener();
listener.Prefixes.Add(XeroService.CallbackUri);
listener.Start();
Console.WriteLine("Waiting for the browser to callback from Xero login page...");//Logs
var context = listener.GetContext();//Does not progress past here
//...
}
public static class XeroService
{
public static string CallbackUri => "xxxxxxxxxxxxx";
static string xeroState = Guid.NewGuid().ToString();
static string oAuth2Token = "";
static XeroClient xeroClient = new XeroClient(new XeroConfiguration
{
ClientId = "XXXXXXXXXXXXXX",
ClientSecret = "XXXXXXXXXXXXXXXXXXXX",
Scope = "openid payroll.employees",
CallbackUri = new Uri(CallbackUri)
});
public static string GetLoginUri()
{
xeroClient.xeroConfiguration.State = xeroState;
return xeroClient.BuildLoginUri();
}
}
Please note all sensitive data has been replaced by "XXXXXXXXX"
I have tested both localhost callback URI's (with specified ports) and custom ones that redirect to localhost via the host file on my machine
I have also tried running it on Windows 11 and Windows 10, both with the firewall enabled and then with it disabled
Any help would be greatly appreciated
The problem was that the listener and the App was set up for https, changing it to http and making sure there was an explicit port resolved the issue

Windows authentication fail with "401 Unauthorized"

I have a MVC client accessing a Web API protected by IDS4. They all run on my local machine and hosted by IIS. The app works fine when using local identity for authentication. But when I try to use Windows authentication, I keep getting "401 Unauthorized" error from the dev tool and the login box keeps coming back to the browser.
Here is the Windows Authentication IIS setting
and enabled providers
It's almost like that the user ID or password was wrong, but that's nearly impossible because that's the domain user ID and password I use for logging into the system all the time. Besides, according to my reading, Windows Authentication is supposed to be "automatic", which means I will be authenticated silently without a login box in the first place.
Update
I enabled the IIS request tracing and here is the result from the log:
As you can see from the trace log item #29, the authentication (with the user ID I typed in, "DOM\Jack.Backer") was successful. However, some authorization item (#48) failed after that. And here is the detail of the failed item:
What's interesting is that the ErrorCode says that the operation (whatever it is) completed successfully, but still I received a warning with a HttpStatus=401 and a HttpReason=Unauthorized. Apparently, this is what failed my Windows Authentication. But what is this authorization about and how do I fix it?
In case anyone interested - I finally figured this one out. It is because the code that I downloaded from IndentityServer4's quickstart site in late 2020 doesn't have some of the important pieces needed for Windows authentication. Here is what I had to add to the Challenge function of the ExternalController class
and here is the ProcessWindowsLoginAsync function
private async Task<IActionResult> ProcessWindowsLoginAsync(string returnUrl)
{
var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName);
if (result?.Principal is WindowsPrincipal wp)
{
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action(nameof(Callback)),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", AccountOptions.WindowsAuthenticationSchemeName },
}
};
var id = new ClaimsIdentity(AccountOptions.WindowsAuthenticationSchemeName);
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name));
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
if (AccountOptions.IncludeWindowsGroups)
{
var wi = wp.Identity as WindowsIdentity;
var groups = wi.Groups.Translate(typeof(NTAccount));
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
}
await HttpContext.SignInAsync(IdentityConstants.ExternalScheme, new ClaimsPrincipal(id), props);
return Redirect(props.RedirectUri);
}
else
{
return Challenge(AccountOptions.WindowsAuthenticationSchemeName);
}
}
Now my windows authentication works with no issues.

Login Check with Active Directory + SSL

I am working on a requirement where I need to validate user with active directory account.
For this I have used LdapConnection with NetworkCredential and PrincipalContext and in all cases I am able to validate user without SSL.
But I need to use validate user with SSL. I have also used the correct port i.e 636/TCP LDAP SSL
Following is the code I did with PrincipalContext
using (principalContext = new PrincipalContext(ContextType.Domain, ldapServerIp, null, ContextOptions.Negotiate | ContextOptions.SecureSocketLayer, userName, password))
{ bool isCredentialValid = principalContext.ValidateCredentials(userName, password);}
Following is code I did with
using (ldapConnection = new LdapConnection(ldapServerIp))
{
var networkCredential = new NetworkCredential(_username, _password, ldapServerIp);
ldapConnection.SessionOptions.SecureSocketLayer = true;
ldapConnection.AuthType = AuthType.Negotiate;
ldapConnection.Bind(networkCredential);
}
Does anyone have did this earlier successfully. If there is any solution that will be very helpful.
Both of those should work just fine, as long as you specify the LDAPS port (usually 636). So your ldapServerIp variable should be set to something like example.com:636.

Google OAuth 2.0 for desktop apps for Windows without Admin privileges

I've heard about Google's plan of modernizing OAuth interactions described here: https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html
Then I was looking at the sample desktop application for Windows found here: https://github.com/googlesamples/oauth-apps-for-windows/tree/master/OAuthDesktopApp.
It's pretty simple and it was working, but once I started Visual Studio without elevated privileges (as a non-admin), I experienced that the HttpListener was not able to start because of the following error: "Access Denied".
It turned out that starting an HttpListener at the loopback address (127.0.0.1) is not possible without admin rights. However trying localhost instead of 127.0.0.1 lead to success.
I found that there is a specific command that allows HttpListener to start at the given address (and port):
netsh http add urlacl url=http://+:80/MyUri user=DOMAIN\user
But it also can be only executed with admin rights, so it's not an option.
Still localhost seems to be the best shot but OAuth 2.0 for Mobile & Desktop Apps states the following regarding this section:
See the redirect_uri parameter definition for more information about the loopback IP address. It is also possible to use localhost in place of the loopback IP, but this may cause issues with client firewalls. Most, but not all, firewalls allow loopback communication.
This is why I'm a bit suspicious to use localhost. So I'm wondering what is the recommended way of Google in this case, as I'm not intending to run our application as administrator just for this reason.
Any ideas?
You can use TcpListener for instance instead of HttpListener. It does not need elevation to listen.
The following is a modified excerpt of this sample:
https://github.com/googlesamples/oauth-apps-for-windows/tree/master/OAuthDesktopApp
// Generates state and PKCE values.
string state = randomDataBase64url(32);
string code_verifier = randomDataBase64url(32);
string code_challenge = base64urlencodeNoPadding(sha256(code_verifier));
const string code_challenge_method = "S256";
// Creates a redirect URI using an available port on the loopback address.
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
string redirectURI = string.Format("http://{0}:{1}/", IPAddress.Loopback, ((IPEndPoint)listener.LocalEndpoint).Port);
output("redirect URI: " + redirectURI);
// Creates the OAuth 2.0 authorization request.
string authorizationRequest = string.Format("{0}?response_type=code&scope=openid%20profile&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}",
authorizationEndpoint,
System.Uri.EscapeDataString(redirectURI),
clientID,
state,
code_challenge,
code_challenge_method);
// Opens request in the browser.
System.Diagnostics.Process.Start(authorizationRequest);
// Waits for the OAuth authorization response.
var client = await listener.AcceptTcpClientAsync();
// Read response.
var response = ReadString(client);
// Brings this app back to the foreground.
this.Activate();
// Sends an HTTP response to the browser.
WriteStringAsync(client, "<html><head><meta http-equiv='refresh' content='10;url=https://google.com'></head><body>Please close this window and return to the app.</body></html>").ContinueWith(t =>
{
client.Dispose();
listener.Stop();
Console.WriteLine("HTTP server stopped.");
});
// TODO: Check the response here to get the authorization code and verify the code challenge
The read and write methods being:
private string ReadString(TcpClient client)
{
var readBuffer = new byte[client.ReceiveBufferSize];
string fullServerReply = null;
using (var inStream = new MemoryStream())
{
var stream = client.GetStream();
while (stream.DataAvailable)
{
var numberOfBytesRead = stream.Read(readBuffer, 0, readBuffer.Length);
if (numberOfBytesRead <= 0)
break;
inStream.Write(readBuffer, 0, numberOfBytesRead);
}
fullServerReply = Encoding.UTF8.GetString(inStream.ToArray());
}
return fullServerReply;
}
private Task WriteStringAsync(TcpClient client, string str)
{
return Task.Run(() =>
{
using (var writer = new StreamWriter(client.GetStream(), Encoding.UTF8))
{
writer.Write("HTTP/1.0 200 OK");
writer.Write(Environment.NewLine);
writer.Write("Content-Type: text/html; charset=UTF-8");
writer.Write(Environment.NewLine);
writer.Write("Content-Length: " + str.Length);
writer.Write(Environment.NewLine);
writer.Write(Environment.NewLine);
writer.Write(str);
}
});
}
By default there is a URL pattern http://+:80/Temporary_Listen_Addresses/ which is allowed for all users (\Everyone)
You can use this as a prefix for your listener. More generally (to avoid collisions with other listeners) you should generate a URL under Temporary_Listen_Addresses (e.g. using a GUID) and use that as your listener prefix.
Unfortunately, a sysadmin can use netsh http to delete this entry or to restrict its usage to only certain users. Also, this does not appear to support listening for an HTTPS request as there is no corresponding ACL entry for port 443.
An admin can list all these permitted URL patterns using netsh http show urlacl as a command.

Redirect HTTP to HTTPS in MVC4 Mobile Application

In My MVC4 Mobile application i have registration, login page and remaining pages. i would like to redirect user to HTTPS connection for all sensitive information pages like registration and login pages and HTTP to remailing pages.
I prefer you to use conditional functionality putting the class
public class RequireHttpsConditional : RequireHttpsAttribute
{
protected override void HandleNonHttpsRequest(AuthorizationContext filterContext)
{
var useSslConfig = ConfigurationManager.AppSettings["UseSSL"];
if (useSslConfig != null)
{
if (!string.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("The requested resource can only be accessed via SSL.");
}
var request = filterContext.HttpContext.Request;
string url = null;
int sslPort;
if (Int32.TryParse(useSslConfig, out sslPort) && sslPort > 0)
{
url = "https://" + request.Url.Host + request.RawUrl;
if (sslPort != 443)
{
var builder = new UriBuilder(url) { Port = sslPort };
url = builder.Uri.ToString();
}
}
if (sslPort != request.Url.Port)
{
filterContext.Result = new RedirectResult(url);
}
}
}
}
and using this [RequireHttpsConditional] above the action result.
i have got this code somewhere in internet and is working fine for me.
in web.config appsettings use <add key="UseSSL" value="443" />
and in the controller above the action result you need put
[RequireHttpsConditional]
public ActionResult SignIn()
{
}
In IIS where you have your project right click and click "Edit Bindings" then you add a custom type https and port no 443 (you can change it)
Note this will work only in production environment. when executed locally it wont be working.
When you execute it locally you have request.Url.Host which will return you only localhost and missing your port number. so if you use it in MVC you will find error loading page for your pages where you put this code.
So this will work when you have the host assigned instead of using the localhost with a specific port number.
Within the controller actions that you wish to be HTTPS add the following code to the top of the method (of course you can simply add this to its own method and then call it):
if (!HttpContext.Request.IsSecureConnection)
{
var url = new UriBuilder(HttpContext.Request.Url);
url.Scheme = "https";
Response.Redirect(url.Uri.AbsoluteUri);
}
It is recommended though that you keep HTTPS on throughout your site to protect against a MITM attack against the auth cookie.