Go - gRPC での認証・認可を Interceptor を使って実装する

# tl;dr

- すべての RPC に共通する処理には gRPC Interceptor を利用する
- 認証 Interceptor はすべての RPC で同じ処理を行って、認可 Interceptor は gRPC サービスごとに別の処理を行えるようにする
- 認証に使う Authorization 値は Metadata として送信する
- 複数の Interceptor をまとめて書くには go-grpc-middleware の chain を使う
- ここでは簡単のため Unary RPC を前提にまとめる

# gRPC Interceptor

gRPC サーバーはオプションとして UnaryServerInterceptor 型を渡すことができる。UnaryServerInterceptor 型はすべての RPC の実行前後に処理のフックを追加することができるので、いわゆるミドルウェアを実装するために使える。

```go
// UnaryServerInterceptor provides a hook to intercept the execution of a unary RPC on the server. info
// contains all the information of this RPC the interceptor can operate on. And handler is the wrapper
// of the service method implementation. It is the responsibility of the interceptor to invoke handler
// to complete the RPC.
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
```

# 認証 Interceptor

認証 Interceptor ではユーザーの Authorization 値が適切かを確認したいとする。すべての RPC で共通する処理として実装する。Authorization 値はすべての RPC に共通するので Metadata としてやり取りすると良い。

```go
package grpcauthentication

import (
	"context"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// DefaultAuthenticateFunc はすべてのサービスに共通する認証処理を行う関数を表す。
type DefaultAuthenticateFunc func(ctx context.Context) (context.Context, error)

// UnaryServerInterceptor はリクエストごとの認証処理を行う、unary サーバーインターセプターを返す。
func UnaryServerInterceptor(authFunc DefaultAuthenticateFunc) grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		newCtx, err := authFunc(ctx)
		if err != nil {
			return nil, status.Error(codes.Unauthenticated, err.Error())
		}
		return handler(newCtx, req)
	}
}
```

```go
package meta

import (
	"fmt"

	"golang.org/x/net/context"
	"google.golang.org/grpc/metadata"
)


const (
	// AuthorizationKey は認証トークンに対応するキーを表す
	AuthorizationKey = "authorization"
)

// Authorization は gRPC メタデータからユーザー認証トークンを取得する
func Authorization(ctx context.Context) (string, error) {
	return fromMeta(ctx, AuthorizationKey)
}

func fromMeta(ctx context.Context, key string) (string, error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return "", fmt.Errorf("not found metadata")
	}
	vs := md[key]
	if len(vs) == 0 {
		return "", fmt.Errorf("not found %s in metadata", key)
	}
	return vs[0], nil
}
```

```go
package main

import (
	"context"

	"github.com/path/to/meta"
	"github.com/path/to/interceptor/grpcauthentication"

	"google.golang.org/grpc"
)


func defaultAuthentication() grpcauthentication.AuthFunc {
	return func(ctx context.Context) (context.Context, error) {
		authorization, err := meta.Authorization(ctx)
		if err != nil {
			return nil, err
		}

		err := verify(authorization)
		if err != nil {
			return nil, err
		}

		return ctx, nil
	}
}

func main() {
	grpcServer := grpc.NewServer(
		grpc.UnaryInterceptor(grpcauthentication.UnaryServerInterceptor(defaultAuthentication())),
	)
...
}
```

# 認可 Interceptor

認可 Interceptor ではサービスや RPC ごとにそのユーザーに権限があるかどうかを確認したいとする。例えば user サービスで提供する RPC ではサービス停止していないかだけを確認したいが、admin サービスで提供する RPC では管理者権限を持っているかどうかを確認したい。

サービスごとに認可処理を行えるように、各サービスは ServiceAuthorize インターフェースを実装する必要がある。

```go
package grpcauthorization

import (
	"context"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// ServiceAuthorize はサービスごとの認可を行う関数を実装するインターフェースを表す。
type ServiceAuthorize interface {
	Authorize(context.Context, string) error
}

// UnaryServerInterceptor はリクエストごとの認可を行う、unary サーバーインターセプターを返す。
func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		var err error
		if srv, ok := info.Server.(ServiceAuthorize); ok {
			err = srv.Authorize(ctx, info.FullMethod)
		} else {
			return nil, fmt.Errorf("each service should implement an authorization")
		}
		if err != nil {
			return nil, status.Error(codes.PermissionDenied, err.Error())
		}
		return handler(ctx, req)
	}
}
```

```go
package userservice

import (
	"context"
)

// Authorize はユーザーの認可を行う。
func (*UserService) Authorize(ctx context.Context, fullMethodName string) error {
	switch fullMethodName {
	// 以下の RPC メソッドはすべてのユーザーが行うことができるメソッドなので無条件に許可する {
	case
		"/userpb.UserService/ReportTrouble":
		return nil
	// }
	default:
		return userAuthFunc(ctx)
	}
}
```

```go
package adminservice

import (
	"context"
)

// Authorize はユーザーの認可を行う。
func (*AdminService) Authorize(ctx context.Context, fullMethodName string) error {
	return adminAuthFunc(ctx)
}
```

```go
package main

import (
	"context"

	"github.com/path/to/interceptor/grpcauthorization"

	"google.golang.org/grpc"
)

func main() {
	grpcServer := grpc.NewServer(
		grpc.UnaryInterceptor(grpcauthorization.UnaryServerInterceptor()),
	)
...
}
```

# 複数の Interceptor をまとめる

https://github.com/grpc-ecosystem/go-grpc-middleware には色々な interceptor 実装がまとまっている。その中にある chain を使うと複数の Interceptor を以下のように書ける。

```go
grpcServer := grpc.NewServer(
	grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
		grpcauthentication.UnaryServerInterceptor(defaultAuthentication()),
		grpcauthorization.UnaryServerInterceptor(),
	)),
)
```

# まとめ

grpc サーバーとサービスを合わせたときの記述は次のようになる。

```go
func main() {
	grpcServer := grpc.NewServer(
		grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
			grpcauthentication.UnaryServerInterceptor(defaultAuthentication()),
			grpcauthorization.UnaryServerInterceptor(),
		)),
	)

	usersv := userservice.NewUserService()
	adminsv := adminservice.NewAdminService()
	userpb.RegisterUserServiceServer(grpcServer, usersv)
	adminpb.RegisterAdminServiceServer(grpcServer, adminsv)
...
}
```