Auth0 パーミッション設定方法

**今回やりたいこと**
・フロントがAuth0でログインした後に取得トークンを持ってAPIにリクエストする。
　API側ではトークンの権限（パーミッション）有無を確認してOKであればユーザー情報を返すようにする。
![image1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3114591/bb42bf06-f1ef-9e57-a496-6489e824be4d.png)

以下を参考にさせて頂きました🙇
https://qiita.com/kourin1996/items/7b79d868de5c126d01d3

**Auth0とは**
・認証・認可プラットフォーム。(IDaaS(Identity as a Service))​
・ID/パスワード、シングルサインオン、MFA認証などがある。

**Auth0設定**
[フロントエンド設定]
・Auth0画面から`Applications`⇒ `Applications`⇒`Create Application`⇒`Create`を行う。
![image2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3114591/b3f209af-30a5-3d5a-1577-1a1190671062.png)
・作成後、`Allowed Callback URLs`,`Allowed Logout URLs`,`Allowd Web Origins`,`Allowd Orgins(CORS)`にフロントエンドURLをセット(`http://localhost:3000`)

[API設定]
・Auth0画面から`Applications`⇒ `APIs`⇒`Create API`⇒`Create`を行う。(`Name`⇒`API`,`Identifier`⇒`http://localhost:8000`を併せて登録)
![image3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3114591/ea933f8e-bfd6-d01b-1367-0ea563d09dfe.png)

[ユーザー設定]
・Auth0画面から`User Management` ⇒ `users`⇒`Create User`⇒`Create`を行う。(`Email`,`Password`を併せて登録)
![image4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3114591/da947b9f-58c4-3400-3343-026cce056a76.png)

[Role設定]
・Auth0画面から`Applications` ⇒ `Applications`⇒ `APIs`⇒ 対象API選択 ⇒ `Permission`でパーミッションを追加する
(ここでは仮に`read:appointment`)
![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3114591/b82f233a-9d9f-c2aa-266c-0c3ba9d7be50.png)

併せて`Settinghs`タブで`Enable RBAC`,`Add Permissons in the Access Token`をオンにする
![b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3114591/890a14c1-de1a-fa8d-0ca9-2799deebbbbe.png)

・Auth0画面から`User Management` ⇒ `Roles`⇒`Create Role`⇒`Create`を行う。(`Name`,`description`を併せて登録)
![image5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3114591/8b4efb4d-ac4d-4d4f-c77d-cd385f900f1d.png)
・作成したRoleを表示⇒`permissions`タブを選択⇒`Add Permissions`にて対象APIを選択。
![image6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3114591/b9ec0dff-4f93-d396-7e14-c1d6827c3f5e.png)
・対象Permissionを選択して登録。
![image7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3114591/d14784f4-b888-bf80-298f-54f39c31682d.png)
・`users`タブを選択⇒`Add Users`にて対象ユーザーをRoleに紐づける。
![image10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3114591/daf0f0f6-0079-3d38-323e-64eb03fbf31b.png)

Auth0の設定は以上となります。
あとは以下のAPI、フロントエンドソースの実装となります。

**API実装**
・golangによる実装

```
>mkdir authzero_server
>cd authzero_server
>go mod init authzero_sample
```

authzero_serverに以下のmain.goを作成。
※ `authZeroDomain`,`claims["sub"]`をご自身のものとしてください
```golang
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/auth0/go-jwt-middleware"
	"github.com/form3tech-oss/jwt-go"
	"github.com/pkg/errors"
	"github.com/rs/cors"
	"net/http"
	"time"
)

const (
	authZeroDomain      = "xxxxx.us.auth0.com ※ApplicationのDomain"
	getPublicKeyTimeOut = time.Second * 30
)

type JsonWebKey struct {
	Kty string   `json:"kty"`
	Kid string   `json:"kid"`
	Use string   `json:"use"`
	N   string   `json:"n"`
	E   string   `json:"e"`
	X5c []string `json:"x5c"`
}

type JsonWebKeys struct {
	Keys []JsonWebKey `json:"keys"`
}

var jwtMiddleware *jwtmiddleware.JWTMiddleware

func main() {

	// 公開鍵取得
	publicKey, err := getPublicKey()
	if err != nil {
		panic(err)
	}

	// jwtMiddleware生成
	jwtMiddleware = jwtmiddleware.New(jwtmiddleware.Options{
		ValidationKeyGetter: validateToken(publicKey),
		SigningMethod:       jwt.SigningMethodRS256,
		ErrorHandler:        func(w http.ResponseWriter, r *http.Request, err string) {},
	})

	// CORSセット
	c := cors.New(cors.Options{
		AllowedOrigins:   []string{"http://localhost:3000"},
		AllowedHeaders:   []string{"Authorization", "Content-Type"},
		AllowCredentials: true,
	})

	corsHandler := c.Handler(&Handler{})

	http.Handle("/getuser", corsHandler)

	// リッスン状態
	http.ListenAndServe(":8000", nil)
}

type Handler struct {
}

func (s Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if err := jwtMiddleware.CheckJWT(w, r); err != nil {
		http.Error(w, err.Error(), http.StatusUnauthorized)
		return
	}
	val := r.Context().Value("user")
	token, ok := val.(*jwt.Token)
	if !ok {
		http.Error(w, "error", http.StatusInternalServerError)
		return
	}
	claims, ok := token.Claims.(jwt.MapClaims)
	if !ok {
		http.Error(w, "error", http.StatusInternalServerError)
		return
	}
	if !isHavePermission(claims) {
		http.Error(w, "don't have permission", http.StatusUnauthorized)
		return
	}
	if claims["sub"] != "auth0|xxxxxxxxxxxxxxxxx ※user_idの値" {
		http.Error(w, "error", http.StatusNotFound)
		return
	}
	// ユーザー情報を返す
	w.WriteHeader(200)
	w.Write([]byte(`{"name": "テスト太郎", "age": 20}`))
	return
}

func getPublicKey() (*JsonWebKeys, error) {
	ctx := context.Background()
	ctx, cancel := context.WithTimeout(ctx, getPublicKeyTimeOut)
	defer cancel()
	req, err := http.NewRequest(
		http.MethodGet,
		fmt.Sprintf("https://%s/.well-known/jwks.json", authZeroDomain),
		nil,
	)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req.WithContext(ctx))
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer resp.Body.Close()

	out := &JsonWebKeys{}
	if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
		return nil, errors.WithStack(err)
	}
	return out, nil
}

func validateToken(publicKeys *JsonWebKeys) func(*jwt.Token) (interface{}, error) {
	return func(token *jwt.Token) (interface{}, error) {
		// 公開鍵作成
		cert, err := createCert(publicKeys, token)
		if err != nil {
			return nil, err
		}
		return jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
	}
}

func createCert(jsonWebKeys *JsonWebKeys, token *jwt.Token) (string, error) {
	cert := ""
	for _, publicKey := range jsonWebKeys.Keys {
		if token.Header["kid"] == publicKey.Kid {
			cert = "-----BEGIN CERTIFICATE-----\n" + publicKey.X5c[0] + "\n-----END CERTIFICATE-----"
			return cert, nil
		}
	}
	return "", errors.New("not found key kid")
}

func isHavePermission(
	claims jwt.MapClaims,
) bool {

	p, ok := claims["permissions"]
	if !ok {
		return false
	}
	permissions := p.([]interface{})
	var i int
	for i = 0; i < len(permissions); i++ {
		if permissions[i] == "read:appointment" {
			return true
		}
	}
	return false
}


```

**フロントエンド実装**
・reactによる実装

プロジェクト作成
```
>npx create-react-app authzero_client
>cd authzero_client
>npm install --save　@auth0/auth0-react
```

`index.js`を以下内容に更新
※ `domain`,`clientId`をご自身のものとしてください

```react
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { Auth0Provider } from "@auth0/auth0-react";

ReactDOM.render(
  <React.StrictMode>
    <Auth0Provider
      domain="xxxxx.us.auth0.com ※ApplicationのDomain"
          clientId="xxxxx ※ApplicationのClient ID"
      authorizationParams={{
        audience: "http://localhost:8000",
        redirect_uri: "http://localhost:3000",
      }}
    >
      <App />
    </Auth0Provider>
  </React.StrictMode>,
  document.getElementById("root")
);
```

`App.js`を以下内容に更新

```react
import { useState, useEffect } from "react";
import "./App.css";
import { useAuth0 } from "@auth0/auth0-react";

const API_URL = "http://localhost:8000";

const useAuth0Token = () => {
  const { isAuthenticated, user, getAccessTokenSilently } = useAuth0();
  const [accessToken, setAccessToken] = useState(null);

  useEffect(() => {
    const fetchToken = async () => {
      setAccessToken(await getAccessTokenSilently({}
      
      ));
    };

    if (isAuthenticated) {
      fetchToken();
    }
  }, [isAuthenticated, user?.sub]);

  return accessToken;
};

function App() {
  const { loginWithRedirect, isAuthenticated } = useAuth0();
  const token = useAuth0Token();
  const [me, setMe] = useState(null);
  const [error, setError] = useState(null);

  const onClickLogin = () => {
    loginWithRedirect({
      appState: {
        returnTo: "/profile",
      },
      authorizationParams: {
        prompt: "login",
      },
    });
  };

  const onClickCall = async () => {
    try {
      const res = await fetch(`${API_URL}/getuser`, {
        method: "GET",
        mode: "cors",
        headers: {
          "Authorization": `Bearer ${token}`,
          "Content-Type": "application/json",
        },
      });
      if (!res.ok) {
        throw new Error(res.statusText);
      }
      const me = await res.json();
      setError(null);
      setMe(me);
    } catch (error) {
      console.log("error", error);
      setError(error);
    }
  };

  return (
    <div className="App">
      <div style={{margin: "50px" }} >
        <button style={{margin: "10px" }} onClick={onClickLogin} disabled={isAuthenticated}>{isAuthenticated ? "logged in" : "login"}</button>
        <button style={{margin: "10px" }} onClick={onClickCall}>getUserInfomation</button>
        <p>user: {JSON.stringify(me)}</p>
        <p>{error ? error.toString() : ""}</p>
      </div>
    </div>
  );
}

export default App;
```

**動作確認**

フロントエンド起動
```
>npm run start
```

API起動
![c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3114591/e8f23eb7-9743-a47c-8541-1adceb0a44ad.png)


`login`ボタンでAuth0ログイン

![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3114591/12c1dc02-5e61-8893-a3c9-0d74973d3664.png)

ログインできるとトークンを取得できます
![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3114591/eabd8eaa-f926-55d0-27d8-6adaceca6806.png)

jwt.ioでトークンをデコードすると以下のようなイメージとなります。
![b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3114591/04e193ba-9cdf-50f9-eb87-47eb3a93156a.png)


`getUserInformation`ボタンでユーザー情報取得
![b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3114591/5026c118-6de6-260e-969a-b00ad76372d1.png)

⇒ユーザー情報取得リクエスト時、APIの`isHavePermission()`でトークンにある`permissions`を確認してアクセス不可を判定して結果を返します🎉