[node] Sign with PKCS 12 (.p12) key
雖然標題是寫 sign with P12 Key ,
不過其實內文是要寫如何 pass google OAuth with JWT 。
其中有一個 step 是 sign with P12 Key,
因為 Google OAuth 給的 Key 是 PKCS 12 的,
如何正確的用他進行簽章這件事情我走了不少冤枉路。
最近搞這東西搞了超過16小時,雖然說是自己做著玩得 project ,
但被認證這麼基本的東西擋在門外,整個是很焦慮。XD
---------------------------------
這一切的起源都是因為我想用 NodeJS 連 Google Calendar API,
自己新增跟更新特定 Calendar 上的 Calendar Event 。
---------------------------------
---------------------------------
我們走 Google OAuth 2.0 ,說到 OAuth 有跟 FB API 打交道過的,
應該都不會太陌生,你需要給他 scope 跟一些有的沒的相關資料,
然後你會取得一個 accessToken ,接著你就可以拿這 accessToken 去作壞事。
讀完 API,首先當然就是要先取得 accessToken
首先我先讀了 Google Developer Doucment ,
其中有好幾種作法。(詳情看下面連結的左側選單)
http://goo.gl/qUZEL
後來決定採用 Service Account,也就是說我自己 server 就是個 user ,
自己跟 server 要 accessToken ,而不是讓第三方使用者,
自己導向他們網站驗證取得 accessToken 的作法。
(理由單純是因為想自己測試用,要驗證很煩;
另一方面是想這種作法他們是怎麼搞驗證的。 )
---------------------------------
@首先先到 Google API Console 申請一個 API Key
先 Create Proejct ,然後選 API Access ,
然後選擇開 client、類別選 Service account。
https://code.google.com/apis/console/b/0/
接著你就會拿到一個 Google 說他們不會保存的 Private key file,
是 PKCS 12 結尾的(副檔名 .p12 )。
另外你會拿到一個密碼(key password),要記住有這回事,後面會用到,
這個密碼叫做 "notasecret" ,應該是固定的。
這個檔案要好好保存,掉了的話就要重新申請一把了。
然後你會看到這些資訊,
http://screencast.com/t/Q8rDbn5AR
包含 ClientID,Email Address ,Public key fingerprints,
其中我們會用到的主要是 Email Address。
萬一你把 P12 Key 弄丟了也可以來這右邊點選 Generate new key。
@JWT
前面的文件其中提到要組一個叫 JWT 的東西,什麼是 JWT 呢,
他全名叫 JSON Web Token ,主要就是用來作線上驗證用的。
目前採用他的,根據我這兩天 Google 看到的,除這個 google OAuth 以外,
還有 Apple 的 In App Purchase 跟 Google 的電子錢包等。
JWT 到底是東西,我一開始根本就沒理他,反正欄位文件都有了,我就照著填,
我就是玩遊戲不看說明書的那種人啦(咦),
不過有興趣的人還是可以詳閱文件,瞭解其為何可以作為安全性的驗證。
http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html
@先試著照文件組組看 JWT
文件還是這篇 http://goo.gl/qUZEL
A JWT is composed as follows:
{Base64url encoded header}.{Base64url encoded claim set}.{Base64url encoded
signature}
注意其中有一個點(.),另外這裡都是 Base64 編碼。
@JWT Header
JWT Header 在 Google OAuth 上是固定的,寫死的,
這個文件上有給了,不用懷疑照著作就對了。
var header = JSON.stringify({
"alg": "RS256",
"typ": "JWT"
});
我是偷懶用 JSON.stringify 直接轉字串,方便閱讀也方便修改。
接著注意到他要做 Base64 Encode,對字串要做 base64 轉換很簡單,
透過 Buffer 作就好。
new Buffer(header).toString("base64")
@JWT Claim Set
一樣,照著文件寫,這裡開始有玄機了。
var claims = JSON.stringify({
"aud": "https://accounts.google.com/o/oauth2/token",
//到期時間(單位是秒,所以getTime 是 ms 要除 1000),
//加上 3600 是指一小時以後過期,這是 Google 允許的最大時間
"exp": parseInt(new Date().getTime()/1000,10) +3600,
//申請時間
"iat": parseInt(new Date().getTime()/1000,10),
//申請者,這裡請用 Api console 那邊看到的 email address
"iss": "xxxxxxxxxxx@developer.gserviceaccount.com",
//Scope ,看你想申請用什麼服務就寫什麼
//這個不太好找,請自行努力,這裡只附上 Calendar 的 scope。
//我是從 http://goo.gl/7pbtn 下面 try it,
//右方有 Authr using OAuth 2.0 點下去看到的。
"scope":"https://www.googleapis.com/auth/calendar"
});
照理說 Claim Set 應該沒問題,只是填資料而已,要特別小心手誤,
很多人會把 iss 填成 client id ,要填的是前面取得的 email address。
一樣要做 Base64 Encoded。
new Buffer(claims).toString("base64")
@Computing the Signature
這個才是大魔王,非常麻煩的東西,也是本文重點,
他要先把前面做出來的兩個元素,透過你取得的 p12 Key 進行簽章(sign)。
幾件事情要注意:
1.用來簽章的是已經 base64 encode 的 header 跟 claims,
別傻傻的用原本的 json 字串來簽。
2.header 跟 claims 中間有一個點(.),但結尾沒有點。
var content = encodedHeader+"."+encodedClaims;
這裡步驟比較複雜,我們一步一步來:
A.安裝 crypto ,這樣你才能進行加解密相關操作。
B.你拿到的是 PKCS 12 (P12) 的 key ,
很不幸的是 nodejs 似乎沒有直接處理 p12 的 client,至少我沒搜到。
所以你得先找台有 open ssl 的機器(linux base的通常都有),
先把 private key 的部份以 RSA 的形式繪出。
(或者傻瓜點的講法,把 P12 轉 Pem。)
指令是
openssl pkcs12 -in my-privatekey.p12 -out privatekey.pem -nodes
-nodes 是指建立 privatekey 時,不需要再用 passphase進行加密。
把 my-privatekey.p12 改成你從 google 拿到的那個檔案的檔名,
privatekey.pem 則是即將建立的 pem 檔案的檔名。
這時候他會問你 password ,請輸入 "notasecret",
順利的話,這時你會取得 privatekey.pem
C.簽章
這件事情很單純,透過 crypto 就行了,範例碼附於後,
但要記得先把 prviatekey.pem 放到同資料夾下:
var fs = require("fs"),
crypto = require("crypto");
function sign(content) {
var key = fs.readFileSync('privatekey.pem');//讀pk,直接當key用
var sign = crypto.createSign('RSA-SHA256');//指定演算法
sign.update(content); //你要簽的內容
var sig = sign.sign(key,'base64');
//進行簽章動作,並指定為 base64輸出
return (sig);
}
var signed = sign(content);
//把之前組好的 header 跟 claims 拿來簽,
//最後再把相關資料一起加起來就大功告成了。
var jwt = content +"."+signed;
//到這一步就可以先驗證 JWT 是否能正確讀取
//https://developers.google.com/in-app-payments/docs/jwtdecoder?hl=zh-tw
最後再作 Post 給 'https://accounts.google.com/o/oauth2/token'
這裡我用的是 restler ,我想這應該對寫 node 的人,
不會有太大障礙才對,也可以試試自己用的方法。
var rest = require('restler');
rest.post('https://accounts.google.com/o/oauth2/token', {
data:{
grant_type:"urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion:jwt
}
}).on("complete", function(data, response) {
console.log(data); //順利的話會看到 access_token ,不然就是錯誤訊息
});
btw base64 Url Safe 的處理,作不作都無所謂,
我測過了都會過,post 通常不需要處理這個。
-------------------------------------------------
過關之後就會覺得好像很簡單,順便寫一下我卡關卡在哪:
1.找怎麼 sign 這件事情 google 很久,才找到一個可以用的範例。XD
然後我其實是一直覺得應該會有人寫好 p12 client 的,
不是很想自己作轉換 pem 的動作,
加上我又懷疑 pem 轉過去後,是不是會有變化導致我沒簽過。
這件事情一直到後來我去找 Google API Java Client ,
直接看他在 Java 世界怎麼作跟怎麼發,用他作為對照組交叉測試很多次之後,
才確定 P12 裡面的 PK 跟轉出來的 pem 是同樣的無誤。
找 pk12 client 這件事情花不少時間,
然後一直找到 node tls 的參考資料,但那完全就是不同事。Orz
2.我一直不確定 SHA256withRSA 是不是等同於 RSA-SHA256 ,這是因為基礎知識不足。
3.我打從一開始就犯一個要命的致命錯誤,因為我寫一個函式幫助我轉換 base64,
function toBase64(obj) {
return new Buffer(obj).toString("base64");
}
然後前面兩個 claim 跟 header 運作很正常,
測出來的資料也就對,我就太相信他了。
另一方面是我第一個找到的 sign 的 sample 是這樣的
var key = fs.readFileSync('privatekey.pem');//讀pk,直接當key用
var sign = crypto.createSign('RSA-SHA256');//指定演算法
sign.update(content); //你要簽的內容
var sig = sign.sign(key);
return (sig);
我根本沒注意到說這裡可以改成出 base64,
另一方面因為我知道 buffer 也可以用來接 binary,
我想說好吧,那就寫 var sig = toBase64(sign.sign(key));
從這裡開始就全錯了......Orz
不是不能這樣寫,只是如果要這樣作的話,要明確指定進入的型別,
像是 var sig = new Buffer(sign.sign(key),"binary").toString("base64");
這樣就會對。
可是最要命的事情是他還是可以產出 base64 的結果,
我猜大概是拿 toString() 的結果直接去做的吧。
我錯過這個檢核點非常多次,一直懷疑是 JWT content 的問題,
最後我終於發現是 sign 的 issue ,
我用 Java 世界可以正常通過的 code 直接簽 "hello world",
過來 nodejs 簽 "hello world" 發現不一致,
過來交叉測試大概花了三個小時,才發現這個低級錯誤。
學到的教訓就是 Buffer 進出的型別要講得很清楚,
不然就會出這種無聲的錯誤。Orz
網路上教學如何用 NodeJS 作 JWT 的資料真的很少,
希望這個資料可以對大家有幫助。囧rz
--
P12 = PKCS 12 ,是 public key + private key + password 的 keystore;
PEM = X.509 cert ,public key + private key 的 key pair
其中 key 分為 RSA key 跟 DSA key 等不同類型。
--
Life's a struggle but beautiful.
--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 1.34.116.11
※ 編輯: TonyQ 來自: 1.34.116.11 (11/17 15:35)
推
11/17 15:39, , 1F
11/17 15:39, 1F
※ 編輯: TonyQ 來自: 1.34.116.11 (11/17 15:51)
推
11/17 23:47, , 2F
11/17 23:47, 2F
推
11/24 12:26, , 3F
11/24 12:26, 3F