├── DependencyInjection └── RiperActiveDirectoryExtension.php ├── Exception ├── ADConnexionException.php └── WrongTokenException.php ├── README.md ├── Resources ├── config │ └── services.yml └── translations │ ├── messages.de.yml │ ├── messages.en.yml │ └── messages.fr.yml ├── RiperActiveDirectoryBundle.php ├── Security ├── Authentication │ └── AdAuthProvider.php ├── Factory │ ├── AdAuthFactory.php │ └── AdldapFactory.php ├── Token │ └── FaultyToken.php └── User │ ├── AdUser.php │ └── AdUserProvider.php ├── Service └── AdldapService.php └── composer.json /DependencyInjection/RiperActiveDirectoryExtension.php: -------------------------------------------------------------------------------- 1 | load('services.yml'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Exception/ADConnexionException.php: -------------------------------------------------------------------------------- 1 | =5.3.0,%205.4,%205.5,%205.6,%207-blue.svg) 11 | ![symfony version](https://img.shields.io/badge/symfony-2.6,%202.7,%202.8,%203-blue.svg) 12 | 13 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/3628b49a-0ab1-4412-94cf-328809040af1/big.png)](https://insight.sensiolabs.com/projects/3628b49a-0ab1-4412-94cf-328809040af1) 14 | 15 | 16 | 17 | Requirements 18 | ---------------- 19 | php 5.3.0 20 | 21 | php_ldap 22 | 23 | ssl configuration for LDAP. see http://adldap.sourceforge.net/wiki/doku.php?id=ldap_over_ssl 24 | 25 | Compatible with Symfony starting from 2.6 26 | 27 | 28 | Installation 29 | ---------------- 30 | 31 | You need to add a package to your dependency list : 32 | 33 | // composer.json 34 | "riper/security-active_directory": "2.*" 35 | 36 | You need to enable the bundle into your kernel 37 | 38 | // app/AppKernel.php 39 | new Riper\Security\ActiveDirectoryBundle\RiperActiveDirectoryBundle(), 40 | 41 | You need to configure your domain specific information 42 | 43 | // app/config/config.yml or app/config/parameters.yml 44 | parameters: 45 | riper.security.active_directory.settings: 46 | account_suffix : riper.fr # without the @ at the beginning 47 | base_dn : DC=RIPER,DC=FR #The DN of the domain 48 | domain_controllers : [ baudrive.kim.riper.fr ] #Servers to use for ldap connection (Random) 49 | admin_username: #Null to use the userConnection 50 | admin_password: #Null to use the userConnection 51 | keep_password_in_token: false #Set to true if you want to re-use the adldap instance to make further queries (This is a security issue because the password is kept in session) 52 | real_primarygroup : true #For Linux compatibility. 53 | use_ssl : false #Set it true need configuration of the server to be useful 54 | use_tls : false #Set it true need configuration of the server to be useful 55 | recursive_grouproles: false #recursive group roles 56 | username_validation_pattern: /^[a-z0-9-.]+$/i #Regex that check the final username value (extracted from patterns below). Must be compliant with your Active Directory username policy. 57 | username_patterns: #username is extracted from the string the user put into the login form 58 | - /([^@]*)@riper.fr/i # like toto@riper.fr 59 | - /RIPER\\(.*)/i #like RIPER\toto 60 | - /RIPER.FR\\(.*)/i #like RIEPER.FR\toto 61 | - /(.*)/i #like toto 62 | 63 | You need to add security parameters 64 | 65 | // app/config/security.yml 66 | encoders: 67 | Riper\Security\ActiveDirectoryBundle\Security\User\AdUser : plaintext #Active directory does not support encrypted password yet 68 | 69 | providers: 70 | my_active_directory_provider : 71 | id: riper.security.active.directory.user.provider 72 | 73 | firewalls: 74 | secured_area: 75 | active_directory: #Replace the 'form_login' line with this 76 | check_path: /demo/secured/login_check 77 | login_path: /demo/secured/login 78 | 79 | 80 | Useful information 81 | ---------------------- 82 | 83 | Roles are got from Active directory. The name is transformed to match the ROLE system of Symfony2 84 | 85 | Domain User => ROLE_DOMAIN_USER 86 | Administrators => ROLE_ADMINISTRATORS 87 | 88 | Nested Groups are not supported yet. Enabling the option wont affect the Role check. 89 | 90 | SSL part of the lib isn't used yet and haven't been tested with Symfony 91 | -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | riper_security_active_directory_token: Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken 3 | #This token is faulty because it does not do one security step. It does not erase the password in order to re-use it. 4 | riper_security_active_directory_token_faulty : Riper\Security\ActiveDirectoryBundle\Security\Token\FaultyToken 5 | 6 | riper_security_tokens_classes : 7 | standard : "%riper_security_active_directory_token%" 8 | faulty : "%riper_security_active_directory_token_faulty%" 9 | 10 | services: 11 | riper.security.active.directory.user.provider: 12 | class: Riper\Security\ActiveDirectoryBundle\Security\User\AdUserProvider 13 | arguments: ["%riper.security.active_directory.settings%", "@riper.security.active.directory.service.adldap", "@translator"] 14 | 15 | riper.security.active.directory.authentication.provider: 16 | class: Riper\Security\ActiveDirectoryBundle\Security\Authentication\AdAuthProvider 17 | arguments: ["@riper.security.active.directory.user.provider", "", "@riper.security.active.directory.service.adldap", "@translator", "%riper_security_tokens_classes%", "%riper.security.active_directory.settings%" ] 18 | 19 | riper.security.active.directory.service.adldap: 20 | class: Riper\Security\ActiveDirectoryBundle\Service\AdldapService 21 | arguments: [ "%riper.security.active_directory.settings%"] 22 | 23 | riper.security.active.directory.factory.adldap: 24 | class: Riper\Security\ActiveDirectoryBundle\Security\Factory\AdldapFactory 25 | arguments: [ "@security.token_storage", "@riper.security.active.directory.service.adldap" ] 26 | -------------------------------------------------------------------------------- /Resources/translations/messages.de.yml: -------------------------------------------------------------------------------- 1 | riper.security.active_directory.invalid_user: "Der Benutzername ist ungültig : \"%reason%\" " 2 | riper.security.active_directory.wrong_credential: "Benutzername oder Passwort falsch" 3 | riper.security.active_directory.username_not_matching_rules: "Der Benutzername \"%username%\" entspricht nicht der Benutzernamenrichtlinie" 4 | riper.security.active_directory.ad.bad_response: "Unerwartete Antwort vom Active Directory Server : %connection_status% - %is_AD%" 5 | riper.security.active_directory.bad_instance: "Instanz von \"%class_name%\" nicht unterstützt" 6 | -------------------------------------------------------------------------------- /Resources/translations/messages.en.yml: -------------------------------------------------------------------------------- 1 | riper.security.active_directory.invalid_user: "The username is invalid : \"%reason%\" " 2 | riper.security.active_directory.wrong_credential: "Bad credentials" 3 | riper.security.active_directory.username_not_matching_rules: "The username \"%username%\" does not match the username policy" 4 | riper.security.active_directory.ad.bad_response: "Bad response from the Active Directory server : %connection_status% - %is_AD%" 5 | riper.security.active_directory.bad_instance: "Instance of \"%class_name%\" is not suported." 6 | -------------------------------------------------------------------------------- /Resources/translations/messages.fr.yml: -------------------------------------------------------------------------------- 1 | riper.security.active_directory.invalid_user: "Le login est invalid : \"%reason%\" " 2 | riper.security.active_directory.wrong_credential: "Identifiants erronés" 3 | riper.security.active_directory.username_not_matching_rules: "Le login \"%username%\" ne correspond à aucune règle" 4 | riper.security.active_directory.ad.bad_response: "L'Active Directory ne repond pas : %connection_status% - %is_AD%" 5 | riper.security.active_directory.bad_instance: "Les instance \"%class_name%\" ne sont pas suportées" 6 | -------------------------------------------------------------------------------- /RiperActiveDirectoryBundle.php: -------------------------------------------------------------------------------- 1 | getExtension('security'); 15 | $extension->addSecurityListenerFactory(new AdAuthFactory()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Security/Authentication/AdAuthProvider.php: -------------------------------------------------------------------------------- 1 | userProvider = $userProvider; 36 | $this->config = $config; 37 | $this->AdldapService = $AdldapService; 38 | $this->translator = $translator; 39 | $this->tokenClasses = $tokenClasses; 40 | $this->riperConfig = $riperConfig; 41 | } 42 | 43 | /** 44 | * Attempts to authenticates a TokenInterface object. 45 | * 46 | * @param TokenInterface $token The TokenInterface instance to authenticate 47 | * 48 | * @return TokenInterface An authenticated TokenInterface instance, never null 49 | * 50 | * @throws AuthenticationException if the authentication fails 51 | */ 52 | public function authenticate(TokenInterface $token) 53 | { 54 | $Adldap = $this->AdldapService->getInstance(); 55 | $User = $this->userProvider->loadUserByUsername($token->getUsername()); 56 | if ($User instanceof AdUser) { 57 | if (!$Adldap->authenticate($User->getUsername(), $token->getCredentials())) { 58 | $msg = $this->translator->trans( 59 | 'riper.security.active_directory.wrong_credential' 60 | ); //'The credentials are wrong' 61 | throw new BadCredentialsException($msg); 62 | } 63 | $this->userProvider->fetchData($User, $token, $Adldap); 64 | } 65 | 66 | if (isset($this->riperConfig['keep_password_in_token']) && $this->riperConfig['keep_password_in_token']) { 67 | $newToken = new $this->tokenClasses['faulty']( 68 | $User, 69 | $token->getCredentials(), 70 | 'riper.security.active.directory.user.provider', 71 | $User->getRoles() 72 | ); 73 | } else { 74 | $newToken = new $this->tokenClasses['standard']( 75 | $User, 76 | $token->getCredentials(), 77 | 'riper.security.active.directory.user.provider', 78 | $User->getRoles() 79 | ); 80 | } 81 | 82 | return $newToken; 83 | } 84 | 85 | /** 86 | * Checks whether this provider supports the given token. 87 | * 88 | * @param TokenInterface $token A TokenInterface instance 89 | * 90 | * @return Boolean true if the implementation supports the Token, false otherwise 91 | */ 92 | public function supports(TokenInterface $token) 93 | { 94 | return $token instanceof UsernamePasswordToken; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Security/Factory/AdAuthFactory.php: -------------------------------------------------------------------------------- 1 | addOption('account_suffix', 'domain.local'); 17 | } 18 | 19 | /** 20 | * Subclasses must return the id of a service which implements the 21 | * AuthenticationProviderInterface. 22 | * 23 | * @param ContainerBuilder $container 24 | * @param string $id The unique id of the firewall 25 | * @param array $config The options array for this listener 26 | * @param string $userProviderId The id of the user provider 27 | * 28 | * @return string never null, the id of the authentication provider 29 | */ 30 | protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId) 31 | { 32 | $providerId = 'security.authentication.provider.riper.active_directory.' . $id; 33 | $container 34 | ->setDefinition( 35 | $providerId, 36 | new DefinitionDecorator('riper.security.active.directory.authentication.provider') 37 | ) 38 | ->replaceArgument(0, new Reference("riper.security.active.directory.user.provider")) 39 | ->replaceArgument(1, $config); 40 | 41 | return $providerId; 42 | } 43 | 44 | public function getKey() 45 | { 46 | return 'active_directory'; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Security/Factory/AdldapFactory.php: -------------------------------------------------------------------------------- 1 | tokenStorage = $tokenStorage; 27 | $this->adldapService = $adldapService; 28 | } 29 | 30 | 31 | public function getAuthenticatedAdLdap() 32 | { 33 | $token = $this->tokenStorage->getToken(); 34 | if ($token instanceof FaultyToken) { 35 | throw new WrongTokenException( 36 | 'The token is not the right one. Did you forget to set "keep_password_in_token" to "true" in bundle configuration ?' 37 | ); 38 | } 39 | $adldap = $this->adldapService->getInstance(); 40 | $adldap->authenticate($token->getUsername(), $token->getCredentials()); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Security/Token/FaultyToken.php: -------------------------------------------------------------------------------- 1 | username = $username; 27 | $this->password = $password; 28 | $this->roles = $roles; 29 | } 30 | 31 | /** 32 | * Returns the password used to authenticate the user. 33 | * 34 | * This should be the encoded password. On authentication, a plain-text 35 | * password will be salted, encoded, and then compared to this value. 36 | * 37 | * @return string The password 38 | */ 39 | public function getPassword() 40 | { 41 | return $this->password; 42 | } 43 | 44 | /** 45 | * Sets the password used to authenticate the user. 46 | * 47 | * This should be the encoded password. 48 | * 49 | * @param string $password 50 | */ 51 | public function setPassword($password) 52 | { 53 | $this->password = $password; 54 | } 55 | 56 | /** 57 | * Returns the salt that was originally used to encode the password. 58 | * 59 | * This can return null if the password was not encoded using a salt. 60 | * 61 | * @return string The salt 62 | */ 63 | public function getSalt() 64 | { 65 | return null; 66 | } 67 | 68 | /** 69 | * Returns the username used to authenticate the user. 70 | * 71 | * @return string The username 72 | */ 73 | public function getUsername() 74 | { 75 | return $this->username; 76 | } 77 | 78 | /** 79 | * Returns the display name of the authenticated user. 80 | * 81 | * @return string 82 | */ 83 | public function getDisplayName() 84 | { 85 | return $this->displayName; 86 | } 87 | 88 | /** 89 | * Set the display name of the authenticated user. 90 | * 91 | * @param string $displayName 92 | */ 93 | public function setDisplayName($displayName) 94 | { 95 | $this->displayName = $displayName; 96 | } 97 | 98 | /** 99 | * Returns the email address of the authenticated user. 100 | * 101 | * @return string 102 | */ 103 | public function getEmail() 104 | { 105 | return $this->email; 106 | } 107 | 108 | /** 109 | * Set the email address of the authenticated user. 110 | * 111 | * @param string $email 112 | */ 113 | public function setEmail($email) 114 | { 115 | $this->email = $email; 116 | } 117 | 118 | /** 119 | * Removes sensitive data from the user. 120 | * 121 | * This is important if, at any given point, sensitive information like 122 | * the plain-text password is stored on this object. 123 | * 124 | * @return void 125 | */ 126 | public function eraseCredentials() 127 | { 128 | //return void; 129 | } 130 | 131 | /** 132 | * Returns the roles granted to the user. 133 | * 134 | * 135 | * public function getRoles() 136 | * { 137 | * return array('ROLE_USER'); 138 | * } 139 | * 140 | * 141 | * Alternatively, the roles might be stored on a ``roles`` property, 142 | * and populated in any number of different ways when the user object 143 | * is created. 144 | * 145 | * @return array Role[] The user roles 146 | */ 147 | public function getRoles() 148 | { 149 | return $this->roles; 150 | } 151 | 152 | /** 153 | * Sets the roles for the user. 154 | * 155 | * @param array $roles 156 | */ 157 | public function setRoles(array $roles) 158 | { 159 | $this->roles = $roles; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Security/User/AdUserProvider.php: -------------------------------------------------------------------------------- 1 | config = $config; 31 | $this->translator = $translator; 32 | 33 | $this->recursiveGrouproles = $this->getConfig('recursive_grouproles', false); 34 | $username_patterns = $this->getConfig('username_patterns', array()); 35 | foreach ($username_patterns as $pat) { 36 | array_push($this->usernamePatterns, $pat); 37 | } 38 | } 39 | 40 | /** 41 | * Retrieves a configuration value. make all required test. 42 | * 43 | * @param string $name Key name 44 | * @param mixed $default Default value 45 | * 46 | * @return mixed 47 | */ 48 | protected function getConfig($name, $default) 49 | { 50 | if (!isset($this->config[$name])) { 51 | $return = $default; 52 | } else { 53 | $return = $this->config[$name]; 54 | } 55 | 56 | return $return; 57 | } 58 | 59 | /** 60 | * Loads the user for the given username. 61 | * 62 | * This method must throw UsernameNotFoundException if the user is not 63 | * found. 64 | * 65 | * @param string $username The username 66 | * 67 | * @return AdUser 68 | * 69 | * @see UsernameNotFoundException 70 | * 71 | * @throws UsernameNotFoundException if the user is not found 72 | * 73 | */ 74 | public function loadUserByUsername($username) 75 | { 76 | // The password is set to something impossible to find. 77 | try { 78 | $userString = $this->getUsernameFromString($username); 79 | $user = new AdUser( 80 | $this->getUsernameFromString($userString), 81 | uniqid(true) . rand(0, 424242), 82 | array() 83 | ); 84 | } catch (\InvalidArgumentException $e) { 85 | $msg = $this->translator->trans( 86 | 'riper.security.active_directory.invalid_user', 87 | array('%reason%' => $e->getMessage()) 88 | ); 89 | throw new UsernameNotFoundException($msg); 90 | } 91 | 92 | return $user; 93 | } 94 | 95 | 96 | /** 97 | * Retrieves the username from the login name, it is transformed using the username patterns. 98 | * 99 | * @param string $string 100 | * 101 | * @return string 102 | * 103 | * @throws \InvalidArgumentException 104 | */ 105 | public function getUsernameFromString($string) 106 | { 107 | $username = $string; 108 | foreach ($this->usernamePatterns as $pattern) { 109 | if ($username == $string && preg_match($pattern, $string, $results)) { 110 | $username = $results[1]; 111 | break; 112 | } 113 | } 114 | $username = strtolower($username); 115 | $pattern = $this->getConfig('username_validation_pattern', '/^[a-z0-9-.]+$/i'); 116 | if (preg_match($pattern, $username)) { 117 | return $username; 118 | } 119 | 120 | $msg = $this->translator->trans( 121 | 'riper.security.active_directory.username_not_matching_rules', 122 | array( 123 | '%username%' => $username 124 | ) 125 | ); 126 | throw new \InvalidArgumentException($msg); 127 | } 128 | 129 | /** 130 | * Refreshes the user for the account interface. 131 | * 132 | * It is up to the implementation to decide if the user data should be 133 | * totally reloaded (e.g. from the database), or if the UserInterface 134 | * object can just be merged into some internal array of users / identity 135 | * map. 136 | * 137 | * @param UserInterface $user 138 | * 139 | * @return UserInterface 140 | * 141 | * @throws UnsupportedUserException if the account is not supported 142 | */ 143 | public function refreshUser(UserInterface $user) 144 | { 145 | if (!$user instanceof AdUser) { 146 | $msg = $this->translator->trans( 147 | 'riper.security.active_directory.bad_instance', 148 | array( 149 | '%class_name%' => get_class($user) 150 | ) 151 | ); 152 | throw new UnsupportedUserException($msg); 153 | } 154 | 155 | return $user; 156 | } 157 | 158 | /** 159 | * Fetches the user data via adLDAP and stores it in the provided $adUser. 160 | * 161 | * @param AdUser $adUser 162 | * @param TokenInterface $token 163 | * @param adLDAP $adLdap 164 | */ 165 | public function fetchData(AdUser $adUser, TokenInterface $token, adLDAP $adLdap) 166 | { 167 | $connected = $adLdap->connect(); 168 | $isAD = $adLdap->authenticate($adUser->getUsername(), $token->getCredentials()); 169 | if (!$isAD || !$connected) { 170 | $msg = $this->translator->trans( 171 | 'riper.security.active_directory.ad.bad_response', 172 | array( 173 | '%connection_status%' => var_export($connected, 1), 174 | '%is_AD%' => var_export($isAD, 1), 175 | ) 176 | ); 177 | throw new ADConnexionException( 178 | $msg 179 | ); 180 | } 181 | /** @var adLDAPUserCollection $user */ 182 | $user = $adLdap->user()->infoCollection($adUser->getUsername()); 183 | 184 | if ($user) { 185 | $groups = $adLdap->user()->groups($adUser->getUsername(), $this->recursiveGrouproles); 186 | /** End Fetching */ 187 | $sfRoles = array(); 188 | $sfRolesTemp = array(); 189 | foreach ($groups as $r) { 190 | if (in_array($r, $sfRolesTemp) === false) { 191 | $sfRoles[] = 'ROLE_' . strtoupper(str_replace(' ', '_', $r)); 192 | $sfRolesTemp[] = $r; 193 | } 194 | } 195 | $adUser->setRoles($sfRoles); 196 | unset($sfRolesTemp); 197 | 198 | $adUser->setDisplayName($user->displayName); 199 | $adUser->setEmail($user->mail); 200 | 201 | return true; 202 | } 203 | } 204 | 205 | /** 206 | * Whether this provider supports the given user class 207 | * 208 | * @param string $class 209 | * 210 | * @return Boolean 211 | */ 212 | public function supportsClass($class) 213 | { 214 | return $class === 'Riper\Security\ActiveDirectoryBundle\Security\User\AdUser'; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /Service/AdldapService.php: -------------------------------------------------------------------------------- 1 | parameters = $parameters; 28 | } 29 | 30 | /** 31 | * Returns an adLDAP instance. 32 | * 33 | * @return adLDAP The instance of the adLdap (lib) 34 | */ 35 | public function getInstance() 36 | { 37 | $this->adLdap = new adLDAP($this->parameters); 38 | 39 | return $this->adLdap; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "riper/security-active_directory", 3 | "type": "symfony-bundle", 4 | "description": "This is a bundle to allow authentication into symfony >= 2.6 by an Active directory", 5 | "keywords": ["active directory","ldap","symfony","bundle"], 6 | "homepage": "https://github.com/RiperFr/Security-ActiveDirectoryBundle", 7 | "license": "CC-BY-4.0", 8 | "authors": [ 9 | { 10 | "name": "Loïc Doubinine", 11 | "email": "ztec@riper.fr" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.3.0", 16 | "adldap2/adldap2": "~4.0" 17 | }, 18 | "conflict": { 19 | "symfony/symfony" : "<2.6" 20 | }, 21 | "autoload": { 22 | "psr-0" : { 23 | "Riper\\Security\\ActiveDirectoryBundle" : "" 24 | } 25 | }, 26 | "target-dir": "Riper/Security/ActiveDirectoryBundle" 27 | } 28 | --------------------------------------------------------------------------------