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. 接続アプリケーションの作成
サンプル↓
デジタル署名を使用にチェックを付けて、ファイルには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のフローで事前に認可するか接続アプリの設定で許可されているユーザを”管理者が承認したユーザは事前承認済み”にして、
対象ユーザのプロファイルを接続アプリに設定する必要があります。
認可されていれば、対象ユーザに対する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証明書で担保しているから大丈夫なんですかね~。