システム開発

Laravelで目標30分の爆速でWebexOAuth連携をしてみた

菅野

こんにちは、株式会社サンエルのシステム開発部部長の菅野です

最近Webex APIを触る機会が多いので何か記事にしたいと思っていたところ、AdventCalenderのお誘いを受けましたので、AdventCalender向けに書いていきます

お題色々悩んだのですが、Webex第1段はLaravelを使ったWebex連携を「爆速」で実装してみたいと思います。

なぜLaravelかというと、MPAで良いシンプルなシステムの場合、私が最も得意なのがLaravelだからです。SPAの場合Django+Nuxt.jsを使うことが多いです。

ということで今回は、

  1. LaravelでOauth2認証
  2. スペースの一覧を取得

までを目標30分で「爆速」実装していきます!

ただし、Laravelの環境構築やWebexのアカウント作成の時間は除きます。

前提条件・対象

以下を対象としています。

この記事を参考にしてもらいたい対象者

  • Laravelを触ったことある人
  • Webexを触ったことあるけどAPI開発したことない人

つまり「WebexAPIを使った開発ってどうなん?」と疑問を持っている人が、「あ〜、こんな感じか」と対応内容を理解していただければ、と思っています。

環境

今回はLaravel8を使います。私はローカル(Mac)を汚したくないので laradock をベースに、ProductionデプロイまでできるようにカスタマイズしたDocker環境を使ってます

ですが、ローカルで既にComposerが入ってる人は、Laravel Sailを使ってもいいかと思います。

以下、今回実装を行った環境になります。

  • Debian 11.1
  • PHP 8.1
  • Laravel 8
  • Webexアカウント[無償]

Webexアカウントは無償でも構いません。有償版でないとGuestIssuerなど一部使えない機能もありますが、スペース作成やメッセージ投稿など、ベーシックな機能は無償版でもほとんど使えます

それでは行きます!

ログイン(15分)

まず、ログイン部分を作ります。

WebexでAPI連携する場合、Botを使う方法Oauth2認証する方法の2つがあります。

どちらもメリット・デメリットがあります。Botの場合はBot用のTokenが発行されるので、Bearer認証すればいいだけです。

なお、Botでのやり方は世の中にたくさん情報が出ているので省略します。

ではまず Webex for Developers にログインして認証に必要な情報を生成します。

Facebookなど他のOauth2認証をしたことがある人にとっては、お馴染みの設定項目だと思います。

もし分からない人は、シスコ コミュニティのページを参考にしてください。

リダイレクト先

認証後にリダイレクトする先を設定します。

今回はローカルで動かすので、次のようにしました。

http://localhost:8080/login/webex/callback

リダイレクト設定

scopeに注意

ここで一つポイントがあります。それはscopeです。詳細は後述しますがspark:allは使わず、必ずspark:people_readを設定してください

本稿では、このあとルームの一覧表示をするので以下のようにしました。

scopeに注意

これでWebexの環境周りの下準備はOKです。

認証パッケージのインストール

今回、認証部分はlaravel/uiを使うことにします。Laravel8では認証パッケージが刷新されており新しく導入されたJetstreamではCSSがBootstrapからTailwindに変更されています

ですが、個人的にまだ手がついていないので、以前から使い慣れてるlaravel/uiを使います。

$ composer require laravel/ui
$ php artisan ui bootstrap --auth
$ php artisan migrate
$ npm install
$ npm run dev

localhost/loginにアクセスしてログイン画面が出たらOKです。

localhost/loginにアクセス

Socialiteの設定

次にLaravel Socialiteを使います。

Socialiteはソーシャルログインを簡単に提供することができるパッケージで、webex用のSocialiteも対応されていますのでcomposerで入れます。

じつは爆速で実装できるポイントはここにあります。以前はこの部分を自力実装してたので、結構手間でした。

$ composer require socialiteproviders/webex

次にconfig/services.phpに設定を追加します。

'webex' => [
  'client_id' => env('WEBEX_CLIENT_ID'),
  'client_secret' => env('WEBEX_CLIENT_SECRET'),
  'redirect' => env('WEBEX_REDIRECT_URI'),
],

config/services.php

Laravel知ってる人ならすぐ分かると思いますが、これだけだと意味がありません。.envに実際の値を設定します。

WEBEX_CLIENT_ID=Developerサイトで生成されたID
WEBEX_CLIENT_SECRET=Developerサイトで生成されたシークレット
WEBEX_REDIRECT_URI=http://localhost::8080/login/webex/callback

.env

最後にProvider周りを設定します。

use SocialiteProviders\Manager\SocialiteWasCalled;

protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ], //↓ここに追加
        SocialiteWasCalled::class => [
             'SocialiteProviders\\Webex\\WebexExtendSocialite@handle',
        ],        
    ];
'providers' => [
        /*
         * Package Service Providers...
         */
        \SocialiteProviders\Manager\ServiceProvider::class,

Webexログインの実装

今回は既存のログイン画面にソーシャルログインボタンを追加して、ユーザーが登録されていなかったら登録します。逆に、登録されていたら更新してログインするようにします。

まずはRouteを作ります。

Route::get('login/{provider}/redirect',[App\Http\Controllers\Auth\LoginController::class, 'redirectToProvider'])
    ->name('login.redirect');
Route::get('login/{provider}/callback',[App\Http\Controllers\Auth\LoginController::class, 'handleProviderCallback'])
    ->name('login.redirect');

Route::group(['middleware' => 'auth'], function(){
    Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])
        ->name('home');
});

redirectcallbackを追加します。今回はWebexだけですが、Socialiteは他の認証も受けれるようにしておきます。

またhomeは認証済みのみアクセスできるようにauthミドルウェアで括ります

次にマイグレーションを修正します。

public function up()
    {
        Schema::create('users',function(Blueprint $table){
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password')->nullable();
            $table->string('access_token');
            $table->rememberToken();
            $table->timestamps()
        });
    }

database/migrations/2014_10_12_000000_create_users_table.php

passwordカラムnullableにして、access_tokenカラムを追加してます。今後パスワード認証を追加する場合を考えると、nullableは危険な香りがします。ただ、今回のシステムはwebexアカウントが無いと全く機能しないので、手っ取り早くnullableにします。

access_tokenをsessionに持たせたり、refresh_tokenもきちんと管理するといったことが色々ありますが、今回は爆速ということで一旦無視します。

次にLoginControllerメソッドを追加します。

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Socialite;
use App\Models\User;

class LoginController extends Controller
{
     ~~~~~省略~~~~~

    public function redirectToProvider(string $provider)
    {
        return Socialite::driver($provider)
            ->scopes(['spark:rooms_read'])
            ->redirect();
    }

    public function handleProviderCallback(string $provider)
    {
        $socialUser = Socialite::driver($provider)->user();

        $user = User::updateOrCreate(
            ['email' => $socialUser->email],
            [
                'name' => $socialUser->name,
                'access_token' => $socialUser->token,
            ]
        );

        Auth::login($user,true);

        return redirect()->route('home');
    }
}

ポイントはredirectToPrividerscopesです。

scopesはDeveloperサイトで設定したscopesと同じものを設定して下さい

ここで罠が1つあります。それは横着してspark:allを使うと、このコードでは動かないです

理由はSocialiteパッケージ内でspark:people_readがコーディングされているからです。

protected $scopes = ['spark:people_read','spark:kms'];

vendor/socialiteproviders/webex/Provider.php

Developerサイトでspark:allを選択するとspark:people_readが選択できなくなるため、Developerサイトのscope設定とリクエストのscope設定が一致できなくなります

もしどうしてもspark:allを使いたいのであれば、setScopesを使って既存のスコープを上書きする必要があります。

ただしセキュリティ上おすすめしません

public function redirectToProvider(string $provider)
{
    return Socialite::driver($provider)
        ->setScopes(['spark:all','spark:kms'])
        ->redirect();
}

また、今回はControllerがUserモデルを直接呼び出してますが、ある一定規模以上のプロダクトならServiceレイヤーやRepositoryレイヤーを入れた方がいいかと思います。

そして最後に、Webexでのログインボタンを追加します。

<div class="row mb-3">
    <div class="col-md-6 offset-md-4">
        <a class="btn btn-primary" href="{{ route('login.redirect',['provider' => 'webex']) }}">
            Webexでログイン
        </a>
    </div>
</div>

Webexでログインボタンが表示されました。
cisco wedex_webexでログイン

ボタンを押すとWebexのログイン画面が表示されます。ここで、メールアドレスとパスワードを入力します。
cisco_webex_メールアドレス

設定したscopeに対する確認画面が出るので承認します。
cisco_webex_承認画面

無事ログインできて右上にWebexアカウント名が表示されてたら成功です。
cisco_webex_右上アカウント名

スペース作成(15分)

アクセストークンさえ取得できればあとは楽です。

WebexAPIを利用する専用クラス作成

個別で実装すると無駄なので、先にWebexへのアクセサを作ってしまいます。WebeClientというクラスにしました。接続先がDBなのかWebexなのかの違いでしかないので、このクラスもModels以下に作成します。

<?php

namespace App\Models;

use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;

class WebexClient
{
    const BASE_URL = 'https://api.ciscospark.com/v1/';

    private $client;

    public function _construct(string $accessToken)
    {
        $this->generateClient($accessToken);
    }
    
    public function generateClient(string $accessToken)
    {
        $headers = [
            'Accept' => 'application/json',
            'Content-Type' => 'application/json; charset=utf-8',
        ];
        $headers['Authorization'] = "Bearer $accessToken";

        $this->client = new Client([
            'base_uri' => self::BASE_URL,
            'headers' => $headers,
        ]);
    }
    
    public function listRooms()
    {
        $response = $this->client->get('rooms');
        return json_decode($response->getBody()->getContents())->items;
    }
}

app/Models/WebClient.php

特に複雑なことはしてません。

スペース一覧を取得するAPIはオプションで、teamIdtypeの指定などオプション指定があります。ただ今回は特に指定せず全取得します。

コントローラーの作成

既にHomeControllerがあるので、これを加工します。

今後スペースの作成や編集機能を追加するなら、専用のRoomControllerを作った方がいいのですが、今回は「爆速」ですから作りません。

元々あったindexメソッドを修正します。

先程作ったWebeClientのlistRoomsを呼ぶだけです。

/**
 * Show the application dashboard.
 *
 * @return \Illuminate\Contracts\Support\Renderable
 */
public function index()
{
    $client = new WebexClient(auth()->user()->access_token);
    $rooms = $client->listRooms();

    return view('home',['rooms' => $tooms]);
}

あとはhome.blade.phpも修正します。

@extends('layouts.app')

@section('content')
<div class="container">
 <div class="row justify-content-center">
  <div class="col-md-8">
    <ul class="list-group">
     @foreach($rooms as $room)
        <li class="list-group-item">{{ $room->title }}</li>
     @endforeach
    </ul>
  </div>
 </div>
</div>
@endsection

ダッシュボード部分にスペースの一覧が出てきたら完成です!

ダッシュボードスペース一覧

おわりに

なんとかギリギリ30分で終わりました。scopeまわりでハマらなければ、もう少し早くできたと思います。

今回は最低限の実装ならどれだけ早く作れるか、というチャレンジでした。

ですが、実際プロダクトレベルにしようとすると、もっと細かい作り込みが必要です。そもそもテストコードすら書いてないですし。

あくまでチャレンジ企画というご認識でお願いします

あと、簡単に実装できたのは、やはりSocialiteにWebexが対応されたことが大きいかと思います。

じつはこの記事を書こうと思ったのは、今までは認証部分を自力で実装してたのですが、お題を考えてるときに、このwebex対応を知ったからです。

https://developer.webex.com/blog/convenient-webex-oauth-for-laravel-projects-with-socialite

対応していただいたCiscoの方に感謝です。

これからWebexAPIを開発する方は、参考にしてみてくださいね。

-システム開発
-, , ,