OCI API Gateway の機能と OCI Vault のシークレットで OCI Functions を保護(認証)してみる。(Oracle Cloud Infrastructure)

OCI API Gateway には認証機能が付いています。今回は API Gateway の認証機能を使用して OCI Functions を保護(認証)してみますやで。
彡(ﾟ)(ﾟ)

# 1. やりたい事＆元ネタ

元ネタは下記の記事となります。

> Oracle Functionsを利用したAPI Gatewayの認証
> https://oracle-japan.github.io/ocitutorials/cloud-native/functions-apigateway-for-intermediates/

上記記事の 認証用Functions を改変して、リクエストヘッダーにセットした文字列(トークン) と OCI Vault のシークレットを突合する、以下のような処理を実装してみます。

> (1). API Gateway の エンドポイント をコールする。
> (2). API Gateway から認証用の Functions がコールされる。
> (3). 認証用Functions で OCI Vault のシークレットを取り出して、ヘッダーの文字列(トークン)と突合する。
> (4). ヘッダーの文字列(トークン) と OCI Vault のシークレット が一致した場合は本体の Functions をコールする。

接続トポロジは以下の通りです。
![apigw_auth001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110066/98ecef7b-7eff-5dd4-d9a5-a59664859765.png)



# 2. シンプルな Functions の作成

詳細は省略します。下記記事を参照して下さい。

https://qiita.com/ora_gonsuke777/items/a9bb52faadcb9f2af38e

下記の結果が得られるように少し改変してみました。wai ga AYU ya!彡(^)(^)

```
$ fn invoke ayu-functions1 ayu-app
Hello, wai ga AYU ya!
```

# 3. API Gateway から OCI Functions の呼び出し設定

これも詳細は省略します。下記記事を参照してシンプルな Functions を API Gateway からコールできるようにしておいて下さい。

https://qiita.com/ora_gonsuke777/items/e2cc19d38f056241fb07


# 4. OCI Vault, キー, シークレットの作成

下記記事を参照して、認証に使用する文字列(トークン)を OCI Vault のシークレットとして登録して下さい。

https://qiita.com/kenwatan/items/5867a06ef6a00749dcf0

作成したシークレットの OCID はこの後使用するので、メモしておいて下さい。
![apigw_auth002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110066/c462e8ee-9e24-49c8-dc66-5e59a2651a5f.png)


# 5. 認証用Functions のダウンロード(git clone)と YAML編集、ビルド

認証用Functions のサンプルを下記に置いておきました。

https://github.com/gonsuke777/Functions

Functions の Cloud Shell から git clone でダウンロードします。

```shell
git clone https://github.com/gonsuke777/Functions
```

ダウンロード後に func.yaml を編集して、シークレットの OCID を登録したものに書き換えて下さい。

```shell
cd Functions/hello-java/
vi vi 
```

```yaml
schema_version: 20180708
name: hello-java
version: 0.0.46
runtime: java
build_image: fnproject/fn-java-fdk-build:jdk17-1.0.146
run_image: fnproject/fn-java-fdk:jre17-1.0.146
cmd: com.example.fn.HelloFunction::handleRequest
timeout: 60
config:
  SECRET1_ID: ocid1.vaultsecret.oc1.ap-tokyo-1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

yaml編集後にビルド＆デプロイします。

```shell
fn -v deploy --app ayu-functions1
```

デプロイされた Functions の OCID を控えておきます。
![apigw_auth003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110066/e052844e-2f72-33ce-7f7b-12071926c080.png)


# 6. 動的グループの作成と Functions からの OCI Vault読取ポリシー(権限)付与(リソース・プリンシプル)

認証用Functions が OCI Vault のシークレットを読み取れるように権限を付与します。(リソース・プリンシプル)

* 動的グループ(ayu-dynamic-group2)のマッチングルール ※Functions の OCID を指定
```shell
All {resource.id = 'ocid1.fnfunc.oc1.ap-tokyo-1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'}
```

* 動的グループ(ayu-dynamic-group2)に付与したポリシー(権限) ※シークレットの読み取り権限を付与
```shell
allow dynamic-group ayu-dynamic-group2 to read secret-family in compartment xxxxx_compartment
```

* OCI Vault のポリシー(権限)をコンパートメントに付与
```shell
allow service VaultSecret to use vaults in compartment ayu_compartment
allow service VaultSecret to use keys in compartment ayu_compartment
```

その他、本記事のメインではありませんが API Gateway用に下記の動的グループ／ポリシー(権限)を付与しています。

* 動的グループ(ayu-dynamic-group1)のマッチングルール ※API Gateway の OCID を指定
```shell
ALL {resource.type = 'ApiGateway', resource.id = 'ocid1.apigateway.oc1.ap-tokyo-1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'}
```

* 動的グループ(ayu-dynamic-group1)に付与したポリシー(権限) ※API Gateway に Functions の実行権限を付与
```shell
ALLOW any-user to use functions-family in compartment ayu_compartment where ALL { request.principal.type= 'ApiGateway', request.resource.compartment.id = 'ocid1.compartment.oc1..xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' }
```

# 7. 認証用Functions の動作確認(Cloud Shell)

Functions の Cloud Shell からデプロイした認証用Functions の動作確認をしてみます。

```shell
# Case1...No token.
fn invoke ayu-functions1 hello-java
# Case2...Invalid token
echo "{\"type\":\"TOKEN\",\"token\":\"xxxxxxxxx\"}" | fn invoke ayu-functions1 hello-java | jq -a
# Case3...Correct token
echo "{\"type\":\"TOKEN\",\"token\":\"yyyyyyyyy\"}" | fn invoke ayu-functions1 hello-java | jq -a
```

ビルドや権限付与が上手く行っていれば、それぞれ異なる結果を返却します。

```shell
$ fn invoke ayu-functions1 hello-java
Error invoking function. status: 502 message: function failed

$ echo "{\"type\":\"TOKEN\",\"token\":\"xxxxxxxxx\"}" | fn invoke ayu-functions1 hello-java | jq -a
{
  "active": false,
  "principal": null,
  "scope": null,
  "expiresAt": "2020-04-30T10:15:30+01:00",
  "wwwAuthenticate": "Bearer realm=\"example.com\", error=\"invalid token\", error_description=\"token should be \"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1nHyDtTwR3SEJ3z489...\"\"",
  "clientId": null,
  "context": {
    "email": "john.doe@example.com"
  }
}

$ echo "{\"type\":\"TOKEN\",\"token\":\"yyyyyyyyy\"}" | fn invoke ayu-functions1 hello-java | jq -a
{
  "active": true,
  "principal": "https://example.com/users/jdoe",
  "scope": [
    "list:hello",
    "read:hello",
    "create:hello",
    "update:hello",
    "delete:hello",
    "someScope"
  ],
  "expiresAt": "2022-04-10T09:47:16.703Z",
  "wwwAuthenticate": null,
  "clientId": "host123",
  "context": {
    "email": "john.doe@example.com"
  }
}
$ 
```

# 8. API Gateway Deployment の作成

API Gateway の Deployment を作成します。この時に認証用Functionsを指定します。

|変数名|入力する値|
|-|-|
|NAME|任意の名前|
|PATH PREFIX|任意のPREFIXを指定|
|AUTHENTICATION TYPE|Custorm|
|CONPARTMENT|認証用Functionsを作成したコンパートメント|
|FUNCTION NAME|デプロイ済みの認証用Functions|
|AUTHENTICATION TOKEN|ヘッダー|
|HEADER NAME|token|
|PATH|任意のアプリケーションパス|
|METHODS|今回は GET を指定|
|TYPE|Oracle Functions を選択|
|APPPLICATION IN xxx_COMPARTMENT|本体のFunctionsを指定|
|FUNCTION NAME|呼び出す本体のFunctions|

![apigw_auth004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110066/c1f7f773-68bd-c2ba-740c-705864b7e223.png)
![apigw_auth005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110066/b37441e0-1944-f82b-b55e-18470336a90b.png)
![apigw_auth006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110066/a01be9d1-4443-4523-0a92-34a996b4b0ac.png)

# 9. ソースコードの簡単な解説

Functions の初期化処理(FnConfigurationアノテーション)で yaml からシークレットの OCID を取得しています。

```java
    @FnConfiguration
    public void setUp(RuntimeContext ctx) throws Exception {
        config = ctx.getConfiguration();
        secret1Id = config.get("SECRET1_ID");
        String version = System.getenv("OCI_RESOURCE_PRINCIPAL_VERSION");
        if( version != null ) {
            provider = ResourcePrincipalAuthenticationDetailsProvider.builder().build();
        } else {
            try {
                provider = new ConfigFileAuthenticationDetailsProvider("~/.oci/config", "DEFAULT");
            }
            catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
```

Autonomous Database に Functions で接続する記事のソースをほぼ流用させて頂いています。OCID を基にシークレットに格納されたトークン(文字列)を取得しています。認証用Functions には前述の通りリソース・プリンシプルでシークレットの読取権限(ポリシー)を付与しています。

```java
    private String getSecret(String secretOcid) {
        try (SecretsClient secretsClient = new SecretsClient(provider)) {
            //region setting
            secretsClient.setRegion(Region.AP_TOKYO_1);
            GetSecretBundleRequest getSecretBundleRequest = GetSecretBundleRequest
                .builder()
                .secretId(secretOcid)
                .stage(GetSecretBundleRequest.Stage.Current)
                .build();
            GetSecretBundleResponse getSecretBundleResponse = secretsClient
                .getSecretBundle(getSecretBundleRequest);
            Base64SecretBundleContentDetails base64SecretBundleContentDetails =
                (Base64SecretBundleContentDetails) getSecretBundleResponse.
                        getSecretBundle().getSecretBundleContent();
            byte[] secretValueDecoded = Base64.decodeBase64(base64SecretBundleContentDetails.getContent());
            return new String(secretValueDecoded);
        } catch (Exception e) {
            throw new RuntimeException("Couldn't get content from secret - " + e.getMessage(), e);
        }
    }
```

ヘッダーのトークンとシークレットの文字列を比較して、合致した場合は正常終了の結果を返却しています。

```java
        if (secret1.equals(input.token)) {
            result = trueResult();
        } else {
            result.wwwAuthenticate = "Bearer realm=\"example.com\", error=\"invalid token\", error_description=\"token should be \"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1nHyDtTwR3SEJ3z489...\"\"";
            return result;
        }
```

結果を返却する部分は元ネタをそのまま流用しています。

https://github.com/oracle-japan/function-authorizer-for-apigw

```java
    private Result trueResult() {
        Result trueResult = new Result();
        trueResult.active = true;
        trueResult.principal = "https://example.com/users/jdoe";
        trueResult.scope = new String[]{"list:hello", "read:hello", "create:hello", "update:hello", "delete:hello", "someScope"};
        trueResult.clientId = "host123";
        trueResult.expiresAt = new Date().toInstant().plusMillis(60000).toString();
        Map<String, Object> contextMap = new HashMap<>();
        contextMap.put("email", "john.doe@example.com");
        trueResult.context = contextMap;
        return trueResult;
    }

    private Result falseResult() {
        Result falseResult = new Result();
        falseResult.active = false;
        falseResult.expiresAt = "2020-04-30T10:15:30+01:00";
        Map<String, Object> contextMap = new HashMap<>();
        contextMap.put("email", "john.doe@example.com");
        falseResult.context = contextMap;
        falseResult.wwwAuthenticate = "Bearer realm=\"example.com\"";
        return falseResult;
    }
```

# 10. PC端末 の PowerShell から API Gateway Deployment のエンドポイント(URL)をコール

API Gateway Deployment画面から作成した Deployment のエンドポイント(URL)をコピーします。
※画面からコピーできるのはPREFIXまでなので、APIGWデプロイ時のアプリケーションパスを追加しておきます。
![apigw_auth007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110066/5b090e01-b601-c0e3-5b69-72e0a623ce34.png)


PowerShell から API Gateway Deployment エンドポイント(URL) をコールしてみます。

```powershell
# Case1...No header
$headers = @{}
curl -Headers $headers https://xxxxxxxxxxxxxxxxxxxxxxxxxx.apigateway.ap-tokyo-1.oci.customer-oci.com/hello/ayuapp

# Case2...No token
$headers = @{}
$headers["type"] = "TOKEN"
curl -Headers $headers https://xxxxxxxxxxxxxxxxxxxxxxxxxx.apigateway.ap-tokyo-1.oci.customer-oci.com/hello/ayuapp

# Case3...Invalid token
$headers = @{}
$headers["type"] = "TOKEN"
$headers["token"] = "xxxxxxxx"
curl -Headers $headers https://xxxxxxxxxxxxxxxxxxxxxxxxxx.apigateway.ap-tokyo-1.oci.customer-oci.com/hello/ayuapp

# Case4...Correct token
$headers = @{}
$headers["type"] = "TOKEN"
$headers["token"] = "yyyyyyyy"
curl -Headers $headers https://xxxxxxxxxxxxxxxxxxxxxxxxxx.apigateway.ap-tokyo-1.oci.customer-oci.com/hello/ayuapp
```

以下のように、正しいトークンを指定すると本体のFunctionsの実行結果("Hello, wai ga AYU ya!")を得られました。

```powershell
PS C:\Users\AYSHIBAT> $headers = @{}
PS C:\Users\AYSHIBAT> curl -Headers $headers https://xxxxxxxxxxxxxxxxxxxxxxxxxx.apigateway.ap-tokyo-1.oci.customer-oci.com/hello/ayuapp
curl : リモート サーバーがエラーを返しました: (401) 許可されていません
発生場所 行:1 文字:1
+ curl -Headers $headers https://xxxxxxxxxxxxxxxxxxxxxxxxxx.apigateway. ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest]、WebException


PS C:\Users\AYSHIBAT> $headers = @{}
PS C:\Users\AYSHIBAT> $headers["type"] = "TOKEN"
PS C:\Users\AYSHIBAT> curl -Headers $headers https://xxxxxxxxxxxxxxxxxxxxxxxxxx.apigateway.ap-tokyo-1.oci.customer-oci.com/hello/ayuapp
curl : リモート サーバーがエラーを返しました: (401) 許可されていません
発生場所 行:1 文字:1
+ curl -Headers $headers https://xxxxxxxxxxxxxxxxxxxxxxxxxx.apigateway. ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest]、WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand


PS C:\Users\AYSHIBAT> $headers = @{}
PS C:\Users\AYSHIBAT> $headers["type"] = "TOKEN"
PS C:\Users\AYSHIBAT> $headers["token"] = "xxxxxxxx"
PS C:\Users\AYSHIBAT> curl -Headers $headers https://xxxxxxxxxxxxxxxxxxxxxxxxxx.apigateway.ap-tokyo-1.oci.customer-oci.com/hello/ayuapp
curl : リモート サーバーがエラーを返しました: (401) 許可されていません
発生場所 行:1 文字:1
+ curl -Headers $headers https://xxxxxxxxxxxxxxxxxxxxxxxxxx.apigateway. ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest]、WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

PS C:\Users\AYSHIBAT> $headers = @{}
PS C:\Users\AYSHIBAT> $headers["type"] = "TOKEN"
PS C:\Users\AYSHIBAT> $headers["token"] = "yyyyyyyy"
PS C:\Users\AYSHIBAT> curl -Headers $headers https://xxxxxxxxxxxxxxxxxxxxxxxxxx.apigateway.ap-tokyo-1.oci.customer-oci.com/hello/ayuapp


StatusCode        : 200
StatusDescription : OK
Content           : Hello, wai ga AYU ya!
RawContent        : HTTP/1.1 200 OK
                    Connection: keep-alive
                    opc-request-id: /7516FB0E1242D7AB12CE7F9DFEAACCBB/708B115FEB126E2B96E78CE9E87376C6
                    X-XSS-Protection: 1; mode=block
                    Strict-Transport-Security: max-age=3153600...
Forms             : {}
Headers           : {[Connection, keep-alive], [opc-request-id, /7516FB0E1242D7AB12CE7F9DFEAACCBB/708B115FEB126E2B96E78
                    CE9E87376C6], [X-XSS-Protection, 1; mode=block], [Strict-Transport-Security, max-age=31536000]...}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 21



PS C:\Users\AYSHIBAT>
```

# 11. まとめ

リクエストのヘッダーに文字列を埋め込むだけというとても簡易的な実装ですが、API Gateway と OCI Vault の機能で Functions を保護(認証)できました。
次回はもう少し凝った実装をしてみたいですやね。彡(^)(^)

# 12. 参考

下記の記事やマニュアルを参考にしました。

---

* APIデプロイメントへの認証と認可の追加

https://docs.oracle.com/ja-jp/iaas/Content/APIGateway/Tasks/apigatewayaddingauthzauthn.htm#Adding_Authentication_and_Authorization_to_API_Deployments

---

* 認可プロバイダ・ファンクションを使用したAPIデプロイメントへの認証および認可の追加

https://docs.oracle.com/ja-jp/iaas/Content/APIGateway/Tasks/apigatewayusingauthorizerfunction.htm

---

* Oracle Functionsを利用したAPI Gatewayの認証

https://oracle-japan.github.io/ocitutorials/cloud-native/functions-apigateway-for-intermediates/

---

* OCI API Gateway の認証・認可機能について

https://qiita.com/shukawam/items/107987bba2e44222c3aa

---

* [OCI] OCIシークレットを使ってOracle FunctionsからAutonomous DBに接続してみた。

https://qiita.com/kenwatan/items/5867a06ef6a00749dcf0

---