OAuth で認証して API を使ってみる

前回の記事で OAuth 認証を試みました。
今回は OAuth 認証したうえで、API へアクセスしてみます。
リフレッシュトークンの取得も試みることができたので、まとめておきます。

目次

参考

作成したもの

OAuth 認証したうえで API へのアクセスできるように実装する。
もしトークンの有効期限が切れていたなら、トークンの更新の上でデータを取得する。

準備

前段として、OAuth の環境を整備するで OAuth 認証を実装しています。
こちらを基に拡張してゆきます。

OAuth クライアントの拡張

データベース拡張

OAuth 認証した時に返却される token をデータベースに保持してゆくために、User テーブルのカラムを拡張します。

以下のマイグレーションファイルを作成してトークンと、リフレッシュトークンを保管できるようにします。

db/migrate/[数字列]_add_columns_to_users.rb
1
2
3
4
5
6
class AddColumnsToUsers < ActiveRecord::Migration[5.2]
def change
add_column :users, :token,:string
add_column :users, :refresh_token,:string
end
end

作成できたら、マイグレーションしておきます。

モデルの改修

User モデルを改修して、追加した token カラム・refresh_token カラムへトークンを保管するようにします。

app/models/user.rb(修正前)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, :omniauthable

# providerとuidでuserを検索できるようにします
def self.find_for_myapp(auth)
puts auth
puts auth.credentials.refresh_token

user = User.find_or_create_by(provider: auth.provider, uid: auth.uid)

unless user.new_record?
return user
end

user.uid = auth.uid
user.email = "#{auth.uid}-#{auth.provider}@myapp.com"
user.password = Devise.friendly_token[0, 20]

user.save
user

end
end
app/models/user.rb(修正後)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, :omniauthable

# providerとuidでuserを検索できるようにします
def self.find_for_myapp(auth)
puts auth
puts auth.credentials.refresh_token

user = User.find_or_create_by(provider: auth.provider, uid: auth.uid)

unless user.new_record?
# 新しく作成したユーザーでないときも、token refresh_tokenを更新しておく
user.token = auth.credentials.token
user.refresh_token = auth.credentials.refresh_token
return user
end

user.uid = auth.uid
user.email = "#{auth.uid}-#{auth.provider}@myapp.com"
user.password = Devise.friendly_token[0, 20]

# token refresh_tokenを保存する
user.token = auth.credentials.token
user.refresh_token = auth.credentials.refresh_token

user.save
user

end
end

もし、新しいユーザーであったときは、トークンを更新する仕組みを作っておくことは必要です。

これで、token と refresh_token を保管できるようになりました。

OAuth プロバイダを拡張

OAuth プロバイダを拡張して、プロバイダに API を生やして上げます。

今回はシンプルに json を返すだけにします。

トークン有効期限と、refresh_token の有効化

今回は動作確認も目的なので、以下のようにトークンの有効期限を 1 分とし、refresh_token を有効化します。

config/initializers/doorkeeper.rb
1
2
3
4
5
6
7
Doorkeeper.configure do

# 省略

access_token_expires_in 1.minutes
use_refresh_token
end

API のコントローラーを拡張

app/controllers/api/users_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
class Api::UsersController < Api::ApiController
before_action :doorkeeper_authorize!
respond_to :json

def info
respond_with current_resource_owner
end

def data
respond_with ({ a:"AA", b:"BB"})
end
end

もともとユーザー情報を返していたコントローラーに json を返すだけのメソッド追加しました。
ルーティングが足りていないので、以下のようにルーティングを修正します。

config/routes.rb
1
2
3
4
5
6
7
8
9
10
11
12
Rails.application.routes.draw do
use_doorkeeper
devise_for :users, controllers: {
sessions: 'users/sessions'
}

namespace :api do
get '/info' => 'users#info'
get '/data' => 'users#data' # <=ここに追加
end

end

Outh クライアントから PAuth プロバイダに問い合わせる

クライアントと、プロバイダの用意ができたので、実際のデータ取得処理を記述してゆきます。

コントローラーの追加

データを取得しビューに渡すコントローラを、作成します。
名前を test コントローラとします。(安直ですね。)

以下のようにします。

app/controllers/test_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require 'rest-client'

class TestController < ApplicationController
before_action :authenticate_user!

def index
token = current_user.token

url = 'http://localhost:5000/api/data'

res = RestClient.get url, { :Authorization => "bearer #{token}", content_type: :json }

@res = JSON.parse(res.body)
end

end

こちらを用意したら、test#indexへのルーティングを追加します。
以下のようになります。

config/routes.rb
1
2
3
4
5
6
7
Rails.application.routes.draw do
devise_for :users, :controllers => {
:omniauth_callbacks => "omniauth_callbacks"
}
get 'index', to: 'test#index'
root to: 'test#index'
end

ログインしたらリダイレクトされる root_path にもしておきました。

表示用の.erbを作成します。
データ表示したいだけなので、かなり適当です。

app/views/test/index.html.erb
1
2
3
4
<%= @res %><br />
<%= @res.inspect %><br />
<%= @res["a"] %><br />
<%= @res["b"] %><br />

一旦動作確認します。
/indexにアクセスすると、以下の様に表示されます。

データの取得、表示ができました。

エラーを考慮して修正する準備

実は今の実装だと、トークンの有効期限が切れるとデータの取得できない問題があります。

とりあえず、トークンを更新する仕組みを実装します。

reflesh トークンを使用してトークンを更新してみる

2 通りの作成ができたので、両方記載します。
どちらかで実装すればいいです。

app/controllers/test_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
require 'rest-client'

class TestController < ApplicationController

# 省略

 # OmniAuth::Strategiesの中から呼び出すパターン
def refresh
strategy = OmniAuth::Strategies::Myapp.new('APP_SECRET','APP_ID',:site => "http://localhost:5000")
client = strategy.client

your_expired_at_from_your_provider = Time.now.to_i

hash = {
access_token: current_user.token,
refresh_token: current_user.refresh_token,
expires_at: your_expired_at_from_your_provider,
}

access_token_object = OAuth2::AccessToken.from_hash(client, hash)
refresh = access_token_object.refresh!

current_user.token = refresh.token
current_user.refresh_token = refresh.refresh_token
current_user.save
end

 # OAuth2::Clientを直接使用するパターン
def refresh2
client = OAuth2::Client.new('APP_ID','APP_SECRET',:site => "http://localhost:5000")
your_expired_at_from_your_provider = Time.now.to_i

hash = {
access_token: current_user.token,
refresh_token: current_user.refresh_token,
expires_at: your_expired_at_from_your_provider,
}

access_token_object = OAuth2::AccessToken.from_hash(client, hash)
refresh = access_token_object.refresh!

current_user.token = refresh.token
current_user.refresh_token = refresh.refresh_token
current_user.save
end

end

APP_ID,APP_SECRETは、config/initializers/devise.rbに書いてあるのですが、こちらから取得ができなかったので、べた書きします。
確認してゆくなかで 2 つのパターンでAPP_ID,APP_SECRETの渡す順番が逆転していることがわかりました。
ライブラリがどこで入れ替えているのか調べてみたんですがはっきりせず。
それどころかAPP_SECRET''で実行も可能でした。(何故だろう?)

作成できたら、ルーティングを追加します。

config/routes.rb
1
2
3
4
5
6
7
8
Rails.application.routes.draw do
devise_for :users, :controllers => {
:omniauth_callbacks => "omniauth_callbacks"
}
get 'index', to: 'test#index'
get 'refresh', to: 'test#refresh'
root to: 'test#index'
end

続けてapp/views/test/refresh.html.erbを作成しておきます。
エラーを出さないだけが目的なので、中身は空です。

/refreshにアクセスすると、users テーブルの token と refresh_token が更新されます。
DB を直接参照して確認しましょう。

token が更新されたので、再度/indexにアクセスするとデータの取得・更新ができます。

トークンの更新ができました。

エラーを考慮したデータ取得を再度実装

トークンの更新ができたので、今度はデータ取得と関連付けて実装します。

app/controllers/test_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
require 'rest-client'

class TestController < ApplicationController
before_action :authenticate_user!

def index
token = current_user.token
url = 'http://localhost:5000/api/data'
fail_count = 0
res=nil

loop{
begin
res = RestClient.get url, { :Authorization => "bearer #{token}", content_type: :json }
break;
rescue =>e
# エラーが発生したら 失敗のカウントを増やし、トークン更新処理refresh_tokenを実行する
fail_count += 1
refresh_token
token = current_user.token
## 失敗カウントが5回を超えたらloop処理から離脱する
return nil if fail_count >5
end
}

@res = JSON.parse(res.body)
end

:private
def refresh_token
strategy = OmniAuth::Strategies::Myapp.new('APP_SECRET','APP_ID',:site => "http://localhost:5000")
client = strategy.client

your_expired_at_from_your_provider = Time.now.to_i

hash = {
access_token: current_user.token,
refresh_token: current_user.refresh_token,
expires_at: your_expired_at_from_your_provider,
}

access_token_object = OAuth2::AccessToken.from_hash(client, hash)
refresh = access_token_object.refresh!

current_user.token = refresh.token
current_user.refresh_token = refresh.refresh_token
current_user.save
end
end

データ取得処理でエラーがあった時、トークンを更新する処理を行います。
5 回失敗した場合を想定し、.erbも編集しておきます。
以下のように修正します。

app/views/test/index.html.erb
1
2
3
4
5
6
<% if @res.present? %>
<%= @res %><br />
<%= @res.inspect %><br />
<%= @res["a"] %><br />
<%= @res["b"] %><br />
<% end %>

こちらの実装ができたら動作確認します。
/indexにアクセスすると、データを取得・表示できます。


OAuth で認証し、データの取得・トークンの更新を実装できました。
今回は固定の値を取得しています。
本来のところならユーザーのトークンで認証してもいるので、ユーザーの関連データを取得できるようにするとよさそうです。

ではでは。