ねら~ITエンジニア雑記

やきうのお兄ちゃんが綴るOracle Databaseメインのブログ

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

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

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

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

https://github.com/gonsuke777/Functions

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

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

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

cd Functions/hello-java/
vi vi 
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編集後にビルド&デプロイします。

fn -v deploy --app ayu-functions1

デプロイされた Functions の OCID を控えておきます。 apigw_auth003.png

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

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

  • 動的グループ(ayu-dynamic-group2)のマッチングルール ※Functions の OCID を指定
All {resource.id = 'ocid1.fnfunc.oc1.ap-tokyo-1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'}
  • 動的グループ(ayu-dynamic-group2)に付与したポリシー(権限) ※シークレットの読み取り権限を付与
allow dynamic-group ayu-dynamic-group2 to read secret-family in compartment xxxxx_compartment
  • OCI Vault のポリシー(権限)をコンパートメントに付与
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 を指定
ALL {resource.type = 'ApiGateway', resource.id = 'ocid1.apigateway.oc1.ap-tokyo-1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'}
  • 動的グループ(ayu-dynamic-group1)に付与したポリシー(権限) ※API Gateway に Functions の実行権限を付与
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 の動作確認をしてみます。

# 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

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

$ 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 apigw_auth005.png apigw_auth006.png

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

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

    @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 には前述の通りリソース・プリンシプルでシークレットの読取権限(ポリシー)を付与しています。

    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);
        }
    }

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

        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

    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

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

# 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!")を得られました。

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


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