ユーザ管理はとても重要な機能ですが、多くのBaaSでは別ドメインになっていたり、カスタマイズが困難だったりと、使い勝手の悪いものになっています。
vte.cxでは、same originでログイン認証ができ、自由にカスタマイズできる画面をブランクプロジェクトにて提供しています。
ここでは、vte.cxにおける、ユーザ登録、ログイン認証、パスワードリセットなどのユーザ管理機能について説明します。
ユーザ管理はBaaSを利用しよう
ユーザ管理は外部からの様々な攻撃から情報を守りつつ、安全に実行されなければなりません。大変面倒くさい機能なのですが、だからといって安易に作ってしまうと脆弱性を生み出すことになってしまいます。
例えば、脆弱性とまではいえないまでも、生パスワードを安易にサーバに保管してしまうと信用はガタ落ちです。
そうなると、Webサービスを運用している事業者の全体のセキュリティレベルが低いとみなされ、サービスは利用されなくなるかもしれません。
セキュリティのある専門家は、Googleがユーザ認証基盤にかけている膨大なセキュリティ管理コストを示すことで、そもそも独自に認証基盤を作るべきではないと主張しています。勝手に作ったら税金を課すとまでいう方もいらっしゃいます。
これらユーザ認証機能や管理機能などは、安全とされる管理方法を熟知したうえで実装していく必要があるのですが、セキュリティに精通している開発者がいつもいるとは限らないため、不安に満ちた状態で開発している方も多いのではないでしょうか。
そこで、BaaSを利用しようという話になります。
BaaSでは、認証機能を安全に実行できるように設計されているため、これを利用すれば、開発者はセキュリティ対応などの面倒な作業から解放されます。
ユーザ管理を安全に使いたいという理由だけでBaaSを採用する方もいらっしゃるほどです。
ログイン画面をカスタマイズする
ログイン画面はWebサービスの玄関口です。
どこのものかわからないログイン画面では信用にも影響しますので、当然、サービス提供者による固有の画面を提供したいという要求があります。
vte.cxでは、セキュリティを担保しつつ、かつ、自由にカスタマイズできるようなログイン画面を提供できます。ログイン画面だけでなく、ユーザ登録画面やパスワードリセット画面もカスタマイズできます。(vtecxblankプロジェクトには、サンプルアプリケーションのログイン画面htmlとJavaScriptが置いてあります。)
これはReactで書かれた単体のWebアプリケーションです。開発者はこれらをカスタマイズしてWebサービス固有の画面を作っていくことができます。
ログイン画面とアタックを防ぐ仕組みについて
ログイン画面は元々jQueryで作られていましたが、Reactに書き直しました。
これら2つを見比べてもわかるように、React版は大変すっきりとした読みやすいものになっています。
ここでは以下にポイントを絞って解説します。
- パスワード漏えい防止
- DoS攻撃対策、CSRF対策
本番運用時はhttpsにより通信が暗号化されますので漏えいリスクは小さいですが、開発時においてはhttpが使われますので漏えいの心配があります。
そこで、生パスワードではなくハッシュ値を送信するようにしています。ハッシュ値はワンタイムなので認証が成功すると同じものは使えません。万一ハッシュ値が漏れたとしても既に使われたものなので無効です。
ちなみに、パスワードは推測が困難なもの「8文字以上、かつ数字・英字・記号を最低1文字含む」を入力する必要があります。ユーザ登録において、以下のような正規表現でチェックしています。
if (!password.match('^(?=.*?[0-9])(?=.*?[a-zA-Z])(?=.*?[!-/@_])[A-Za-z!-9@_]{8,}$')) {
また、DoS攻撃対策のために、reCapcha を利用しています。
これは、ログイン試行攻撃、つまり、メアドとパスワードを複数回登録されてアタックされるのを防ぐ機能です。ログイン時において2回パスワード間違えた場合にCapchtaが出ます。この機能はCSRF対策にもなっています。
また、ユーザ登録やパスワードリセットにおいては、アタック防止のため、常にCaptchaの入力が要求されます。
ユーザ登録画面
ユーザ登録における処理は、「ユーザ仮登録->メール送信->リンククリックにより本人確認・本登録」というような流れになります。メールによって本人確認をしないとサービスは使えません。
XHR通信には、axiosというライブラリを使っています。
ユーザ仮登録の該当の個所は以下のようなコードになります。
const reqData = {'feed': {'entry':[{'contributor': [{'uri': 'urn:vte.cx:auth:'+ e.target.account.value +','+ this.getHashPass(password) +''}]}]}}
const captchaOpt = '&g-recaptcha-response=' + this.state.captchaValue
axios({
url: '/d/?_adduser' + captchaOpt,
method: 'post',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
data : reqData
}).then( () => {
this.setState({isCompleted: true})
}).catch((error) => {
if (error.response) {
if (error.response.data.feed.title.indexOf('User is already registered') !== -1) {
this.setState({isAlreadyRegistered: true})
}else {
this.setState({isError: true})
}
} else {
this.setState({isError: true})
}
})
- ユーザ情報の仮登録では、
POST /d/?_adduser
により、{'feed': {'entry':[{'contributor': [{'uri': 'urn:vte.cx:auth:{ユーザアカウント}:{パスワードハッシュ}'}]}]}}
がサーバに送信されることで実行されます。 - このとき、リクエストヘッダには、
'X-Requested-With': 'XMLHttpRequest'
をつけてください。 - getHashPass()により、パスワードをハッシュ化します。このハッシュはワンタイムの認証トークンとして使われます。
- captchaOptには、reCaptchaで検証するための文字列がセットされます(reCaptchaの詳細については後述します)
- 正常に登録されると200が返り、state.isCompletedにtrueがセットされます。stateが更新されると同時に再描画(render)され、「仮登録が完了しました」が表示されます。
- エラーの場合で、feed.titleに'User is already registered'が返ってきた場合は既にユーザが登録されていたことを意味します。
reCaptchaの利用
前述したように、認証画面にCaptchaを使用することで、DoS攻撃対策やCSRF対策になります。
vte.cxでは、googleのreCaptchaというReactコンポーネントを利用しています。
これはコンポーネント化されているため、シンプルでとても使いやすいものになっています。まず、captchaを表示するところに以下のタグ(コンポーネント)を追加します。
<ReCAPTCHA
sitekey="6LfBHw4TAAAAAMEuU6A9BilyPTM8cadWST45cV19"
onChange={(value)=>this.capchaOnChange(value)}
/>
コールバック関数は以下のとおりです。パラメータcaptchaValueが渡ってくるのでstate.captchaValueに保存します。
capchaOnChange(value:string) {
this.setState({captchaValue: value})
}
captchaValueはユーザ仮登録を実行するPOSTのURLパラメータ(g-recaptcha-response)に追加します。
const captchaOpt = '&g-recaptcha-response=' + this.state.captchaValue
axios({
url: '/d/?_adduser' + captchaOpt,
method: 'post',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
data : reqData
}).then( () => {
・・・
これとjQuery版とを比べると、Reactコンポーネントがいかにシンプルに書けるかがよくわかると思います。
補足:ログインにおけるCaptcha要求について
ログインにおいては、認証失敗が2回以上続く場合にかぎり、Captchaが要求されます。Captchaが必要かどうかはサーバからのレスポンス(feed.titleが'Captcha required at next login.')をチェックすることで判断します。
const authToken = getAuthToken(e.target.account.value,e.target.password.value)
const captchaOpt = this.state.requiredCaptcha ? '&g-recaptcha-response=' + this.state.captchaValue : ''
axios({
url: '/d/?_login' + captchaOpt,
method: 'get',
headers: {
'X-WSSE': authToken,
'X-Requested-With': 'XMLHttpRequest'
}
}).then( () => {
location.href = 'index.html'
}).catch((error) => {
if (error.response) {
if (error.response.data.feed.title==='Captcha required at next login.') {
this.setState({requiredCaptcha: true,isLoginFailed: true})
}else {
this.setState({isLoginFailed: true})
}
} else {
this.setState({isLoginFailed: true})
}
})
}
パスワードリセット
パスワードリセットは、パスワード変更メール送信とパスワード変更の組み合わせになります。
パスワード変更メール送信後、メールからパスワード変更のリンクをクリックすることで、パスワード変更画面に遷移します。このとき、ログインしていなくてもパスワード変更を実行できます。
以下は、パスワード変更処理の該当箇所です。
const shaObj = new jsSHA('SHA-256', 'TEXT')
shaObj.update(password)
const hashpass = shaObj.getHash('B64')
const reqData = {'feed': {'entry':[{'contributor': [{'uri': 'urn:vte.cx:auth:,'+ hashpass +''}]}]}}
const captchaOpt = '&g-recaptcha-response=' + this.state.captchaValue
axios({
url: '/d/?_changephash' + captchaOpt,
method: 'put',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
data : reqData
}).then( () => {
・・・
-
{'feed': {'entry':[{'contributor': [{'uri': 'urn:vte.cx:auth:,{ハッシュ化パスワード}'}]}]}}
をPUT /d/?_changephash
送信することでパスワードを変更します。 - パスワードはsha256でハッシュ化したものになります。この値はサーバ側でさらにハッシュ化されて保存されます。(サーバ側のハッシュ化アルゴリズムは非公開)
React+vte.cx勉強会を開催します
もう空は少ないですが、初心者歓迎!Reactとvte.cxでWebアプリケーションを作成する#1 <動作確認~ソース解説>を開催します。
皆さん、ぜひご参加ください。
それでは。