Bookstack: support login via LDAP/OAuth

Created on 1 Jan 2016  路  10Comments  路  Source: BookStackApp/BookStack

Most helpful comment

To login into BookStack with a oauth2 provider is also one of my needs. +1

All 10 comments

+1 for LDAP
Bonus point, if you add reverse proxy auth compatibility via HTTP basic authentification (for SSO) I will probably make a Yunohost package.

+1 for LDAP as well.

Just setting up a local raspberry pi LDAP server now so I can get familiar with LDAP and implement it properly :smile:

As of release 0.7 (commit 148e172fe812a3acdd4d8e172c58e283835577ad) LDAP user auth is now part of BookStack. It's only basic at the moment, and experimental since it has only been tested by me. Setup/Usage instructions are in the readme.

Since login is functional in some capacity I will close this issue. For any extra LDAP features or bugs new ,more focused, issues should be created.

@MVprobr Thanks for letting me know. In regards to oAuth, What exactly is it that you want implemented in BookStack?

  • Some kind of generic oAuth implementation?
  • A specific oAuth provider to login with?
  • Do you want BookStack to act as a oAuth provider?

Sorry for the questions, I'm not too familiar with the actual use cases when it comes to oAuth.

I need to setup a specific oauth2 provider in my case. This should be easy to implement.
I will also try to make the admin options for it.

To login into BookStack with a oauth2 provider is also one of my needs. +1

@younes0 did you ever get sorted with this? I'd like to experiment with it against identityserver4 and possibly use this system.

@dealproc I've setup Socialite on my bookstack instance, and I handle user creation from my main app.

Socialite:

namespace BookStack\Providers;

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
use BookStack\User;
use BookStack\Icra\IcramProvider;


class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        // Custom validation methods
        \Validator::extend('is_image', function($attribute, $value, $parameters, $validator) {
            $imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
            return in_array($value->getMimeType(), $imageMimes);
        });

        $socialite = $this->app->make('Laravel\Socialite\Contracts\Factory');

        $socialite->extend('icram', function ($app) use ($socialite) {
            $config = $app['config']['services.icram'];
            return $socialite->buildProvider(IcramProvider::class, $config);
        });

    }
}

<?php

namespace BookStack\Icra;

use Laravel\Socialite\Two\AbstractProvider;
use Laravel\Socialite\Two\ProviderInterface;
use Laravel\Socialite\Two\User;

class IcramProvider extends AbstractProvider implements ProviderInterface
{
    /**
     * {@inheritdoc}
     */
    protected function getAuthUrl($state)
    {
        return $this->buildAuthUrlFromBase(env('ICRAM_URL').'/oauth/authorize', $state);
    }

    /**
     * {@inheritdoc}
     */
    protected function getTokenUrl()
    {
        return env('ICRAM_URL').'/oauth/token';
    }

    /**
     * {@inheritdoc}
     */
    public function getAccessToken($code)
    {
        $response = $this->getHttpClient()->post($this->getTokenUrl(), [
            'headers' => ['Authorization' => 'Basic ' . base64_encode($this->clientId . ':' . $this->clientSecret)],
            'body'    => $this->getTokenFields($code),
        ]);

        return $this->parseAccessToken($response->getBody());
    }

    /**
     * {@inheritdoc}
     */
    protected function getTokenFields($code)
    {
        return array_add(
            parent::getTokenFields($code), 'grant_type', 'authorization_code'
        );
    }

    /**
     * {@inheritdoc}
     */
    protected function getUserByToken($token)
    {
        $response = $this->getHttpClient()->get(env('ICRAM_URL').'/api/me', [
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
            ],
        ]);

        return json_decode($response->getBody(), true);
    }

    /**
     * {@inheritdoc}
     */
    protected function mapUserToObject(array $user)
    {
        return (new User)->setRaw($user)->map([
            'id'       => $user['id'],
            'nickname' => $user['username'],
            'name'     => $user['firstname'].' '.$user['lastname'],
            'avatar'   => null,
        ]);
    }

}

User Creation:
```php

namespace Icram\Models;

use Yeb\Laravel\ExtendedModel;
use Carbon\Carbon;

class WikiUser extends ExtendedModel
{
protected $connection = 'wiki';

public static $unguarded = false;

public $table = 'users';

protected $guarded = [''];

/**
 * Define the relationship with the user's roles.
 *
 * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
 */
public function roles()
{
    return $this->belongsToMany(WikiRole::class, 'role_user', 'user_id', 'role_id');
}

public function getUser()
{
    return User::where('email', $this->email)->first();
}

public static function firstOrCreateFromUser(User $user, $delete = true)
{
    if ($model = $user->getWikiUser()) {
        if ($delete) {
            $model->delete();

        } else {
            return $model;
        }
    } 

    $model = static::create(static::getDefaults($user));

    // group
    $roleName = $user->is_admin ? 'admin' : 'viewer';
    $role = WikiRole::where('name', $roleName)->first();

    $model->roles()->attach($role);
    $model->save();

    $con = \DB::connection('wiki');

    // social account
    $con->table('social_accounts')->insert([
        'user_id'   => $model->id,
        'driver'    => 'icram',
        'driver_id' => $user->id,
        'avatar'    => '',
    ]);

    // image
    $imageId = $con->table('images')->insertGetId([
        'name'       => $user->id.'.jpg',
        'url'        => $user->photoUrl,
        'path'       => '/photos/'.$user->id.'.jpg',
        'type'       => 'jpeg',
        'created_at' => Carbon::now(),
        'updated_at' => Carbon::now(),
        'created_by' => $model->id,
        'updated_by' => $model->id,
    ]);

    $model->update(['image_id' => $imageId]);

    return $model;
}

public function syncInfos()
{
    $this->update(static::getDefaults($this->getUser()));

    return $this;
}

static protected function getDefaults($user)
{
    return [
        'name'             => $user->fullname, 
        'email'            => $user->email,
        'email_confirmed'  => true,
        'password'         => $user->password ?: str_random(),
        'external_auth_id' => '',
        'image_url'        => $user->photoUrl, 
    ];
}

}

namespace Icram\Models;

use Yeb\Laravel\ExtendedModel;

class WikiRole extends ExtendedModel
{
protected $connection = 'wiki';

public static $unguarded = false;

public $table = 'roles';

protected $guarded = [''];

/**
 * Define the relationship with the group's users.
 *
 * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
 */
public function users()
{
    return $this->belongsToMany(WikiUser::class, 'role_user');
}

}

// usage from main app
/*

  • Create/Update related when User is created/updated
    *
  • @return void
    */
    public static function boot()
    {
    parent::boot();
// created
static::created(function($model) {
    $model->profile()->create([]);
});

// updated
static::updated(function($model) {
    // wiki
    $wikiUser = $model->getWikiUser();

    if ($wikiUser) {
        $wikiUser->syncInfos();

    } else if ($model->isComplete()) {
        WikiUser::firstOrCreateFromUser($model);
    }
});

// deleted
static::deleting(function($model) {
       if ($wikiUser = WikiUser::where('email', $model->email)->first()) {
        $wikiUser->delete();
    }
});

}

I have no idea of how to make that dance. Will have to bookmark this and come back to it once i get a little read through and see if its possible. my hope was to have a user directed to this, and let them create their user within the system as a "user", and then assign them into what categories they'd need thereafter. I like the idea of this system as you're publishing eBooks per topic/product/whatever, which is what intrigued me about it.

Was this page helpful?
0 / 5 - 0 ratings