2014-04-06

force.comのOAuth 2.0 JWT Bearer Token Flow試してみた。

force.comのOAuthをJWT投げ入れるだけで出来るらしい(ヘルプ)ので、試してみた。

 

JWT投げ入れるだけでOAuth2.0でアクセス許可を得られると言っても

対象ユーザが既に認可しているか、管理者が事前にユーザを承認していなければ利用できません。

つまり、refresh_tokenの代替手段になります。

 

JWTではコンシューマの秘密(client_secret)の代わりに

x509証明書をアプリに登録して、Webアプリ側で登録したx509に基づく秘密鍵を使って

JWTのSignature生成を行うことで、なりすまし等の不正アクセスを防ぎます。

 

ということで、実際に進めてみます。

1. x509証明書を作成

linuxとかでこんな感じに作成します。
$ openssl genrsa 2048 > server.key
$ openssl req -new -key server.key > server.csr
$ openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt

 

2. 接続アプリケーションの作成

サンプル↓

接続アプリケーション  OpenIDConnect ~ Salesforce - Developer Edition

 

デジタル署名を使用にチェックを付けて、ファイルには1で作成した証明書(server.crt)をアップロード。

また、選択したOAuth範囲に"ユーザに代わっていつでも要求を実行"を必ず入れてください。

これを入れないと、認可しても以下のエラーが出てトークンが発行できなくなります。

{"error_description":"user hasn't approved this consumer","error":"invalid_grant"}

 

それ以外は、通常のOAuthアプリと同様に作成すればOKです。

 

3. Webアプリ側でJWTを生成する

JWTヘッダは{"alg":"RS256"}

JWTクレームセットは

"iss" → SFDCアプリケーションのClientID "prn" → SFDCのユーザ名 "aud" → https://login.salesforce.com or https://test.salesforce.com "exp" → 発行したJWTの有効期限タイムスタンプ(5分以内)

で生成します。

 

pythonのサンプルはこんな感じ。

import base64
import json
import hmac
import hashlib
import Crypto
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
import datetime
import time

def base64url_decode(input):
    input += '=' * (4 - (len(input) % 4))
    return base64.urlsafe_b64decode(input)

def base64url_encode(input):
    return base64.urlsafe_b64encode(input).replace('=', '')

header = {
    'alg':'RS256'
}

dt = datetime.datetime.now()
ts = str(int(time.mktime(dt.timetuple()))+240)

payload = {
    'iss':'client_id',
    'aud':'https://login.salesforce.com',
    'prn':'username',
    'exp':ts
}

seed = base64url_encode(json.dumps(header)) + "." + base64url_encode(json.dumps(payload))

priv = RSA.importKey(open('/home/user/work/server.key', 'r').read())

h = SHA256.new(seed)
signer = PKCS1_v1_5.new(priv)
signature = base64url_encode(signer.sign(h))

print seed + "." + signature

 

4. 既に認可しているユーザに対して生成したJWTをSalesforceに投げてaccess_tokenを取得

認可したユーザが存在しない場合は、通常のOAuth2.0のフローで事前に認可するか

接続アプリの設定で許可されているユーザを"管理者が承認したユーザは事前承認済み"にして、

対象ユーザのプロファイルを接続アプリに設定する必要があります。

接続アプリケーション  OpenIDConnect ~ Salesforce - Developer Edition (1)

 

認可されていれば、対象ユーザに対するJWTを3で作成し

https://[sfdcのドメイン]/services/oauth2/tokenに

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=[JWT token]

をPOSTしてあげればレスポンスとして

{
    "scope":"web",
    "instance_url":"https://***.salesforce.com",
    "token_type":"Bearer",
    "access_token":"Access Token"
}

が返って来ます。

 

このフローではrefresh_tokenは返って来ません。

JWTが生成できればいつでもAccessTokenを取得できるので

ユーザ名とパスワードが予めわかっているパターンと同様にユーザの認証操作が必要ないためです。(多分)

 

 

JWTベアラートークンフローでのエラー

検証中は以下のエラーが出たので、簡単にご紹介。

{"error_description":"expired authorization code","error":"invalid_grant"}

→アプリ側サーバのクロックがズレているか、expタイムスタンプが5分以上の値に設定されていたり

とにかくexpタイムスタンプが不正の場合に発生するエラー。

 

{"error_description":"invalid client credentials","error":"invalid_client"}

→RSA/SHA256じゃなくて、RSAでSignature作ってたらこのエラーでハマりました。

client_idしかクレデンシャル無いのに何でだ!って思ってけど、"証明書のキーが不正ですよ"っていうエラーみたい。

 

{"error_description":"user hasn't approved this consumer","error":"invalid_grant"}

→上述したとおり、refresh_tokenの許可をアプリ側で設定していない

あるいはユーザ自身が認可していないときに発生するエラー。

管理者がユーザを承認する場合は、対象ユーザのプロファイルが、接続アプリに対して許可されていればOK。

 

JWTベアラートークンフローの利点と活用法

・refreshトークンの管理が不要

・ユーザIDだけで認可可能

が大きいのかなと。

 

特に後者のユーザIDだけで認可可能という特性とfrontdoor.jspでログインさせる機能を使うと

シングルサインオン的な実装が容易になります。

管理者側でユーザ承認するアプリであれば、ログインIDだけで認可画面無しにログインできるし。

ログインIDだけで認証って言うと大丈夫なの?って感じだけど

セキュリティはx509証明書で担保しているから大丈夫なんですかね~。

このエントリーをはてなブックマークに追加