GridDBでAuthlibを実装する Part II

はじめに

以前のブログで、GridDBでauthlibを実装する方法について詳しく説明しました。そのブログはこちらからご覧いただけます。そのブログでは、OAuthの概念を紹介し、認証ライブラリの使用と実装の基本的な作業フローを説明しました。

今回のブログでは、より現実的なユースケースを実証するために、実際のブラウザ内でAuthlibを使用する方法について紹介します。また、クライアントIDとシークレットを生成し、その情報を直接GridDBインスタンスに保存する方法も紹介します。

このブログでは、GridDBを実行pythonクライアントがあることを前提にしています。

最初のステップ

まず始めに、testというデフォルトのユーザーを作成し、同名のパスワードを設定しておきます。次に、以下のコマンドでサーバーを立ち上げます。FLASK_APP=app.py flask run --host=0.0.0.0 ブラウザを開くと、ログインフォームが表示されます。テストユーザーとパスワード(user=test, password=test)を使ってログインし、トークンを取得します。ブラウザはトークンを保存します。これについては後で説明します。

新機能

クライアントID、クライアントシークレットの概要

次に、クライアントを作成するボタンをクリックすると、クライアントIDとクライアントシークレットを作成するためのページに移動することができます。クライアントとクライアントシークレットの簡単な説明は、こちらを参照してください。アプリやapiを使用する開発者がクライアントIDとクライアントシークレットを使うことで、アタッカーからアプリを保護することができます。

OAuthシステムでは、アプリケーションやウェブサイトがユーザー名とパスワードを使ってログインしようとする際に、アプリケーションにOAuthサーバーによって生成されたクライアントIDの提供を求めます。これは二重認証と呼ばれ、単純なユーザー名とパスワードのシステムよりもセキュリティのレイヤーを追加するものです。

他のコンテナ(TOKENSなど)については前回のブログで説明しましたが、今回のブログではCLIENTSコンテナを紹介します。これは比較的シンプルなコレクションコンテナで、クライアント ID、名前、グラントタイプなどを格納します。このコンテナに行を入れる方法については、後のセクションで説明します。

コンテナスキーマは以下のようになります。

        conInfo = griddb.ContainerInfo("CLIENTS",
            [["client_id", griddb.Type.STRING],
             ["client_name", griddb.Type.STRING],
             ["client_uri", griddb.Type.STRING],
             ["grant_types", griddb.Type.STRING_ARRAY],
             ["redirect_uri", griddb.Type.STRING_ARRAY],
             ["response_types", griddb.Type.STRING_ARRAY],
             ["scope", griddb.Type.STRING],
             ["token_endpoint_auth_method", griddb.Type.STRING],
             ["client_secret", griddb.Type.STRING]],
            griddb.ContainerType.COLLECTION, True)

このブログで見せるpython Flaskサーバーは、コード自体にすでに組み込まれているデフォルトのクライアントIDをあらかじめロードしています。これにより、一種の概念実証(PoC)として、ユーザは認証システムから有効なトークンをすぐに取得することができます。しかし、これが実際のアプリケーションであれば、ログインしようとしたときに、有効なクライアントIDがサーバーに渡されるまで、トークンは生成されずに、サーバーはエラーコードで応答するでしょう。

新しいクライアントIDとシークレットを作成するには、使用したいURIを入力します。グラントタイプには password を入力します。これで、ユーザーがアプリケーションの API やデータに適切にアクセスするためのトークンを生成するための、シンプルなユーザーIDとパスワードを設定することができます。

ユーザー認証情報の保存と照会

Cookie

クライアントIDとシークレットの機能と共に、ユーザーのトークンとステータスを保存するブラウザCookieが新たに導入されました。Cookieは、ユーザーの現在のセッションについてクライアント側(つまりブラウザ)に保存されるデータのパケットです。これは、お気に入りのウェブサイトを訪れたときに、好みの設定や履歴を保存するために使われる仕組みです。

今回の実装では、ユーザがログインすると、トークンがGridDBに保存され、さらにブラウザにCookieとして保存されます。ブラウザでテストするには、開発ツールを開き、「Application」セクションに移動して、Cookieセクションを見つけます。その中に、クライアントID、シークレットとユーザ名、パスワードの組み合わせでログインした際に割り当てられたトークンがあります。

GridDBをバックエンドとして実際のWebサイトを構築する場合の手順がスムーズに進むよう、Cookieの利用をお勧めします。Cookieを使えば、ユーザは面倒な手続きなしに以前の状態に戻ることができます。また、アプリケーションに明示的に指示しない限り、認証情報を保存し、既にログインしている状態にすることもできます。

次に、クライアントIDとシークレットがどのように作成され、GridDBサーバに保存されるかを見てみましょう。

クライアントID、シークレットの保存と照会

クライアント ID と シークレットの機能は create_client() 関数に由来し、 routes.py ファイルにある同名のルートにあります。最初に、この関数はエンドポイントにリクエストを行おうとしている現在のユーザーがログインしており、有効なトークンを持っているかどうかを確認します。トークンの実際のクエリについては、次のセクションで詳しく説明します。

次に、HTTPリクエストの種類をチェックします。もし GET リクエストであれば、前述したウェブページでクライアント ID とシークレットの組み合わせを作成するために使用する create_client.html をレンダリングします。もし POST リクエストであれば、GET リクエストページからユーザーの情報をもとにクライアント ID とシークレットを生成して保存します。

POST リクエストでクライアント ID に対応する一意のコードを生成し、 issued_at 値も生成します。次に、client_secret を (gen_salt を使って) 生成し、変数に保存します。これでクライアントIDとシークレットが生成されたので、GridDBデータベースに入力できるようになりました。

コレクションコンテナのスキーマがについては、ここまでで説明しました。次に、そのコンテナに入れるには、以下のようになります。

    cn.put([client_id, form["client_name"], form["client_uri"], split_by_crlf(form["grant_type"]),
            split_by_crlf(form["redirect_uri"]), split_by_crlf(form["response_type"]), 
            form["scope"], form["token_endpoint_auth_method"], client_secret])

ほとんどの値はユーザーがウェブページで直接入力したものですが、クライアント ID と シークレットは werkzeug.securitygen_salt 関数を使用して生成しました。

機能全体は以下のようになります。

@bp.route('/create_client', methods=('GET', 'POST'))
def create_client():
    user = get_session_token()
    if not user:
        return redirect('/')
    if request.method == 'GET':
        return render_template('create_client.html')

    client_id = gen_salt(24)
    client_id_issued_at = int(time.time())

    
    form = request.form

    if form['token_endpoint_auth_method'] == 'none':
        client_secret = ''
    else:
        client_secret = gen_salt(48)

    conInfo = griddb.ContainerInfo("CLIENTS",
        [["client_id", griddb.Type.STRING],
         ["client_name", griddb.Type.STRING],
         ["client_uri", griddb.Type.STRING],
         ["grant_types", griddb.Type.STRING_ARRAY],
         ["redirect_uri", griddb.Type.STRING_ARRAY],
         ["response_types", griddb.Type.STRING_ARRAY],
         ["scope", griddb.Type.STRING],
         ["token_endpoint_auth_method", griddb.Type.STRING],
         ["client_secret", griddb.Type.STRING]],
        griddb.ContainerType.COLLECTION, True)

    cn = gridstore.put_container(conInfo)

    q = cn.query("select * where client_name = '"+form['client_name']+"' or client_uri = '"+form['client_uri']+"'")
    rs = q.fetch(False)
    if rs.has_next():
        return render_template('create_client.html', error = "Duplicate")

    cn.put([client_id, form["client_name"], form["client_uri"], split_by_crlf(form["grant_type"]),
            split_by_crlf(form["redirect_uri"]), split_by_crlf(form["response_type"]), 
            form["scope"], form["token_endpoint_auth_method"], client_secret])

    return render_template('create_client.html', client_id=client_id, client_secret=client_secret)

クライアントIDとシークレットの確認

models.pyファイルには、クライアント ID とグラントタイプの真偽を問い合わせるための関数も含まれています。この2つの関数は、check_client_secretcheck_grant_type という名前になっています。

グラントタイプは、クライアントアプリケーションにトークンを付与するためのメソッドと考えることができます。このブログでは、簡単にパスワードのグラントタイプを使用することにします。これは、単にユーザー名とパスワードの組み合わせで、サーバーから特別なトークンを付与されることを意味します。

CLIENTS コンテナで使用されているグラントタイプを確認するクエリは、次のようになります。

            cn = gridstore.get_container("CLIENTS")
            q = cn.query("select * where client_id = '"+self.get_client_id()+"'")

これは client_secretを確認するのと同じ方法です。クエリーは以下のようになります。

            cn = gridstore.get_container("CLIENTS")
            q = cn.query("select * where client_id = '"+self.get_client_id()+"' and client_secret='"+client_secret+"'")

値がデータベース内に存在する場合は True を、存在しない場合は False を返します。

ユーザトークンの問い合わせ

先に述べたように、ユーザのトークンはブラウザに保存されますが、この値がバックエンドでチェックされ、検証されない限り、何の意味もありません。その検証のために、認証サーバは get_session_token() と呼ばれる関数を呼び出します。

その機能は以下の通りです。

def get_session_token():
    print("Session=",session)
    if 'token' in session:
        try:
            cn = gridstore.get_container("TOKENS")
            print("Session: " + session['token'])
            token = session['token']
            que = "select * where token = '"+token+"'"
            q = cn.query(que)
            print(session['token']) 
            q = cn.query("select * where token = '"+session['token']+"'")
            rs = q.fetch(False)
            print(rs.has_next())
            if rs.has_next():
                return session['token']
        except griddb.GSException as e:
            for i in range(e.get_error_stack_size()):
                print("[", i, "]")
                print(e.get_error_code(i))
                print(e.get_location(i))
                print(e.get_message(i))
                print ("ISSUE: ")
                return None
    return None

この関数は、ユーザーが保護されたエンドポイントにアクセスしようとするたびに呼び出され、基本的に有効なトークンを強制的にチェックすることができます。この関数が最初に行うことは、ユーザがブラウザに保存された “token” というデータのパケットを持っているかどうかを確認することです。もしユーザーがエラーなくログインしていれば、tokenというセッションパッケージが存在するはずです。

次に、GridDBに対して簡単な問い合わせを行い、ユーザがクライアントサイドで保存したトークンとGridDBに保存されているトークンが一致することを確認します。一致した場合、関数はトークン・データを返して、残りの認証保護されたサービスは許可され、通常通り実行されます。

まとめ

今回はOAuthを使ったGridDBの使い方を紹介しました。GridDBインスタンスを他のアプリケーションと共有することがいかに簡単であるかということがお分かりいただければ幸いです。

ブログの内容について疑問や質問がある場合は Q&A サイトである Stack Overflow に質問を投稿しましょう。 GridDB 開発者やエンジニアから速やかな回答が得られるようにするためにも "griddb" タグをつけることをお忘れなく。 https://stackoverflow.com/questions/ask?tags=griddb

Leave a Reply

Your email address will not be published. Required fields are marked *