こんにちは。AI・IoTサービス開発部の青木です。
最近はWebフレームワークやチームビルディング系をやっていますが、 元々はIoTエンジニアとしてのキャリアを積むべくIoTに関わるプログラミングをメインでやっていました。
今回はOPTiM Cloud IoT OSや、OPTiM IoTのデバイス認証として用いられる JSON Web Token (JWT) Profile for OAuth 2.0 を利用して実際に認証を行ってみたいと思います。
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer とは
JWTを秘密鍵で署名し認可フローとしてOAuth2.0のAccessTokenを要求します。
秘密鍵を利用して認可ができることから、OPTiM Cloud IoT OSではユーザIDやパスワードの入力が出来ないサーバやデバイスなどの認可に用いられます。 これらを利用するには事前に公開鍵を登録しておく必要があります。
おおまかなフロー
- 事前に鍵ペアを作成、登録をしておく
- サービスによってはResource Server側で鍵ペアを作成し秘密鍵をダウンロードするケースや、デバイス側でペアを作成しResource Serverへ公開鍵を登録するものなどがあります。
- JWTの作成、署名を行う
- JWTを用いたアサーションを指定し、Token Requestを行う
- Authorization Serverはトークンリクエストの内容を検証し、正当な場合はAccess Tokenを返却する
- Access Tokenを用いてリソースへアクセスする
Golangでやってみる
各所解説する前に、まず全体のソースを書いておきます。 とりあえずの実装なので、エラー時はすべてFatalになっていたりと雑な部分がありますので コピペで実装される場合は注意してください。
package main import ( "io/ioutil" "log" "net/http" "net/url" "strings" "time" jwt "github.com/dgrijalva/jwt-go" "github.com/google/uuid" ) func main() { privateKeyPath := "./jwtRS256.key" // 秘密鍵 Path clientID := "" // Resource Serverに公開鍵を登録した際に発行されるClientID URL := "https://{{FQDNを指定}}/connect/token" // Token取得用のURL scope := "user" // アクセスしたいリソースのスコープ grantType := "urn:ietf:params:oauth:grant-type:jwt-bearer" // Grant Typeを指定 `urn:ietf:params:oauth:grant-type:jwt-bearer`固定 // Read Private Key privateKeyDataPem, err := ioutil.ReadFile(privateKeyPath) if err != nil { log.Fatal(err) } // ParseRSAPrivateKeyFromPEM privateKeyData, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyDataPem) if err != nil { log.Fatal(err) } // Create JWT Payload // サービスによってIssuerなどの指定が異なる場合があるのでドキュメントを要チェック jwtDate := time.Now() claims := jwt.StandardClaims{ Issuer: clientID, Subject: clientID, Audience: URL, IssuedAt: jwtDate.Unix(), ExpiresAt: jwtDate.Add(3 * time.Minute).Unix(), Id: uuid.New().String(), } // sign by secret key token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) tokenString, err := token.SignedString(privateKeyData) if err != nil { log.Fatal(err) } tokenString = url.QueryEscape(tokenString) // Get Token Request : HTTP Request requestBody := "grant_type=" + grantType + "&client_id=" + clientID + "&assertion=" + tokenString + "&scope=" + scope req, err := http.NewRequest("POST", URL, strings.NewReader(requestBody)) if err != nil { log.Fatal(err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") client := &http.Client{} res, err := client.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() responseBody, err := ioutil.ReadAll(res.Body) }
秘密鍵の読み込み
ここで秘密鍵(PEM形式)を読み込んでいます。 このあとに秘密鍵による署名を行います。
// Read Private Key privateKeyDataPem, err := ioutil.ReadFile(privateKeyPath) if err != nil { log.Fatal(err) } // ParseRSAPrivateKeyFromPEM privateKeyData, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyDataPem) if err != nil { log.Fatal(err) }
JWTのPayloadの作成
IssuerやSubjectなど、指定方法は各サービスによって異なる場合があります。 OPTiM Cloud IoT OS では、以下のように双方OAuth Clientの Client IDとなります。
ExpiresAt
も、サービスによって推奨値などがありますが、今回は3分間の有効期限とします。
Access Tokenの有効期限ではないので、短いに越したことは無いでしょう。
claims := jwt.StandardClaims{
Issuer: clientID,
Subject: clientID,
Audience: URL,
IssuedAt: jwtDate.Unix(),
ExpiresAt: jwtDate.Add(3 * time.Minute).Unix(),
Id: uuid.New().String(),
}
署名を行う
RS256
というアルゴリズムで秘密鍵を利用しJWTを署名します。
本来であればJWTのHeaderにRS256などの指定をしなければなりませんが、 jwt-go
ではHeaderを自動で付与してくれます。
生成されたJWTをAccessTokenの取得に利用します。
// sign by secret key token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) tokenString, err := token.SignedString(privateKeyData) if err != nil { log.Fatal(err) }
Access Tokenのリクエスト、及び取得
こちらもサービスによって差がある部分ですので、それぞれのサービス仕様を確認しましょう。 このサンプルコードでは OPTiM Cloud IoT OS の場合に利用できるものとなります。
// Get Token Request : HTTP Request requestBody := "grant_type=" + "&client_id=" + clientID + "&assertion=" + tokenString + "&scope=" + scope req, err := http.NewRequest("POST", URL, strings.NewReader(requestBody)) if err != nil { log.Fatal(err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") client := &http.Client{} res, err := client.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() responseBody, err := ioutil.ReadAll(res.Body)
まとめ
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
は当初思っていたよりも簡単に実装できちゃうところもあり、サンプルコードが少ないと感じていました。
今回はGolangで行いましたが、Javascript,Typescriptやその他の言語でもできるように整えて行きたいと思います。
では、また今度。
オプティムでは、一緒に働く仲間を募集しています。興味のある方は、こちらをご覧ください。