Materiaを導入してログイン認証してみる

[Materia](https://github.com/karabiner-inc/materia)については、
[「Webサービスバックエンドライブラリー　Materia」](https://qiita.com/tuchiro/private/eb0c5e9a7a1ce67cf1e3)
を参照ください。

# 前提環境

```
> mix hex.info
Hex:    0.18.2
Elixir: 1.6.1
OTP:    20.2.4

> mix phx.new -v
Phoenix v1.3.4
```

# プロジェクトの作成とMateriaの導入


### プロジェクトを作成する

```
> mix phx.new materia_sample --no-brunch
> cd materia_sample
```


```elixir:materia_sample/mix.exs

defp deps do
    [
      {:phoenix, "~> 1.3.2"},
      {:phoenix_pubsub, "~> 1.0"},
      {:phoenix_ecto, "~> 3.2"},
      {:plug_cowboy, "~> 1.0"},  #<- add here
      {:postgrex, ">= 0.0.0"},
      {:phoenix_html, "~> 2.10"},
      {:phoenix_live_reload, "~> 1.0", only: :dev},
      {:gettext, "~> 0.11"},
      {:cowboy, "~> 1.0"},
      {:materia, "~> 0.1.0"}, #<- add here
    ]
  end

```

### guardian(認証ライブラリ)の設定

※必ずdeps.getの前に実施する

```elixir:materia_sample/config/config.exs

# Configures Guardian
config :materia, Materia.Authenticator,
  issuer: "materia_sample",
  access_token_ttl: {10, :minutes}, #必須
  refresh_token_ttl: {1, :days}, # refresh_tokenを定義しない場合sign-inはaccess_tokenのみ返す
  user_registration_token_ttl: {35, :minutes},
  password_reset_token_ttl: {35, :minutes},
  secret_key: "your secusecret token",
  allowed_algos: ["HS256"]

# Configures GuardianDB
config :guardian, Guardian.DB,
repo: MateriaSample.Repo,  #<- mod your app repo
schema_name: "guardian_tokens", # default
#token_types: ["refresh_token"], # store all token types if not set
sweep_interval: 60 # default: 60 minutes

# Configures calender utils locale (default is server locale)
config :materia_utils, calender_locale: "Asia/Tokyo"
```

```elixir:materia_sample/config/dev.exs
# Configure materia repo
config :materia, :repo, MateriaSample.Repo  #<- add your app repo
```


deps.get

```
mix deps.get
```

シークレットキーの生成

```
> mix phx.gen.secret
hogehogehogehogheohgoehgoheohgeohgoheohgoehgoheohgeohgoehogeho
```

config.exsの「Configures Guardian」の末尾にシークレットキーを追加

```elixir:materia_sample/config/config.exs
# Configures Guardian
config :materia, Materia.Authenticator,
  issuer: "materia_sample",
  …
  allowed_algos: ["HS256"],   # カンマを追加
  # Generate mix task
  # > mix phx.gen.secret
  secret_key: "hogehogehogehogheohgoehgoheohgeohgoheohgoehgoheohgeohgoehogeho"

```

※オプション guardian sweeper(tokenの自動削除)の有効化

```elixir:materia_sample/lib/materia_sample/application.ex
defmodule MateriaSample.Application do
  use Application

〜中略〜

    # Define workers and child supervisors to be supervised
    children = [
      # Start the Ecto repository
      supervisor(MateriaSample.Repo, []),
      # Start the endpoint when the application starts
      supervisor(MateriaSampleWeb.Endpoint, []),
      # Start your own worker by calling: MateriaSample.Worker.start_link(arg1, arg2, arg3)
      # worker(MateriaSample.Worker, [arg1, arg2, arg3]),
      worker(Guardian.DB.Token.SweeperServer, []), #<- if you wont auto sweep invalid token, you must add GuardianDB worker.
    ]

〜中略〜

end

```

seedsの設定

Materiaのテスト用[seeds.exs](https://github.com/karabiner-inc/materia/blob/master/priv/repo/seeds.exs)から転記

```elixir:


```

Migrationファイルの生成&DBマイグレーション

```
> mix guardian.db.gen.migration
> mix materia.gen.migration
> mix ecto.create
> mix ecto.migrate
> mix run priv/repo/seeds.exs
```

基本的な導入作業はここまでです。

# 基本的なAPIを有効にして使ってみる

#### Materiaで用意されたルーティングを追加して起動

※　実際のルーティング定義はサービス要件に合わせて設定する必要があります。
　具体的にはユーザー情報の検索などは管理者権限の運用者のみ利用できるように設定すべきです。

```elixir:materia_sample/lib/materia_sample_web/router.ex

defmodule MateriaWeb.Router do
  use MateriaWeb, :router

  # 今回は使わないのでコメントアウト
  #pipeline :browser do
  #  plug :accepts, ["html"]
  #  plug :fetch_session
  #  plug :fetch_flash
  #  plug :protect_from_forgery
  #  plug :put_secure_browser_headers
  #end

  pipeline :guardian_auth do
    plug Materia.AuthenticatePipeline
  end

  pipeline :tmp_user_auth do
    plug Materia.UserRegistrationAuthPipeline
  end

  pipeline :pw_reset_auth do
    plug Materia.PasswordResetAuthPipeline
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  pipeline :guardian_auth do
    plug Materia.AuthenticatePipeline
  end

  pipeline :tmp_user_auth do
    plug Materia.UserRegistrationAuthPipeline
  end

  pipeline :pw_reset_auth do
    plug Materia.PasswordResetAuthPipeline
  end

  scope "/api", MateriaWeb do
    pipe_through :api

    post "/sign-in", AuthenticatorController, :sign_in
    post "/refresh", AuthenticatorController, :refresh
    post "/tmp-registration", UserController, :registration_tmp_user
    post "/request-password-reset", UserController, :request_password_reset

  end

  scope "/api", MateriaWeb do
    pipe_through [ :api, :tmp_user_auth]

    get "/varidation-tmp-user", AuthenticatorController, :is_varid_token
    post "/user-registration", UserController, :registration_user
    post "/user-registration-and-sign-in", UserController, :registration_user_and_sign_in

  end

  scope "/api", MateriaWeb do
    pipe_through [ :api, :pw_reset_auth]

    get "/varidation-pw-reset", AuthenticatorController, :is_varid_token
    post "/reset-my-password", UserController, :reset_my_password

  end

  scope "/api", MateriaWeb do
    pipe_through [ :api, :guardian_auth]

    get "/user", UserController, :show_me
    post "/grant", GrantController, :get_by_role
    post "/sign-out", AuthenticatorController, :sign_out
    get "/auth-check", AuthenticatorController, :is_authenticated
    post "/search-users", UserController, :list_users_by_params
    resources "/addresses", AddressController, except: [:new, :edit]
    post "/create-my-addres", AddressController, :create_my_address

  end

〜中略〜

  #今回はいらないのでコメントアウト
  #scope "/", MateriaSampleWeb do
  #  pipe_through :browser # Use the default browser stack
  #
  #  get "/", PageController, :index
  #end

end
```

phoenixを起動する

```
> mix phx.server
```

※　ここからは自分の好きなHTTPクライアントを使用してください。
サンプルはREST Clientで記載しています。
[VS Code上でHTTPリクエストを送信し、VS Code上でレスポンスを確認できる「REST Client」拡張の紹介](https://qiita.com/toshi0607/items/c4440d3fbfa72eac840c)

### ログインする

- Request

```
POST http://localhost:4000/api/sign-in HTTP/1.1
Content-Type: application/json

{
    "email": "hogehoge@example.com", 
    "password": "hogehoge"
  }
```

- Response

```
HTTP/1.1 201 Created
server: Cowboy
date: Fri, 14 Dec 2018 08:16:32 GMT
content-length: 740
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate

{
  "refresh_token": "hogehoge",
  "id": 1,
  "access_token": "fugafuga"
}


```

### ユーザー情報を確認する

- Request

```
GET http://localhost:4000/api/user HTTP/1.1
Content-Type: application/json
Authorization: Bearer fugafuga
```

- Response

```
HTTP/1.1 200 OK
server: Cowboy
date: Fri, 14 Dec 2018 08:21:53 GMT
content-length: 941
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate

{
  "status": 1,
  "role": "admin",
  "phone_number": null,
  "organization": {
    "users": [],
    "status": 1,
    "profile_img_url": "https://hogehoge.com/prof_img.jpg",
    "phone_number": null,
    "one_line_message": "let's do this.",
    "name": "hogehoge.inc",
    "lock_version": 1,
    "id": 1,
    "hp_url": "https://hogehoge.inc",
    "back_ground_img_url": "https://hogehoge.com/ib_img.jpg",
    "addresses": []
  },
  "name": "hogehoge",
  "lock_version": 2,
  "id": 1,
  "icon_img_url": null,
  "external_user_id": null,
  "email": "hogehoge@example.com",
  "descriptions": null,
  "back_ground_img_url": null,
  "addresses": [{
    "zip_code": "810-ZZZZ",
    "user": [],
    "subject": "billing",
    "organization": [],
    "longitude": null,
    "lock_version": 0,
    "location": "福岡県",
    "latitude": null,
    "id": 2,
    "address2": "大名 x-x-xx",
    "address1": "福岡市中央区"
  }, {
    "zip_code": "810-ZZZZ",
    "user": [],
    "subject": "living",
    "organization": [],
    "longitude": null,
    "lock_version": 0,
    "location": "福岡県",
    "latitude": null,
    "id": 1,
    "address2": "港 x-x-xx",
    "address1": "福岡市中央区"
  }]
}

```

### 他のユーザーに対する汎用検索を実施

- Request

```
### ユーザー汎用検索
POST http://localhost:4000/api/search-users HTTP/1.1
Content-Type: application/json
Authorization: Bearer fugafuga

 {
    "and": [{"role": "admin"}],
    "or": [{"status": 8}, {"status": 1}]
  }
```


- Response

```
[{
  "status": 1,
  "role": "admin",
  "phone_number": null,
  "organization": {
    "users": [],
    "status": 1,
    "profile_img_url": "https://hogehoge.com/prof_img.jpg",
    "phone_number": null,
    "one_line_message": "let's do this.",
    "name": "hogehoge.inc",
    "lock_version": 1,
    "id": 1,
    "hp_url": "https://hogehoge.inc",
    "back_ground_img_url": "https://hogehoge.com/ib_img.jpg",
    "addresses": []
  },
  "name": "hogehoge",
  "lock_version": 2,
  "id": 1,
  "icon_img_url": null,
  "external_user_id": null,
  "email": "hogehoge@example.com",
  "descriptions": null,
  "back_ground_img_url": null,
  "addresses": []
}]

```

#### ログアウトする

- Request

```
POST http://localhost:4000/api/sign-out HTTP/1.1
Content-Type: application/json
Authorization: Bearer fugafuga
```


- Response

```
HTTP/1.1 200 OK
server: Cowboy
date: Fri, 14 Dec 2018 08:41:12 GMT
content-length: 11
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate

{
  "ok": true
}

```

### 再度ログインせずに実行すると認証エラー

- Request

```
### ユーザー汎用検索
POST http://localhost:4000/api/search-users HTTP/1.1
Content-Type: application/json
Authorization: Bearer fugafuga

 {
    "and": [{"role": "admin"}],
    "or": [{"status": 8}, {"status": 1}]
  }
```

- Response

```
HTTP/1.1 401 Unauthorized
server: Cowboy
date: Fri, 14 Dec 2018 08:42:26 GMT
content-length: 27
cache-control: max-age=0, private, must-revalidate

{"message":"invalid_token"}
```