Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
Upcoming SlideShare
What to Upload to SlideShare
Next
Download to read offline and view in fullscreen.

1

Share

React(TypeScript) + Go + Auth0 で実現する管理画面

Download to read offline

20210128 エムスリー BIR勉強会資料
https://m3-engineer.connpass.com/event/200495/

Related Books

Free with a 30 day trial from Scribd

See all

Related Audiobooks

Free with a 30 day trial from Scribd

See all

React(TypeScript) + Go + Auth0 で実現する管理画面

  1. 1. React(TypeScript) + Go + Auth0 で実現する管理画面 Kenta Endoh. M3, Inc. 2020/01/28
  2. 2. About me 遠藤健太 @en_ken 好きなこと: サウナ・銭湯、ラジオ、お酒 好きな言語: TypeScript - 2020年1月にエムスリー(BIR)入社 - 前職はメーカー開発者 - 組み込み〜Windowsアプリ〜業務支援系
  3. 3. 背景 一般論として - 認証・認可機能を自前で実装するのは負担大 - 実装ミスはセキュリティリスクに直結 - 保守コスト(security update)の負担 BIRの方針として - 実現したいアンケートごとにマイクロサービスとして アンケートシステムを構築していく方針 → 各サービスごとに管理画面の認証・認可機能が必要 3 認証・認可の実現は外部サービスに移譲して 必要な機能実現にエンジニアリソースを集中したい
  4. 4. Auth0 https://auth0.com Webアプリやモバイルなどに対して認証・認可 機能をクラウドで提供するIDaaS(IDentity as a Service)ベンダー - ソーシャルアカウント連携が豊富 - 2021/01/22時点で50以上 - 標準的なSSO連携技術をサポート - OIDC, SAML, AD etc… - 多要素認証(MFA)可能 - 導入検討時点では、 Firebase Authは未サポート 4
  5. 5. なんでこの発表するの? 公式ドキュメントは充実 - https://auth0.com/docs - 言語ごとの実装例も割とあるので実装自体はどうにかなる 一方で、以下の要因でスムーズに導入するは難しい - Auth0のモデルの理解不足 - 豊富な設定項目から必要な設定のみを行うのが困難 - 英語... 5 Auth0のモデルの概要や最低限必要な設定項目を 中心に整理・紹介します
  6. 6. この発表で話すこと - シンプルな管理画面(SPA) ユースケースでの認可実現方法 - Auth0の基本的な設定 - 実装の紹介 この発表で話さないこと - Auth0が使っている技術の説明 - Json Web Token(JWT), etc... - 複雑な認証・認可ユースケース での実現方法 - 多要素認証, etc... 6 2.API呼び出し (+トークン) 1.認証 トークン 公開鍵取得 3.検証(認可) App Auth0
  7. 7. Auth0のトークン Auth0の発行するトークンには大きく2種類ある IDトークン(JWT) Auth0ではあくまで ユーザー属性情報提供のためのデータという位置づけ (JWT形式の意味はない気がする...) アクセストークン(JWT) 検証することで、認可(特定の条件において、 リソースへのアクセス権限を付与すること )の判断ができる → API呼び出しに利用するトークンはこちら 7 2.API呼び出し (+トークン) 1.認証 トークン 公開鍵取得 3.検証(認可) App Auth0 ※ ライブラリ(auth0-reactなど)によっては そもそも生のIDトークンは取れない ※ アクセストークンはaudienceを指定して リクエストしないとJWT形式にならない
  8. 8. やらなければいけないこと Auth0の設定 1. Applicationの作成 2. APIの作成 3. Permissionの追加 4. Roleの作成 5. Roleの付与 フロントエンド(React)の実装 1. 認証していなければ Auth0のログインページへリダイレクト 2. API呼び出し時にアクセストークンを取得し Authorizationヘッダに設定 サーバーサイド(Go)の実装 1. Auth0テナント公開鍵の取得 2. 公開鍵によるJWTの検証 3. アプリケーションごとの検証 8 2.API呼び出し (+トークン) 1.認証 トークン 公開鍵取得 3.検証(認可) App Auth0
  9. 9. やらなければいけないこと Auth0の設定 1. Applicationの作成 2. APIの作成 3. Permissionの追加 4. Roleの作成 5. Roleの付与 フロントエンド(React)の実装 1. 認証していなければ Auth0のログインページへリダイレクト 2. API呼び出し時にアクセストークンを取得し Authorizationヘッダに設定 サーバーサイド(Go)の実装 1. Auth0テナント公開鍵の取得 2. 公開鍵によるJWTの検証 3. アプリケーションごとの検証 9 2.API呼び出し (+トークン) 1.認証 トークン 公開鍵取得 3.検証(認可) App Auth0
  10. 10. Tenant Auth0のモデル概要 10 Application API (= audience) Permission (=scope) Role User 利用する アプリケーションを識 別する概念 どのAPIに対し どのPermissionを 持っているかを定義 認可を必要とする APIのまとまり 必要な権限
  11. 11. Tenant Auth0のモデル概要 11 Application API (= audience) Permission (=scope) Role User 1.Userがaudience,scopeを指定して アクセストークン要求 2. Userに紐づくRoleを取得 3. 指定されたAPI(audience)に対する 指定されたPermission(scope)を Roleが保有しているかを検証し、 保有していたらアクセストークンを発行
  12. 12. Auth0の設定 1. Applicationの作成 - Single Page Applicationとして作成 - Application URLsの各項目に対象 アプリケーションのURLを設定 12 ※2020/01/22時点での設定画面・デフォルト設定をもとにして います
  13. 13. Auth0の設定 2. APIの作成 APIs -> Settings - (必要なら)アクセストークンの 有効期限変更 - デフォルトは24h - RBAC(Role Based Access Control)を有 効 - これをONにしないとユーザーのRole に基づいて認可されない 13 ※2020/01/22時点での設定画面・デフォルト設定をもとにして います
  14. 14. Auth0の設定 3. Permissionの追加 API -> Permissions - APIが利用するPermissionの追 加 14 ※2020/01/22時点での設定画面・デフォルト設定をもとにして います
  15. 15. Auth0の設定 4. Roleの作成 - API x Permissionを追加 15 ※2020/01/22時点での設定画面・デフォルト設定をもとにして います
  16. 16. Auth0の設定 5. Roleの付与 - Roleの付与方法はユースケース次第なので詳細は割愛 - 例えば... - ユーザーが多くない → 申請ベースで手動で付与 - 特定条件でデフォルトで付与すべき Role → Ruleでサインアップイベントをフックして付与 - 権限付与が複雑 → 権限管理画面を作って、Management APIで付与 16
  17. 17. Auth0の設定 Auth0の設定はコード管理可能 - auth0-deploy-cli https://auth0.com/docs/extensions/deploy-cli-tool テナントの設定のimport/export可能 - Terraformでもサポート https://registry.terraform.io/providers/alexkappa/auth0/latest/docs 17
  18. 18. やらなければいけないこと Auth0の設定 1. Applicationの作成 2. APIの作成 3. Permissionの追加 4. Roleの作成 5. Roleの付与 フロントエンド(React)の実装 1. 認証していなければ Auth0のログインページへリダイレクト 2. API呼び出し時にアクセストークンを取得し Authorizationヘッダに設定 サーバーサイド(Go)の実装 1. Auth0テナント公開鍵の取得 2. 公開鍵によるJWTの検証 3. アプリケーションごとの検証 18 2.API呼び出し (+トークン) 1.認証 トークン 公開鍵取得 3.検証(認可) App Auth0
  19. 19. Reactの実装 - Auth0の公式ライブラリは色々ある - https://auth0.com/docs/libraries - 使用するライブラリは@auth0/auth0-react - SPA向けの@auth0/auth0-spa-jsを更にラップしてhooks化した もの 19
  20. 20. Reactの実装 import { Auth0Provider } from "@auth0/auth0-react"; // ... render( <Auth0Provider domain={auth0Config.domain} clientId={auth0Config.clientId} audience={auth0Config.audience} scope={auth0Config.scope} redirectUri={window.location.origin} > <DimmingLoaderProvider> <AuthController> <App /> </AuthController> </DimmingLoaderProvider> </Auth0Provider>, document.body.querySelector("#root") ); 20 Auth0Providerの設定 - domain, clientId Applications -> Settings -> Basic Information に記載 - audience APIの識別子 - scope APIの要求するPermission - redirectUri 認証後にリダイレクトするURL → 現在のURL @auth0/auth0-react: 1.2.0
  21. 21. Reactの実装 21 import { useAuth0 } from "@auth0/auth0-react"; // ... const AuthController: React.FC = ({ children }) => { const loader = useLoader(); const { isAuthenticated, isLoading, error, loginWithRedirect, logout, getAccessTokenSilently, } = useAuth0(); useEffect(() => { loader.displayLoader(isLoading); }, [loader, isLoading]); useEffect(() => { configureAuthFunc({ getAccessTokenSilently, logout }); }, [getAccessTokenSilently, logout]); if (isLoading) { return null; } if (!isAuthenticated) { loginWithRedirect(); return null; } if (error) { return <エラー表示 />; } return {children}; }; 認証関連処理の実装 - isLoading = falseになるまで 待機 - isAuthenticated = falseなら ログイン画面にリダイレクト - isAuthenticated = trueなら 画面を表示 1. 認証していなければ Auth0のログインページへ リダイレクト API呼び出しで使えるように 設定 @auth0/auth0-react: 1.2.0
  22. 22. Reactの実装 22 export const fetchWithAuthz = async ( apiPath: string, apiConfig: RequestInit ) => { try { const token = await getAccessTokenSilently(); apiConfig.headers = { ...apiConfig.headers, Authorization: `Bearer ${token}`, }; } catch (e) { return new Failure(e, "auth error"); } let resp: Response; try { resp = await fetch(apiPath, apiConfig); } catch (e) { return new Failure(e, "unknown error"); } // … } API呼び出しの実装例 - getAccessTokenSilently()は呼び 出し時に必ず呼ぶ - トークンのキャッシュなどは内 部でやってくれる 2. API呼び出し時にアクセス トークンを取得し、 Authorizationヘッダに設定 @auth0/auth0-react: 1.2.0
  23. 23. やらなければいけないこと Auth0の設定 1. Applicationの作成 2. APIの作成 3. Permissionの追加 4. Roleの作成 5. Roleの付与 フロントエンド(React)の実装 1. 認証していなければ Auth0のログインページへリダイレクト 2. API呼び出し時にアクセストークンを取得し Authorizationヘッダに設定 サーバーサイド(Go)の実装 1. Auth0テナント公開鍵の取得 2. 公開鍵によるJWTの検証 3. アプリケーションごとの検証 23 2.API呼び出し (+トークン) 1.認証 トークン 公開鍵取得 3.検証(認可) App Auth0
  24. 24. Goの実装 サーバーサイドの実装でやること 1. Auth0テナント公開鍵の取得 2. 公開鍵によるJWTの検証 3. アプリケーションごとの検証 - issuerがAuth0テナントのドメインURLであること(不要かも?) - audienceに要求するAPI識別子があること - scopeに要求するPermissionがあること ミドルウェアとして実現 - アプリケーションによらず処理が共通なので再利用可能 24
  25. 25. Goの実装 ルーティング層 r := chi.NewRouter() //... r.Route("/admin", func(r chi.Router) { //... r.Route("/api", func(r chi.Router) { r.Use(auth0util.Auth0Middleware(appConfig.Auth0Config)) //各APIのルーティングの記載 }) }) //... 25 以下を参考に実装 Auth0 Docs Go Authorization Create a middleware to validate Access Tokens https://auth0.com/docs/quickstart/backend/go lang/01-authorization#validate-access-tokens github.com/auth0/go-jwt-middleware v1.0.0 github.com/form3tech-oss/jwt-go v3.2.2 github.com/go-chi/chi v4.1.2+incompatible
  26. 26. Goの実装 Middleware実装① - go-jwt-middlewareは auth0のライブラリだが、汎 用的 → それなりに実装が必要 26 公開鍵取得 URL import ( //(省略) jwtmiddleware "github.com/auth0/go-jwt-middleware" "github.com/form3tech-oss/jwt-go" ) const ContextTokenKey = "Token" func Auth0Middleware(auth0Config *config.Auth0Config) func(next http.Handler) http.Handler { middleware := jwtmiddleware.New(jwtmiddleware.Options{ ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) { cert, err := getPemCert(auth0Config.PemCertURL, token) if err != nil { return nil, errors.Wrap(err, "公開鍵の取得") } result, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert)) if err != nil { return nil, errors.Wrap(err, "公開鍵のパース") } return result, nil }, UserProperty: ContextTokenKey, SigningMethod: jwt.SigningMethodRS256, }) return func(next http.Handler) http.Handler { /* 検証処理(後述)*/ } } 検証時、Contextに このKeyでトークン情報が保存される 公開鍵取得 URL 以下を参考に実装 Auth0 Docs Go Authorization Create a middleware to validate Access Tokens https://auth0.com/docs/quickstart/backend/go lang/01-authorization#validate-access-tokens github.com/auth0/go-jwt-middleware v1.0.0 github.com/form3tech-oss/jwt-go v3.2.2 github.com/go-chi/chi v4.1.2+incompatible 1. Auth0テナント 公開鍵の取得
  27. 27. Goの実装 27 func getPemCert(pemURL string, token *jwt.Token) (string, error) { resp, err := http.Get(pemURL) if err != nil { return "", err } defer resp.Body.Close() var jwks = Jwks{} if err = json.NewDecoder(resp.Body).Decode(&jwks); err != nil { return "", err } cert := "" for k := range jwks.Keys { if token.Header["kid"] == jwks.Keys[k].Kid { cert = "-----BEGIN CERTIFICATE-----n" + jwks.Keys[k].X5c[0] + "n-----END CERTIFICATE-----" } } if cert == "" { return "", errors.New("鍵が見つかりません") } return cert, nil } Middleware実装② - 「1.Auth0テナント公開鍵」の 取得の中身 以下を参考に実装 Auth0 Docs Go Authorization Create a middleware to validate Access Tokens https://auth0.com/docs/quickstart/backend/go lang/01-authorization#validate-access-tokens github.com/auth0/go-jwt-middleware v1.0.0 github.com/form3tech-oss/jwt-go v3.2.2 github.com/go-chi/chi v4.1.2+incompatible
  28. 28. Goの実装 公開鍵取得URLは Applications -> Settings -> Advanced Settings -> Endpoints -> JSON Web Key Set 28 ※2020/01/22時点での設定画面・デフォルト設定をもとにして います
  29. 29. Goの実装 29 return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := middleware.CheckJWT(w, r); err != nil { w.WriteHeader(http.StatusForbidden) return } token := r.Context().Value(ContextTokenKey) claims := token.(*jwt.Token).Claims.(jwt.MapClaims) if !verifyAccessTokenClaims( r.Context(), claims, auth0Config.Issuer, auth0Config.Audience, auth0Config.Scope) { w.WriteHeader(http.StatusForbidden) return } next.ServeHTTP(w, r) }) } } Auth0のテナントドメイン URL(Issuer) 要求するAPI(Audience) 要求するPermission(Scope) UserPropertyで指定した Keyでトークン情報取得 2. 公開鍵による JWTの検証 Middleware実装③ - 検証処理 github.com/auth0/go-jwt-middleware v1.0.0 github.com/form3tech-oss/jwt-go v3.2.2 github.com/go-chi/chi v4.1.2+incompatible 以下を参考に実装 Auth0 Docs Go Authorization Create a middleware to validate Access Tokens https://auth0.com/docs/quickstart/backend/go lang/01-authorization#validate-access-tokens 3. アプリケーション ごとの検証
  30. 30. Goの実装 30 Middleware実装④ - 「3.クレームの検証」の 中身 func verifyAccessTokenClaims(ctx context.Context, claims jwt.MapClaims, expIss, expAud, expScope string) bool { logger := logging.GetLogger(ctx) if !claims.VerifyIssuer(expIss, true) { logger.Warn("Issuerが%sではありません", expIss) return false } audience, ok := claims["aud"].([]interface{}) if !ok { logger.Warn("audクレームがありません") return false } verifyAudience := func() bool { for _, a := range audience { if a.(string) == expAud { return true } } return false } if !verifyAudience() { logger.Warn("Audienceに%sが含まれません", expAud) return false } //次ページへ 以下を参考に実装 Auth0 Docs Go Authorization Create a middleware to validate Access Tokens https://auth0.com/docs/quickstart/backend/go lang/01-authorization#validate-access-tokens github.com/auth0/go-jwt-middleware v1.0.0 github.com/form3tech-oss/jwt-go v3.2.2 github.com/go-chi/chi v4.1.2+incompatible jwt.MapClaims.VerifyAudience()は []stringにキャストしようとして失敗する ので、自前で検証 ドキュメントの実装だと issuer,audienceの検証は ValidationKeyGetter内でやっているが、凝 集度的にはここでやるほうが自然だと思う ...
  31. 31. Goの実装 31 //つづき scope, ok := claims["scope"].(string) if !ok { logger.Warn("scopeクレームがありません") return false } verifyScope := func() bool { for _, s := range strings.Split(scope, " ") { if s == expScope { return true } } return false } if !verifyScope() { logger.Warn("Scopeに%sが含まれません", expScope) return false } return true } Middleware実装④ - 「3.クレームの検証」の 中身 以下を参考に実装 Auth0 Docs Go Authorization Create a middleware to validate Access Tokens https://auth0.com/docs/quickstart/backend/go lang/01-authorization#validate-access-tokens github.com/auth0/go-jwt-middleware v1.0.0 github.com/form3tech-oss/jwt-go v3.2.2 github.com/go-chi/chi v4.1.2+incompatible
  32. 32. まとめ - React(TypeScript) + Goでの 管理画面の認可におけるAuth0の導入方法を紹介 - 設定項目さえ把握できれば導入は容易 - フロントエンドの実装は軽微 - サーバーサイドの実装は再利用できることを考えると、 2つ目以降のアプリケーションでは認証・認可の実装はほとんど 不要 32
  33. 33. “ ありがとうございました。 33
  34. 34. “ APPENDIX 34
  35. 35. Auth0の設定 5. Rule - Role自動付与したい場合に利用 - ユーザーサインアップ時の イベントをフックして処理を する - 社内のユーザーに特定のロール を自動付与するケースの実装例 を紹介 35 async function addAdminRole(user, context, callback) { const ManagementClient = require("auth0@2.30.0").ManagementClient; if (!user.email || !user.email_verified) { return callback(null, user, context); } const shouldAddRole = function (user, context) { const assignedRoles = (context.authorization || {}).roles || []; if (assignedRoles.includes("[付与すべき Role名]")) { console.log(`${user.name} has already had default roles`); return false; } const endsWith = "@[対象ドメイン ]"; if ( user.email && user.email.substring( user.email.length - endsWith.length, user.email.length ) === endsWith ) { return true; } return false; }; //次ページへ メール確認が 済んでいるかチェック ロールを付与すべきか判定
  36. 36. Auth0の設定 36 //give default roles if the user doesn't already have any roles assigned if (shouldAddRole(user, context)) { try { const management = new ManagementClient({ token: auth0.accessToken, domain: auth0.domain, }); await management.assignRolestoUser( { id: user.user_id }, { roles: ["[付与すべき RoleのID"] } ); console.log(`Default role has been assigned to ${user.name}.`); } catch (ex) { console.error("Failed to add default role to user", ex); } } return callback(null, user, context); } ロールの付与 5. Rule - Role自動付与したい場合に利用 - ユーザーサインアップ時の イベントをフックして処理を する - 社内のユーザーに特定のロール を自動付与するケースの実装例 を紹介
  37. 37. Auth0の設定 5. Rule - ロールIDはUI上には表示されな い - Roleの設定画面のURLの一部に 表示される 37 ※2020/01/22時点での設定画面・デフォルト設定をもとにして います
  • ksakiyama134

    Feb. 10, 2021

20210128 エムスリー BIR勉強会資料 https://m3-engineer.connpass.com/event/200495/

Views

Total views

278

On Slideshare

0

From embeds

0

Number of embeds

12

Actions

Downloads

2

Shares

0

Comments

0

Likes

1

×