JWT(Json Web Token)を利用するためのOAuth 2.0拡張仕様での認可をGolangでやってみる

こんにちは。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 とは

datatracker.ietf.org

JWTを秘密鍵で署名し認可フローとしてOAuth2.0のAccessTokenを要求します。

秘密鍵を利用して認可ができることから、OPTiM Cloud IoT OSではユーザIDやパスワードの入力が出来ないサーバやデバイスなどの認可に用いられます。 これらを利用するには事前に公開鍵を登録しておく必要があります。

おおまかなフロー

https://cdn-ak.f.st-hatena.com/images/fotolife/o/optim-tech/20220107/20220107165841.png

  • 事前に鍵ペアを作成、登録をしておく
    • サービスによっては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やその他の言語でもできるように整えて行きたいと思います。

では、また今度。


オプティムでは、一緒に働く仲間を募集しています。興味のある方は、こちらをご覧ください。

www.optim.co.jp