LoginSignup
15
3

【人柱】Elixir PhoenixでSAML認証がでる事を確認したよ

Posted at

はじめに

Pythonを使ってSAML認証の検証を行機会があり、Pythonで動かせたので、SAMLの勉強がてら、Elixirでも試してみることにしました。
samlyを試してみます

簡単にできるかと思ったんですが、認証エラーの原因が特定に結構苦労しました。
最終的には動作させる事ができたので、ハマりポイントも含めて、結果を紹介します。

Phoenix初期状態

$ mix phx.new hello_saml --database sqlite3
$ cd hello_saml
$ mix ecto.create
$ mix phx.server

image.png

動作することを確認しておきます。sqlite3にしてるのは、私の好みです。DBはなくても問題ないとおもいます。

Samlyの組み込み

ドキュメントのReadmeに従って組み込みます。

/mix.exs
      {:samly, "~> 1.3.0"},
/lib/hello_saml/application.ex
  def start(_type, _args) do
    children = [
     # ...
      {Samly.Provider, []},
    ]
/lib/hello_saml_web/router.ex
  scope "/sso" do
    forward "/", Samly.Router
  end

same_site:の記述をコメントアウトしないとcookieによるセッションの保存が行われずうまくいきませんでした。はまりポイントの一つでした。
samlyのソースにIO.inspect仕込んでセッションの情報が無いことがわかって、解決したんですが、cookieの設定だったとは。

/lib/hello_saml_web/endpoint.ex
  @session_options [
    store: :cookie,
    key: "_hello_saml_key",
    signing_salt: "6MKq1HMr"
    # same_site: "Lax"
  ]

証明書の作成

IDpとの通信で使用する証明書作成

$ mix phx.gen.cert --output priv/cert/samly_sp sp.samly

ブラウザーとの通信で使用する証明書作成

$ mix phx.gen.cert --output priv/cert/hello_saml hello.saml

Httpsのエンドポイント作成

/config/config.exs
config :hello_saml, HelloSamlWeb.Endpoint,
  http: [port: 4000],
  https: [
    port: 4001,
    cipher_suite: :strong,
    certfile: "priv/cert/hello_saml.pem",
    keyfile: "priv/cert/hello_saml_key.pem"
  ]

Samlyのconfigを追加

最低限の設定をしてみました。
base_urlは、設定しないと、IdPで認証が成功し、リダイレクトで戻ってくる段階でエラーが発生します。base_urlの問題とわかるようなエラーメッセージではなかったので、ハマりました。

/config/config.exs
config :samly, Samly.State,
  store: Samly.State.Session,
  opts: [key: :my_assertion_key]

config :samly, Samly.Provider,
  idp_id_from: :path_segment,
  service_providers: [
    %{
      id: "sp1",
      entity_id: "urn:samly.howto:helo_saml",
      certfile: "priv/cert/samly_sp.pem",
      keyfile: "priv/cert/samly_sp_key.pem",
    }
  ],
  identity_providers: [
    %{
      id: "idp1",
      sp_id: "sp1",
      base_url: "https://hello.saml:4001/sso",
      metadata_file: "metadata.xml",
    }
  ]

ログイン画面を作成

samly_howtoのログイン画面を使いました。Phoenix 1.7だと、heexの記述がエラーになる部分があったので直してます。

/lib/hello_saml_web/controllers/page_html/home.html.heex
<section class="row">
  <div class="column">
    <%= if @uid do %>
      <a class="button" style="width: 100%;"
        href={URI.encode(@signout_uri)}>Sign out</a>
    <% else %>
      <a class="button" style="width: 100%;"
        href={URI.encode(@signin_uri)}>Sign in</a>
    <% end %>
  </div>
  <div class="column">
    <a class="button button-outline" style="width: 100%;"
        href={URI.encode(@metadata_uri)}>SP Metadata</a>
  </div>
</section>
<section class="row">
  <div class="column">
    <div class="signin-status">
      <%= if @uid do %>
        <strong>Signed in as <%= Plug.HTML.html_escape(@uid) %></strong>
      <% else %>
        Not signed in
      <% end %>
      <%= if @idp_id do %>
        <small>(IdP: <%= Plug.HTML.html_escape(@idp_id) %>)</small>
      <% end %>
    </div>
  </div>
</section>


<%= if @attributes do %>
  <strong>Assertions in SAML Response</strong>
  <table class="table attribute-table">
    <thead>
      <tr><th>Attribute</th><th>Value</th></tr>
    </thead>
    <tbody>
      <%= for {k, v} <- @attributes do %>
        <tr>
          <td><%= Plug.HTML.html_escape(k) %></td>
          <td><%= Plug.HTML.html_escape([v] |> List.flatten() |> Enum.join(", ")) %></td>
        </tr>
      <% end %>
    </tbody>
  </table>
<% end %>

<%= if @computed do %>
  <strong>Computed Attributes</strong>
  <table class="table attribute-table">
    <thead>
      <tr><th>Attribute</th><th>Value</th></tr>
    </thead>
    <tbody>
      <%= for {k, v} <- @computed do %>
        <tr>
          <td><%= Plug.HTML.html_escape(k) %></td>
          <td><%= Plug.HTML.html_escape([v] |> List.flatten() |> Enum.join(", ")) %></td>
        </tr>
      <% end %>
    </tbody>
  </table>
<% end %>

ログイン画面のコントローラです。
ところどころにIO.inspect入れてます。

/home/masa/phenix/hello_saml/lib/hello_saml_web/controllers/page_controller.ex
defmodule HelloSamlWeb.PageController do
  use HelloSamlWeb, :controller

  alias Samly.Assertion

  def home(conn, params) do
    assertion = Samly.get_active_assertion(conn)
    IO.inspect(assertion, label: "assertion")

    opts = Application.get_env(:samly, Samly.Provider, [])
    idp_in_uri_type = Keyword.get(opts, :idp_id_from, :path_segment)
    IO.inspect(opts, label: "opts")

    idp_id =
      case {idp_in_uri_type, get_assertion_idp_id(assertion)} do
        {_, idp_id} when idp_id != nil -> idp_id
        {:path_segment, _} -> Map.get(params, "idp", "idp1")
        _ -> nil
      end
    IO.inspect(idp_id, label: "idp_id")

    demo_target_qp = %{a: "value one", b: "value two"}

    demo_target_qp =
      case idp_in_uri_type do
        :path_segment -> Map.put(demo_target_qp, :idp_id, idp_id)
        :subdomain -> demo_target_qp
      end

    demo_target_uri = URI.to_string(%URI{path: "/", query: URI.encode_query(demo_target_qp)})

    IO.inspect(demo_target_uri, label: "target_url")

    signin_uri_with_target =
      URI.to_string(%URI{
        path: get_signin_uri(idp_id, idp_in_uri_type),
        query: URI.encode_query(%{target_url: demo_target_uri})
      })

    signout_uri_with_target =
      URI.to_string(%URI{
        path: get_signout_uri(idp_id, idp_in_uri_type),
        query: URI.encode_query(%{target_url: demo_target_uri})
      })

    assigns = %{
      idp_id: idp_id,
      uid: Samly.get_attribute(assertion, "uid"),
      attributes: get_assertion_attributes(assertion),
      computed: get_assertion_computed_attributes(assertion),
      metadata_uri: get_metadata_uri(idp_id, idp_in_uri_type),
      signin_uri: signin_uri_with_target,
      signout_uri: signout_uri_with_target
    }

    render(conn, :home, assigns)
  end

  defp get_assertion_idp_id(%Assertion{idp_id: idp_id}), do: idp_id
  defp get_assertion_idp_id(nil), do: nil

  defp get_assertion_attributes(%Assertion{attributes: attributes}), do: attributes
  defp get_assertion_attributes(nil), do: nil

  defp get_assertion_computed_attributes(%Assertion{computed: computed}), do: computed
  defp get_assertion_computed_attributes(nil), do: nil

  defp get_metadata_uri(_idp_id, :subdomain), do: "/sso/sp/metadata"
  defp get_metadata_uri(idp_id, :path_segment), do: "/sso/sp/metadata/#{idp_id}"

  defp get_signin_uri(_idp_id, :subdomain), do: "/sso/auth/signin"
  defp get_signin_uri(idp_id, :path_segment), do: "/sso/auth/signin/#{idp_id}"

  defp get_signout_uri(_idp_id, :subdomain), do: "/sso/auth/signout"
  defp get_signout_uri(idp_id, :path_segment), do: "/sso/auth/signout/#{idp_id}"
end

hostsファイルの変更

IdPとSPが直接通信することはないので、SPはインターネットに公開されている必要もありません。
名前でアクセスできる必要もたぶんなくて、localhostのままでも大丈夫だとおもいますが、samlyのサンプルでhostsファイルで127.0.0.1に名前を付けて、名前で参照する方法がとられていたので真似してみます。
Windowsを使っているので、PowerToysのホストファイルエディターを使って hello.saml を登録しました。

image.png

これでhttps://hello.saml:4001/でブラウザーで開けるようになります。

連携するIdPの用意

samly hotwoでは、ローカルでIdPを動作させる方法が紹介されています。
これでもよいと思いますが、IdPの無料トライアルで試してみました。
GMO Trust
okta

結論から書くと、GMO TrustはSamlyで試したところ、エラーとなりうまくいきませんでした。なにか私の設定問題かもしれませんが、解決できててません。
samlyのドキュメントで動作確認されているoktaでは、動作しました。

oktaの設定

GMO TrustでIdP+Python-samlで設定した経験があったので、oktaの設定できましたが、慣れないと難しいと思いました。
設定の詳細は省略しますが、スクリーンショットの様に設定すれば連携できると思います。

Single Sign On URLが、IdPで認証を行った後に、SPに制御が戻ってくるURLになります。
https://hello.saml:4001/sso/sp/consume/[idpのID]を指定して、認証結果をsamlyが受け取るようにします。
このURLは、samlyのドキュメントのSAML Assertion Consumer ServiceのURLに説明があります。

okta のアプリケーション設定

image.png

oktaの管理画面で、metadataのxmlファイルをダウンロードして、metadate.xmlとして保存します。
configのidentity_providersのmetadata_fileにこのファイルが指定してあるので、idp1として利用できるようになります。

  identity_providers: [
    %{
      id: "idp1",
      sp_id: "sp1",
      base_url: "https://hello.saml:4001/sso",
      metadata_file: "metadata.xml",
    }
  ]

動作確認

トップページを開くと作成したページが表示されます。

image.png

Sign in をクリックするとIdPの認証画面が表示されるのでログインします。

image.png

ログイン後、Signed in asの後にログインしたユーザのuidが表示されています。
uid以外のAttributeもokta側で設定しているものが取得できている事がわかります。

image.png

【余談】ブラウザーとの通信をhttpsにする必要があるか?

httpのサイトでSAML認証を行うと、iDpで認証後acsに戻ってくるときに、送信してよいかを尋ねるダイアログが表示されます。動作を確認するだけなら、httpのサイトでも大丈夫でした。

image.png

みかけた不具合

  • Sign outのボタンを押すと、esaml内でエラーが発生します
  • GMO Trustでは動作しませんでした
  • samlyのgithubのissueも確認したほうがいいかも

まとめ

  • サンプルのsamly-howtoはバージョンが古くPhonix1.7ではエラーになる部分があるので注意。基本的な部分に違いはない
  • ドキュメントもしっかり書かれていて、使い方も理解できれば、分かりやすい。
  • 実際のIdP(okta)で動作させてみて、トライandエラーの末、認証できた。
  • 認証できなかったIdPもあり、いろいろ試してみる必要がありそう。
  • 最近メンテナンスされていないのが課題
15
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
3