既存のHyperstackアプリにAzureADによる認証を導入

#### 参照
- [Railsでユーザー認証機能を実装しよう～定番のgem「devise」活用法（2）](https://codezine.jp/article/detail/10899)
- [RailsでSNS認証を実装しよう〜定番gem「OmniAuth」活用法](https://codezine.jp/article/detail/10970)
- [Devise with Hyperloop Tutorial](http://ruby-hyperloop.org/tutorials/hyperlooprails/hyperloop-devise-tutorial/)
- [OmniAuth Azure Active Directory](https://github.com/AzureAD/omniauth-azure-activedirectory)

model名を使うところと、model名に関係なく user とするところがあるので、区別するために model名を Userではなく Agentとして試してみました。

1. **[devise gemの導入](#install_devise_gem)**
- **[omniAuth AzureAD の導入](#install_omniAuth_AzureAD)**
- **[Login画面を通さず、リンクのクリックでAzureAD認証をさせる](#login_link)**

<H2 id='install_devise_gem'>devise gem の導入</H2>
#### devise gemのインストール

```ruby:Gemfile
gem 'devise'
```
```
$ bundle install
```
```
$ bundle exec rails generate devise:install
```
#### modelの生成 と migrate
```
$ bundle exec rails generate devise Agent

Running via Spring preloader in process 468
      invoke  active_record
      create    db/migrate/20190622011638_add_devise_to_agents.rb
      insert    app/models/agent.rb
       route  devise_for :agents
```
```
$ bundle exec rails db:migrate
== 20190630011828 DeviseCreateAgents: migrating ==
-- create_table(:agents)
   -> 0.0010s
-- add_index(:agents, :reset_password_token, {:unique=>true})
   -> 0.0007s
== 2019063011828 DeviseCreateAgents: migrated (0.0030s) ===
```

#### app/models/agent.rb を app/hyperstack/models/agent.rb へ移動<BR>
ここまでで、http://localhost:5000/agents/sign_up にサインアップ画面が見られるようになる。

#### application_controller.rb に、before_action :authenticate_\<model名\>! を追加<BR>

```ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  before_action :authenticate_agent!   # 追加
```

これで http://localhost:5000 にアクセスすると、まず Log in画面が表示されるようになる。<BR>
サインアップしてログインすれば、もとのHyperstackアプリに戻ることができる。
![login.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228732/0a57e2cb-cf28-9c02-0fcf-f618ad0c15f7.png)

#### current_\<model名\> の aliasとして acting_user を作る
acting_\<model名\> ではなく acting_**user** にする

```ruby:app/controllers/application_controller.rb
def acting_user
  current_agent
end
```
これで、Hyperstack::Application.acting_user_id で、ログイン中のAgentのidにアクセスできるようになる。

#### model にクラスメソッド current を定義

```ruby:app/hyperloop/models/agent.rb
class << self
  def current
    id = Hyperstack::Application.acting_user_id
    id ? find(id) : new
  end
```

#### model の devise行に unless RUBY_ENGINE == 'opal' を追加

```ruby:app/hyperstack/models/agent.rb
class Agent < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable unless RUBY_ENGINE == 'opal'
```
<H2 id='install_omniAuth_AzureAD'> omniAuth AzureADの導入</H2>
#### 先に Azure Portalにアプリを登録してCLIENT IDとTENANT IDを取得しておく
[Azureポータル](https://portal.azure.com/#home)

![register.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228732/0bfac4b7-0658-a9da-0006-cb27bee01589.png)

Azureポータルには、「アプリケーション(クライアントID)」、「ディレクトリ(テナントID)」と書かれている

![Screenshot from 2019-07-02 10-08-27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228732/59ac9817-6869-7610-f4ea-fe0c5f115c93.png)

#### "ID Token"をenableにしておく

そうしないと、あとでこのエラーを見ることになる。[参考 stackoverflow Q&A](https://stackoverflow.com/questions/49422588/aadsts70005-response-type-id-token-is-not-enabled-for-the-application)

```
AADSTS700054: response_type 'id_token' is not enabled for the application.
```

"App registrations"で登録したアプリ名を開いて、左側の「認証」を選択し、「ID トークン」をチェック
![IDtoken.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228732/73a81104-db35-35af-5a2a-69c5b60bc44a.png)

#### "Redirect URI"に、callbackを設定
```
http://localhost:5000/agents/auth/azureactivedirectory/callback
```
![redirectURI.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228732/e7721832-cf78-9684-ad30-d0befece6421.png)

#### omniauth gem と dotenv-rails のインストール

```ruby:Gemfile
group :development, :test do
  ...
  gem 'dotenv-rails'
end
...

gem 'omniauth'
gem 'omniauth-azure-activedirectory'
```
```
$ bundle install
```

#### .env に、Azureポータルから取得した CLIENT ID と TENANT IDを設定しておく

```ruby:.env
AAD_CLIENT_ID = 'abcdabcd-abcd-abcd-abcd-abcdabcdabcd'
AAD_TENANT    = 'abcdabcd-abcd-abcd-abcd-abcdabcdabcd'
```
(githubなどにアップするなら .envを .gitignoreに登録しておくのを忘れずに)

#### config/initializers/devise.rb に 登録

```ruby:config/initializers/devise.rb
config.omniauth :azure_activedirectory, ENV['AAD_CLIENT_ID'], ENV['AAD_TENANT']
```

#### modelに provider, uid カラムを追加
必須ではないが、後で使うために name カラムも追加しておく

```
$ bundle exec rails generate migration AddColumnsToAgents name:string provider:string uid:string
$ bundle exec rails db:migrate
```

#### Deviseにcontrollerを作らせる

```
$ bundle exec rails g devise:controllers agents
Running via Spring preloader in process 341
      create app/controllers/agents/confirmations_controller.rb
      create app/controllers/agents/passwords_controller.rb
      create app/controllers/agents/registrations_controller.rb
      create app/controllers/agents/sessions_controller.rb
      create app/controllers/agents/unlocks_controller.rb
      create app/controllers/agents/omniauth_callbacks_controller.rb
=========================================================================

Some setup you must do manually if you haven't yet:

  Ensure you have overridden routes for generated controllers in your routes.rb.
  For example:

    Rails.applicationo.routes.draw do
      devise_for :users, controllers: {
        sessions: 'users/sessions'
      }
    end

===========================================
```

#### routingを定義

```ruby:app/config/routes.rb
Rails.application.routes.draw do
  devise_for :agents, controllers: {
    omniauth_callbacks: 'agents/omniauth_callbacks',
    registrations: 'agents/registrations'
  }
  
  mount Hyperstatck::Engine => '/hyperstack'
  get '/(*other)', to: 'hyperstack#app'
end
```

#### Modelの devise行に :omniauthable 等を追加

```ruby:app/hyperstack/models/agent.rb
class Agent < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :omniauthable, omniauth_providers: [:azureactivedirectory] unless RUBY_ENGINE == 'opal'
```
:azure_activedirectory ではなく、:azureactivedirectory にする。
でないと、サインインしようとしたときに "Not found. Authentication passthru" を見ることになる。
[Devise issue# 4208](https://github.com/plataformatec/devise/issues/4208)

#### callbackを定義し、protect_from_forgery から :azureactivedirectory を除外する

```ruby:app/controllers/agents/omniauth_callbacks_controller.rb
class Agents::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  protect_from_forgery with: :exception, except: [:azureactivedirectory]  # 追加

  def azureactivedirectory
    callback
  end

  def passthru
    super
  end

  def failure
    super
  end

  private

    def callback
      @agent = Agent.find_or_create_for_oauth(request.env['omniauth.auth'])

      if @agent.persisted?
        sign_in_and_redirect @agent
      else
        session['devise.user_attributes'] = @agent.attributes
        redirect_to new_agent_registration_url
      end
    end
end
```

```ruby:app/hyperstack/models/agent.rb
class Agent < ApplicationRecord
…（略）…

  class << self

    def current
      id = Hyperstack::Application.acting_user_id
      id ? find(id) : new
    end

    def find_or_create_for_oauth(auth)
      find_or_create_by!(email: auth.info.email.downcase) do |user|  # downcaseが必要
        user.provider = auth.provider
        user.uid = auth.uid
        user.name = auth.info.name
        user.email = auth.info.email
        password = Devise.friendly_token[0..5]
        logger.debug password
        user.password = password
      end
    end

    def new_with_session(params, session)
      if user_attributes = session['devise.user_attributes']
        new(user_attributes) { |user| user.attributes = params }
      else
        super
      end
    end
  end
end
```

これで、http://localhost:5000 で表示される "Sign in with Azureactivedirectory" から AzureADの認証へ進むようになった。

![Screenshot from 2019-07-02 10-31-17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228732/caaeecf5-20c7-0b97-e41e-f5ac47f6a2d3.png)

![Screenshot from 2019-07-02 10-32-19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228732/be8a14bb-e9f6-3582-2d3b-88f4460c0bf3.png)

<H2 id='login_link'> Login画面を通さず、リンクのクリックでAzureAD認証をさせる</H2>
#### HeaderコンポーネントのNavbarにLoginリンクを置いてみる
ログイン後はリンクの代わりにAgent.current.nameを表示させる

```ruby:app/hyperstack/components/header.rb
class Header < HyperComponent

  render(HEADER) do
    Navbar('light', color: 'success') do
      Nav(class: 'ml-auto') do
        NavItem do
          Agent.current.name || login_link
        end
      end
    end
  end

  def login_link
    NavLink(class: 'login-link', href: 'agents/auth/azureactivedirectory') do
      'ログイン'
    end
  end
end
```

リンク先に agent_azureactivedirectory_omniauth_authorize_path を使いたいが、Hyperstackコンポーネントから使う方法がわからないので、パス 'agents/auth/azureactivedirectory' を直接指定した。

#### Login画面を表示しないようにする
```ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  # before_action :authenticate_agent!    # コメントアウト

  def acting_user
    current_agent
  end
end
```
これで、「ログイン」リンクをクリックすると、AzureADの認証に進むようになった。