Laravel 8 with Wikimedia OAuth login

January 6, 2022 0 By addshore

I recently wrote a little app called wikicrowd (blog post to follow) using Laravel and MediaWiki / Wikimedia authentication. It certainly wasn’t entirely out of the box, and the existing docs still need some tweaking.

This post reflects the steps I went through to set this app up, and it should only take a few minutes.

You can find a tag of the code at the end of this walkthrough on Github for PHP 8. (There is also a tag for PHP 7.4)

Shout out to the developers that worked on the Wikidata Mismatch Finder which is also a Laravel app with MediaWiki OAuth and was used as inspiration when writing this post, along with the documentation for the package used by Taavi.

Setup Laravel

First off I need a Laravel installation. Currently 8.x is the stream of the latest versions, and the installation docs say to run the below command.

curl -s https://laravel.build/demo-laravel-mediawiki-auth | bashCode language: JavaScript (javascript)

I’m not a fan of running random code on the internet on my machine, but this is what the docs say. It creates a directory at the location you specify at the end of the URL, in my case demo-laravel-mediawiki-auth , creates a laravel/laravel project, and does a composer install.

Modify Laravel

We also want to delete a couple of the migrations that we will not be using.

rm ./database/migrations/2014_10_12_000000_create_users_table.php
rm ./database/migrations/2014_10_12_100000_create_password_resets_table.php

Replacing one of them with a new user table in a file something like 2021_12_29_000000_create_users_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUsersTable extends Migration
{
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('username')->unique();
            $table->timestamps();
        });
    }
    public function down()
    {
        Schema::dropIfExists('users');
    }
}Code language: PHP (php)

And tweak the model to match

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    protected $fillable = [
        'username',
    ];
}
Code language: PHP (php)

Run Laravel

A previous command suggested to start running things locally using sail, which I’m going to use!

./vendor/bin/sail up

This will use sail and a docker-compose file that’s held within the project to set up the needed services, and probably too many of them. For the initial development of your app, you could likely comment out the meilisearch and selenium services.

You’ll always want to create the DB tables, as we will be using some of them for users. (You might want to remove some of these default tables later)

./vendor/bin/sail artisan migrate

You should now find your app accessible on localhost. For me this was http://localhost:80

Socalite

We will be using the socalite plugin for Laravel for authentication alongside taavi/laravel-socialite-mediawiki which enables socalite to work with MediaWiki. So we need to install these.

composer require laravel/socialite
composer require taavi/laravel-socialite-mediawikiCode language: JavaScript (javascript)

The documentation of these packages contains everything you need.

First we need to be able to configure the laralve-socialite-mediawiki plugin, with a new config file created at config/services.php, or this snippet added to the existing config.

return [
    'mediawiki' => [
        'identifier' => env('MEDIAWIKI_OAUTH_CLIENT_ID'), // oauth client id
        'secret' => env('MEDIAWIKI_OAUTH_CLIENT_SECRET'), // oauth client secret

        'callback_uri' => env('MEDIAWIKI_OAUTH_CALLBACK_URL'), // redirect url
        'base_url' => env('MEDIAWIKI_OAUTH_BASE_URL'), // base url of wiki, for example https://meta.wikimedia.org
    ],
];Code language: PHP (php)

And we need a basic OAuthLoginController in the app/Controllers directory. This will handle the redirect to the OAuth target, and the response of the callback, creating a new user within Laravel using the default model.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Laravel\Socialite\Facades\Socialite;
use Illuminate\Support\Facades\Auth;
use App\Models\User;

class OauthLoginController extends Controller {
	public function __construct() {
		$this->middleware( 'guest' )->only( [ 'login', 'callback' ] );
		$this->middleware( 'auth' )->only( 'logout' );
	}

	public function login() {
		return Socialite::driver( 'mediawiki' )
			->redirect();
	}

	public function callback() {
		$socialiteUser = Socialite::driver( 'wiki' )->user();

		$user = User::firstOrCreate( [
			'username' => $socialiteUser->name,
		] );

		Auth::login( $user, false );
		return redirect()->intended( '/' );
	}

	public function logout( Request $request ) {
		$this->guard()->logout();
		$request->session()->invalidate();
		return redirect( '/' );
	}

	private function guard() {
		return Auth::guard();
	}
}Code language: PHP (php)

We can make a new file for the auth routes at routes/auth.php

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\OAuthLoginController;

Route::get('/login', [OAuthLoginController::class, 'login'])
    ->name('login');

Route::get('/callback', [OAuthLoginController::class, 'callback'])
    ->name('oauth.callback');

Route::get('/logout', [OAuthLoginController::class, 'logout'])
    ->name('logout');Code language: PHP (php)

And load these routes in app/Providers/RouteServiceProvider.php by adding the following to the boot method.

Route::prefix('auth')
->middleware('web')
->namespace($this->namespace)
->group(base_path('routes/auth.php'));Code language: PHP (php)

From the socialite side of things, everything should now be ready to go!

MediaWiki OAuth

I’ll be making an app that can login using Wikimedia OAuth, but the same steps should work for any MediaWiki site that has the OAuth extension installed. There is quite a good guide for developers which will let you know what is happening behind the scenes here.

I can create a new OAuth consumer using the Special:OAuthConsumerRegistration page and clicking the Propose new consumer link. It doesn’t matter which Wikimedia site you perform this step on as OAuth is central to all sites.

I need to fill out the form, giving my app a name, making sure OAuth 1.0a is selected as version 1.5.0 of the socalite mediawiki plugin, and that only supports OAuth 1.0a currently. A bunch of other fields are also required.

You need to configure an OAuth callback (you won’t be able to change this) for where your app will be hosted. And if you want to perform local testing you’ll need a second consumer with a different callback URL. For this demo app I’m using http://localhost/auth/callback

Basic rights are enough for login, but if you want to perform actions in the future you may want to select more.

Accept, and hit Propose Consumer!

You’ll be presented with a consumer token and secret token. This would be the time to “write down” the secret so you don’t forget it. (Though you can always reset it)

By default, you, the proposer of a consumer, can start using it straight away, though it would need approval for other accounts to be able to use it.

We can now do the final configuration in the .env file of laravel.

MEDIAWIKI_OAUTH_CLIENT_ID=<consumer_token eg: hr83j081wjgt19ujgn0m1ghgj901>
MEDIAWIKI_OAUTH_CLIENT_SECRET=<consumer_secret eg: j901tjwf1j9gjsagj89ajgajkb921>
MEDIAWIKI_OAUTH_CALLBACK_URL=oob
MEDIAWIKI_OAUTH_BASE_URL=<wiki_url eg: https://meta.wikimedia.org>Code language: HTML, XML (xml)

Testing it!

You should now be able to use the starting Laravel UI and the Login link in the top right to complete your login flow.

If you were successful you should now only see a Home link in the top right, rather than login.

You can check your database using tinker

./vendor/bin/sail artisan tinker

And selecting all the users

>>> DB::select("select * from users");
=> [
     {#3541
       +"id": 1,
       +"username": "Addshore",
       +"created_at": "2022-01-06 08:59:56",
       +"updated_at": "2022-01-06 08:59:56",
     },
   ]Code language: PHP (php)

FAQ

Q: I want to be able to use the authenticated user tokens in the Laravel code to make API requests.

This is quite easily possible and I’ll write a followup post about this. You can find some currently working code in my wikicrowd app on Github.

Q: I get an error about a socialite argument being passed by reference

Taavi\LaravelSocialiteMediawiki\Socialite\One\WikiHmacSha1Signature::Taavi\LaravelSocialiteMediawiki\Socialite\One\{closure}(): Argument #2 ($value) must be passed by reference, value givenCode language: PHP (php)

You are likey running PHP8 and a version of the taavi/laravel-socialite-mediawiki package that is too old. If you want to use PHP 8 be sure to have taavi/laravel-socialite-mediawiki 1.6.0+

Q: I get an error about driver not being supported

Driver [wiki] not supported.Code language: CSS (css)

You have copied a bad example from somewhere. ‘mediawiki’ needs to be used in both your services and Controller, not ‘wiki’.

Q: What is ‘oob’ in the MediaWiki OAuth callback

oauth_callback must be set, and must be set to ‘oob’ when not using the prefix option. ‘oob’ stands for Out Of Band