LoginSignup
12

More than 3 years have passed since last update.

GraphQLにおける認証認可事例(Auth0 RBAC仕立て)

Last updated at Posted at 2020-05-23

お題

以下の組み合わせで作成しているWebアプリケーションにAuth0による認証認可機能を入れてみる。
認証はID(メールアドレス)とパスワードによる方式を採用。

■通信方式
 ・GraphQL
■フロントエンド
 ・Vue.js
 ・Nuxt.js
 ・TypeScript
 ・Apollo
■バックエンド
 ・Golang
 ・gqlgen

挙動としては以下のようになる。

(1)ログイン前。「LOG IN」ボタンを押下する。
Screenshot at 2020-05-23 15-31-42.png

(2)Auth0のログイン画面(カスタマイズもできるらしい)に飛ばされる。メアドとパスワードを入れて「Continue」ボタンを押下する。
Screenshot at 2020-05-23 15-32-02.png

(3)認証が通るとアクセストークン付きでコールバック(あらかじめAuth0に設定しておく)が呼ばれる。
Screenshot at 2020-05-23 15-33-43.png

(4)ログイン後のトップ画面を表示する。
Screenshot at 2020-05-23 15-34-01.png

(5)サイドメニューから「Movie」を選択して動画一覧ページに遷移する。
Screenshot at 2020-05-23 15-34-37.png

(6)ログインユーザに権限があるので動画一覧ページが表示される。
Screenshot at 2020-05-23 15-34-53.png

(7)いったんログアウトする。
Screenshot at 2020-05-23 15-35-12.png

(8)別の(動画一覧表示権限が与えられていない)ユーザでログインする。
(9)サイドメニューから「Movie」を選択して動画一覧ページに遷移する。
(10)ログインユーザに権限がないので動画一覧ページが表示されない。
Screenshot at 2020-05-23 15-38-31.png

画面的には、そっけないエラーページだけど、コンソールログを見ると権限がない旨のエラーログが出ている。
Screenshot at 2020-05-23 15-39-26.png

前提

  • Auth0のSign in方法から各機能の具体的な操作方法など特に細かくは説明しない。
  • Vue.jsやGolangの実装方法について1行単位で詳しく解説はしない。

開発環境

# OS - Linux(Ubuntu)

$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.4 LTS (Bionic Beaver)"

# フロントエンド

Nuxt.js

$ cat yarn.lock | grep "@nuxt/vue-app"
    "@nuxt/vue-app" "2.12.2"
"@nuxt/vue-app@2.12.2":
  resolved "https://registry.yarnpkg.com/@nuxt/vue-app/-/vue-app-2.12.2.tgz#cc4b68356996eb71d398a30f3b9c9d15f7d531bc"

パッケージマネージャ - Yarn

$ yarn -v
1.22.4

IDE - WebStorm

WebStorm 2020.1
Build #WS-201.6668.106, built on April 7, 2020

# バックエンド

# 言語 - Golang

$ go version
go version go1.13.9 linux/amd64

IDE - Goland

GoLand 2020.1.2
Build #GO-201.7223.97, built on May 1, 2020

関連記事索引

全ソース

バックエンド分

フロントエンド分

実践

Auth0の設定

ApplicationとWebAPIの作成

Auth0は、ドキュメントやチュートリアルが充実してるので、ちゃんと読めば、わりかしハマることは少ないと思う。

フロントエンドの設定について

フロントエンドはモードとしてはSSRなんだけど、形式としてSPAを選択している。
それが正しいのかは、ちょっとわかっていない。
screenshot-manage.auth0.com-2020.05.23-17_07_36.png

ログインやログアウトの処理でAuth0のSDKを使うわけだけど、その際の接続先に関する情報として以下が必要。
screenshot-manage.auth0.com-2020.05.23-16_44_54.png

また、Auth0での認証処理が成功した後に再びアプリ側に戻ってくるためのコールバックURLを指定。
あと、ログアウト後の遷移先URLも。
screenshot-manage.auth0.com-2020.05.23-16_52_56.png

バックエンドの設定について

バックエンドはWebAPIとして機能するのでAPIsで設定。
screenshot-manage.auth0.com-2020.05.23-16_09_53.png

フロントエンドから渡されたJWTのチェックをするために Identifier を使う。
screenshot-manage.auth0.com-2020.05.23-17_12_51.png

あと、今回は認可機能の実装のために、Auth0が提供しているRBACの仕組みを使うので、Auth0上で定義したRoleとPermissionがJWTのClaimに積まれるようにしておく。
screenshot-manage.auth0.com-2020.05.23-17_15_21.png

RoleとPermissionの定義

WebAPIが提供する(予定の)各機能にPermissionを定義していく。
この部分は、WebAPIがどんな機能を提供するかや、どういった粒度でPermission制御が必要かについて設計した上で設定する。
今回は仮に「組織」、「ユーザ」、「コンテンツ(今回だと動画コンテンツ)」といったリソースに対するCRUD操作それぞれに認可制御をする想定で設計。
また、APIを叩いた(認証済み)ユーザが、自分で所有(自分で登録したものや他の人から権限を与えられたもの)するものしか権限がないのか、すべてのリソースに対する権限があるのかという観点でも分けてみた。
まあ、実際のアプリケーションでは、もっといろんな状況があると思うので、このあたりは、要件によりけり。
また、Auth0では、別にAuth0上のWebAPI定義にPermissionを細かく作っていかなくても、実際に動かすアプリケーションの方で機能ごとにPermissionを定義して、それをユーザ作成時にアプリケーションの機能としてAuth0ユーザのMetadataに貼り付けるといったこともできる。
柔軟に制御したいといった場合は、Metadata使う方式の方が後々困らないかもしれない。

ともあれ、とりあえず今回は以下みたいな感じでPermissionを定義。
screenshot-manage.auth0.com-2020.05.23-17_43_34.png
            ・
            ・
            ・
screenshot-manage.auth0.com-2020.05.23-17_45_15.png

で、ユーザにはそれぞれのPermissionを1つ1つ付与していくのではなくRoleを付与するので、Roleを作成。
これも、Roleの定義は作りたいアプリケーションの要件によってまちまちなので、今回のは参考程度に見てもらえると。
screenshot-manage.auth0.com-2020.05.23-17_29_53.png

このRoleに先ほどのPermissionを割り当てていく。
要するに、このRoleはこのWebAPIに関してどれだけの機能を叩くPermissionを有しているかを設定していく。
先ほどのPermissionもそうだけど、こういった作業を画面でポチポチやるのが地味に時間を要する。
1回きりといえばそうなんだけど、例えば、実際の開発現場では、開発環境、ステージング環境、本番環境を用意し、Auth0のテナントもそれぞれに用意するはず。
各環境用にこの画面ポチポチをつどつどやれって言われたら、正直しんどい。
まあ、たぶん、設定のExport/Import機能くらいあるんだろう、きっと。
Auth0の設定画面になくても、Auth0が用意するManagement系のAPIを使うと、わりとさくっとできるかな。

話がズレた。とりあえずRoleごとのPermission割り当てだけど、AdminとNoneの事例を。
screenshot-manage.auth0.com-2020.05.23-17_54_12.png
            ・
            ・
            ・

Adminは要するに全Permission持ってるということ。

screenshot-manage.auth0.com-2020.05.23-17_54_31.png

Userを作成

このあたりは実際のサービスなら、まず、1人だけAdmin的な権限のあるユーザを作成。
Adminの人だけが触れる管理画面でも用意して、Auth0のManagement系APIを使って、別のRole別のユーザを作成していくといった流れになるんだろう。
今回は、まだそういう機能は用意してないので、とりあえず適当に3ユーザくらいAuth0の画面から作成。
screenshot-manage.auth0.com-2020.05.23-18_01_49.png

フロントエンド実装

Auth0ではSPAアプリ用にSDKが用意されている。
でも、今回ぐらいの例だとNuxtが提供しているAuthモジュールを使うことで事足りるので、SDKは使わない。

nuxtjs/auth導入

以下参照。
https://auth.nuxtjs.org/guide/setup.html

今回用の設定は下記。

src/nuxt-config.js
export default {
  mode: 'universal',
   〜〜〜
  modules: [
   〜〜〜
    '@nuxtjs/apollo',
    '@nuxtjs/auth'
  ],
   〜〜〜
  apollo: {
   〜〜〜
    tokenName: 'auth._token.auth0',
    authenticationType: '' // default の Bearer だと「Bearer: Bearer」というように重複が起きるため
  },
   〜〜〜
  auth: {
    redirect: {
      login: '/login',
      logout: '/login',
      callback: '/callback',
      home: '/'
    },
    strategies: {
      auth0: {
        domain: process.env.AUTH0_DOMAIN,
        client_id: process.env.AUTH0_CLIENT_ID,
        audience: process.env.AUTH0_AUDIENCE
      }
    },
    plugins: ['~/plugins/auth.ts']
  },
   〜〜〜

Auth0用の設定事例は下記に記載している。
https://auth.nuxtjs.org/providers/auth0.html#usage

domainclient_idはAuth0のフロントエンド分のApplication設定にて表示されていたやつ。
audienceがちょっとわかりづらいけど、バックエンドのWebAPI設定にて表示されていた「Identifier」の値を指定。
このへんの説明は↓に少しだけ書かれている。
https://auth.nuxtjs.org/providers/auth0.html#obtaining-client-id-domain-and-audience

client_idとか晒したくないのでnuxtjs/dotenvを使ってる。
あと、GraphQLライブラリとしてApolloを使っていて、

authenticationTypeはデフォルトだとBearerと付くようで、そのままバックエンドに送ると以下のようになってしまう。
なので、明示的にブランクを指定してBearerが重複しないようにしている。

authorization: Bearer Bearer eyJhbVZn〜〜〜

【参考】
https://stackoverflow.com/questions/61277352/nuxt-apollo-module-request-authorization-header-with-double-bearer

ログインページ

関連ソース

$ tree
.
├── layouts
│   └── unCertified.vue
├── pages
|   ├── callback.vue
│   ├── login.vue

未認証時専用のレイアウト

src/layouts/unCertified.vue
<template>
  <v-app dark>
    <v-container>
      <nuxt />
    </v-container>
  </v-app>
</template>

ログインページ

src/pages/login.vue
<template>
  <v-row justify="center">
    <v-col md="auto">
      <div>fs-mng-app</div>
    </v-col>
    <v-col md="auto">
      <v-btn @click="login">Log in</v-btn>
    </v-col>
  </v-row>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

@Component({ layout: 'unCertified' })
export default class LoginPage extends Vue {
  async login() {
    try {
      await this.$auth.loginWith('auth0')
    } catch (err) {
      this.$toast.error(err)
    }
  }
}
</script>

nuxt.config.jsにてauthの名で設定した内容を踏まえたオブジェクトが$authという名前でアクセスできる。
で、$auth.loginWith('auth0') と書いただけでAuth0へ認証をかけにいくことができる。
つまり、先述の↓に飛ばしてくれる。
Screenshot at 2020-05-23 15-32-02.png
なんだかもう、いろいろなことを隠蔽してくれていて、便利なんだけど怖い。

ログイン完了後、Cookieにauth._token.auth0という名前でトークンが書き込まれている。
Screenshot at 2020-05-23 22-44-08.png

たぶん設定の問題だけど、デフォルトでLocalStorageの方にも同じ内容で書き込まれている。
Screenshot at 2020-05-23 22-44-39.png

コールバックページ

Auth0上での認証成功後に、アプリケーションに戻ってこないといけないので、そのためのページを用意。
Auth0の設定で用意したページを認証後のリダイレクト先として指定している。

src/pages/callback.vue
<template>
  <v-row justify="center">
    <v-col md="auto">
      <div>Trying to callback</div>
    </v-col>
  </v-row>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

@Component({ layout: 'unCertified' })
export default class LoginPage extends Vue {
  created() {
    this.$router.push('/')
  }
}
</script>

やってることは、単にトップページに遷移するだけ。

動画一覧ページ

ここが今回、認可処理を実装した部分ではあるのだけど、まずは、そもそも認証通ってないと、このページ開けないよの部分から。
バックエンドの方の実装は後ほど示すとして、とりあえずここでは、バックエンドではJWT(要するにCookieに積まれていたトークン)をヘッダに積んでないとエラーを返す実装が入っていることを前提とする。

まずは、先述の通りログイン完了後にCookieにトークンが入っている状態で動画一覧ページの表示を試みると、はい、開く。
Screenshot at 2020-05-23 22-55-21.png

このとき、ChromeのDevツールでNetworkタブより「Headers」を見てみると、authorizationヘッダにCookieに入っていたトークンが指定されている。
Screenshot at 2020-05-23 22-57-01.png

つまり、nuxt.config.jsに記述した apolloモジュールとauthモジュールの設定だけで、Auth0ログインにて取得したJWTをHTTPヘッダに積んでGraphQL通信する流れが出来上がっている。

さて、試しに、CookieとLocalStorageからトークンを消して、動画一覧ページを開こうとすると、
Screenshot at 2020-05-23 23-08-54.png
Screenshot at 2020-05-23 23-09-10.png

失敗する。(ただ、これはどちらかというと通信自体に失敗した感じ)
Screenshot at 2020-05-23 23-12-07.png
Screenshot at 2020-05-23 23-12-26.png

トークンを消すのではなく改ざんっぽく変えてみてから、再度、動画一覧ページを開こうとすると、
画面上は同じエラー画面だけど、通信ログがこのようになる。
401 Unauthorized」これは、わかりやすい。
Screenshot at 2020-05-23 23-16-47.png

ともかく、適切なトークンを積んでないとGraphQL通信に失敗する仕組みができている。

ログアウト

関連ソース

$ tree
.
├── layouts
│   └── default.vue

ログアウト

「ログアウト」ボタンは、ログイン後のどのページからでも即座に実行できるようメニューの右端に仕込んである。

src/layouts/default.vue
<template>
  <v-app dark>
    〜〜〜
    <v-navigation-drawer v-model="rightDrawer" :right="right" fixed>
      <v-list>
        <v-list-item @click="logout">
          <v-list-item-action>
            <v-icon>
              mdi-eject
            </v-icon>
          </v-list-item-action>
          <v-list-item-title>Log out</v-list-item-title>
        </v-list-item>
      </v-list>
    </v-navigation-drawer>
    〜〜〜
  </v-app>
</template>

<script lang="ts">
import { Component, Provide, Vue } from 'nuxt-property-decorator'

@Component({})
export default class DefaultLayout extends Vue {
   〜〜〜
  @Provide()
  right: boolean = true

  @Provide()
  rightDrawer: boolean = false
   〜〜〜
  async logout() {
    try {
      await this.$auth.logout()
    } catch (err) {
      this.$toast.error(err)
    }
  }
}
</script>

例によって$authオブジェクトが使えるので、$auth.logout()とするだけでAuth0的にログアウト処理を実行してくれる。
あ〜、便利。
ログアウトされて、ログインページが表示される。
Screenshot at 2020-05-23 23-31-33.png
この状態で、あえて、ログイン後に遷移できるはずのトップページの表示を試みると、
Screenshot at 2020-05-23 23-31-49.png
表示できず、ログインページにリダイレクトされる。
Screenshot at 2020-05-23 23-32-06.png

バックエンド実装

さて、バックエンド側。

WebAPIサーバ起動動線

src/cmd/main.go
 〜〜〜
func main() {
    // 環境変数からJWTのチェックに使うAuth0のドメインやオーディエンスを取得
    e := loadEnv()
     〜〜〜
    var router *chi.Mux
    {
        router = chi.NewRouter()
        router.Group(func(r chi.Router) {
          〜〜〜
            // 認証認可チェック用(今はJWTのチェックのみ実装)
            a := auth.New(e.Auth0Domain, e.Auth0Audience, e.AuthDebug, e.AuthCredentialsOptional)
            r.Use(a.CheckJWTHandlerFunc())
            r.Use(a.HoldPermissionsHandler)

            // GraphQLリゾルバー
            resolver := &graph.Resolver{
                DB:           db,
                    〜〜〜
            }

            // GraphQLエンドポイント
            r.Handle("/query", graph.DataLoaderMiddleware(resolver, graphQlServer(resolver)))
        })
    }

    log.Printf("connect to http://localhost:%s/ for GraphQL playground", e.ServerPort)
    if err := http.ListenAndServe(":"+e.ServerPort, router); err != nil {
        fmt.Println(err)
    }
}

WebAPIサーバとしてはライブラリとしてchiを使っている。
で、各リクエストのつどデコレートするためにカスタムミドルウェアを2つ定義している。
CheckJWTHandlerFunc
HoldPermissionsHandler
これにより、JWTのチェックと認可チェックをしている。

JWTのチェック

src/auth/auth.go
package auth

import(
     〜〜〜
    jwtMiddleware "github.com/auth0/go-jwt-middleware"
    "github.com/dgrijalva/jwt-go"
)

type Auth struct {
    domain              string
    audience            string
    debug               bool
    credentialsOptional bool
}

func New(domain, audience string, debug, credentialsOptional bool) *Auth {
    return &Auth{domain, audience, debug, credentialsOptional}
}

func (a *Auth) CheckJWTHandlerFunc() func(next http.Handler) http.Handler {
    middleware := jwtMiddleware.New(jwtMiddleware.Options{
        ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
            // Verify 'aud' claim
            checkAud := token.Claims.(jwt.MapClaims).VerifyAudience(a.audience, false)
            if !checkAud {
                return token, xerrors.New("Invalid audience.")
            }
            // Verify 'iss' claim
            checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(fmt.Sprintf("https://%s/", a.domain), false)
            if !checkIss {
                return token, xerrors.New("Invalid issuer.")
            }

            cert, err := a.getPemCert(token)
            if err != nil {
                return token, xerrors.New("Invalid token.")
            }

            result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
            return result, nil
        },
        SigningMethod:       jwt.SigningMethodRS256,
        Debug:               a.debug,
        CredentialsOptional: a.credentialsOptional,
    })
    return middleware.Handler
}

func (a *Auth) getPemCert(token *jwt.Token) (string, error) {
    cert := ""
    resp, err := http.Get(fmt.Sprintf("https://%s/.well-known/jwks.json", a.domain))

    if err != nil {
        return cert, xerrors.Errorf("failed to http.Get: %w", err)
    }
    defer func() {
        if err := resp.Body.Close(); err != nil {
            fmt.Println(err)
        }
    }()

    var jwks = Jwks{}
    err = json.NewDecoder(resp.Body).Decode(&jwks)

    if err != nil {
        return cert, xerrors.Errorf("failed to decode jwks: %w", err)
    }

    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 cert, xerrors.New("cert is blank.")
    }

    return cert, nil
}

type Jwks struct {
    Keys []JSONWebKeys `json:"keys"`
}

type JSONWebKeys 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"`
}

goにおけるJWTのチェック用ライブラリとしてgo-jwt-middlewareを使う。
チェックの内容については以下を丸コピぐらいの勢いで流用。
https://auth0.com/docs/quickstart/backend/golang/01-authorization#validate-access-tokens

認可チェックのためのPermission取得

さて、JWTのチェックはしたものの、「このログインしたユーザは、この叩かれた機能を実行できていいんだっけ?」の部分は、まだチェックできていない。
それ用のミドルウェアが「HoldPermissionsHandler」。
下記は先述の「CheckJWTHandlerFunc」と同じファイルにあるので Auth構造体やgetPemCertメソッドは共有している。

src/auth/auth.go
package auth
   〜〜〜
func (a *Auth) HoldPermissionsHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        authHeaderParts := strings.Split(r.Header.Get("Authorization"), " ")
        tokenString := authHeaderParts[1]

        parser := func(token *jwt.Token) (interface{}, error) {
            cert, err := a.getPemCert(token)
            if err != nil {
                return nil, xerrors.Errorf("failed to getPemCert: %w", err)
            }
            result, err := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
            if err != nil {
                return nil, xerrors.Errorf("failed to ParseRSAPublicKeyFromPEM: %w", err)
            }
            return result, nil
        }

        token, err := jwt.Parse(tokenString, parser)
        if err != nil {
            log.Printf("failed to ParseWithClaims: %v", err)
            next.ServeHTTP(w, r)
            return
        }
        if !token.Valid {
            log.Print("invalid token")
            next.ServeHTTP(w, r)
            return
        }

        claims, ok := token.Claims.(jwt.MapClaims)
        if !ok {
            log.Print("not implemented MapClaims")
            next.ServeHTTP(w, r)
            return
        }

        authenticatedUser := &AuthenticatedUser{
            PermissionSet: util.NewBlankStringSet(),
        }
        for k, v := range claims {
            switch k {
            case "sub":
                authenticatedUser.ID = v.(string)
            case a.audience + "/email":
                authenticatedUser.EMail = v.(string)
            case "permissions":
                if permissionArray, ok := v.([]interface{}); ok {
                    for _, permission := range permissionArray {
                        authenticatedUser.PermissionSet.Add(permission.(string))
                    }
                    break
                }
            }
        }
        next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), authenticatedUserKey, authenticatedUser)))
    })
}

ざっくりと、このメソッドの要点だけ言うと、claims, ok := token.Claims.(jwt.MapClaims)によってJWTから取り出したクレームの中から、ログインユーザが持つPermissionを取得してリクエストスコープのコンテキストに格納している。

クレームの中身をデバッグしてみると、こんな感じ。
Screenshot at 2020-05-24 00-17-16.png
Auth0のAPIのPermission設定で登録していった全てのPermissionが列挙されている。
これは、Adminロールを持つユーザでログインしたため。(例えばこれがNoneロールを持つユーザでログインした場合だと、持ってるPermissionは「user:read:mine」だけとなる)

認可チェック

実際の認可チェックはGraphQLの各リゾルバーの中で行う。

src/graph/movie.resolver.go
 〜〜〜
func (r *queryResolver) Movies(ctx context.Context) ([]*model.Movie, error) {
    user := auth.GetAuthenticatedUser(ctx)
    if !user.HasReadMinePermission("content") {
        err := xerrors.New("no permissions")
        log.Print(err)
        return nil, err
    }
     〜〜〜
    return results, nil
}

user := auth.GetAuthenticatedUser(ctx)によって、リクエストスコープのコンテキストに格納したPermission群を保持する以下構造体を取得できる。

// 認証チェック済みのユーザー情報を保持
type AuthenticatedUser struct {
    ID            string
    EMail         string
    PermissionSet *util.StringSet
}

そして、user.HasReadMinePermission("content")により、Permission群の中に”自分の” contentREAD 権限があるかどうかがチェックできる。
このあたりのチェック用に以下のユーティリティメソッド群を用意している。

func (u *AuthenticatedUser) HasPermission(funcName string, crud operation, t target) bool {
    if u == nil || u.PermissionSet == nil {
        return false
    }
    return u.PermissionSet.Contains(fmt.Sprintf("%s:%v:%v", funcName, crud, t))
}

func (u *AuthenticatedUser) HasNoTargetPermission(funcName string, crud operation) bool {
    if u == nil || u.PermissionSet == nil {
        return false
    }
    return u.PermissionSet.Contains(fmt.Sprintf("%s:%v", funcName, crud))
}

func (u *AuthenticatedUser) HasReadAllPermission(funcName string) bool {
    return u.HasPermission(funcName, READ, ALL)
}

func (u *AuthenticatedUser) HasReadMinePermission(funcName string) bool {
    return u.HasPermission(funcName, READ, MINE)
}

func (u *AuthenticatedUser) HasCreatePermission(funcName string) bool {
    return u.HasNoTargetPermission(funcName, CREATE)
}

type operation string
type target string

const (
    READ   operation = "read"
    CREATE           = "create"
    UPDATE           = "update"
    DELETE           = "delete"

    ALL  target = "all"
    MINE        = "mine"
)

これにより、リゾルバー毎に求めるPermissionを指定して認可チェックすることができる。

まとめ

いつも、まとめようとする頃には力尽きている・・・。
次は、Management系APIを使った機能の実装事例かdataloadenの事例かな。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12