devise_token_authのtoken_validation_responseをオーバーライドする
こんにちは!kossyです!
さて、今回はRailsでのトークン認証機能を提供するGem、devise_token_authの
token_validatio_responseメソッドをオーバーライドする方法をブログに残してみたいと思います。
方法
devise_token_authのsessions_controller.rbのcreateアクションを叩いた後、認証に成功した場合に呼び出されるメソッドの中身を見ると、
def render_create_success render json: { data: resource_data(resource_json: @resource.token_validation_response) } end
@resourceに対して、token_validation_responseを呼び出していることがわかります。
この「token_validation_response」は、DeviseTokenAuth::Concerns::Userをモデルにincludeすることで、
生えるメソッドになります。
# includeしている場合 class User include DeviseTokenAuth::Concerns::User end $ user.token_validation_response => { id: 1, name: 'hoge' ... } # includeしていない場合 $ user.token_validation_response => NoMethodError
定義元のソースコードを見てみます。
def token_validation_response self.as_json(except: [ :tokens, :created_at, :updated_at ]) end
モデルクラスから:token, :created_at, :updated_atを除外してjsonとして返却するメソッドになっていますね。
これ、例えば2段階認証用のtoken等のあまりユーザー側に返却したくないデータがあると、困りものですよね。
そんな時はオーバーライドしてしまいましょう。
class User include DeviseTokenAuth::Concerns::User def token_validation_response self.as_json(except: [ :tokens, :created_at, :updated_at, :two_factor_verification_code ]) end end
この状態でコンソールで試してみましょう。
# オーバーライドしていない場合 user.token_validation_response => {"id"=>1, "provider"=>"email", "uid"=>"test+0@gmail.com", "allow_password_change"=>false, "first_name"=>"健太朗", "last_name"=>"久保", "first_name_kana"=>"ケンタロウ", "last_name_kana"=>"クボ", "email"=>"test+0@gmail.com", "disable"=>false, "tenant_id"=>7, "uuid"=>nil, "two_factor_verification_code": nil } # オーバーライドしている場合 $ user.token_validation_response => {"id"=>1, "provider"=>"email", "uid"=>"test+0@gmail.com", "allow_password_change"=>false, "first_name"=>"健太朗", "last_name"=>"久保", "first_name_kana"=>"ケンタロウ", "last_name_kana"=>"クボ", "email"=>"test+0@gmail.com", "disable"=>false, "tenant_id"=>7, "uuid"=>nil }
two_factor_verification_codeが返っていないことが確認できました。
勉強になりました。
技術ブログネタがない時のTIPS
こんにちは!kossyです!
さて、今回は、ブログネタがない時のTIPSをブログに残してみたいと思います。
はじめに
年末年始毎日ブログ更新をすると決めた筆者ですが、
そう毎日ネタを思いつくこともなく、日々何を書こうか思い悩む日々になっています。
一生懸命ブログを更新しようとする技術者の方々は同じような悩みを抱えているのではないでしょうか。
そこで、私が考えたブログネタがない時のTIPSを今回紹介できればいいなと思います。
Gemのコードリーディングをする
Gemはすごく便利なのですが、ドキュメントをチラッと読んで使い方だけ把握して使用していて、
内部のロジックは知らないまま使いがちです。
そこで、Gemのコードリーディングをブログネタとして扱ってしまおうというわけです。
自分の知識の肥やしにもなるし、アウトプットにもなるし、読んでくれる方の役にも立てるわけで、一石三鳥です。
実際に自分がコードを書くときに役に立つ書き方なんかも知ることができますし、
バグを見つけたらfixのPRを送ってマージされればあなたも立派なOSSコミッターです。
もはやいいことしかないので、Gemのコードリーディングは積極的に行うようにしましょう。
deviseでパスワードが正しいか未定義かを確認する方法とvalid_password?のコードリーディング
こんにちは!kossyです!
さて、今回は認証機能を提供するRubyのGemである「devise」を使っている際に、
パスワードが正しいか未定義かを確認する方法と、valid_password?メソッドのコードを読んでみたので、
ブログに残してみたいと思います。
deviseを使って認証機能を提供しているシステムの場合、
CSチームからこんな問い合わせが来たりしませんか?
「そのお客さん、このパスワードで設定したはずなんですけど、ログインできなくて、、、
パスワードがちゃんと設定されてるか試してもらえませんか?」
deviseを普通に使っていればDBにはハッシュ化されたパスワードが保存されるため、
DBを見てもパスワードはわかりません。
しかし、「このパスワード」が正しいかどうかは判定することができます。
valid_password?メソッドを使えば判定ができます。
# パスワードが正しい時 valid_password?("test1234") => true # パスワードが間違っている時 valid_password?("test4321") => false # パスワードが未定義の時 valid_password?("test4321") => nil
パスワードが正しければ true が、間違っていれば false が、パスワードが設定されていない(レアケースですが、、、)場合は、 nil が返却されます。
これだけで終わりにするには味気ないので、ソースコードを追ってみましょう。
Devise::Encryptor.compare
valid_password?メソッドの定義はこちらです。
# Verifies whether a password (ie from sign in) is the user password. def valid_password?(password) Devise::Encryptor.compare(self.class, encrypted_password, password) end
まずはDevise::Encryptor.compareメソッドを追ってみます。
Devise::Encryptor.compareメソッドの定義元はこちら
def self.compare(klass, hashed_password, password) return false if hashed_password.blank? bcrypt = ::BCrypt::Password.new(hashed_password) if klass.pepper.present? password = "#{password}#{klass.pepper}" end password = ::BCrypt::Engine.hash_secret(password, bcrypt.salt) Devise.secure_compare(password, hashed_password) end end
早期リターン文は説明不要ですかね。
BCrypt::Passwordクラスの中身を見てみましょう。
と思ったら外部のGemの処理みたいです。
def initialize(raw_hash) if valid_hash?(raw_hash) self.replace(raw_hash) @version, @cost, @salt, @checksum = split_hash(self) else raise Errors::InvalidHash.new("invalid hash") end end
うーんいまいちわからんので動かしてみましょうか。
bcrypt = BCrypt::Password(User.first.encrypted_password) bcrypt.version => "2a" bcrypt.cost => 12 bcrypt.salt => "$2a$12$U5Yc/wGr2vQMBixITdTcxe" bcrypt.checksum => "LHVc35YygLEHakakiOmSOX.1h6zivK6"
この数字の意味するところは
のブログが詳しかったです。
要はBCrypt::Passwordのinitializeで渡されたパスワードをBCrypt暗号化しているようです。
klass.pepper
次はklass.pepper.present?のコードです。
if klass.pepper.present? password = "#{password}#{klass.pepper}" end
klass.pepperが存在していれば、引数で渡されたpaswordにklass.pepperを結合させて新たなpasswordとしています。
pepperとはなんでしょうか。
平文パスワードにあらかじめ設定されている文字列(pepper)をくっつけ、結合された文字列をハッシュ化する。
さらに、ランダムに生成されたハッシュ(salt)を追加して、encrypted_passwordとする。
平文パスワードに塩こしょうをして暗号化するという洒落。
とのことで、特に設定を行っていないとpepperはかかりません。
残りのコードを読んでみます。
password = ::BCrypt::Engine.hash_secret(password, bcrypt.salt) Devise.secure_compare(password, hashed_password)
BCrypt::Engine.hash_secretはパスワードのハッシュ化を行っているコードですね。
Devise.secure_compareはちゃんと中身みてみましょう。
# constant-time comparison algorithm to prevent timing attacks def self.secure_compare(a, b) return false if a.blank? || b.blank? || a.bytesize != b.bytesize l = a.unpack "C#{a.bytesize}" res = 0 b.each_byte { |byte| res |= byte ^ l.shift } res == 0 end
ゴリゴリにRubyのメソッドが使われてますね、、、
「タイミング攻撃を防ぐための一定時間比較アルゴリズム」とコメントアウトがあるので、
セキュアに比較を行うために必要な処理なのだと思います。
deviseの中身は追うのがとても大変ですね、、、見てもいまいちわからんし。
とはいえ、ブラックボックスのままツールを使い続けるのもあまりよろしくないので、
時間を見つけて理解を進めたいなと思います。
勉強になりました。
参考にさせていただいたサイト
この場を借りてお礼を申し上げます。
curlでproxyを設定する
こんにちは!kossyです!
さて、今回はcurlでproxyを設定したくなるシチュエーションに出会ったので、
ブログに残してみたいと思います。
困った
Rails APIモードでAPIサーバーを開発しているときに、リクエスト元のIPアドレスを保存する要件がありました。
そこで、以下のようにモデルにバリデーションを設けました。
require 'resolv' validates :ip_address, presence: true, format: { with: Resolv::IPv4::Regex }
この場合、proxyを設定せずにリクエスト元のIPアドレスをremote_ipメソッドで取得しようとすると、
IPアドレスのフォーマット外の::1という値が返るので、バリデーションに弾かれてしまいます。
このままだとローカル環境でAPIの動作確認ができなくなるので、
curlでproxyを設定してリクエストを送ることにしました。
curl localhost:3000/auth/sign_in -X POST -d '{"email":"test1@gmail.com", "password":"test1234"}' -H "content-type:application/json" -i --proxy http://127.0.0.1:3000
-
- proxyオプションの後に、URLを渡すことでproxyを設定することができます。
これで動作を確認してみます。
# binding.pryでデバッグしている想定 request.remote_ip => "127.0.0.1"
きちんとproxyの設定ができていることが確認できました。
Postmanでリクエスト元のIPアドレスを変更する方法について
こんにちは、kossyです!
AngularのToken認証ライブラリ「angular-token」のInterceptor周りのソースコードを読んでみた
こんにちは!kossyです!
さて、今回はAngularのToken認証ライブラリである、「angular-token」の
Interceptor周りのコードを読んで見たので、ブログに残したいと思います。
環境
Angular 11.0.4
npm 6.14.8
node 12.13.1
TypeScript 4.0.5
Interceptorとは?
本格的にソースコードを読む前に、AngularのInterceptorの仕組みに触れたいと思います。
AngularにはHttp通信用のライブラリとしてHttpClientModuleという組み込みライブラリを提供していますが、
そのHTTP通信のリクエストとレスポンスに介入する仕組みのことをInterceptorと呼んでいます。
主な目的として、リクエストの介入ではHeaderに認証用のtokenを付与することや、
レスポンスの介入ではレスポンスを受け取ったことを知らせるような処理に使われたりします。
Interceptorの実装例はlacolacoさんのzennの記事が非常に詳しかったです。
angular-tokenのinterceptorのソースコードを見る
お待たせしました。それではコードを読んで見ます。
// projects/angular-token/src/lib/angular-token.interceptor.ts import { Injectable } from '@angular/core'; import { HttpEvent, HttpRequest, HttpInterceptor, HttpHandler, HttpResponse, HttpErrorResponse } from '@angular/common/http'; import { AngularTokenService } from './angular-token.service'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @Injectable() export class AngularTokenInterceptor implements HttpInterceptor { constructor( private tokenService: AngularTokenService ) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { // Get auth data from local storage this.tokenService.getAuthDataFromStorage(); // Add the headers if the request is going to the configured server const authData = this.tokenService.authData.value; if (authData && (this.tokenService.tokenOptions.apiBase === null || req.url.match(this.tokenService.tokenOptions.apiBase))) { const headers = { 'access-token': authData.accessToken, 'client': authData.client, 'expiry': authData.expiry, 'token-type': authData.tokenType, 'uid': authData.uid }; req = req.clone({ setHeaders: headers }); } return next.handle(req).pipe(tap( res => this.handleResponse(res), err => this.handleResponse(err) )); } // Parse Auth data from response private handleResponse(res: HttpResponse<any> | HttpErrorResponse | HttpEvent<any>): void { if (res instanceof HttpResponse || res instanceof HttpErrorResponse) { if (this.tokenService.tokenOptions.apiBase === null || (res.url && res.url.match(this.tokenService.tokenOptions.apiBase))) { this.tokenService.getAuthHeadersFromResponse(res); } } } }
改行やコメントアウトを除くと約20行ほどの処理になっています。
まずは17行目を見てみましょう。
// Get auth data from local storage this.tokenService.getAuthDataFromStorage();
getAuthDataFromStorageメソッドでtokenを取得するコードのようです。
メソッドの中身を見てみましょう。
// projects/angular-token/src/lib/angular-token.service.ts // Try to get auth data from storage. public getAuthDataFromStorage(): void { const authData: AuthData = { accessToken: this.localStorage.getItem('accessToken'), client: this.localStorage.getItem('client'), expiry: this.localStorage.getItem('expiry'), tokenType: this.localStorage.getItem('tokenType'), uid: this.localStorage.getItem('uid') }; if (this.checkAuthData(authData)) { this.authData.next(authData); } }
LocalStorageに保存したAuthDataを取得して、そのAuthDataの中身をチェックしてパスすれば
後続の処理につなげていますね。
checkAuthDataの処理も見てみましょう。
// projects/angular-token/src/lib/angular-token.service.ts // このクラス内からしか呼び出せなくて // 引数で受け取るauthDataはAuthData型のデータで // 返り値はboolean型のデータ // Check if auth data complete and if response token is newer private checkAuthData(authData: AuthData): boolean { // authDataの各種値が全てnullじゃなければ if ( authData.accessToken != null && authData.client != null && authData.expiry != null && authData.tokenType != null && authData.uid != null ) { // BehaviorSubjectで定義されたselfが持つauthDataのvalueがnullでなければ if (this.authData.value != null) { // tokenの有効期限を比較 return authData.expiry >= this.authData.value.expiry; } return true; } return false; }
authDataが存在するか、authDataの有効期限が切れていなければtrueが返るコードになってますね。
getAuthDataFromStorageメソッドは、「tokenの有効性をチェックした上でLocalStorageからtokenを取得するコード」でした。
次は20行目です。
// Add the headers if the request is going to the configured server const authData = this.tokenService.authData.value;
tokenService内のBehaviorSubjectで定義されたauthDataのvalueをconst宣言でauthDataとして定義しています。
BehaviorSubjectの説明も先述のlacolacoさんのtutorialが非常に詳しいです。
次は22行目から42行目までです。
// tokenService内にSetされたauthDataがあり、apiBaseがnullまたはrequestのurlがapiBaseにmatchすれば if (authData && (this.tokenService.tokenOptions.apiBase === null || req.url.match(this.tokenService.tokenOptions.apiBase))) { const headers = { 'access-token': authData.accessToken, 'client': authData.client, 'expiry': authData.expiry, 'token-type': authData.tokenType, 'uid': authData.uid }; // requestにheaderを加えたものをcloneして新たなrequestとして定義 req = req.clone({ setHeaders: headers }); } // handleメソッドにreqを渡して後続の処理を呼び出して、返ってきたobservableオブジェクトにhandleResponseで処理を加える return next.handle(req).pipe(tap( res => this.handleResponse(res), err => this.handleResponse(err) )); }
認証をパスした後、どうやって認証トークンをrequest_headerに忍ばせているんだろう?と思っていましたが、
interceptor内で行っていたんですね。
handleResponseはHTTPリクエストを行った結果をParseするような処理だったため、割愛します。
angular-tokenさん、よしなに実装して下さって感謝です。
まとめ
Interceptorの実装は行数も多くなく処理も複雑でもないので、自分でカスタマイズもできそうですね。
Headerにもっとパラメータ追加した、Responseにログ忍ばせたい、など。
勉強になりました。
graphqlクライアントの Altairでトークン認証を行う方法
こんにちは!kossyです!
さて、今回はgraphql-ruby + Rails APIモードでdevise_token_authを用いてトークン認証する開発において、
Altairを使ってトークン認証をパスする方法についてブログに残してみたいと思います。
環境
Rails 6.0.3.4
Ruby 2.6.3
MacOS Catalina
なお、devise + devise_token_auth で Userモデルを作成し、
CORSの設定やマイグレーション周りの設定が済んでいるものとします。
devise + devise_token_auth のセットアップ手順はこちらの記事が詳しかったです。
トークン認証だとgraphiql使えない
トークン認証を利用している時にgraphiqlを使う方法をググってもHackしたやり方しか出てこなかった(jsでXHR送るみたいな、、、)ので、
Altairを使って認証をパスするようにします。
Altairのダウンロードページはこちら
まず、curlでログインのリクエストをして認証トークンを取得します。(Userは作成されているものとします。)
$ curl -i localhost:3000/auth/sign_in -X POST -d '{"email":"tanaka@example.com", "password":"test1234"}' -H "content-type:application/json" HTTP/1.1 200 OK X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff X-Download-Options: noopen X-Permitted-Cross-Domain-Policies: none Referrer-Policy: strict-origin-when-cross-origin Content-Type: application/json; charset=utf-8 access-token: 7VQ68wVMh8bPUN5ckr0CfQ token-type: Bearer client: NihWpbgns0SDpI3HURikfw expiry: 1609249216 uid: tanaka@example.com ETag: W/"3805fe2871c8a4277c3d9174dcd5c249" Cache-Control: max-age=0, private, must-revalidate X-Request-Id: ecb32107-da6b-4bbe-86e9-678dcc01cf55 X-Runtime: 1.022058 Vary: Origin Transfer-Encoding: chunked
ダウンロードできたら、Altairを開いて、

左上のAltairのアイコンの下のマークをクリックしてください。
グレーアウトしてモーダルが表示されるので、以下のようにkeyとvalueを入力します。(valueは先ほどcurlで叩いたレスポンスヘッダーの値を入れてください)
この状態でリクエストを投げると、認証をパスできます。

勉強になりました。
Rails6からデフォルトで付与されるようになったprecisionとは?
こんにちは!kossyです!
さて、今回はRails6系からデフォルトで付与されるようになった、
precisionオプションについて、ブログに残してみたいと思います。
環境
Ruby 2.6.3
Rails 6.0.3
MacOS catalina
precisionとは?
Railsガイドによると、
precision: decimalフィールドの精度 (precision) を定義します。この精度は、その数字の総桁数で表されます。
とのことで、カラムの値(decimal)の精度を指定できるオプションになっています。
精度ってなんやねん
wikipedia先生に聞いてみました。
精度は、数値を表現する細かさであり、その数字の正確さとは(厳密には)異なる概念である(正確度と精度を参照)。ただし、機械加工などでは慣用的に、正確さに対して「精度」という言葉が使われている。
ふむ、数値を表現する細かさのことのようですね。
よくプログラムを書いていると「丸め誤差」という事象に遭遇することがありますが、
presicionを使えば、指定した桁数までは誤差を発生させずに計算ができるのでしょうか。
なぜデフォルトでpresicionが付与されるようになった?
実際のRailsプルリクがこちら
該当discussionがこちらですね。
I think the precision is for MySQL.
Well, if we could make it omit the precision on Postgres/SQLite and only add it on MySQL if the server supports it, that seems ideal to me.精度はMySQL用だと思います。
そうですね、Postgres / SQLiteの精度を省略し、サーバーがサポートしている場合にのみMySQLに追加できるとしたら、それは私にとって理想的なようです。
presicionの指定はどういう時に使う?
たとえばDBに緯度経度の情報を格納したい時に使えます。
class CreateGeocodes < ActiveRecord::Migration[6.0] def change create_table :geocodes do |t| t.string :address, null: false, default: "" t.decimal :latitude, precision: 11, scale: 8 t.decimal :longitude, precision: 11, scale: 8 t.timestamps end end end
緯度・経度はMAX総桁数と小数点以下のMAX桁数が確定しているので、
桁数がわかる場合や、正確な計算が求められる場合は、presicionの指定をした方が良さそうです。
まとめ
presicionのデフォルト指定は、MySQLのバージョンアップデートに関する修正だったようです。
exifrでExifを自在に操ってみた
こんにちは!kossyです!
さて、今回は画像に付記されることがあるExif情報をRubyで操作できるGem、
exifrの使い方について、ブログに残してみたいと思います。
環境
Ruby 2.6.3
exifr 1.3.9
# 使い方
exifrはjpegまたはtiff拡張子の形式の画像に対応しています。
https://ja.wikipedia.org/wiki/Exchangeable_image_file_format
画像はこちらをお借りしました。
https://photo-studio9.com/exif/
require 'exifr/jpeg' image = EXIFR::JPEG.new('sample.jpeg') # 画像の横幅を求める $ image.width => 630 # 画像の縦幅を求める $ image.height => 460 # exifデータが存在するかどうか $ image.exif? => true # 撮影した機材を文字列で表示 $ image.model => "iPhone" # 撮影日をTimeクラスのインスタンスで返却 $ image.date_time => 2014-01-08 23:02:05 +0900 # シャッター速度 $ image.exposure_time.to_s => "1/17" # 緯度をFloatクラスのインスタンスとして返す $ image.gps.latitude => 35.31966666666667 # 経度をFloatクラスのインスタンスとして返す $ image.gps.longitude => 139.54766666666666
このGemを使えば、例えば、
「アップロードされた画像の位置情報から、住所を自動出力する」みたいなこともできると思います。
geocoderあたりと組み合わせると良さそうです。
また別でブログに書いてみようかと思います。
勉強になりました。
大いに参考にさせていただいたサイト
GitHub - remvee/exifr: EXIF Reader
EXIF Reader for Ruby API Documentation
知らないとヤバい!? 写真のExifから個人情報を守る方法まとめ! - studio9
この場を借りてお礼を申し上げます。
HEYのGemfileが公開されていたので見てみた(その1)
こんにちは!kossyです!
さて、今回はRuby on Railsの作者として知られているDHHが開発に参加している、HEYという有料の電子メールサービスの
GemfileがGithub上に公開されていたので、どんなGemが使われているかを見てみたいと思います。
connection_pool
Rubyでコネクションプールを貼るためのGemです。
Railsではジョブ管理にRedisを用いられるケースが多いですが、connection_poolはそういったツールとの疎通をよしなに行うGemのようです。
参考:
railsでコネクションプールを使ってredisに接続する - Qiita
ActiveJobでsidekiqを使う場合、connection_poolの値はconcurrency + 1以上にしよう – repl.info
pwned
公式の英語を翻訳してみます。
Troy Hunt's Pwned Passwords API V2 allows you to check if a password has been found in any of the huge data breaches.
TroyHuntのPwnedPasswords API V2を使用すると、巨大なデータ侵害のいずれかでパスワードが見つかったかどうかを確認できます。
Pwned is a Ruby library to use the Pwned Passwords API's k-Anonymity model to test a password against the API without sending the entire password to the service.
Pwnedは、Pwned Passwords APIのk-Anonymityモデルを使用して、パスワード全体をサービスに送信せずにAPIに対してパスワードをテストするためのRubyライブラリです。
The data from this API is provided by Have I been pwned?. Before using the API, please check the acceptable uses and license of the API.
このAPIからのデータは、Have I pwned?によって提供されます。 APIを使用する前に、APIの許容される使用法とライセンスを確認してください。
メールアドレスやパスワードが漏洩していないかをチェックできるGemですね。
使い方を簡単に確認します。
# Gemfile gem 'pwned' $ bundle $ rails c $ pass = Pwned::Password.new("test1234") $ pass.pwned? => true $ pass.pwned_count => 54917
test1234というパスワードは、パスワードを侵害されたことがあって、
侵害された回数が54917回あるということがわかります、、、
ご自身がよく使うパスワードも調べてみてはいかがでしょうか。

