認証プラグイン

認証プラグインを使用すると、サーバーのアクセス時に認証トークンが必要になり、同時に、サーバーに認可処理を実装出来るようになります。
また、Web及びデータ管理画面にログイン処理が実装され、認証トークンを用いた状態でのサーバー通信が可能になります。

認証の処理自体はOAuthを提供するプロバイダーにFirebase経由で行われます。
認証プラグインを利用することにより、簡単にOAuthのログインを構築することが出来ます。

認証プラグインを無効にすると全てのAPIが一般公開されます。
全てのAPIを一般公開するプロジェクト以外は必ず認証プラグインを有効にしてください。

認証プラグインの有効化

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

なお、ここで入力するFirebase 構成オブジェクトは後述するFirebase 構成オブジェクトの設定で説明します。

TemPlat Console プラグイン設定画面

認証プラグインの使い方

認証プラグインはOAuthを提供するプロバイダーにFirebase経由で認証を行います。
(一般的なIDとパスワードでの認証もFirebaseは提供しています。)
クライアント側ではFirebaseのライブラリを用いてログインを行い、そこで得られたトークンをHeaderに設定して、そのトークンをサーバーで認証及び認可を行う流れになります。

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

TemPlatでは認証プラグインを有効にすることで、Web及びデータ管理画面にFirebase経由でGoogleログインを行う処理を出力します。

以下にはFirebaseの設定手順と実装方法のリンクを記載しますが、Firebaseでの認証方法はWebにも多数のサンプルがありますので、不明点がある場合はそちらも合わせてご確認ください。
Webでの検索の際はUIライブラリを合わせて検索するとより効率的です。
例: “Nuxt Firebase 認証”, “React Native Firebase 認証”

1. FirebaseのAuthenticationの有効化

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

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

プロジェクトを選択したらAuthenticationを選択し、Sign-in methodから有効にしたいプロバイダーを選択すれば設定完了です。
なお、TemPlatが生成するWebではGoogleでのログインを実装していますので、そのままお使いいただきたい場合はこのタイミングでGoogleのログインを有効にしてください。

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

2. Firebaseにアプリケーションを登録

2-1. アプリケーションの作成

次に、ログイン処理を実装したいクライアントに合わせて、Firebaseにアプリケーションを登録します。
アプリケーションの登録は以下のボタンから行ってください。
左から、『iOS』、『Android』、『Web』を作成する場合のボタンになります。

複数のアプリケーションを登録することが可能ですが、TemPlatが生成するWebやデータ管理画面でログインを行いたい場合は『Web』の作成が必須になりますのでご注意ください。

(Webのみ)2-2. Firebase 構成オブジェクトの設定

アプリケーションの作成が完了したら、『プロジェクトの設定』ページの『全般』タブの下部にある、『SDK の設定と構成』からFirebase 構成オブジェクトをコピーしてTemPlat Consoleに入力します。
こちらの入力を行うことで、TemPlatが生成するWebのログイン処理にFirebase 構成オブジェクトが挿入されます。
なお、ラジオボタンのnpm、CDN、構成から必ず『構成』を選択した上でコピーを行ってください。

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

3-A. Webでのログイン

認証プラグインを有効にすることにより、TemPlatが生成するWeb及びデータ管理画面ではFirebaseUIを用いたログイン処理が出力されます。
Webでのログインを行う方法はFirebaseUIを用いるほかに、Firebase SDK Authenticationを用いて行う方法もあります。
それぞれメリット、デメリットがありますので詳しくは以下のリンクをご確認ください。
FirebaseUIとFirebase SDK Authenticationの違い

FirebaseUIで実装する場合はTemPlatが生成するコードの他、公式の以下も参考になります。
FirebaseUIでのログインの実装方法

3-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}`}

TemPlatが生成したWebのソースコード上ではplugins/auth.tsに当該コードが存在します。
ソースコードではログインが完了した際にaxiosにinterceptorとしてtokenの取得処理を実装しています。
これにより、アクセスを行う度にtokenの有効期限の確認が可能です。

WEB_ROOT/plugins/auth.ts

import {Context} from '@nuxt/types'
import firebase from 'firebase/app'
import globalAxios from 'axios'

export default async ({route}: Context) => {
  await new Promise((resolve) => {
    firebase.auth().onAuthStateChanged(async (user) => {
      if (user) {
        globalAxios.interceptors.request.use(async request => {
          request.headers.common = {'Authorization': `Bearer ${await user.getIdToken()}`}
          return request
        })
      }
      resolve(null)
    })
  })
}

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

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

なお、Userテーブルに登録する処理は上記ソースコードのtoken取得処理実装後、13行目に実装することが出来ます。

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にアクセス出来るので、他テーブルとの関連を見て個々の認可処理を行うようなことも可能です。

3. サーバーでアクセスしたFirebaseのIDを使用する

Application handler files (*_handler.go) でもFirebaseのIDを使用可能です。
Application handler filesでFirebaseのIDを使用することで、APIをアクセスしたユーザーに限定した情報を返すことが可能です。
こちらで紹介する例も、UserテーブルといったUserを管理するテーブルを定義してあり、そのテーブルのKeyがFirebaseのIDになっている場合のサンプルになります。

SERVER_ROOT/src/app/handler/*_handler.go

import (
	"main/auth"
	// ... 以下省略	
)

func (h *appHandler) getCurrentUser(c echo.Context) error {
	userID := c.Get(auth.IDKey).(string)
	user, err := h.rm.User.Get(c, userID)
	if err != nil {
		result := &model.Response{}
		result.Error = constant.DataBaseErrorOnSelect
		util.Logger.Error(result.Error, err)
		return c.JSON(http.StatusInternalServerError, result)
	}
	// ... 以下省略
}

実際にFirebaseのIDを取得している箇所は以下です。

	userID := c.Get(auth.IDKey).(string)

サーバー内の他の箇所でも呼び出し可能ですので、必要に応じて使用してください。