認証プラグイン

認証プラグインについて

認証プラグインを使用すると、サーバーのアクセス時に認証トークンが必要になり、同時に、サーバーに認可処理を実装出来るようになります。
認証トークンはクライアント側でFirebase経由で取得します。
認証プラグインを無効にすると全てのAPIが一般公開されます。
全てのAPIを一般公開するプロジェクト以外は必ず認証プラグインを有効にしてください。

認証プラグインの有効化

プラグイン設定にて、認証プラグインを有効にしてください。
認証プラグインを有効にした状態でサーバーのビルドを行うと、サーバーのアクセス時に認証トークンが必要になり、同時に、サーバーに認可処理を実装出来るようになります。

認証プラグインの使い方

認証プラグインはFirebaseを使って認証を行います。
クライアント側ではFirebaseのライブラリを用いてログインを行い、そこで得られたトークンをHeaderに設定して、そのトークンをサーバーで認証するといった流れになります。
なお、現状TemPlatではクライアント側の処理は提供していないので、独自にFirebaseのライブラリを用いて実装する必要があります。

1. クライアントでログイン処理を実装する

TemPlatではクライアント側の認証処理は提供していませんが、以下に実装の参考になるFirebaseライブラリのリンクを記載します。
Firebaseでの認証方法はWebにも多数のサンプルがありますのでそちらも合わせてご確認ください。
Webでの検索の際はUIライブラリを合わせて検索するとより効率的です。
例: “Nuxt Firebase 認証”

1. FirebaseのAuthenticationの有効化

FirebaseのConsoleにアクセスし、設定を行いたいプロジェクトを選択します。
設定はコチラから

FirebaseのConsoleにプロジェクトが表示されない場合は、『プロジェクトを追加』から設定を行いプロジェクトを選択してください。
TemPlatではGCPのプロジェクトが必ず作成されますので、プロジェクトを選択するだけで初期設定は完了します。

プロジェクトを選択したらAuthenticationを選択し、Sign-in methodから有効にしたいプロバイダーを選択すれば設定完了です。

プロバイダーによっては追加の設定が必要になることがありますので、詳細は以下をご確認ください。
Firebase Authenticationについて

2. クライアントにログイン処理を実装

2-A. Webでのログイン

WebでのログインはFirebaseUIまたは、Firebase SDK Authenticationを用いて行います。それぞれメリット、デメリットがありますので詳しくは以下のリンクをご確認ください。
FirebaseUIとFirebase SDK Authenticationの違い

FirebaseUIで実装する場合は公式の以下が参考になります。
FirebaseUIでのログインの実装方法

2-B. アプリ(React Native)でのログイン

React Nativeでのログインの場合は、@react-native-firebaseのライブラリを用いて行います。
以下の公式ページのドキュメントが充実していますのでそちらをご確認ください。
React Nativeでのログイン実装方法

3. ログインしたユーザーのTokenをHeaderに設定

ユーザーがログイン出来た場合はHeaderのAuthorizationにトークンを設定してください。
これによりTokenがサーバーに送信され、認証が可能になります。
なお、下記サンプルのtokenはログイン完了後にアプリで取得してください。

import globalAxios from 'axios'

globalAxios.defaults.headers.common = {'Authorization': `Bearer ${token}`}

(任意) 4. ログインしたユーザーをDBに登録する

ログインしたユーザーをサーバー側でも判別するために、Userテーブルに登録します。
この時、UserテーブルのIDはStringにし、FirebaseのIDを設定することをお勧めします。
例えば、以下のようなUserテーブルをERDに定義しておき、登録しておきましょう。(Userテーブルのテーブル名の変更や、Fieldの変更/追加はアプリに合わせて行ってください。)

2. サーバーで認証/認可処理を実装する

サーバー側の処理は認証と認可処理の実装を行います。
認証と認可の違いを簡単にまとめると、以下のようになります。
認証 … APIを実行した人がFirebaseからログインを行っているか確認
認可 … FirebaseでログインしたユーザーがAPIを実行する権限があるか確認
つまり、認証はFirebaseでログインしていれば誰でもOKとするのに対し、認可はFirebaseでログインしたユーザーが誰かを判別し、APIを実行する権限があるか確認するといった違いがあります。
なお、認可処理は個々のAPIに対して権限制御を行わない場合は不要になります。

2-1. 認証処理を実装する

認証プラグインを有効にすると認証処理も有効になり、追加の実装は必要ありません。
ただし、認証を必要としないAPI(一般公開するAPI)を同時に提供する場合はそちらを認証処理から除外する必要があります。
除外する認証処理はconfig.goのSkipperに実装します。

SERVER_ROOT/src/app/auth/config.go

package auth

import (
	"bytes"
	"encoding/json"
	"io/ioutil"
	"main/repository"
	"main/service"
	"net/http"
	"strconv"

	"github.com/labstack/echo/v4"
)

const IDKey = "firebase-id"

var Config = &MiddlewareConfig{
	ContextIDKey: IDKey,
	ErrorCode:    403,
	ErrorMessage: http.StatusText(403),

	// Skipper defines a function to skip verify ID token. Returning true skips verify ID token.
	Skipper: func(c echo.Context) bool {
		//if c.Path() == "/api/v1/hello-world" {
		//	return true
		//}
		return false
	},

	// AfterFunc defines a function which is executed just after the storing ID. Returning true continue process.
	AfterFunc: func(c echo.Context, rm repository.Master, sm service.Master) (bool, error) {
		//userID := c.Get(IDKey).(string)
		//user, err := rm.User.Get(c, userID)
		//if err != nil {
		//	return false, err
		//}
		//if c.Path() == "/api/v1/hello-world" && !user.Admin {
		//	return false, nil
		//}
		return true, nil
	},
}

func intID(c echo.Context) (int64, error) {
	id := c.Param("id")
	intID, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		return 0, err
	}
	return intID, nil
}

func bind(c echo.Context, i interface{}) error {
	bodyBytes, err := ioutil.ReadAll(c.Request().Body)
	if err != nil {
		return err
	}
	if err := json.Unmarshal(bodyBytes, i); err != nil {
		return err
	}
	c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
	return nil
}

SkipperはPATH等の情報から認証を必要としないAPI(一般公開するAPI)を判別して、認証が不要な場合にtrueを返します。
なお、デフォルトでは全てfalseが返るようになっているので、全て認証が必要なAPIということになっています。
例えば、”/api/v1/hello-world”のPATHのAPIを認証不要にしたい場合はSkipperを以下のように実装します。

SERVER_ROOT/src/app/auth/config.go

Skipper: func(c echo.Context) bool {
	if c.Path() == "/api/v1/hello-world" {
		return true
	}
	return false
},

こちらで認証不要APIの実装が可能になります。
PATHだけではなくMETHODでも判定出来ますので、実際のアプリではGETは全て認証不要とする、といったことも可能です。

2-2. 認可処理を実装する

認可処理はアプリ固有のものになるため、TemPlatでは枠組みのみ提供し、中身はアプリ側で実装する必要があります。
TemPlatを用いた開発では、サーバーの改修の大部分を締めるのがこの認可処理になるでしょう。
認可処理はconfig.goのAfterFuncに記載します。
なお、認可処理は認可するターゲットを減らせるため、不要なAPIの削除後に行った方が効率が良いです。
不要なAPIの削除について詳しくはコチラ

SERVER_ROOT/src/app/auth/config.go

package auth

import (
	"bytes"
	"encoding/json"
	"io/ioutil"
	"main/repository"
	"main/service"
	"net/http"
	"strconv"

	"github.com/labstack/echo/v4"
)

const IDKey = "firebase-id"

var Config = &MiddlewareConfig{
	ContextIDKey: IDKey,
	ErrorCode:    403,
	ErrorMessage: http.StatusText(403),

	// Skipper defines a function to skip verify ID token. Returning true skips verify ID token.
	Skipper: func(c echo.Context) bool {
		//if c.Path() == "/api/v1/hello-world" {
		//	return true
		//}
		return false
	},

	// AfterFunc defines a function which is executed just after the storing ID. Returning true continue process.
	AfterFunc: func(c echo.Context, rm repository.Master, sm service.Master) (bool, error) {
		//userID := c.Get(IDKey).(string)
		//user, err := rm.User.Get(c, userID)
		//if err != nil {
		//	return false, err
		//}
		//if c.Path() == "/api/v1/hello-world" && !user.Admin {
		//	return false, nil
		//}
		return true, nil
	},
}

func intID(c echo.Context) (int64, error) {
	id := c.Param("id")
	intID, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		return 0, err
	}
	return intID, nil
}

func bind(c echo.Context, i interface{}) error {
	bodyBytes, err := ioutil.ReadAll(c.Request().Body)
	if err != nil {
		return err
	}
	if err := json.Unmarshal(bodyBytes, i); err != nil {
		return err
	}
	c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
	return nil
}

AfterFuncはその名前の通り、認証を行った後の後続処理として呼ばれます。
認証を行った後なので、APIを実行したユーザーのFirebaseのIDを利用して、ユーザーが誰かを判別し、そのユーザーがAPIを実行する権限があるかどうかを確認出来ます。
ここでの戻り値の一つめのboolがtrueの場合のみ実際のAPIの処理が実行され、falseを返した場合は403エラーがクライアントに返されます。
例えば、”/api/v1/hello-world”のPATHのAPIがUserテーブルのAdminがtrueの場合のみ実行可能にしたい場合は以下のように実装します。
(なお、ここでいうUserテーブルやAdminといった概念はあくまでアプリの実装例なのでTemPlatが提供するものではありませんが、基本的にはUserテーブルのようなUserを管理するテーブルを用意するのが一般的です。)

SERVER_ROOT/src/app/auth/config.go

AfterFunc: func(c echo.Context, rm repository.Master, sm service.Master) (bool, error) {
	userID := c.Get(IDKey).(string) // FirebaseのID
	user, err := rm.User.Get(c, userID) // FirebaseのIDからUserを取得
	if err != nil {
		return false, err
	}
	if c.Path() == "/api/v1/hello-world" && !user.Admin {
		return false, nil // 認可失敗
	}
	return true, nil // 認可成功
},

簡単に説明を行うと、2行目でContextからFirebaseのIDを取得し、そのIDをもとにDBからUserを取得し、権限を確認しています。
AfterFunc内では、rm (repository.Master) で全てのrepositoryにアクセス出来るので、他テーブルとの関連を見て個々の認可処理を行うようなことも可能です。