This is a detailed step-by-step Laravel installation manual adapted for adminless LDAP authentication.
There is no user management at all. Users are either allowed to use the website or rejected. That's it.
Of course, you can add a "role" attribute to your LDAP directory and use that to control access to different pages or resources. But you won't be able to modify the role from this website, or add/search/modify/delete users. User management is done via the LDAP server.
If you need user management, use Adldap2/Adldap2-Laravel instead. It's a great library but it requires an administrator user in the LDAP server -the same way that you need a database user in MySQL- in order to perform all user-related operations, including checking if a user exists and the password is correct. In my case I didn't have any available admin user in the LDAP server, so I had to adapt the library default behaviour to this specific use case.
As testing environment, we will be using a local Sqlite database and this publicly available testing LDAP server:
http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/
Tested on 2017-11-09 with Laravel v5.5.20 and Adldap2-Laravel v3.0.5.
composer create-project laravel/laravel laravel-simple-ldap-auth
cd laravel-simple-ldap-auth
composer require adldap2/adldap2-laravel
'providers' => [
...
Adldap\Laravel\AdldapServiceProvider::class,
Adldap\Laravel\AdldapAuthServiceProvider::class,
],
'aliases' => [
...
'Adldap' => Adldap\Laravel\Facades\Adldap::class,
],
php artisan vendor:publish --tag="adldap"
'providers' => [
'users' => [
'driver' => 'adldap', // was 'eloquent'
'model' => App\User::class,
],
],
Here, I tried to add a new connection and leave default
untouched, but it didn't work.
Adldap2 kept trying to connect as administrator using the default setup, so I had to modify default
directly:
'connections' => [
'default' => [
'auto_connect' => false,
'connection' => Adldap\Connections\Ldap::class,
'schema' => Adldap\Schemas\ActiveDirectory::class,
'connection_settings' => [
'account_prefix' => env('ADLDAP_ACCOUNT_PREFIX', ''),
'account_suffix' => env('ADLDAP_ACCOUNT_SUFFIX', ''),
'domain_controllers' => explode(' ', env('ADLDAP_CONTROLLERS', 'corp-dc1.corp.acme.org corp-dc2.corp.acme.org')),
'port' => env('ADLDAP_PORT', 389),
'timeout' => env('ADLDAP_TIMEOUT', 5),
'base_dn' => env('ADLDAP_BASEDN', 'dc=corp,dc=acme,dc=org'),
'admin_account_suffix' => env('ADLDAP_ADMIN_ACCOUNT_SUFFIX', ''),
'admin_username' => env('ADLDAP_ADMIN_USERNAME', ''),
'admin_password' => env('ADLDAP_ADMIN_PASSWORD', ''),
'follow_referrals' => true,
'use_ssl' => false,
'use_tls' => false,
],
],
],
This configuration specifies which fields are copied from the LDAP server into the local database for each logged in user.
Some examples of extra attributes to synchronize could be "role" to control access to certain areas or "session_expiration_in_minutes" to force logout after some time. I am sure you can think of many other uses.
The number of fields available in the testing LDAP server is limited, so we will add 'phone'
as an example.
'usernames' => [
'ldap' => env('ADLDAP_USER_ATTRIBUTE', 'userprincipalname'), // was just 'userprincipalname'
'eloquent' => 'username', // was 'email'
],
'sync_attributes' => [
// 'field_in_local_db' => 'attribute_in_ldap_server',
'username' => 'uid', // was 'email' => 'userprincipalname',
'name' => 'cn',
'phone' => 'telephonenumber',
],
FYI, configuration that is secret, i.e. API tokens or database passwords, should be stored in this file,
which Laravel includes by default in .gitignore
.
ADLDAP_CONNECTION=default
ADLDAP_CONTROLLERS=ldap.forumsys.com
ADLDAP_BASEDN=dc=example,dc=com
ADLDAP_USER_ATTRIBUTE=uid
ADLDAP_USER_FORMAT=uid=%s,dc=example,dc=com
DB_CONNECTION=sqlite # was 'mysql'
DB_HOST=127.0.0.1 # remove this line
DB_PORT=3306 # remove this line
DB_DATABASE=homestead # remove this line
DB_USERNAME=homestead # remove this line
DB_PASSWORD=secret # remove this line
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('username')->unique(); // was 'email'
$table->string('password');
$table->string('name'); // to be read from LDAP
$table->string('phone'); // extra field to read from LDAP
$table->rememberToken();
$table->timestamps();
});
}
protected $fillable = [
// replace 'email' with 'username' and add 'phone'
'name', 'username', 'password', 'phone',
];
Before migrating, make sure that your database is configured and working properly.
touch database/database.sqlite
php artisan migrate
php artisan make:auth
This last command installs many controllers and views that we are not going to need, so let's remove them.
app/Http/Controllers/Auth/ForgotPasswordController.php
app/Http/Controllers/Auth/RegisterController.php
app/Http/Controllers/Auth/ResetPasswordController.php
resources/views/auth/register.blade.php
resources/views/auth/passwords
--> remove folder and all files inside
<li><a href="{{ route('register') }}">Register</a></li>
<a href="{{ url('/register') }}">Register</a>
<div class="form-group{{ $errors->has('username') ? ' has-error' : '' }}">
<label for="username" class="col-md-4 control-label">Username</label>
<div class="col-md-6">
<input id="username" type="text" class="form-control" name="username" value="{{ old('username') }}" required autofocus>
@if ($errors->has('username'))
<span class="help-block">
<strong>{{ $errors->first('username') }}</strong>
</span>
@endif
</div>
</div>
Don't forget the use
instructions.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Adldap\Laravel\Facades\Adldap;
class LoginController extends Controller {
...
public function username() {
return config('adldap_auth.usernames.eloquent');
}
protected function validateLogin(Request $request) {
$this->validate($request, [
$this->username() => 'required|string|regex:/^\w+$/',
'password' => 'required|string',
]);
}
protected function attemptLogin(Request $request) {
$credentials = $request->only($this->username(), 'password');
$username = $credentials[$this->username()];
$password = $credentials['password'];
$user_format = env('ADLDAP_USER_FORMAT', 'cn=%s,'.env('ADLDAP_BASEDN', ''));
$userdn = sprintf($user_format, $username);
if(Adldap::auth()->attempt($userdn, $password, $bindAsUser = true)) {
// the user exists in the LDAP server, with the provided password
$user = \App\User::where($this->username(), $username) -> first();
if (!$user) {
// the user doesn't exist in the local database, so we have to create one
$user = new \App\User();
$user->username = $username;
$user->password = '';
// you can skip this if there are no extra attributes to read from the LDAP server
// or you can move it below this if(!$user) block if you want to keep the user always
// in sync with the LDAP server
$sync_attrs = $this->retrieveSyncAttributes($username);
foreach ($sync_attrs as $field => $value) {
$user->$field = $value !== null ? $value : '';
}
}
// by logging the user we create the session so there is no need to login again (in the configured time)
$this->guard()->login($user, true);
return true;
}
// the user doesn't exist in the LDAP server or the password is wrong
// log error
return false;
}
protected function retrieveSyncAttributes($username) {
$ldapuser = Adldap::search()->where(env('ADLDAP_USER_ATTRIBUTE'), '=', $username)->first();
if ( !$ldapuser ) {
// log error
return false;
}
// if you want to see the list of available attributes in your specific LDAP server:
// var_dump($ldapuser->attributes); exit;
// needed if any attribute is not directly accessible via a method call.
// attributes in \Adldap\Models\User are protected, so we will need
// to retrieve them using reflection.
$ldapuser_attrs = null;
$attrs = [];
foreach (config('adldap_auth.sync_attributes') as $local_attr => $ldap_attr) {
if ( $local_attr == 'username' ) {
continue;
}
$method = 'get' . $ldap_attr;
if (method_exists($ldapuser, $method)) {
$attrs[$local_attr] = $ldapuser->$method();
continue;
}
if ($ldapuser_attrs === null) {
$ldapuser_attrs = self::accessProtected($ldapuser, 'attributes');
}
if (!isset($ldapuser_attrs[$ldap_attr])) {
// an exception could be thrown
$attrs[$local_attr] = null;
continue;
}
if (!is_array($ldapuser_attrs[$ldap_attr])) {
$attrs[$local_attr] = $ldapuser_attrs[$ldap_attr];
}
if (count($ldapuser_attrs[$ldap_attr]) == 0) {
// an exception could be thrown
$attrs[$local_attr] = null;
continue;
}
// now it returns the first item, but it could return
// a comma-separated string or any other thing that suits you better
$attrs[$local_attr] = $ldapuser_attrs[$ldap_attr][0];
//$attrs[$local_attr] = implode(',', $ldapuser_attrs[$ldap_attr]);
}
return $attrs;
}
protected static function accessProtected ($obj, $prop) {
$reflection = new \ReflectionClass($obj);
$property = $reflection->getProperty($prop);
$property->setAccessible(true);
return $property->getValue($obj);
}
}
We're done!
Don't forget to set the web server port to 8000
in your local testing .env
file:
APP_URL=http://localhost:8000
Let's run the website and try to log in.
php artisan serve
Visit http://localhost:8000
in your favourite browser.
Try to visit http://localhost:8000/home
before logging in.
Remember that you have these users available in the testing LDAP server:
riemann
, gauss
, euler
, euclid
, einstein
, newton
, galieleo
and tesla
.
The password is password
for all of them.
Log in and play around.
Was this article useful? Give it a star!