Snowflake External OAuthについての公式ドキュメントを読んでみた話

はじめに

Enterpriseにおいて「お前は誰か?」を確認する手段は非常に多岐にわたる。
セキュリティと絡んで手段は拡大傾向にあり、新しい認証手段への追従が求められるケースは多い。
自前で認証情報を保有、管理し、セキュリティの保証を担保した手順を用意するのは不可能に近い。
現実的には認証情報の保有と管理、および認証手段を専用のプラットフォームに移譲させたい。
実際、認証の泥臭いプロセスはIdP(Identity Provider)が面倒を見てくれる。
SnowflakeはIdPと薄く関係して、IdPによる認証結果を使い回すことができる。
SnowflakeはIdPがどういったプロセスで認証したのかは一切関与しない。

認証後、「お前にこの権限を与えて良いか?」を実装しなければならない場合、
アプリ側に機能サポートがなければ、コードでそれを保証しなければならない。
Snowflakeは、ここをExternal OAuth統合として汎化しフルにサポートしている。
具体的には、SnowflakeはExternal OAuth統合として汎化していて、
OAuth2.0認可サーバと統合し、RBACとの紐付けまでを面倒みてくれる。
RBACの最小範囲であるスキーマより細かい粒度を区別する場合でなければ、
RBACだけで区別が完了することとなり、大幅な工数削減と品質安定化を達成できる。

昔Fitbit APIのOAuth2.0フローを実装した時から始まり、
過去に何件かWebアプリ開発で認証認可まわりの実装をしたと思う。
Webアプリの認証認可F/Wはかなり枯れていて、正直中身を知らなくても書けてしまう。
開発者人口が少ないSaaSサービスであるSnowflakeがブラックボックス化した
認証認可の仕組みを読み解くのは、Webアプリのそれとは次元の違う大変さがある。
(こと認証認可の文脈では安全性の保証がセットとなるため)

Snowflake External OAuthについて厳密に調べる機会があったので、
生成AIを使わず100%自分の思考と言葉で記事を起こしていく。

認証(AuthN)

認証、つまり、Authenticationは、「お前は誰か」を確認すること。
IdPにID/PWを登録しておきID/PWを入力したりMFAを通ることで「確かに〇〇さんだ」と確認すること。
単一要素認証(SFA)、多要素認証(MFA)、パスキー認証、FIDO2認証、他、多様な認証方式がある。
またシングルサインオン(SSO)、により組織を跨ぐ連携を行うことができる。
サービス間のSSO方式としてSAML2.0、API等のSSO方式としてOIDC2.0が広く使われている。
顧客管理のIdPによる認証を本IdPに引き継ぐIDフェデレーションにより組織間認証連携を実現できる。

認可(AuthZ)

一方認可、つまり、Authorizationは、「お前にこの権限を与えて良いか」を確認すること。
認可とは「誰がどのデータにどんなルールでアクセスして良いか」をコントロールする設計パターン。
「ルール作りの設計思想」と「システム間で権限をやり取りする技術規格」がごっちゃに扱われがち
だが、レイヤが異なる2つの話を分けておくと少しわかりやすくなる。

「ルール作りの設計思想」

例えば以下のようにルールを定める。

  • ロールベースアクセス制御/Role Based Access Control
    • ユーザ個人ではなく役割に対して権限を付与しユーザをそのロールに所属させる方式。管理者権限のユーザには作成・削除を与え、一般権限のユーザには閲覧のみを与えるなど、一般的な認可方式。SnowflakeのロールモデルはまさにRBACに基づく。
  • 属性ベースアクセス制御/Attribute Based Access Control
    • ロールだけでなくユーザの所属、勤務地、アクセスする時間帯、デバイスの種類など、複数の属性(コンテキスト)を組み合わせて動的に認可を判断する方式。
「システム間で権限をやり取りする技術規格」

例えば以下のようにルールを実現する技術規格を表す。

  • OAuth2.0
    • 現在のWebで最も普及している「トークンベース」の認可フレームワーク。認可サーバーが発行した「アクセストークン(時限式のカードキー)」をアプリが提示し、リソースサーバー(Snowflakeなど)がそれを検証してアクセスを許可する。「権限の証明書」としてJWT(JSON Web Token)が実際にやり取りされる。JWTは、SON形式のデータを暗号論的に署名したもので、中身に「ユーザー名」「有効期限」、「付与されたロール(権限スコープ)」などが書き込まれている。
  • ケルベロス認証・認可 (Kerberos)
    • 主に一昔前からの 社内ネットワーク(Active Directory)環境などで広く使われている方式。チケット」と呼ばれる暗号化されたデータをやり取りすることで一度のログインで社内のファイルサーバーやプリンタなどの利用権限(認可)をシームレスに得る。

あああ

External OAuth

External OAuthは顧客のOAuth2.0認可サーバを統合してシームレスなSSOを実現する。
認証プロセスはサービス側が気にするものではなく、本機能は認可の統合であることに注意すること。

なお公式(外部 OAuth の概要)は間違いなく認証・認可と言う言葉をごっちゃにしている。
OAuth2.0はRFC6749でThe OAuth2.0 Authorization Frameworkと定義されている。
受け渡しされるトークンはOIDCのような認証トークンではなく、OAuth2.0の認可トークンである。
外部OAuthという(認可の)仕組みをSnowflakeに設定しておくことで、
「外部のIdPが認証したという証明書」をSnowflakeが安全に受け取ってデータアクセス認可する仕組みだ。

公式(外部 OAuth の概要)によると、以下に公式に対応している。
公式にない場合は、外部 OAuth 用のカスタム認証サーバーを構成するで構成できる。
なお「公式」でないからといって「非対応」ではない。「公式」になくても汎用OAuth2.0用のカスタム認証サーバーとして構成できる。

公式にはExternal OAuthのメリットとして以下が挙げられている。

  • トークンの発行を認証サーバーに委任し、発行されたトークンの管理に集中できるようになる。
  • ログイン時のセキュリティルール(MFAやIP制限、承認フローなど)を、Prj IdP側に統合できる。
  • ユーザがその認証と許可に関する厳しいルール(テスト)をクリアしない限り、IdPはトークンを発行しない。
  • 怪しいユーザはSnowflakeの入り口にすら辿り着けず、データは完璧に守られる。
  • 認証をIdPに持たせることでSnowflake側から認証情報を除去できるためセキュアになる。

一見して認証のことしか書かれていないようだが、implicitに認可が書かれている。
Snowflakeは認可をIdPに完全に移譲し、認証とセットで認可が行われたトークンを確認するだけ、
ということは、Snowflake側に認可コードを一切書くことなしに認可を実現することと同義。

External OAuthの認証部分の基本フロー

公式に基本フローの図が貼ってある。ステップ1だけ構成時にのみ行う。他は都度実行される。
最初にセキュリティ統合の構成と、アプリ内の実装が開発者側の責務となる。
ベスプラに従ってルールから逸脱しないように構成することで、後はSaaSサービス間の自動連携となる。

  1. 外部OAuth認証サーバとSnowflakeのセキュリティ統合を構成し信頼性を確立する
  2. ユーザはアプリを介してSnowflakeにアクセスしようとする。アプリはユーザを確認しようとする
  3. 認証サーバはOAuthトークンをアプリに返す
  4. SnowflakeドライバはOAuthトークンを使用して接続文字列をSnowflakeに渡す
  5. SnowflakeはOAuthトークンを検証する
  6. Snowflakeはユーザ検索を実行する
  7. Snowflakeはユーザのロールに基づいてセッションをインスタンス化する

External OAuthの認可部分、スコープ

いきなり「スコープ」というワードが出てくるが、これ、JWTの”scope”キー/バリューのこと。
OAuth2.0においてJWTで認可範囲を設定するのだ、という理解と記憶がなければ読めない。

JWTは以下のような構成となっておりscopeを格納する場所がある。
認可サーバ側で何らかの許可処理の結果、ユーザのスコープが決まり、Snowflakeに送られる。
このトークンがSnowflakeに届くと、Snowflakeはscopeキーのバリューを読み取り、
「このユーザにはST_USER_ROLEというロール(権限)を適用してセッションを始めるべき」と判断する。


{
  "iss": "https://your-project-idp.auth0.com/",
  "sub": "user_12345",
  "email": "user@client.com",
  "exp": 1719100000,
  
  "scope": "session:role:ST_USER_ROLE"  <-- 🌟これが「スコープ」
}

Okta, PingFederate, カスタムの場合は以下のパターンを使用しなければならない。

スコープ 説明
session:role:<custom_role> Snowflakeのカスタムロールにマップする。例えばsession:role:ST_USER_ROLEで、ST_USER_ROLEにマップ
session:role:public Snowflakeの PUBLIC ロールにマップ
session:role-any 外部OAuthサーバでのSnowflakeロール管理を行わない場合これを渡す。特定のロールを固定せず、そのユーザに付与されているロールであれば、ログイン後に自由に切り替えて(USE ROLEして)使って良い、という少し緩めの認可

なお、以下のビルトインロールはデフォルトではブロックされる。

  • ACCOUNTADMIN
  • GLOBALORGADMIN
  • ORGADMIN
  • SECURITYADMIN

Snowflake OAuthは、セッション中のロールのセカンダリロールへの切り替えをサポートしていないが、
External OAuthでのセカンダリロールの使用はサポートしている。

External OAuth特有のセキュリティの抜け穴と対策

Snowflakeにおいて、アカウントレベルでネットワークポリシーによりIP制限をかけていたとしても、
External OAuthと合わせて構成するSecurity Integrationを経由してログインしてくる場合、
そのユーザ個人のIP制限が無視されてしまう、という仕様がある。
つまり、IdP側のIP制限が破られたり、トークンが盗まれたりした場合、
攻撃者はどこからでもSnowflakeのデータにアクセスできてしまう状態になる。

Snowflakeは、External OAuth自体にもネットワークポリシーを直接紐づけることを推奨している。
具体的にはSecurity Integrationにネットワークポリシーを直接紐づける。
これによりIdPから届いたトークンであっても、ネットワークポリシーで許可されたIPアドレス以外からの
リクエストであれば、Snowflakeはセッションを開始しない。

これはIdPフェデレーション等で複雑化したユーザ組織の通信経路を全て把握する必要性を言っている。
こういうの、デフォルトで安全側に振って欲しいなとは思う。

カスタム認証サーバーの構成・トークンペイロード要件

カスタム認証サーバーがSnowflakeに送信するアクセストークンには、下表が含まれている必要がある。

クレーム 説明
scp Snowflake のカスタムロールを指定する文字列が含まれていること。値として session:role:ST_USER_ROLE のような Snowflake 指定の形式の文字列を、配列またはスペース区切りの文字列で必ず埋め込まなければならない。
scope 同上。IdPプロダクトによりscpかscopeのどちらかを入れる。
aud Snowflake アカウントの完全な URL(https://<アカウント識別子>.snowflakecomputing.com)が含まれている必要がある。
exp 有効時間。トークンの有効期限が UNIX タイムスタンプ(エポック秒)で刻まれている必要がある。Snowflake はトークンを受け取った瞬間の時刻とこの exp を比較します。有効期限が過去の時刻になっている(期限切れ)場合は、その時点で認可を即座に拒否する。
iss 発行者。アクセストークンを発行したプリンシパルを文字列 URI として識別。つまりトークンを発行した IdPのアイデンティティ(例: https://your-project-idp.auth0.com/)。最後のスラッシュ(/)の有無まで1文字違わず一致させる必要がある。Snowflake 側の EXTERNAL_OAUTH_ISSUER で指定した文字列と完全に一致する必要がある。
iat 発行時刻。必須。JWT が発行された時刻を識別

カスタム認証サーバーの構成・セキュリティ統合の作成

External OAuth を実現する Snowflakeのリソースの実体。
カスタム認証サーバからのアクセストークンと安全に通信して検証し、アクセストークンに
関連付けられたユーザーロールに基づいてSnowflakeへのアクセスをユーザに提供する。


create security integration external_oauth_custom
    type = external_oauth
    enabled = true
    external_oauth_type = custom
    external_oauth_issuer = ''
    external_oauth_rsa_public_key = ''
    external_oauth_audience_list = ('', '')
    external_oauth_token_user_mapping_claim = 'upn'
    external_oauth_snowflake_user_mapping_attribute = 'login_name';

それぞれの内容は下表の通り。

パラメータ 説明
EXTERNAL_OAUTH_ISSUER 外部認証サーバー(IdP)を一意に識別するURL(発行元URL)を指定する。IdPから発行されるアクセストークン(JWT)の iss クレームの値と完全に一致する必要がある。
EXTERNAL_OAUTH_JWS_KEYS_URL 外部認証サーバーが公開している、デジタル署名の検証に必要な公開鍵(JWKS)が配置されたURLを指定する。SnowflakeはこのURLにアクセスしてトークンの妥当性を検証する。
EXTERNAL_OAUTH_TOKEN_USER_MAPPING_CLAIM 外部認証サーバーが発行するアクセストークン(JWT)の中で、ユーザーの識別情報(メールアドレスやユーザーIDなど)が格納されている「キー(クレーム名)」を指定する。
EXTERNAL_OAUTH_SNOWFLAKE_USER_MAPPING_ATTRIBUTE トークンから抽出したユーザー識別情報を、Snowflake側の USER オブジェクトのどの属性(EMAIL_ADDRESS または LOGIN_NAME)と一致させるかを指定する。

カスタム認証サーバーの構成・テスト

公式では、最短パスで構成を検証するため、curl で HTTP Post を送る手順が書かれている。

  1. IdP側にテストユーザを作成しておく。テストユーザはパスワードを持つ必要がある
  2. Snowflake側にも、上記と同じメールアドレス(または識別子)を持つ USER オブジェクトを事前に作っておく。login_name, または emailでマッピングする
  3. IdP側の画面でこのテスト用のアカウントを作成し、専用のClientID, ClinetSecretを取得する
  4. 次のように、 OAuth 2.0クライアントがカスタムトークンエンドポイントに POST リクエストすることを許可
    • OAuth 2.0の用語でいう grant_type = password(Resource Owner Password Credentials Grant)方式を使うこと。すなわち「リソース所有者に設定された付与タイプ」であり、アプリ画面を介さず、ユーザーのID/PWを直接リクエストに含めてトークンを即時発行してもらう、テスト専用の最短ルートを構築する。
    • 準備で用意したclientID と clientSecretをHTTP Basic認証ヘッダーに含めること
    • リクエストのBody(送信データ)には、FORM形式(application/x-www-form-urlencoded)で、テストユーザーのID/PWと、Snowflakeに渡したいスコープを指定すること

curl -X POST -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" \
  --user : \
  --data-urlencode "username=" \
  --data-urlencode "password=" \
  --data-urlencode "grant_type=password" \
  --data-urlencode "scope=session:role:analyst" \
  

公式対応認証サーバーと非公式(カスタム対応)の違い

公式対応認証サーバーと、非公式(カスタム対応)の違いをまとめてみる。

ケース1:IdPの「署名用公開鍵」がローテーション(変更)されたとき

JWT(トークン)が偽造されていないかを証明するための「公開鍵」は、
セキュリティ担保のために数ヶ月ごとに自動で新しいものにローテーションするのが一般的。

公式対応の場合、SnowflakeがOkta側の鍵更新スケジュールや新しい公開鍵の
取得先をあらかじめ知っているため、Snowflake側が自動で追従する。
開発者は何のアクションも起こす必要はなく、システムは止まらない。

カスタム、つまり非公式の場合であっても基本的には指定したURL
(.well-known/jwks.json)を見に行ってくれるので自動追従するが、
もしIdP側のメジャーアップデート等で「公開鍵を配置するURLの仕様そのもの」
が変わった場合は、Snowflakeの設定パラメータ(EXTERNAL_OAUTH_JWS_KEYS_URL)
を開発者が手動で新しいURLに書き換えるまで、認証・認可がすべてエラーになってシステムが停止する。

IdP側のセキュリティ仕様やエンドポイントの仕様が変更されたとき

近年、サイバー攻撃の高度化に伴い、IdP側(OktaやMicrosoftなど)がトークンの発行ルールや、
検証用APIの仕様(プロトコル)をより安全なものへ強制アップデートすることがある。

SnowflakeはOktaやMicrosoftと強固な技術パートナーシップを結んでいるため、
IdP側の仕様変更がリリースされる前に、Snowflake側の「特急レーン(専用プログラム)」を
事前にアップデートして追従させる。そのため、開発者がコードや設定を修正することなく、
シームレスに新しいセキュリティ基準へ移行できる。

カスタム、つまり非マネージドの場合、Snowflakeは「汎用的なOAuth 2.0の標準規格(RFC)」に
準拠していることしか保証しない。そのため、IdP側が独自のセキュリティ拡張を行ったり、
標準規格の解釈を変更したりした場合、トークンのペイロード構造(キー・バリュー)が変わり、
Snowflakeがトークンを解読できなくなるリスクがある。
この場合、開発者がIdP側の設定を手動で修正して追従する必要がある。

まとめ

SnowflakeにおけるExternal OAuth統合の仕組みを「認証」と「認可」のレイヤを分離して読んでみた。
認証・認可を完全にIdPに移譲し、Snowflakeアプリケーション内で一切の認可コードを書かずに済む。

数あるIdPのうち、いくつかについてはSnowflakeが公式対応している。
公式IdP構成はテクノロジーパートナーシップに基づき、Snowflakeのマネージド構成の一部として、
Snowflake側がIdP側の変更に自動追従する可能性が高い。結果としてダウンタイムの発生を回避できる。

公式対応IdPでなくても、OAuth2.0 RFC準拠の認証サーバとしてカスタム連携することができるが、
SnowflakeがIdP側の変更に自動追従する性質ではなく、運用者・開発者がIdP側の変更に適用する必要がある。