Daprのミドルウェアを使ってアプリをOAuth2で保護してみる

Daprのミドルウェアを使うとコード修正無しで簡単にOAuth2による保護を導入できます
2021.07.24

CX事業本部@大阪の岩田です。先日のブログに引き続きDaprのクイックスタートを試してみました。今回試したのはミドウェアを使ってアプリをOAuth2で保護するという内容です。そもそもDaprって何?という方は前回のブログを参照して下さい。

Daprのミドルウェアについて

Daprにはミドルウェアというコンポーネントが存在します。アプリケーションにミドルウェアを追加することでリクエスト/レスポンスを処理する独自のパイプラインが自由に構築できます。以下は公式ドキュメント記載の画像です。

Daprのミドルウェアがサイドカーコンテナとして動作し、クライアントとアプリケーション間のリクエスト/レスポンスに独自処理を実行できることが分かります。

パイプラインにはデフォルトで

  • tracing middleware
  • CORS middleware

が組み込まれていますが、必要に応じてさらに追加のミドルウェアを構成可能です。

2021/7時点では以下のようなミドルウェアが提供されています。Alpha版がほとんどなので、本番環境での利用はもう少し様子を見たほうが良さそうです。

Name Description Status Component version
Rate limit Restricts the maximum number of allowed HTTP requests per second Alpha v1
OAuth2 Enables the OAuth2 Authorization Grant flow on a Web API Alpha v1
OAuth2 client credentials Enables the OAuth2 Client Credentials Grant flow on a Web API Alpha v1
Bearer Verifies a Bearer Token using OpenID Connect on a Web API Alpha v1
Open Policy Agent Applies Rego/OPA Policies to incoming Dapr HTTP requests Alpha v1
Uppercase Converts the body of the request to uppercase letters GA (For local development) v1

Middleware component specs

公式から提供されているミドルウェアを利用する以外にも、ミドルウェアインターフェースを実装した独自ミドルウェアを自作して利用することも可能です。ミドルウェアのインターフェースは以下の定義になります。

type Middleware interface {
  GetHandler(metadata Metadata) (func(h fasthttp.RequestHandler) fasthttp.RequestHandler, error)
}

やってみる

ミドルウェアの概要が分かったので、サンプルアプリをEKS上にデプロイして動かしてみます。今回はGitHubのリポジトリで提供されている以下のサンプルアプリをデプロイしていきます。

https://github.com/dapr/quickstarts/tree/master/middleware

環境

今回利用した環境です

  • Kubernetes: 1.19
  • eksctl: 0.56.0
  • kubectl: v1.19.6-eks-49a6c0
  • Dapr CLI: 1.2.0
  • Helm: v3.6.2+gee407bd

EKSクラスタの構築とDaprの初期化は完了している前提で進めていきます。手順が分からない場合は前回のブログを参照して下さい。

Helmチャートのインストール

このクイックスターとではIngressコントローラーとしてNginxを利用します。以下の手順でHelmチャートをインストールし、クラスタにNginxを追加しましょう。

まずはリポジトリの追加

$ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
"ingress-nginx" has been added to your repositories

Helmチャートのインストール

$ helm install my-release ingress-nginx/ingress-nginx
NAME: my-release
LAST DEPLOYED: Fri Jul 23 06:56:29 2021
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
The ingress-nginx controller has been installed.
It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status by running 'kubectl --namespace default get services -o wide -w my-release-ingress-nginx-controller'

An example Ingress that makes use of the controller:

  apiVersion: networking.k8s.io/v1beta1
  kind: Ingress
  metadata:
    annotations:
      kubernetes.io/ingress.class: nginx
    name: example
    namespace: foo
  spec:
    rules:
      - host: www.example.com
        http:
          paths:
            - backend:
                serviceName: exampleService
                servicePort: 80
              path: /
    # This section is only required if TLS is to be enabled for the Ingress
    tls:
        - hosts:
            - www.example.com
          secretName: example-tls

If TLS is enabled for the Ingress, a Secret containing the certificate and key must also be provided:

  apiVersion: v1
  kind: Secret
  metadata:
    name: example-tls
    namespace: foo
  data:
    tls.crt: <base64 encoded cert>
    tls.key: <base64 encoded key>
  type: kubernetes.io/tls

NginxのPodが稼働していることを確認しておきましょう

$ kubectl get pods
NAME                                                   READY   STATUS    RESTARTS   AGE
my-release-ingress-nginx-controller-74586884b9-5vg9v   1/1     Running   0          86s

Google APIs ConsoleからOAuth 2.0 クライアント IDを作成

続いてアプリをOAuth2で保護するために、Google APIs ConsoleからOAuth 2.0 クライアント IDを発行します。

「認証情報を作成」からOAuthクライアントIDを選択します

「アプリケーションの種類」にウェブアプリケーションを選択し、「承認済みのリダイレクト URI」には自分がRoute53で管理しているドメインから適当な名前を払い出して設定します。

※クイックスタートではdummy.comという名前とhostsファイルを利用しているのすが、せっかくなので後ほどデプロイされるALBにエイリアスレコードを設定して独自ドメインで動かしてみます

パイプラインのデプロイ

$ cd quickstarts/middleware/

deploy/oauth2.yamlを編集し

  • clientId
  • clientSecret
  • redirectURL

に先程作成したOAuth2クライアントの情報を設定します

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: oauth2
spec:
  type: middleware.http.oauth2
  version: v1
  metadata:
  - name: clientId
    value: "<ここにクライアントIDを入力>"
  - name: clientSecret
    value: "<ここにクライアントシークレットを入力>"
  - name: scopes
    value: "https://www.googleapis.com/auth/userinfo.email"
  - name: authURL
    value: "https://accounts.google.com/o/oauth2/v2/auth"
  - name: tokenURL
    value: "https://accounts.google.com/o/oauth2/token"
  - name: redirectURL
    value: "<ここに承認済みのリダイレクト URIを入力>"
  - name: authHeaderName
    value: "authorization"

設定できたらデプロイメントを作成します

$ kubectl apply -f deploy/oauth2.yaml 
component.dapr.io/oauth2 created

続いてパイプラインのデプロイメントを作成

$ kubectl apply -f deploy/pipeline.yaml

参考までにdeploy/pipeline.yamlの定義は以下の通りです

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: pipeline
spec:
  tracing:
    samplingRate: "1"
    zipkin:
      endpointAddress: "http://zipkin.default.svc.cluster.local:9411/api/v2/spans"
  httpPipeline:
    handlers:
    - type: middleware.http.oauth2
      name: oauth2

サンプルアプリのデプロイ

パイプラインの準備ができたのでアプリ本体をデプロイします。アプリのコードは以下の通りで、/echoというルートでリクエストを待ち受け、リクエストヘッダのAuthorizationをそのまま返却するだけのアプリです

// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------

const express = require('express');
const bodyParser = require('body-parser');
const app = express();

app.use(bodyParser.json());

const daprPort = process.env.DAPR_HTTP_PORT || 3500;
const port = 3000;

app.get('/echo', (req, res) => {
    var text = req.query.text;    
    console.log("Echoing: " + text);
    res.send("Access token: " + req.headers["authorization"] + " Text: " + text);    
});

app.listen(port, () => console.log(`Node App listening on port ${port}!`));

アプリをデプロイします

$ kubectl  apply -f deploy/echoapp.yaml
deployment.apps/echoapp created

デプロイしたアプリのPodが動作していることを確認します

$ kubectl get pods
NAME                                                   READY   STATUS    RESTARTS
AGEechoapp-c56bfd446-vnp2s                                2/2     Running   0          20s
my-release-ingress-nginx-controller-74586884b9-5vg9v   1/1     Running   0          25m

最後にアプリをIngressコントローラーの配下に紐付けます

$ kubectl apply -f deploy/ingress.yaml
Warning: extensions/v1beta1 Ingress is deprecated in v1.14+, unavailable in v1.22+; use networking.k8s.io/v1 Ingressingress.extensions/echo-ingress created

最後にIngressコントローラーのFQDNを確認し、サンプルアプリ用に払い出した名前にエイリアスレコードを作成します。まずはALBのFQDNを確認

$ kubectl get svc my-release-ingress-nginx-controller --output 'jsonpath={.status.loadBalancer.ingress[0].hostname}'a04104cf898dc446ab5e74a185037ff4-956392576.ap-northeast-1.elb.amazonaws.com

確認したFQDNをもとにエイリアスレコードを作成します

サンプルアプリにアクセスしてみる

一通り準備ができたのでサンプルアプリにアクセスしてみます。ブラウザのアドレスバーに http://<サンプルアプリ用に払い出した名前>/v1.0/invoke/echoapp/method/echo?text=hello と入力すると...

Googleのログイン画面にリダイレクトされました。Googleアカウントにログインすると...

アプリの画面にリダイレクトされ、Authorizationヘッダの中身が表示されました。動作確認成功です

ちなみに、この時のアクセスをChromeの開発者ツールからCopy as cURLすると以下のようなテキストがコピーされました。

curl 'http://<サンプルアプリ用のFQDN>/v1.0/invoke/echoapp/method/echo?text=hello' \
  -H 'Connection: keep-alive' \
  -H 'Pragma: no-cache' \
  -H 'Cache-Control: no-cache' \
  -H 'Upgrade-Insecure-Requests: 1' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36' \
  -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' \
  -H 'Accept-Language: ja' \
  -H 'Cookie: gosessionsid=xxxxxx' \
  --compressed \
  --insecure

サンプルアプリはAuthorizationヘッダの中身をそのまま返却する仕様ですが、ブラウザはAuthorizaionヘッダはセットしておらず、Cookieを送信していることが分かります。この挙動から考えると、Daprのミドルウェア側でCookieとトークンの紐付けを管理し、サンプルアプリへのリクエスト時にAuthorizaionヘッダをセットしてくれているようですね。

まとめ

Daprのミドルウェアを使うとコード修正無しで簡単にアプリケーションをOAuth2で保護できることが確認できました。認証/認可のような定型的な処理についてはミドルウェアをうまく活用して開発者の負担を軽減していきたいですね。

参考