I'm trying to create a user provider so that I can authenticate through an Active directory server.
The problem is that, unlike most other LDAP servers, Active directory doesn't allow to retrieve some user's password attribute, even encrypted.
Here is my User class :
class LdapUser implements UserInterface
{
private $username;
private $first_name;
private $last_name;
private $password;
private $salt;
private $roles;
public function __construct($username, $first_name, $last_name, $password, $salt, array $roles) {
$this->username = $username;
$this->first_name = $first_name;
$this->last_name = $last_name;
$this->password = $password;
$this->salt = $salt;
$this->roles = $roles;
}
...
}
And here is my loadUserByUsername method (In my UserProvider class) :
public function loadUserByUsername($username)
{
$server = "my_ldap_server";
$root_dn = "my_root_dn";
$root_pw = "my_root_pw";
$ds = ldap_connect($server);
if ($ds) {
ldap_bind($ds, $root_dn, $root_pw);
$search = ldap_search($ds, "my_branch", "(sAMAccountName=".$username.")", array("sn", "givenName"));
$info = ldap_get_entries($ds, $sr);
if($info['count'] > 0) {
$user = $info[0];
return new LdapUser($username, $user['givenName'][0], $user['sn'][0], '???PASSWORD???', '???SALT???', array('ROLE_USER'));
} else {
throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
}
ldap_close($ds);
} else {
echo "Connexion au serveur LDAP impossible";
}
}
As you can see, I can't pass the password to my LdapUser class, since it's not accessible through Active Directory.
However, I think it's still possible to authenticate te user, by doing a ldap_bind($username, $password) with the password entered by the user in the login form. The problem is I can't figure out how to access this password in my LdapUserProvider class.
I tried $_POST['password'] but I got an undefined index error...
Any help would be welcome :)
There are a few LDAP Authentication bundles ready to use.
As pointed out below, you can try FR3D/LdapBundle, but it needs FOSUserBundle to work. You can also try IMAG (BorisMorel/LdapBundle), if you don't want to (or don't need to) use FOSUserBundle. BorisMorel uses php-ldap to work, it requires no additional bundle nor any zend components.
Even if you don't want to use them, I'd suggest you check out BorisMorel's implementation for more information. The code's quite simple to understand as it's very straightforward.
Your code sets the authentication state of a connection by transmitting a bind request. Does this code attempt to search for an entry, retrieve the password from that entry and then bind as that distinguished name? If so, even if you could retrieve the password, a properly configured directory server will not allow the transmission of a pre-encoded password. Passwords should always be transmitted in clear text over a secure connection (TLS or SSL) so the server can perform password quality and history validation. Therefore, your code must know the clear-text password beforehand. Alternatively, a LDAP-compliant server might allow the use of a proxied authentication for certain entries.
see also
LDAP: Mastering Search Filters
LDAP: Search best practices
LDAP: Programming practices
Try FR3dLdapBundle
The 2.0.x branch has support with Active Directory if you install Zend\Ldap component from Zend Framework 2
Related
I am following this tutorial:
http://mobilefirstplatform.ibmcloud.com/tutorials/en/foundation/8.0/authentication-and-security/user-authentication/security-check/
There they check the credentials, that they have received from the user's input in the app:
#Override
protected boolean validateCredentials(Map<String, Object> credentials) {
if(credentials!=null && credentials.containsKey("username") && credentials.containsKey("password")){
String username = credentials.get("username").toString();
String password = credentials.get("password").toString();
if(!username.isEmpty() && !password.isEmpty() && username.equals(password)) {
return true;
}
}
return false;
}
As you can see, the authentication returns true when usernameand password are equal. I have a mysqldatabse, where I have saved the registered users. So I want to check the entered credentials against the data in my database. How do I have to change the adapter and the method so I can do this?
To achieve this , you will need to write the code to connect to your DB, invoke the SQL query to check the data in DB.
Within your security check's validateCredentials method, you should write the code to connect to your DB that holds the registered user information. Check for the user details against the DB and based on the outcome , return true of false.
A sample Java SQL adapter is listed here. You can use it for your reference.
I am trying to plug in my own legacy password service into Symfony3 to passively migrate users from a legacy database table.
The legacy system has passwords hashe with the same hard-coded $salt variables used across all members (therefore my FOSUserBundle table currently has the salt column empty for all members that are to be migrated).
The legacy method uses:
sha1($salt1.$password.$salt2)
The new method is Symfony's FOSUserBundle standard bcrypt hash.
I am trying to implement it so that when a legacy user first logs in, Symfony will try to:
Log in using FOSUserBundle's standard bcrypt method.
If #1 did not succeed then try the legacy algorithm.
If #2 succeeeds the password hash and salt in the database table will be updated to comply with standard FOSUserBundle method
I have been reading around about how to plug in a service to get this working and I think the below that I have seems to be correct in theory - if not any corrections/guidance would be appreciated as I've not been able to test it!
However, I'm unsure how I should go about connecting it all into Symfony so that the normal FOSUserBundle processes will carry out steps 2 and 3 if step 1 fails
services.yml:
parameters:
custom-password-encoder:
class: AppBundle\Security\LegacyPasswordEncoder
security.yml:
security:
encoders:
#FOS\UserBundle\Model\UserInterface: bcrypt Commented out to try the following alternative to give password migrating log in
FOS\UserBundle\Model\UserInterface: { id: custom-password-encoder }
BCryptPasswordEncoder (standard FOSUserBundle):
class BCryptPasswordEncoder extends BasePasswordEncoder
{
/* .... */
/**
* {#inheritdoc}
*/
public function encodePassword($raw, $salt)
{
if ($this->isPasswordTooLong($raw)) {
throw new BadCredentialsException('Invalid password.');
}
$options = array('cost' => $this->cost);
if ($salt) {
// Ignore $salt, the auto-generated one is always the best
}
return password_hash($raw, PASSWORD_BCRYPT, $options);
}
/**
* {#inheritdoc}
*/
public function isPasswordValid($encoded, $raw, $salt)
{
return !$this->isPasswordTooLong($raw) && password_verify($raw, $encoded);
}
}
LegacyPasswordEncoder:
namespace AppBundle\Security;
use Symfony\Component\Security\Core\Encoder\BasePasswordEncoder;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
class LegacyPasswordEncoder extends BasePasswordEncoder
{
/**
* {#inheritdoc}
*/
public function encodePassword($raw,$salt)
{
if ($this->isPasswordTooLong($raw)) {
throw new BadCredentialsException('Invalid password.');
}
list($salt1,$salt2) = explode(",",$salt);
return sha1($salt1.$raw.$salt2);
}
/**
* {#inheritdoc}
*/
public function isPasswordValid($encoded, $raw, $salt)
{
list($salt1,$salt2) = explode(",",$salt);
return !$this->isPasswordTooLong($raw) && $this->comparePasswords($encoded,sha1($salt1.$raw.$salt2));
}
}
The solution to your problem is to use the Symfony feature allowing to change the password hashing algorithm dynamically based on the user: https://symfony.com/doc/current/cookbook/security/named_encoders.html
This way, you can mark any non-migrated user as using a legacy algorithm. Then, when updating the password, you would reset the algorithm being used before saving the user, so that the new password is hashed using the new stronger algorithm
Start by mapping your user class to the desired encoder:
security:
hide_user_not_found: false
encoders:
Cerad\Bundle\UserBundle\Entity\User: # Replace with your user class
id: cerad_user.user_encoder # Replace with the service id for your encoder
That should be enough to get your encoder plugged in.
Then you need to actually write your custom encoder by extending BCryptPasswordEncoder and override the isPasswordValid method. And of course create a service for it. Lots to learn.
How to call BcryptPasswordEncorder followed by LegacyPasswordEncoder? You don't. At least not directly. Symfony does not have a chain password encoder. Instead, write your own encoder and implement the chaining yourself.
class MyEncoder extends BCryptPasswordEncoder
{
function isPasswordValid($encoded,$raw,$salt)
{
// Check the bcrypt
if (parent::isPasswordValid($encoded,$raw,$salt)) return true;
// Copied from legacy
list($salt1,$salt2) = explode(",",$salt);
return
!$this->isPasswordTooLong($raw) &&
$this>comparePasswords($encoded,sha1($salt1.$raw.$salt2));
And make sure you define your encoder under services and not parameter. Also be sure to pass the cost (default is 13) as a constructor argument.
I am facing a problem with an LDAP operation. I want to dynamically add a member to an LDAP group when selected by the user from GUI / browser. I paste the code below which works perfectly well when I run it in a Test class (using com.sun.jndi.ldap.LdapCtxFactory). But, when I package it in my build, deploy on websphere app server 7.0 (using com.ibm.websphere.naming.WsnInitialContextFactory), and invoke this method according to user's selection, then I get the error below. I wonder what's wrong I am doing. Doesn't WAS provide implementation of ldap connection factory? I also tried deploying on WAS with the sun's ldap which otherwise works on the Test class, but I am getting the same exception as below. I'd appreciate if anybody can give a clue.
Problem adding member: javax.naming.OperationNotSupportedException: [LDAP: error code 53 - 00000561: SvcErr: DSID-031A120C, problem 5003 (WILL_NOT_PERFORM), data 0
My Code:
public class LDAPManager
{
String GROUPS_OU = "cn=users,dc=mit,dc=hq,dc=com";
public Boolean addMember(String user, String group)
{
Hashtable env = new Hashtable();
String adminName = "CN=Administrator,CN=Users,DC=mit,DC=hq,DC=com";
String adminPassword = "asdfasdf21Q";
String ldapURL = "ldap://mybox451Dev.mit.hq.com:389";
String userName = "CN="+user+",CN=Users,DC=mit,DC=hq,DC=com";
String groupName = "CN="+group+",CN=Users,DC=mit,DC=hq,DC=com";
//env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.ibm.websphere.naming.WsnInitialContextFactory");
//set security credentials, note using simple cleartext authentication
env.put(Context.SECURITY_AUTHENTICATION,"simple");
env.put(Context.SECURITY_PRINCIPAL,adminName);
env.put(Context.SECURITY_CREDENTIALS,adminPassword);
//connect to my domain controller
env.put(Context.PROVIDER_URL, "ldap://mybox451Dev.mit.hq.com:389");
try {
// Create the initial directory context
InitialDirContext ctx = new InitialDirContext(env);
//Create a LDAP add attribute for the member attribute
ModificationItem mods[] = new ModificationItem[1];
mods[0]= new ModificationItem(DirContext.ADD_ATTRIBUTE, new BasicAttribute("member", userName));
//update the group
ctx.modifyAttributes(groupName,mods);
ctx.close();
//System.out.println("Added " + userName + " to " + groupName);
}
catch (NamingException e) {
System.err.println("Problem adding member: " + e);
}
return true;
}
}
I got it solved. Posting solution here, hope this helps someone.
Use the standard JNDI context of sun, not websphere.
Additional properties I was missing in the hashtable, once I added them, it worked like a charm.
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
//env.put(Context.INITIAL_CONTEXT_FACTORY,"com.ibm.websphere.naming.WsnInitialContextFactory");
//set security credentials, note using simple cleartext authentication
env.put(Context.SECURITY_AUTHENTICATION,"simple");
env.put(Context.SECURITY_PRINCIPAL,adminName);
env.put(Context.SECURITY_CREDENTIALS,adminPassword);
env.put(Context.URL_PKG_PREFIXES, "com.sun.jndi.url");
env.put(Context.REFERRAL, "ignore");
Well, it's been more than a year since this question has been asked; so, I don't know answering will add any value. But, here it is. See WAS Javadocs for details on how what that factory class actually does and how it works. You may need to adjust your jndiprovider.properties file for WAS.
I have a WCF service where I use a customUserNamePasswordValidatorType (specified in the behaviors\serviceBehaviors\serviceCredentials\userNameAuthentication section of the web.config file).
My custom UserNamePasswordValidator works that way:
public bool Authenticate(string userName, string password)
{
If ( IsUserValid(username, password) )
{
UserInfo currentUser = CreateUserInfo(username);
//
// Here I'd like to store the currentUser object somewhere so that
// it can be used during the service method execution
//
return true;
}
return false;
}
During the service call execution, I need to access the info of the authenticated user. For instance I would like to be able to implement:
public class MyService : IService
{
public string Service1()
{
//
// Here I'd like to retrieve the currentUser object and use it
//
return "Hello" + currentUser.Name;
}
}
My question is how and where should I store the information during the authentication process so that it can be accessed during the call execution process? That storage should only last as long as the "session" is valid.
By the way, I don't use (and don't want to use) secure sessions and/or reliable sessions. So I have both establishSecuritytContext and reliableSessions turned off.
I'm thinking of enabling ASP.NET Compatibility Mode to store the user info in the HttpContext.Current.Session but I have the feeling it's not how it should be done.
Store anything that needs to be persisted into a persistant store - e.g. a database, that's the best way to go.
Store the user info in a user table, e.g. the ASP.NET membership system or something of your own. Keep some kind of a identifying token (username, ID etc.) at hand to retrieve that info from the database when needed.
You should strive to have a stateless WCF service whenever possible - it should never depend on a "state" of any kind other than what's safely stored in a database.
I have an intranet server on a Windows domain (server is Windows 2003, IIS6, NTFS permissions). It is on the domain Domain01. I have users from two domains in the same forest that access this intranet: Domain01 and Domain02 (DCs also running Windows 2003). Currently, the users are required to login by entering either:
Domain01\username or username#Domain01
My users are completely and thoroughly confused by having to enter the domain each time they log in.
Is there any way to simply allow them to log in by entering just their username and password WITHOUT the domain? For example, have the server try Domain01 by default, and if the login fails to try Domain02?
NOTE: I would like to do this via IIS or server settings if possible, rather than programmatically (for reference, I am using ASP.NET 2.0).
Yes. Usually what I do is do a global catalog search using the supplied user name as the sAMAccountName. Doing this with a PrincipalSearcher requires getting the underlying DirectorySearcher and replacing it's SearchRoot. Once I find the corresponding user object I extract the domain from the user object's path and use that as the domain for the authentication step. How you do the authentication varies depending on what you need it to do. If you don't need impersonation you can use PrincipalContext.ValidateCredentials to make sure that the username/password match using a PrincipalContext that matches the domain of the user account that you previously found. If you need impersonation check out this reference.
// NOTE: implement IDisposable and dispose of this if not null when done.
private DirectoryEntry userSearchRoot = null;
private UserPrincipal FindUserInGlobalContext( string userName )
{
using (PrincipalSearcher userSearcher = new PrincipalSearcher())
{
using (PrincipalContext context
= new PrincipalContext( ContextType.Domain ))
{
userSearcher.QueryFilter = new UserPrincipal( context );
DirectorySearcher searcher
= (DirectorySearcher)userSearcher.GetUnderlyingSearcher();
// I usually set the GC path from the existing search root
// by doing some string manipulation based on our domain
// Your code would be different.
string GCPath = ...set GC path..
// lazy loading of the search root entry.
if (userSearchRoot == null)
{
userSearchRoot = new DirectoryEntry( GCPath );
}
searcher.SearchRoot = userSearchRoot;
using (PrincipalContext gcContext =
new PrincipalContext( ContextType.Domain,
null,
GCPath.Replace("GC://",""))
{
UserPrincipal userFilter = new UserPrincipal( gcContext );
userFilter.SamAccountName = userName;
userSearcher.QueryFilter = userFilter;
return userSearcher.FindOne() as UserPrincipal;
}
}
}
}