AllIsHackedOff

Just a memo, just a progress

Firebase Authから返ってくるJWTをゴニョゴニョする (2)

前回の記事 allishackedoff.hatenablog.com の続きです。引き続きゴニョゴニョします。

JWTをDecodeする

golangのJWTのライブラリは github.com が最も使われているようです。 otiai10.hatenablog.com christina04.hatenablog.com この辺りでも利用されているようで、今回トライする際に参考にさせて頂きました。

Firebase Authから返ってくるJWTを検証する

検証するにはヘッダーをdecodeしないといけません。 headerに含まれる kid に対応するCERTIFICATEを使ってtokenが正しいかどうかを検証します。 「文字列としてcertificateを渡せばよしなにやってくれるやろ」とか甘いことを考えていましたが、しっかりrsa.PublicKey型のkeyを抜き出してやらないと verifyできません。

検証の流れは下記です。

  • JWTのヘッダーをDecode
  • https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com からcertificateのリストを取得
  • 取得したcertificateのリストから、 ヘッダーの kid に対応するkeyを見つける
    • 見つからない場合はexpireしている
  • 見つけたkeyをx509 パッケージを利用して rsa.PublicKey 型の変数にする
  • Tokenをdecode
  • decodeで生成されたjwt.Token Objectの Validを見る
    • expireしている / publickeyがうまく取得できていない場合は token.Valid == false になる

ソースコード (in Golang)

package main

import (
    "encoding/json"
    "encoding/pem"
    "errors"
    "fmt"
    "log"
    "net/http"
    "strings"

    "crypto/x509"

    "crypto/rsa"

    jwt "github.com/dgrijalva/jwt-go"
)

type header struct {
    Alg string `json:"alg"`
    Kid string `json:"kid"`
}

type payload struct {
    jwt.StandardClaims
}

func main() {
    const (
        publicKeySrcURL = "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"
    )
    rawToken := "<PASTE YOUR Firebase Auth JWT"
    segs := strings.Split(rawToken, ".")

    bHeader, err := jwt.DecodeSegment(segs[0])
    if err != nil {
        log.Fatalln("error in decoding segment header")
    }

    var h header
    json.Unmarshal(bHeader, &h)

    resp, err := http.Get(publicKeySrcURL)
    if err != nil {
        log.Fatalln("error in getting public key")
    }
    defer resp.Body.Close()
        
       // 一応Streamで扱っておく
    decoder := json.NewDecoder(resp.Body)
    pubkeyMap := make(map[string]string)
    err = decoder.Decode(&pubkeyMap)
    if err != nil {
        fmt.Printf("%v", err)
        log.Fatalln("error on decoding http response body")
    }

    rawPubkey, ok := pubkeyMap[h.Kid]
    if !ok {
        log.Fatalln("public key corresponding to XXX is not found")
    }
    pk, err := parseKey([]byte(rawPubkey))
    if err != nil {
        panic(err)
    }

    token, err := jwt.Parse(rawToken, func(token *jwt.Token) (interface{}, error) {
        if _, ok = token.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, errors.New("signing methods other than RSA are not expected")
        }
        if token.Method != jwt.SigningMethodRS256 {
            return nil, errors.New("Signing methods other than RS256 are not expected")
        }

        return pk, nil
    })

    //spew.Dump(token)
    if !token.Valid {
        fmt.Println("token is invalid")
    }
}

// --CETIFICATE -> rsa.PublicKeyへの変換
func parseKey(rawPubkey []byte) (interface{}, error) {
    block, _ := pem.Decode(rawPubkey)
    cert, _ := x509.ParseCertificate(block.Bytes)
    pk := cert.PublicKey.(*rsa.PublicKey)

    fmt.Println(pk)
    return pk, nil
}

string -> []byteへのキャストをやっているのがイケてないですが、とりあえずこれでdecodeできます。

NOTE

Certificate -> rsa.PublicKeyへの変換が所見だったのでこのあたりの知識を補強しておく

Firebase Authから返ってくるJWTをゴニョゴニョする(1)

同じモチベーションの人がいたら幸いです。

モチベーション

  • Rails にしろGoにしろ自前でAuth周りを書くのがだるい

    • ログイン機能は必要だけど、さっさと作って使われるか検証したい
    • 認証なしでやればいいじゃん?って話もあるけど、なんだかんだMyhogehogeみたいな機能を実装して試したい
  • Rails -> devisev? => なにか起こる度にRack::MiddleWareとかにログを指して折ってくのつらい..

    • 2010年くらい(@Masashisalvadorが学生だったころ)から同じような感じ..
  • Go

    • コードは読みに行けるけど..自前で実装すんのかよ => 本質的な機能の部分をさっさと実装したい (=認証は外付けにしたい)
  • FirebaseにAuthあるじゃん

    • 公式SDKはNodeとかか…
    • Googleがやってくれるならメンテしてくれるだろうし、無茶苦茶なことにはならないだろうからいいな
    • ID/Passwordログインも、各種サービスのOAuthに対応してる => 勝てる?
    • AuthはRest API経由だとできないのか => JWTを使えばまあ認証サーバの切り出し(=Firebaseにおまかせ)はできそうだな。
    • 3rd partyでホゲる際のことも公式ドキュメントに書いてあるし、いきなりAuthだけつかうんじゃねえ!とはならなそう。
    • ついでにPushとかもFirebase経由で送れるのは便利だ。

最終的にやりたいこと

  • Firebaseから飛んでくるJWTをよしなにパースして認証したい。
    • Rails or Goで(Goは技術調査も兼ねた自前プロジェクトで使う)
    • RailsだったらGem入れる or コードコピーしてrequireしてゴニョるくらいで使いたい = 使い回しを容易にしたい

とりあえず認証をザクっと書いてJWTの中身を覗いてみる

コードはクソ雑ですが…

onClickSignInWithFacbook() {
    firebase.auth().signInWithPopup(provider).then((result) => {
      // This gives you a Facebook Access Token. You can use it to access the Facebook API.
      global.rrr = result;
      let token = result.credential.accessToken;
      // The signed-in user info.
      let user = result.user;
      // ...3
      this.setState({ user });
      console.log(token);
      console.log(user);
    }).catch((error) => {
      console.log(error);
      console.log("Error on facebook auth");
      // Handle Errors here.
      let errorCode = error.code;
      let errorMessage = error.message;
      // The email of the user's account used.
      let email = error.email;
      // The firebase.auth.AuthCredential type that was used.
      let credential = error.credential;
      // ...
    });
  }
  onClickSignInWithGoogle() {
    firebase.auth().signInWithPopup(gProvider).then((result) => {
      // This gives you a Google Access Token. You can use it to access the Google API.
      global.rrr = result;
      let token = result.credential.accessToken;
      // The signed-in user info.
      let user = result.user;
      this.setState({ user });
  
      console.log(token);
      console.log(user);
      // ...
    }).catch((error) => {
      console.log("error on google auth");
      console.log(error);
      // Handle Errors here.
      let errorCode = error.code;
      let errorMessage = error.message;
      // The email of the user's account used.
      let email = error.email;
      // The firebase.auth.AuthCredential type that was used.
      let credential = error.credential;
      // ...
    });    
  }

Google Authの場合

rrr.credential.idToken # .でくぎられたJWTが入ってる
rrr.credential.idToken.split(".") # header / claims / signatureに別れてる

1つめのセグメント = splitした配列の先頭はただのヘッダのはず

echo -n "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdhOWY5Yzc5ZGYzMDU5MzhlMjY4ODk3Y2JkNDc1ZDQ3MjY4MWI0ZWEifQ" | base64 -D                                                                                                                 
{"alg":"RS256","kid":"7a9f9c79df305938e268897cbd475d472681b4ea"%

JWTのヘッダが入っている。閉じカッコがないのが気になるがこれは? kidは公開鍵を探しに行く時に使う。

echo -n "2つ目のセグメント" | base64 -D | jq .
{
  "azp": "xxxxx.apps.googleusercontent.com",
  "aud": "xxxxx.apps.googleusercontent.com",
  "sub": "1111111111111111111",
  "email": "hoge@example.com",
  "email_verified": true,
  "at_hash": "xxxxxxx",
  "iss": "accounts.google.com",
  "iat": 1495084945,
  "exp": 1495088545
}

JWTのStandardぽいものに、認証に使ったemailなどが入っている。

Facebook Authの場合

上記コードの result.user.Yd というキーで JWTが入っていた。google Auth の場合も同様でした。(googleの場合はcredentialsにも入っている)

rrr.user.Yd.split(".")
{
  "iss": "https://securetoken.google.com/xxxx",
  "name": "Masashi Salvador Mitsuzawa",
  "picture": "https://scontent.xx.fbcdn.net/v/t1.0-1/p100x100/16142336_1426152514084193_2027921832216883945_n.jpg?oh=8d6238d7d50034e2fd7208c8a4208fc1&oe=59745742",
  "aud": "xxxx",
  "auth_time": 1495086800,
  "user_id": "xxxx",
  "sub": "xxxx",
  "iat": 1495086800,
  "exp": 1495090400,
  "email": "xxxxx@yahoo.co.jp",
  "email_verified": false,
  "firebase": {
    "identities": {
      "facebook.com": [
        "1562766520422791"
      ],
      "email": [
        "m_m_moonty@yahoo.co.jp"
      ]
    },
    "sign_in_provider": "facebook.com"
  }
}

こんな感じでClaimsがDecodeできる。

} (closing bracket)が消える問題

echo -n "eyJhbGciOiJSUzI1NiIsImtpZCI6IjdhOWY5Yzc5ZGYzMDU5MzhlMjY4ODk3Y2JkNDc1ZDQ3MjY4MWI0ZWEifQ" | base64 -D                                                                                                                 
{"alg":"RS256","kid":"7a9f9c79df305938e268897cbd475d472681b4ea"% # }が消えてる

同じ文字列を Golang で書いた適当なコードでdecodeしてやると

(string) (len=64) "{\"alg\":\"RS256\",\"kid\":\"e62eaba0e06fe1463746c69efa974c1152234634\"}"

問題なく閉じカッコが出現する

}エンコードすると

echo -n "}" | base64
fQ==

となるので、たしかに文字列としてはたりなさそうに見える。 Base64自体はRFCかなにかで規定されている(はず)なので、挙動差異は変なのだが…何なのだろう?

Rails5のAPIModeつかってるけどRailsAdminもheroku上でサクッと使いたいよねというお話(失敗談)

API Mode

Rails 5 をAPI modeで動かしている。 * RailsAdminは使いたいよな(管理画面のフロントをSPAで書くのは工数の無駄) * herokuは使いたい(今更deploy周りとかサーバのセットアップとかだるい)

ということで Client -> rack app (routing) -> API @ port 3000 -> Admin (RailsAdmin) @ port 19998

構成

Gemfile
Gemfile.lock
app.rb      
config.ru   
vendor      
web1        
web2
ADMIN_PATH = '/admin'
WEB_SERVER_PORT = 3000
ADMIN_SERVER_PORT = 19899

class SimpleApp
  def call(env)
    request = Rack::Request.new(env)
    request_path = env['REQUEST_PATH']

    if request_path.start_with?(ADMIN_PATH)
      [ 301, {'Location' => "http://localhost:#{ADMIN_SERVER_PORT}#{request_path}" }, self ]
    else
      [ 301, {'Location' => "http://localhost:#{WEB_SERVER_PORT}#{request_path}" }, self ]
    end
  end
  def each(&block)
  end
end

こんな感じでheroku上で動かそうとしたが、そもそもlocalhostヘリダイレクトはクライアント側のローカルホストにリダイレクトされてしまうのでだめですよね。という当然の結論にheroku上で立ち上がっているappサーバにうまく通信しに行く方法はないんだろうか

2017年にもなってHerokuでハマるクラスタ (heroku MySQL)

初期データ投入のため

heroku run rake db:seed_fu
ActiveRecord::StatementInvalid: Mysql2::Error: User 'ba2df6a9fed0b0' has exceeded the 'max_questions' resource (current value: 3600): INSERT INTO `item_resources` (`item_id`, `resource_id`, `created_at`, `updated_at`) VALUES (1741, 2, '2017-05-01 04:53:46', '2017-05-01 04:53:46')

max_questionsてなんやねんと思ったら、MySQL側でユーザごとのクエリ数とかを制限する仕組みらしい。 そういった制限のあるところでMySQLを使ったことがなかったので初めて遭遇するエラーである。 Heroku上にはQPHの制限について書いてなかったので、ググっていると下記のサイトを見つけた。

getsatisfaction.com なるほど、関連テーブルもあるからクエリ数が足りなすぎる問題。どうしたものか。

そもそも放置気味になっているN+1問題を引き起こすエンドポイントとかあるので…直さないと一瞬で食いつぶされてしまう…orz

MacのxargsのJ option (replacerを使う記法)

command  | xargs -i cp {} target_dir

的なことがやりたかったがどうもMacのxargsだとだめらしい(これだからMacは…)

manを開くと下記が記載されていたので事なきを得た。

     -J replstr
             If this option is specified, xargs will use the data read from standard input to replace the first occurrence of replstr instead of appending that data after all other arguments.  This option will not
             affect how many arguments will be read from input (-n), or the size of the command(s) xargs will generate (-s).  The option just moves where those arguments will be placed in the command(s) that are
             executed.  The replstr must show up as a distinct argument to xargs.  It will not be recognized if, for instance, it is in the middle of a quoted string.  Furthermore, only the first occurrence of the
             replstr will be replaced.  For example, the following command will copy the list of files and directories which start with an uppercase letter in the current directory to destdir:

                   /bin/ls -1d [A-Z]* | xargs -J % cp -rp % destdir

エレガントに削る / エレガントに実装するということ

BtoBにしろCtoCにしろBtoCにしろ、プロダクトを開発するエンジニアに求められるエレガントさについてふと思うことがあったので書いておく。

言われた仕様や要件をそのまま作ってしまうエンジニアはどんなにコードを書く速度が早かったり難しい実装ができたり、 難しいアルゴリズムを考案することができたとしてもプロダクト開発には向かないと思う。 前提としてリスペクトがあることには留意しておくが、 単純に尖ったエンジニアリングがしたい / その能力が高いならばOSSへコミットしたり言語を作ったり、 研究をやっていたほうが人類を前に進められると思う (どのみち、それらの分野においても要求を「そのまま作る」ことしかできなければ役に立たないと思うが…)

エンジニアに求められるのはある種のエレガントさだ。 ここでのエレガントさは

  1. エレガントに要求を削れること=問題を適切にハンドルできること.
     ・必要なもの/解決すべき問題に対して適切に技術を用いることができること.
  2. エレガントに実装できること.
     ・解決すべき問題を別の問題を誘発せずに解決することができること.
      ・負債を生まない.
      ・運用上の手間を増やさない.
        ・手作業.
        ・コミュニケーション

の2つに大別されると思う。 A.に関しては、下手に要求を削ってしまうと骨抜きになってしまうし、要求を課題解釈して問題に適切に対処できないと巨大な城を建築することになってしまう。どちらの場合も、リソースを使ってゴミを生生していることにほかならない。チームのスループットを確実に低下させる。 ゴミを作らないためにはAのエレガントさが必要だ。Aのエレガントさを高めるためには、技術的な勘所とビジネスドメイン特有のお作法(何をするとユーザに喜ばれるか)の両方を体得していなければいけない。技術的な勘所は削る判断をするために必要なコストパフォーマンスについての示唆をもたらしてくれる。ビジネスドメインの知見に関しては要求が本質的かどうかを見極めるのに使うことができる。
これらの感覚は意識して磨くことができると思う。磨くための方法論に関しては僕も整理がしきれていない(故に記載はしない)。

B.が何故必要かに関しては、大抵の場合は本番でフルスクラッチでコードを書くことなく、エレガントな実装ができなければリリースまでに必要な時間が膨らむし、変なコードを既存コードに混ぜ込んでしまうことで負債を生んだり、別の問題を生んだりしてしあうことがその理由と言える。

AとBどちらに関しても、ユーザ / チームとのコミュニケーションに長けていることは必要条件だと思う。
エンジニアはコミュ障でもいいみたいな意見もあると思うのですが、実際問題無理ゲーだと思う。

このエレガントはプランナーが別に居て企画を立てる場合でもそうでない場合も成り立つかなと。

Crystalの配列(0)

配列

[1,2,3] [1, "aaa", 'ks']

文字列配列はRubyと同じ表記が可能

%w(one two three)

シンボルの配列

%i(one two three)

配列ライクな型

https://crystal-lang.org/api/0.20.3/Array.html

You can use a special array literal syntax with other types too, as long as they define an argless .new method and a #<< method. Set is one such type:

set = Set(typeof(1, 2, 3)).new
set << 1
set << 2

引数なしのnewと << が定義されていれば Array特有の文法を使うことができる。

Arrayはgenericな型、複数の型を含む配列の方はUNION型と呼ばれる [1, "aaa", 'ks']など

structural subtyping的になっているので、呼び出されるメソッドに応答するればその型の 条件を満たしているとみなされ、コンパイルが通る

=> 不用意に型制約をすると柔軟性が減る