主に個人向けで作っているサービスでLogin with Notionを実装する必要があったので、その時の動作検証を行った時の作業ログです。
Login with Notionと仰々しく言っていますが、端的に言えばNotionと連携するためにOAuthを利用するだけの話です。
Public Integrationを作成する
Notion公式ドキュメントに従って、Public Integrationを作成します。
今回の動作検証時のIntegrationの設定は、以下のようになっています。
適当
と書いてあるところは適当に値を設定しています。
- Basic Information
Name
... 適当
- Capabilities
Content Capabilities
...Read content
User Capabilities
...Read user information including email addresses
- Organization Information
Company name
... 適当Website or homepage
... 適当Privacy policy
... 適当Terms of use
... 適当Support email
... 適当
- OAuth Domain & URIs
Redirect URIs
... 動作確認のためにhttp://localhost:3000/callback
実装
Sinatraを使って簡易的に検証します。
今回は使っていないですが、OmniAuthが使える状況であればomniauth-notionというGemが存在するので、それを使うと楽かもしれません。
Gem
Gemfileは以下のようにSinatraを使うためにsinatra
とpuma
を導入し、Sinatra::Reloaderを使いたいのでsinatra-contrib
も導入しておきます。
APIリクエストはNet::HTTP
を使いますが、Faradayなどを使った方が間違いなく楽なのでお好みで使ってください。
# frozen_string_literal: true source "https://rubygems.org" gem 'sinatra' gem 'sinatra-contrib' gem 'puma'
仕様
本格的な実装の前に今回作成するアプリケーションの大まかな仕様を決めておきます。
- Endpoints
/
... ログインしているユーザーの名前を表示。ログインしていない場合は/login
へリダイレクト/login
... Public IntegrationのAuthorization URL
のリンクを配置/logout
... セッションをクリアし、/
へリダイレクト/callback
... Authorization URLアクセス後に呼び出されるコールバックなので、渡された情報を元にアクセストークンとセッションの生成を行う
- Credentials
Sinatraの設定
app.rb
というファイルを作成して、Sinatraの設定を書きます。
# frozen_string_literal: true require 'uri' require 'net/http' require 'sinatra' require 'sinatra/reloader' class Application < Sinatra::Base CLIENT_ID = ENV['NOTION_CLIENT_ID'] CLIENT_SECRET = ENV['NOTION_CLIENT_SECRET'] REDIRECT_URI = 'http://localhost:3000/callback' # Notionで設定したRedirect URLsと同じである必要アリ configure do # セッションを有効化 # Cookieストアなので注意 enable :sessions set :port, ENV.fetch('PORT', 3000) set :bind, ENV.fetch('BIND', '0.0.0.0') # ローカルで実行するなら不要 # Hot reloadを有効化 register Sinatra::Reloader end def authorization_url uri = URI.parse('https://api.notion.com/v1/oauth/authorize') uri.query = URI.encode_www_form( client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, response_type: 'code', scope: 'all' ) uri.to_s end end Application.run!
ログイン画面を実装する
ログイン画面を実装します。
とはいっても、Authorization URLするだけなので、/
へアクセスされた場合に/login
へリダイレクトするのも実装しておきます。
class Application < Sinatra::Base ... get '/' do redirect '/login' if session[:access_token].nil? # セッションにアクセストークンがない場合は/loginへリダイレクト 'TBD' end get '/login' do "<a href=\"#{authorization_url}\">Login with Notion</a>" # Authorization URLのリンクを表示 end end
この状態でアプリケーションを ruby app.rb
で起動し、localhost:3000へアクセスすると、/login
へリダイレクトされます。
ログアウトを実装する
Notionからのコールバックを実装する前に先にログアウト処理を実装しておきます。
今回はセッションをクリアするだけでOKです。
class Application < Sinatra::Base ... # POSTの方がよいがGETのが動作検証が楽なのでGET get '/logout' do session.clear redirect '/' end end
コールバックを実装する
Notionからのコールバックを実装します。 アクセストークンを発行したいので Create a token に従います。
class Application < Sinatra::Base ... # FYI: https://developers.notion.com/reference/create-a-token get '/callback' do code = params[:code] uri = URI.parse('https://api.notion.com/v1/oauth/token') bearer_token = Base64.strict_encode64("#{CLIENT_ID}:#{CLIENT_SECRET}") # Client ID,Secetを結合してBase64でエンコード http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true response = http.post( uri.path, # Payload URI.encode_www_form( grant_type: 'authorization_code', redirect_uri: REDIRECT_URI, code: code ), # Headers { 'Authorization' => "Basic #{bearer_token}", 'Content-Type' => 'application/x-www-form-urlencoded;charset=UTF-8', 'Notion-Version' => '2022-06-28', } ) # 各種情報をセッションに保存 session[:access_token] = JSON.parse(response.body)['access_token'] # アクセストークン session[:user_id] = JSON.parse(response.body)['owner']['id'] # ユーザID redirect '/' end end
これでログイン処理の実装は終わりです。
後はユーザ名を表示すれば検証は終わりです。
ユーザ名を表示する
/
でユーザ名を表示するようにします。
ユーザ情報を取得するには、Retrieve a userに従ってAPIリクエストします。
class Application < Sinatra::Base ... get '/' do redirect '/login' if session[:access_token].nil? uri = URI.parse("https://api.notion.com/v1/users/#{session[:user_id]}") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true response = http.get( uri.path, # Headers { 'Authorization' => "Bearer #{session[:access_token]}", 'Notion-Version' => '2022-06-28', } ) results = JSON.parse(response.body)['results'] user = results.first <<~HTML Hello, <b>#{user['name']}</b>! <br /> <a href="/logout">Logout</a> HTML end ... end
ここまで実装すれば一連の流れを画面上で操作でき、動作検証ができることが分かりました。
おわりに
Login with Notionを使ったサンプルをあまり見かけないので放流だけしておきました。
検証目的での実装なのでセキュリティは非常に脆弱なコードなので、間違ってもそのまま流用しないでください。