BookStackApp / BookStack

A platform to create documentation/wiki content built with PHP & Laravel
https://www.bookstackapp.com/
MIT License
15.17k stars 1.9k forks source link

support login via LDAP/OAuth #39

Closed fibis closed 8 years ago

mbugeia commented 8 years ago

+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.

TJuberg commented 8 years ago

+1 for LDAP as well.

ssddanbrown commented 8 years ago

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

ssddanbrown commented 8 years ago

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.

ssddanbrown commented 8 years ago

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

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

younes0 commented 8 years ago

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.

fibis commented 8 years ago

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

dealproc commented 5 years ago

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

younes0 commented 5 years ago

@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, 
        ];
    }
}

<?php

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();
        }
    });
}
dealproc commented 5 years ago

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.