こんにちは、株式会社サンエルのシステム開発部部長の菅野です
最近Webex APIを触る機会が多いので何か記事にしたいと思っていたところ、AdventCalenderのお誘いを受けましたので、AdventCalender向けに書いていきます。
お題色々悩んだのですが、Webex第1段はLaravelを使ったWebex連携を「爆速」で実装してみたいと思います。
なぜLaravelかというと、MPAで良いシンプルなシステムの場合、私が最も得意なのがLaravelだからです。SPAの場合Django+Nuxt.jsを使うことが多いです。
ということで今回は、
- LaravelでOauth2認証
- スペースの一覧を取得
までを目標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を設定してください。
本稿では、このあとルームの一覧表示をするので以下のようにしました。
これで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です。
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');
});
redirectとcallbackを追加します。今回は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');
}
}
ポイントはredirectToPrividerのscopesです。
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でログインボタンが表示されました。
ボタンを押すとWebexのログイン画面が表示されます。ここで、メールアドレスとパスワードを入力します。
設定したscopeに対する確認画面が出るので承認します。
無事ログインできて右上に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はオプションで、teamIdやtypeの指定などオプション指定があります。ただ今回は特に指定せず全取得します。
コントローラーの作成
既に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を開発する方は、参考にしてみてくださいね。