今回やりたいこと
・フロントがAuth0でログインした後に取得トークンを持ってAPIにリクエストする。
API側ではトークンの権限(パーミッション)有無を確認してOKであればユーザー情報を返すようにする。
以下を参考にさせて頂きました🙇
https://qiita.com/kourin1996/items/7b79d868de5c126d01d3
Auth0とは
・認証・認可プラットフォーム。(IDaaS(Identity as a Service))
・ID/パスワード、シングルサインオン、MFA認証などがある。
Auth0設定
[フロントエンド設定]
・Auth0画面からApplications
⇒ Applications
⇒Create Application
⇒Create
を行う。
・作成後、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
を併せて登録)
[ユーザー設定]
・Auth0画面からUser Management
⇒ users
⇒Create User
⇒Create
を行う。(Email
,Password
を併せて登録)
[Role設定]
・Auth0画面からApplications
⇒ Applications
⇒ APIs
⇒ 対象API選択 ⇒ Permission
でパーミッションを追加する
(ここでは仮にread:appointment
)
併せてSettinghs
タブでEnable RBAC
,Add Permissons in the Access Token
をオンにする
・Auth0画面からUser Management
⇒ Roles
⇒Create Role
⇒Create
を行う。(Name
,description
を併せて登録)
・作成したRoleを表示⇒permissions
タブを選択⇒Add Permissions
にて対象APIを選択。
・対象Permissionを選択して登録。
・users
タブを選択⇒Add Users
にて対象ユーザーをRoleに紐づける。
Auth0の設定は以上となります。
あとは以下のAPI、フロントエンドソースの実装となります。
API実装
・golangによる実装
>mkdir authzero_server
>cd authzero_server
>go mod init authzero_sample
authzero_serverに以下のmain.goを作成。
※ authZeroDomain
,claims["sub"]
をご自身のものとしてください
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
をご自身のものとしてください
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
を以下内容に更新
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
login
ボタンでAuth0ログイン
jwt.ioでトークンをデコードすると以下のようなイメージとなります。
getUserInformation
ボタンでユーザー情報取得
⇒ユーザー情報取得リクエスト時、APIのisHavePermission()
でトークンにあるpermissions
を確認してアクセス不可を判定して結果を返します🎉