SSOサービスKeycloakとgolangのHTTPサーバを連携する

# TL;DR

* golangでWebアプリケーションを開発するにあたり，SAML認証を組み込む方法を紹介します
    * Keycloakの機能を利用します
    * ```net/http```で作るWebアプリケーションに，認証を付けるサンプルを紹介します
* [crewjam/saml](https://github.com/crewjam/saml)ライブラリを使います
    * [Getting Started as a Service Provider](https://github.com/crewjam/saml#getting-started-as-a-service-provider)のプログラムを参考に進めます
    * 最も単純な構造のWebアプリケーションを実装します

# 準備

## 環境想定

* Webアプリケーションは，以下のような，超シンプルなものをつくります
    * URLにリクエストを発行すると，ログインが求められます
    * ログインするとユーザ名が表示されます
* テスト環境なので，Webアプリケーションはローカルホストで稼働させます
    * keycloakへ認証の問い合わせを行うのでネットワークに接続されている必要があります

## 必要環境

* go 1.12.5

```bash
go version
go version go1.12.5 darwin/amd64
```

* [crewjam/saml](https://github.com/crewjam/saml)

```bash
go get github.com/crewjam/saml/samlsp
```

* Keyclockサーバ
    * 各種サービスと連携するSSOサービスです
    * 「[OSSなシングルサインオンサービスKeycloakをdockerで立ち上げる](https://qiita.com/myoshimi/items/7e9f1de7373427233880)」で立ち上げ方法を紹介しています
    * [gitlab](https://qiita.com/myoshimi/items/37c90c2a63dca295edb4)および[growi](https://qiita.com/myoshimi/items/f26cf3f179602a12a5ac)との連携方法の記事もあるので，参考にしてください

* バージョンは多少前後しても動くと思います

# Keycloak設定

* Keycloakに管理者権限でログインし，連携したいRealmを選択します
* "Clients"から"Create"を選択します
    * ClientIDを```http://localhost:8000/saml/metadata```のように，サービスを稼働させるURLを設定します
    * Client Protocolを"saml"でClientを作成します．
    * Clientの各タブのうち，サンプルの段階で設定を要するのは以下のとおりです

## Settingsタブ

| フィールド名              | 値                                                                   |
| ------------------------- | ---------------------------- |
| Client Signature Required | OFF       |
| Root URL                  | ```http://localhost:8000``` |

## Mappersタブ

* usernameだけ設定します
    * usernameをマップします

| Name     | Mapper Type | Property | Friendly Name | SAML Attribute Name | SAML Attribute NameFormat |
| -------- | ----------- | -------- | ------------- | ------------------- | ------------------------- |
| username | username    | username |               | username            | Basic                     |


## SAML Keysタブ：x.509証明書の取得

* ClientのSAML Keysタブから，Signing Keyを2つのファイルにコピーします
    * ```Private Key``` : myservice.key
    * ```Certificate``` : myservice.cert
    * キーが生成されていなければ，"Generate new keys"ボタンをクリックして生成します
        * ![00_samlkeys.jpg](/attachment/5ce7687bc45bcc003916c4f8)

* 各ファイルには，下記のように接頭行，末尾行を追加します
    * myservice.key
    * ```
      -----BEGIN PRIVATE KEY-----
      MII.....<snip>.....
      -----END PRIVATE KEY-----
      ```
    * myservice.cert
    * ```
      -----BEGIN CERTIFICATE-----
      MII.....<snip>.....
      -----END CERTIFICATE-----
      ```

# プログラム

* [GitHub crewjam/saml](https://github.com/crewjam/saml)の[Getting Started as a Service Provider](https://github.com/crewjam/saml#getting-started-as-a-service-provider)のプログラムを参考に進めます
* 以下のようなプログラム```main.go```を作成します
    * ```main```メソッドの最初にある5行で，SAML設定に要する文字列を設定しています
    * ```confIDPMetadataURL```: メタデータのURIを記載します．Keycloakの場合は，Keycloakのドメインに，```realms/<Realm>/protocol/saml/descriptor```のパスを加えたものです
        * 下記コードでは例としてKeycloak(```https://sso.eample.com```), Realm(```dev```)の場合を記載しています
        * ```https://sso.example.com/auth/realms/dev/protocol/saml/descriptor```
    * ```x509cert```，```x509key```: 前項で作った証明書ファイルです．main.goと同じディレクトリに配置して，プログラムで読み込みます
    * ```confRootURL```，```confPort```: Webアプリケーションの設定です．KeycloakのClientIDにはconfRootURLと同じ文字列が設定されている必要があります．

```go
//main.go
package main

import (
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"net/http"
	"net/url"

	"github.com/crewjam/saml/samlsp"
)

func hello(w http.ResponseWriter, r *http.Request) {
	fmt.Printf("%+v", samlsp.Token(r.Context()))
	fmt.Fprintf(w, "Hello, %s!", samlsp.Token(r.Context()).Attributes.Get("username"))
}

func main() {
	confIDPMetadataURL := "https://sso.example.com/auth/realms/dev/protocol/saml/descriptor"
	x509cert    := "myservice.cert"
	x509key     := "myservice.key"
	confRootURL := "http://localhost:8000"
	confPort    := ":8000"

	keyPair, err := tls.LoadX509KeyPair(x509cert, x509key)
	if err != nil {
		panic(err) // TODO handle error
	}
	keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
	if err != nil {
		panic(err) // TODO handle error
	}

	idpMetadataURL, err := url.Parse(confIDPMetadataURI)
	if err != nil {
		panic(err) // TODO handle error
	}

	rootURL, err := url.Parse(confRootURL)
	if err != nil {
		panic(err) // TODO handle error
	}

	samlSP, _ := samlsp.New(samlsp.Options{
		URL:            *rootURL,
		Key:            keyPair.PrivateKey.(*rsa.PrivateKey),
		Certificate:    keyPair.Leaf,
		IDPMetadataURL: idpMetadataURL,
	})
	app := http.HandlerFunc(hello)
	http.Handle("/hello", samlSP.RequireAccount(app))
	http.Handle("/saml/", samlSP)
	http.ListenAndServe(confPort, nil)
}
```

# 動作確認

## Webアプリの実行

* 作成したmain.go, myservice.key, myservice.confを同じディレクトリに置いて，```go run```コマンドで実行します
    * ```
      go run main.go
      ```
    
## ブラウザで確認
    
* Webアプリケーション```http://localhost:8000/hello```にアクセスすると，Keycloakのログイン画面に遷移します
* Realmに登録されたユーザでログインすると，Webアプリケーションの画面に戻り，ユーザ名を使用した表示が行われます
    * ![01_webapp.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309617/0c353bdc-f7ed-1540-47bd-0c09a9c9ba7f.jpeg)

# 参考

* [GitHub crewjam/saml](https://github.com/crewjam/saml)
* [OSSなシングルサインオンサービスKeycloakをdockerで立ち上げる](https://qiita.com/myoshimi/items/7e9f1de7373427233880)
* [シングルサインオンサービスKeycloakとGitLabを連携する](https://qiita.com/myoshimi/items/37c90c2a63dca295edb4)
* [シングルサインオンサービスKeycloakとWikiシステムGrowiを連携する](https://qiita.com/myoshimi/items/f26cf3f179602a12a5ac)