RailsとGoogle OAuthで他者のAWS環境を操作する方法

こんにちは！hagurereです。今回は個人開発中にエンドユーザーのAWS環境をRailsサーバーから操作する用件があったので方法を調べて実装してみました。なかなかない要件で同じようなことをやっているドキュメントが見当たらなかったのでここに残しておきます

# 概要

google-oauth2で発行されるid_tokenとユーザーから入力してもらったIAMロールを元にAWS::STS::Client.assume_role_with_web_identityから一時的なaccess_keyを発行し、それを使ってs3からオブジェクトを取得します。

※ 検証用の簡易的な実装ですので細かいことは考慮してません

# 環境
rails(7.0.4)
ruby(3.1.4)
aws-sdk-s3 (1.136.0)
omniauth(2.1.1)
omniauth-google-oauth2(1.1.1)
redis(4.8.1)

# google認証とid_token取得
![スクリーンショット 2023-11-05 18.38.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178067/ee137a94-4c0f-1485-e7b0-d42e615cc3ab.png)

まずomniauth-google-oauth2でのOIDC認証から作っていきます。

適当なrailsサーバーを立ち上げてsessionを使えるようにしておいてください。自分はredis使ってます。

google apiからoauth2の設定をしてclient_idを発行しておいてください。

google apiコンソール側の設定の仕方は公式ドキュメント見てください (てきとーですみません)

gemを追加します。

```ruby
gem 'omniauth'
gem 'omniauth-rails_csrf_protection'
gem 'omniauth-google-oauth2'
gem 'redis'
gem 'redis-actionpack'
```

bundle installしてomniauth.rbを作成します。

id_tokenを発行してもらうためにscopeにopenidを入れてます。

```ruby
# config/initializes/omniauth.rb

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :google_oauth2,
           ENV['GOOGLE_OAUTH_CLIENT_ID'],
           ENV['GOOGLE_OAUTH_CLIENT_SECRET'],
           {
             scope: 'openid,profile,email,offline',
             prompt: 'select_account consent',
             access_type: 'offline',
             provider_ignores_state: Rails.env.development? # セッションストアのドメイン問題でCSRFエラーが出るため開発環境のみtrue
           }
end
OmniAuth.config.allowed_request_methods = [:post, :get]
```

session_store.rbを作成します。

```ruby
host = ENV.fetch("SESSION_STORE_REDIS_HOST")
port = ENV.fetch("SESSION_STORE_REDIS_PORT")
db = ENV.fetch("SESSION_STORE_REDIS_DB")

servers = ["redis://#{host}:#{port}/#{db}/session"]

Rails.application.config.session_store :redis_store,
                                       servers:,
                                       key: '_chocozap_mypage_session',
                                       secure: Rails.env.production?,
                                       httponly: true,
                                       same_site: :strict
```

routes.rbはこんな感じです。

```ruby
Rails.application.routes.draw do
  get '/auth/:provider/callback', to: 'sessions#create'
  get 'auth/failure', to: 'sessions#failure'
end
```

sessions_controllerを作成します。検証用なので超てきとーです。render inlineってめっちゃ使いやすくないですか？笑笑

※ 実際に実装する場合はaccess_token, id_token, refresh_tokenの扱いには注意してください

```ruby
class SessionsController < ApplicationController
  def create
    info = request.env['omniauth.auth']['info']
    session[:name] = info['name']
    session[:email] = info['email']
    session[:image] = info['image']
    session[:access_token] = request.env['omniauth.auth']['credentials']['token']
    session[:refresh_token] = request.env['omniauth.auth']['credentials']['refresh_token']
    session[:id_token] = request.env['omniauth.auth'].extra[:id_token]
    render inline: "name: #{session[:name]}, email: #{session[:email]}, id_token: #{session[:id_token]}"
  end

  def failure
    render inline: 'failure', status: 401
  end
end
```

.envはこんな感じです。

```ruby

SESSION_STORE_DISABLE_REDIS_CLUSTER=true
SESSION_STORE_REDIS_HOST=redis
SESSION_STORE_REDIS_PORT=6379
SESSION_STORE_REDIS_DB=0
GOOGLE_OAUTH_CLIENT_ID=xxxxxxxxxxxxx.apps.googleusercontent.com
GOOGLE_OAUTH_CLIENT_SECRET=xxxxxxxxx
```

これで/auth/google-oauth-2にアクセスしたら動いてくれるはずです。

こんな感じの画面が出たら成功です。
![スクリーンショット 2023-11-05 19.15.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178067/44f1f85b-aa23-a70b-2d31-b69b8563e886.png)

# IAMロールの発行
![FireShot Capture 005 - ロールを作成 - IAM - Global - us-east-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1178067/ff34ef84-978a-38a6-0174-f02454b04cc1.png)

画像のように作成します。

AudienceにはGoogleのclient_idを入れてください

許可を追加で好きなポリシーをアタッチして適当な名前,タグをつけたら完成です。

# IAMロールとid_tokenから一時的なaccess_keyを発行してs3にアクセス

ここからはrails consoleで検証していきます。rails consoleって便利ですよねー

実際に実装する際にはlibの下にクラス作って実装する形になると思います

認証時に発行されるid_tokenを入れてください

```ruby
irb(main):001:0> google_access_token = 'eyJhbGから始まるid_token'
```

Aws::STSを呼び出してassume_roleを作成します。

role_arnには先ほど作成したIAMロールのARNを入れてください。

```ruby
irb(main):002:0> sts_client = Aws::STS::Client.new(region: 'us-east-1')
irb(main):003:2* resp = sts_client.assume_role_with_web_identity({
irb(main):004:2*   role_arn: "arn:aws:iam::111111111111:role/aws-assumerole-test",
irb(main):005:2*   role_session_name: "適当な文字列",
irb(main):006:2*   web_identity_token: google_access_token
irb(main):007:0> })
=> 
#<struct Aws::STS::Types::AssumeRoleWithWebIdentityResponse
```

先ほど作成したassume_roleを使ってAws::Credentialsオブジェクトを作成します

```ruby
irb(main):008:1* credentials = Aws::Credentials.new(
irb(main):009:1*   resp.credentials.access_key_id,
irb(main):010:1*   resp.credentials.secret_access_key,
irb(main):011:1*   resp.credentials.session_token
irb(main):012:0> )
=> #<Aws::Credentials access_key_id="ASIA3GTOZJWOHDDUPQKS">
```

あとは作成したcredentialを使っていつも通りs3オブジェクトを取得するだけです。

あらかじめs3に検証用のファイルを置いておいてください

17行目でhogeという文字列が出力されています。s3に置いたファイルの中身なので成功です。

```ruby
irb(main):013:1*   response = s3.get_object(
irb(main):014:1*     bucket: 'aws-assumerole-test',
irb(main):015:1*     key: 'hoge.txt'
irb(main):016:0>   )
=> 
#<struct Aws::S3::Types::GetObjectOutput
...
irb(main):017:0> puts response.body.read
hoge
=> nil
```

# まとめ

ユーザーのAWS環境を操作というのはあまりないと思いますが管理画面からAWSを操作するような要件があったりすると今回やったような実装になると思います。

最後まで見てくださってありがとうございます。

補足やアドバイス等受け付けておりますのでぜひコメントしてください！