- 投稿日:2022-02-01T23:20:48+09:00
【Nuxt】vue-chartjsの読み込みに失敗する!
TypeError: chart_js_WEBPACK_IMPORTED_MODULE_0_.default is not a constructor こんなメッセージが出てくる。なんでやねん!グラフ出したいだけなんですけど?! https://reffect.co.jp/vue/nuxt-js-vue-chartjs-easy-to-use こことかを参考にやってんだけどなぁ・・・ なんでこう記事では簡単に導入できてんのに、自分のときはエラーが出るんだ~!!って毎回思うw 解決の際に参考したサイト https://zenn.dev/kigi/scraps/f8c72302c0f84e https://stackoverflow.com/questions/66940954/why-does-nuxt-give-me-this-error-with-vue-chartjs 解決策 package.jsonのchart.jsのバージョンを2.9.4に書き換える package.json "dependencies" : { "chart.js": "^2.9.4", //このバージョンに書き換える ... "vue-chartjs": "^3.4.0", ... } .nuxtとnode_modulesとpackage-lock.jsonを削除 一旦上記のフォルダを削除 node install を実行 $ node install 実行後にnode_modulesとpackege-lock.jsonが作成される yarn devを実行 $ yarn dev このタイミングで.nuxtが作成される。 上記の手順で何故かうまくいきました。 どこが要だったのかよくわかりませんが、chart.jsのバージョンの変更は間違いなく必須だと思います。
- 投稿日:2022-02-01T20:52:07+09:00
Ubuntu 20.04 LTSでnode.jsのバージョンを上げる
Ubuntu 20.04 LTSだとapt installではバージョン10.19.0が当たるらしい。 # sudo apt install nodejs # node -v v10.19.0 apt install nodejsからの導入では、難ありなので、npmのインストールからやった方が良かった。 apt install npmでnpmをインストールする。 # sudo apt install npm npmでnパッケージをインストールする。 nで任意のバージョンをインストールできる。 ここでは現在の安定版の最新版の14.17.5を指定している。 # sudo npm install n -g # sudo n 14.17.5 installed : v14.17.5 (with npm 6.14.14) nコマンドを実行してバージョンを選択する。 ここでは先のバージョンしかインストールしていないので一つしか出ないはず。 # sudo n ο node/14.17.5 Use up/down arrow keys to select a version, return key to install, d to delete, q to quit 設定を反映する。 # sudo ln -sf /usr/local/bin/node /usr/bin/node node.jsのバージョンを確認する。 # node -v v14.17.5 先に指定したバージョンになっていれば完了。
- 投稿日:2022-02-01T14:36:00+09:00
formを用いたHTMLからMySQLへの値の挿入方法について。
formを用いたHTMLからMySQLへの値の挿入方法 Node.jsとMySQLを用いたアプリの開発途中に少し詰まったので備忘録として書いておきます。 HTML.CSS <form action="new" method="post"> <p>名前</p> <input type="text" name="name" > <p>性別</p> <input type="text" name="gender"> <p>年齢</p> <input type="text" name="age" > <p>生年月日</p> <input type="date" name="dob"> <input href="/new" type="submit" value="送信する"> </form> 例えば、このようなformの内容をMySQLに挿入したい場合、Node.js上でこのように記述することで挿入が可能です。 Node.js //データの追加はpost送信を用いる app.post('/new', (req, res) => { console.log(req.body); //formからの内容はreq.bodyに格納されるため、その中からformの内容を取り出し変数に挿入する処理。 const name = req.body.name; const gender = req.body.gender; const age = req.body.age; const dob = req.body.dob; console.log(name, gender, age, dob); //MySQLへ接続し、値を挿入する処理 connection.query( //変数を挿入したい部分は"?"で記述する。 'INSERT INTO users (name, gender, age, dob, level) VALUES (?, ?, ?, ?, ?)', //[]で囲んだ中に"?"に対応する変数を記述する。 [name, gender, age, dob], (error, results) => { console.log(results) console.log(error) res.redirect('/top',); } ); });
- 投稿日:2022-02-01T14:28:15+09:00
Webの勉強はじめてみた その27〜モジュールごとの実装とデータベース〜
N予備校「プログラミング入門Webアプリ」を受講しています。 今回は第3章20節〜26節です。 承認されたユーザーだけが使える匿名掲示板の作成。 気をつけたい箇所や気付いた点だけをまとめました。 設計の進め方 システム要件の定義 UIの設計: ページに何をどう配置するか URIの設計: パスの作り方(RESTful) モジュールの設計 URIとモジュール リクエストを具体的な処理に振り分けることをルーティング、 リクエストに対し具体的な処理をする関数を リクエストハンドラという サーバーを起動するもの リクエストを処理するもの ルーティングを行うもの など、それぞれ役割を決めて実装する。 メインとなる機能から実装していくのが良い。 処理を実装する前にそれぞれの機能などをコメントする。 覚えておきたいこと テンプレートエンジンなどを使う場合、のちにhtmlになるものはviewsディレクトリに格納する。 以下はPOSTされた時の処理 // POSTの処理 let body =[]; req.on('data', (chunk) => { body.push(chunk); }).on('end', ()=>{ // クエリから値を取得 body = Buffer.concat(body).toString(); const params = new URLSearchParams(body); const content = params.get('content'); console.info(`投稿されました: ${content}`); }); querystringが非推奨になったのでURLSearchParamを使う。URIエンコードされた文字列のデコードも同時に行うので、decodeURIComponentはいらない。 認証機能 http-authモジュールの、ファイルを利用した認証 users.htpasswd admin:apple guest1:1234 guest2:5678 const basic = auth.basic({ realm: 'Enter username and password.', file: './users.htpasswd' }) Basic認証では、特定のURLにアクセスした際、ステータスコード 401 - Unauthorized を返すことでログアウトされる。 function handleLogout(req, res){ handleStatus(req, res, 401, 'ログアウトしました'); } function handleNotFound(req, res){ handleStatus(req, res, 404, 'ページが見つかりません'); } function handleBadRequest(req, res){ handleStatus(req, res, 400, '未対応のメソッドです'); } /** * HTTPレスポンスの共通処理 * @param {Object} req HTTP Request * @param {Object} res HTTP Response * @param {Number} statusCode * @param {String} msg Message */ function handleStatus(req, res, statusCode, msg){ res.writeHead(statusCode, { 'Content-Type' : 'text/plain; charset=utf-8' }); res.end(msg); } データベース 今回はPostgreSQLをsequelizeモジュールで操作。 docker-compose.yml services: app: depends_on: - db db: image: postgres:12 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: secret_board TZ: "Asia/Tokyo" docker-compose.ymlにdbというサービス名でPostgreSQLのコンテナを追加。 仮想環境上でappとdbの二つのサーバーが稼働する状態。 Sequelize yarn add sequelize@6.5.0 yarn add pg@8.5.1 yarn add pg-hstore@2.3.3 データベースの構成を設定することを、データモデリングという post.js // sequelizeの基本設定 const { Sequelize, DataTypes } = require('sequelize'); // データベース全体の設定 const sequelize = new Sequelize( // IDとパスワードを渡す 'postgres://postgres:postgres@db/secret_board', { // 起動ログなどのログを出力しない logging: false } ); // データモデリング const Post = sequelize.define( // テーブル名 'Post', { // データ定義 id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, content: { type: DataTypes.TEXT }, postedBy: { type: DataTypes.STRING }, trackingCookie: { type: DataTypes.STRING } }, { // テーブル名の固定 freezeTableName: true, // createdAt, updatedAtを自動追加 timestamps: true } ); // このファイルの起動時にデータベースの設定を同期する Post.sync(); // データモデリングしたオブジェクトをモジュールとして公開 module.exports = Post; insert const Post = require('./post'); Post.create({ //登録したいデータ }).then(() => { //登録後に実行してほしい処理 }) 非同期処理となる(Promiseオブジェクトが返ってくる)ので、thenメソッドを利用する。 コンソール上でのデータ確認 dbコンテナへ入る docker-compose exec db bash su postgres : ユーザーの変更(今回はpostgresというユーザー) psql : Postgres用コンソールに入る ¥c secret_borad : データベースに接続(今回はsecret_boardというデータベース) ¥q : 終了 select データの全件取得 // データの全件取得 Post.findAll({order:[['id', 'DESC']]}).then((posts) => { res.end( pug.renderFile('./views/posts.pug', { posts }) ); }); renderFileの引数に変数を指定することで、pugで参照可能となる。 delete destroyで指定したキーのデータを削除 投稿者自身か管理者のみが削除できるようにする function handleDelete(req, res){ switch(req.method){ case 'POST': let body = []; req.on('data', (chunk) => { body.push(chunk); }).on('end', () =>{ body = Buffer.concat(body).toString(); const params = new URLSearchParams(body); const id = params.get('id'); Post.findByPk(id).then((post) => { if(req.user === post.postedBy || req.user === 'admin'){ post.destroy().then(() => { handleRedirectPosts(req, res); }) } }) }) break; default: util.handleBadRequest(req, res); break; } } curlなどで第三者がdeleteを送る可能性があるため、サーバー側でもユーザーのチェックをすること。 実行するとデータベースから実際にデータを削除することを物理削除、実際にデータを削除する代わりに「削除された」ことを表すフラグを立ててデータが削除されたとみなすことを論理削除という。 削除処理は慎重に。実用的にも論理削除が良い。 データベースの永続化 dockerを破棄するとデータも破棄されるので、PC側の別の場所にファイルを保存する。 docker-compose.yml db: image: postgres:12 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: secret_board TZ: "Asia/Tokyo" volumes: - ../secret-board-db:/var/lib/postgresql/data PC側: ../secret-board-db docker側: var/lib/postgresql/data docker起動時に同期させる トラッキングCookie ユーザーの行動を追跡するために付与される Cookie のことをトラッキング Cookie と呼ぶ。 cookiesというnpmモジュールを利用する yarn add cookies@0.8.0 const Cookies = require('cookies'); const trackingIdKey = 'tracking_id'; // TrackingCookie const cookies = new Cookies(req, res); addTrackingCookie(cookies); function addTrackingCookie(cookies){ // Cookieが存在しない場合 if(!cookies.get(trackingIdKey)){ // ランダムな数値を登録 const trackingId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); // 有効期限を翌日(24時間後)まで const tomorrow = new Date(Date.now() + (1000 * 60 * 60 * 24)); cookies.set(trackingIdKey, trackingId, {expires: tomorrow}); } } MAX_SAFE_INTEGER : JavaScriptで扱える最大値 PUG側 each post in posts - let isPostedAdmin = (post.postedBy === 'admin') if isPostedAdmin h3 #{post.id} : 管理人 ★ else h3 #{post.id} : ID:#{post.trackingCookie} p!= post.content p 投稿日時: #{post.createdAt} - let isAdmin = (user === 'admin') if isAdmin p 投稿者: #{post.postedBy} - let isDeletable = (user === post.postedBy || isAdmin) if isDeletable form(method="post" action="/posts?delete=1") input(type="hidden" name="id" value=post.id) button(type="submit") 削除 hr each in 渡されてきたコレクションをループ。 - pugテンプレート内にJavaScriptを直接記述することができる。 p!= post.content pugテンプレート内で文字列に含まれたタグを認識させる。 まとめ モジュールごとの実装、わかりにくかったのは名前のせいもあるのかな。 formからのPOSTメソッド、データベースのPost、データベースモデリングのpost.js、URIクエリのposts。 データベースからの削除は、なるほどと思った。 実際に削除するとなるとほかのテーブルも意識しないといけないので、実務的にも慎重になる事案。 しかし設計の重要性というのをつくづく感じる今日この頃。
- 投稿日:2022-02-01T14:14:24+09:00
Node.jsでのグローバル変数の記述法について。
Node.jsでのグローバル変数記述法について。 Node.jsを用いたアプリの開発中に少し詰まったので備忘録として書いておきます。 JavaScriptでは変数の宣言はvar name = valueと記述するのですが、Node.jsではvar name = valueと記述するとローカル変数とみなされてしまいます。 解決法 どうやらglobal.variable_name = valueという形で変数を宣言することでグローバル変数として扱えるようです。
- 投稿日:2022-02-01T12:29:54+09:00
AWS Lambda Layer(レイヤー)を作成する方法
Lambda Layerとは 複数のLambda関数に、共通の外部ライブラリやビジネスロジックを追加できる仕組みです。 1つのLambda関数に、5つまでLayerを追加できます。 しかし、Layerのサイズには、制限があります。 Lambda Layerを作成する方法 nodejsディレクトリを作成する ディレクトリ名は必ずnodejsにする必要があります。 (それ以外の名前では、ダメだと思います。) 必要なライブラリをインストールする npm init npm install (必要なライブラリ) npm initとnpm install (必要なライブラリ)を行い、node_modulesディレクトリが作成され、中にインストールしたものが入っていれば大丈夫です。 zipにしてアップロードする 上記で作成したnodejsディレクトリをzip形式にして、アップロードします。 アップロードする場所は、AWSのコンソール→Lambda→レイヤー→レイヤーの作成の順に進むと、アップロードするところがあります。 既存のレイヤーのバージョンを更新する方法 新しいレイヤーを作成せずに、既存のレイヤーを更新する場合は、既存レイヤーのnodejsディレクトリをインストールできます。 AWSのコンソール→Lambda→レイヤー→(更新したいレイヤーを選ぶ)の順に進み、ダウンロードすると、既存レイヤーのnodejsディレクトリがダウンロードできます。 そのディレクトリのなかに、必要なライブラリをインストールしてください。 その後、AWSのコンソール→Lambda→レイヤー→(更新したいレイヤーを選ぶ)の順に進み、新しいバーションを作成すると、既存レイヤーを更新できます。 既存のレイヤーを更新した場合でも、新しいバージョンのレイヤーを付け直す必要があります。 何か間違いや追加情報があれば、訂正してください!
- 投稿日:2022-02-01T09:50:19+09:00
【後編】OAuth2.0の勉強でAuthorization Code Grantをクライアント側として体感してみた
はじめに 本記事は「【前編】OAuth2.0の勉強でAuthorization Code Grantをクライアント側として体感してみた」の後編にあたる記事です。まずは前編として【前編】OAuth2.0の勉強でAuthorization Code Grantをクライアント側として体感してみたを参照ください。 以下、前編の「はじめに」に書いた内容と同じです。 ============================================================== 認証と認可に関しては、OpenID Connectがデファクトな状況だと思われる。今回はOAuth2.0を拡張した仕様であるOpenID Connectで定義されている、認可のフローであるAuthorization Code Grantについて、認可をもらう側(クライアント側)としてフローを実行する事で、理解を深めてみたいと思う。 流れとしては、 認可エンドポイントへ認可リクエスト 認証(今回はGoogleのOpenID Connectを使うので、Googleの認証)と認可 リダイレクトするので、それをserverで受け取りトークンエンドポイントへアクセストークンをリクエスト の中の、1と3の部分を実際に実装する。 ※今回はあえてCSRF対策やPKCE対策に必要になるパラメータを省略している。CSRF対策やPKCE対策については今後記事を執筆予定。 ※今回、Google Cloud PlatformでGoogle APIsの設定をしてAuthorization Code Flowを体感する。その際料金はかからない想定で本記事は書いている(無料枠があるので)。詳細はGoogle Cloud の無料プログラムを参照。ただし、Authorization Code Flowで取得したアクセストークンを使ってAPIを呼び出すが、その際に選んだAPIによっては課金される場合もあるため注意。本記事の内容を実践して万が一課金されても責任は負えません。Google Cloud Platformの利用にかかる課金やその他の事由については自己責任でお願い致します。) ※本記事中で筆者の理解に誤りがあればご指摘頂けると幸いです。 ※今回、文章量が多くなってしまったため、記事を前編と後編の2つに分割した。前編では、「OpenID Connectとは?という話からAuthorization Code Flowを体感するための事前準備まで」を書いている。後編では、「実際にクライアント側としてAuthorization Code Flowを実装し、アクセストークンを用いてAPIを実行するまで」を書いている。 ソースコード全体は以下。 ============================================================== 実際に実装してみる 今回はWebアプリケーションを想定して、Node.jsのExpressでアプリを構築してみる。フロントエンドは特にアプリケーションとしての機能を作りたい訳ではないので簡素に実装する(Vue.js・Vuetifyで見た目は少しきれいになるようにするが)。 実装するものとしては、 認可エンドポイントへ認可リクエストを行う部分 ユーザの認可後の認可レスポンスでredirectする先となるエンドポイント アクセストークンを使ってCalendarList: list を実行する部分 の3つ。 ※実際のプロダクトで使えるような実装ではないので注意。あくまでAuthorization Code Flowを体感するためのサンプル実装。 実装したコードの全体としては以下を参照。 認可エンドポイントへ認可リクエストを行う部分 画面からユーザが認可を行う(具体的にはGoogle Calendarとの連携を開始する)ためのボタンを作成する。ボタンをクリックすると、サーバサイドで認可リクエストのためのURLを作成し、それを302のリダイレクトでフロントエンドに返す事で、認可リクエストを行うという流れ。 コードとしては以下。 index.ejs <!DOCTYPE html> <html lang="ja"> <head> ... </head> <body> <v-app id="app"> ... <v-main> <v-container class="fill-height justify-center"> <v-card> <form action="/begin" method="GET"> <v-card-title> Google Calendarとの連携(認可リクエストを行う) </v-card-title> <v-card-text> <p>(例として…)</p> <p> Google Calendarとの連携を行うには、連携をクリックしてください </p> </v-card-text> <v-card-actions> <v-spacer></v-spacer> <v-btn type="submit" color="primary"> 連携する(認可リクエスト送る) </v-btn> </v-card-actions> </form> </v-card> </v-container> </v-main> ... </v-app> <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script> <script> new Vue({ el: '#app', vuetify: new Vuetify(), data: () => ({ ... }) }); </script> </body> </html> server.js import axios from 'axios'; import express from 'express'; import config from 'config'; import qs from 'qs'; ... app.get('/begin', async (req, res) => { const { data: openidConfig } = await axios.get(config.get('discovery')); const params = { client_id: process.env.CLIENT_ID, response_type: 'code', scope: config.get('authRequest.scopes').join(' '), redirect_uri: config.get('redirectUri') }; res.redirect( `${openidConfig.authorization_endpoint}?${qs.stringify(params)}` ); }); { "discovery": "https://accounts.google.com/.well-known/openid-configuration", "authRequest": { "scopes": [ "https://www.googleapis.com/auth/calendar.readonly", "https://www.googleapis.com/auth/calendar" ] }, "redirectUri": "https://example.com:8080/oauth2/callback" } 一部、ソースコードについて補足をする。 await axios.get(config.get('discovery')) 今回アクセストークンを払い出すGoogleの認証・認可サーバは、OpenID Connectの仕様を満たすものとして実装されており、OpenID Connectの仕様の中にはOpenID Connect Discovery 1.0という仕様も含まれている。このdiscoveryエンドポイントからはOpenID Providerのプロバイダー情報をJSONで取得できるので、プロバイダー情報内の認可エンドポイントの情報を取得するためにgetを行っている。 ちなみに、どのような情報が取得できるか?は3. OpenID Provider Metadataに書かれているものが全量で、Googleの場合にはThe Discovery documentに記載がある。 ※認可エンドポイントは常に固定というものではなく、可変なため毎回discoveryエンドポイントから最新の情報を取得するのが望ましいようである。 scope CalendarList: list Authorizationを見ると必要なscopeは以下の2つなので、それぞれをscopeに指定している。 https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar また、Authentication URI parametersに書かれているように、scopeは半角スペースで区切る必要があるので注意(。 All scope values must be space-separated.(すべてのスコープ値はスペースで区切る必要があります。) ちなみに、OpenID Connectの方(5.4. Requesting Claims using Scope Values)では、 Multiple scope values MAY be used by creating a space delimited, case sensitive list of ASCII scope values.(複数のスコープ値を使用するには、スペースで区切られたASCIIスコープ値のリストを作成すればよい(MAY)。) となっており、半角スペース区切りにするのは必須とは定義されていないのでOpenID Providerによりscopeが複数の場合の扱いは違う可能性があるのだろう。 const params = {...} 今回はscopeにopenidがないので、(基本的に)RFC6749の仕様に従うパターンになる。RFC6749の4.1.1. Authorization Requestに書かれている仕様に沿ってクエリパラメータを設定するためのJSONを定義している(JSONをクエリパラメータに変換するにはStringifying(qs.stringify(params))を利用)。 各キーに関する説明としては以下の通り。 response_type認可サーバに何を返してほしいのか?など、使用する認証処理フローを決定するための項目。必須。今回はAuthorization Code Flowでアクセストークンを払い出す認可フローなのでcodeになる。RFC6749にもValue MUST be set to "code".(値は "code "に設定しなければならない(MUST)。)と書かれている。 redirect_uri認可レスポンスで、そのレスポンスのLocationに設定されるURIを指定するための項目。今回はWebアプリなので、認可をリクエストするWebアプリのサーバのエンドポイントになる。これはOpenID Connectの仕様では必須となっているが、RFC6749では任意となっている。ちょっとこの辺りでどちらの仕様になるのか?が理解できていないが、Googleの場合、その動きを見ている限りOpenID Connectの必須の仕様で動いているようである(redirect_uriを指定しないで認可リクエストを行うと400エラーになった)。 client_id認可サーバ側に登録しているクライアント(今回はWebアプリ)を識別するためのIDを設定する項目。これはもちろん必須。ここでクライアントを作成するにて登録したclient_idが使われる。 scope認可を得たい権限を列挙して設定する項目。任意。今回はGoogleなのでOAuth 2.0 Scopes for Google APIs に全てのscopeが列挙されている。 ※今回は最低限のキーしか設定しないが、実際には10.12. Cross-Site Request Forgeryの対策としてのstateや、RFC7636の4.3. Client Sends the Code Challenge with the Authorization Requestに書かれているようにPKCE対策のためのcode_challenge・code_challenge_methodを追加する事が推奨されている。 ※scopeにopenidがない場合にはRFC6749になるというのは、3.1.2.1. Authentication Requestのscopeに、 OpenID Connect requests MUST contain the openid scope value. If the openid scope value is not present, the behavior is entirely unspecified.(OpenID Connectリクエストはopenidスコープ値を含まなければなりません(MUST)。openid スコープ値が存在しない場合、動作は全く規定されていません。) と書かれており、scopeにopenidがない場合にはOpenID Connectの仕様としては何も決まりがない=RFC6749になるという事だと思っている(参考情報として、1. response_type=codeには response_type の値が code の場合、これだけでは RFC 6749 の認可コードフローと何も変わりません。 と書かれており、また、認可リクエストの資料の中のRFC 6749 か OIDC かの動的判定の部分で、 scope パラメーターに openid が含まれていれば OIDC リクエストとみなす と書かれている)。 ユーザの認可後の認可レスポンスでredirectする先となるエンドポイント 認可リクエストを行うと、以下のような画面が表示されて、ユーザは認可を行う。 Googleにログイン(ID) Googleにログイン(パスワード) テストアプリの旨 認可 ※今回のクライアント(Webアプリ)はOAuth同意画面を設定するでテストモードで作成しているので上記のような警告が表示される 上記の「認可」でContinueを実行する=ユーザの認可が完了すると、Googleの認証・認可サーバから認可レスポンスが返ってくるが、その認可レスポンスのリダイレクト先となるエンドポイントの実装がここでの実装になる。ソースコードとしては以下のような感じ。 server.js import axios from 'axios'; import express from 'express'; import config from 'config'; import camelcaseKeys from 'camelcase-keys'; import AccessToken from './lib/access-token'; ... app.locals.AccessToken = new AccessToken(); ... app.get('/oauth2/callback', async (req, res) => { // eslint-disable-next-line no-shadow const { AccessToken } = req.app.locals; try { const { data: openidConfig } = await axios.get(config.get('discovery')); const params = new URLSearchParams(); params.append('client_id', process.env.CLIENT_ID); params.append('client_secret', process.env.CLIENT_SECRET); params.append('grant_type', 'authorization_code'); params.append('code', req.query.code); params.append('redirect_uri', config.get('redirectUri')); const { data } = await axios.post(openidConfig.token_endpoint, params); const camelCaseData = camelcaseKeys(data); AccessToken.setToken(camelCaseData.accessToken); res.render('./redirect.ejs', camelCaseData); } catch (error) { res.end(error.message); } }); access-token.js export default class AccessToken { constructor() { this.token = null; } setToken(token) { this.token = token; } getToken() { return this.token; } } redirect.ejs <!DOCTYPE html> <html lang="ja"> <head> ... </head> <body> <v-app id="app"> ... <v-main> <v-container> <v-row> <v-col cols="8"> <v-card> <v-card-title> Google Calendarへのアクセストークン </v-card-title> <v-card-text> <v-simple-table> <template v-slot:default> <thead> <tr> <th class="text-left">key</th> <th class="text-left">value</th> </tr> </thead> <tbody> <tr> <td>アクセストークン(access_token)</td> <td><%= accessToken %></td> </tr> <tr> <td>有効期間(expires_in)(単位は秒)</td> <td><%= expiresIn %></td> </tr> <tr> <td>スコープ(scope)</td> <td><%= scope %></td> </tr> <tr> <td>トークン種別(token_type)</td> <td><%= tokenType %></td> </tr> </tbody> </template> </v-simple-table> </v-card-text> <v-card-actions> <v-spacer></v-spacer> <v-btn color="primary" @click="getCalendarList"> 実際にAPIを実行 </v-btn> </v-card-actions> </v-card> </v-col> <v-col cols="8" v-if="items"> <v-card> <v-card-title> CalendarList: listの結果 </v-card-title> <v-card-text> <p v-for="item of items"> {{ item.summary }}:{{ item.description }} </p> </v-card-text> </v-card> </v-col> </v-row> </v-container> </v-main> ... </v-app> <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script> <script> new Vue({ el: '#app', vuetify: new Vuetify(), data: () => ({ ... items: null }), methods: { async getCalendarList() { try { const response = await fetch('/calendarList', { method: 'GET' }); const { items } = await response.json(); this.items = items; console.log(this.items); } catch (error) { console.log(error); } } } }); </script> </body> </html> 実際に認可レスポンスを受けて画面が描画されると以下のようになる。 一部、ソースコードについて補足をする。 const params = new URLSearchParams(); RFC6749の4.1.3. Access Token Requestに書かれている仕様に沿ってパラメータ(エンコードされたクエリパラメータ)を設定している。 各キーに関する説明としては以下の通り。 grant_type 認可グラント(認可コードの形式)を設定する項目。必須。今回はAuthorization Code Flow(Grant)なのでauthorization_codeになる(RFC6749にも、Value MUST be set to "authorization_code".(値は "authorization_code "に設定されなければならない(MUST)。)と書かれている)。 code 認可レスポンスに含まれている、認可サーバから渡される一時的なコードを設定する項目。必須。このコードと引き換えにアクセストークンを払い出してもらう。 redirect_uri 認可リクエストの際に指定したredirect_uriと同じものを指定する。認可リクエスト時にredirect_uriを設定している場合、必須(RFC6749にif the "redirect_uri" parameter was included in the authorization request as described in Section 4.1.1, and their values MUST be identical.(セクション4.1.1で述べられているように、「redirect_uri」パラメータが 認可リクエストに含まれていた場合、それらの値は同一でなければならない [MUST])と書かれている)。 client_id 認可リクエストの際に指定したclient_idと同じで、認可サーバ側に登録しているクライアント(今回はWebアプリ)を識別するためのIDを設定する項目。必須。 client_secret 認可サーバでクライアントを認証するために必要になる値を設定する項目。この項目はRFC6749の4.1.3. Access Token Requestには特に記載がないように思えるが、よく読むと If the client type is confidential or the client was issued client credentials (or assigned other authentication requirements), the client MUST authenticate with the authorization server as described in Section 3.2.1.(クライアントのタイプが機密である場合、またはクライアントがクライアント認証情報を発行された(または他の認証要件を割り当てられた)場合、クライアントはセクション 3.2.1 に記述されているように認可サーバーで認証されなければならない[MUST]。) と書かれているように、クライアントの認証方式によってはトークンエンドポイントへリクエスト(POST)する際に追加で必要になる項目がある事が分かる。今回はどうなのか?を確かめるために、まず、RFC6749の3.2.1. Client Authenticationを読むと、 Confidential clients or other clients issued client credentials MUST authenticate with the authorization server as described in Section 2.3 when making requests to the token endpoint.(機密保持クライアントまたはクライアント認証情報を発行された他のクライアントは、トークン・エンドポイントにリクエストを行う際に、セクション2.3の説明に従って認可サーバーで認証を行わなければならない(MUST)) と書かれており、今回はクライアント認証情報を発行されたに該当する(【前編】のクライアントを作成するでclient_secretが発行されている)ので、今度はRFC6749の2.3. Client Authenticationを読むと、 the client and authorization server establish a client authentication method suitable for the security requirements of the authorization server. The authorization server MAY accept any form of client authentication meeting its security requirements.(クライアントと認可サーバーが、認可サーバーのセキュリティ要件に適したクライアント認証方法を確立すること。認可サーバーは、そのセキュリティ要件を満たすいかなる形式のクライアント認証も受け入れてもよい(MAY)) となっており、2.3.1. Client Passwordにclient_sercretの事が書かれている。 今回のGoogleの認可サーバは、OpenId Discoveryのエンドポイントから取得できるプロバイダー情報のtoken_endpoint_auth_methods_supportedを見ると、そのクライアントの認証方式はclient_secret_post or client_secret_basicの2ついずれかである事が分かる。その事からも、クライアントの認証のためにclient_secretが必須で必要であり、client_secret_postで対応可能なので、クエリパラメーターにclient_secretを含める実装をしている。詳細は参考に示したOAuth 2.0 クライアント認証を参照。 ※当たり前だが、Googleの公式ページ4. Exchange code for access token and ID tokenにもトークンエンドポイントへのリクエストで何が必要か?は書かれている。 ※ちなみに、クライアントの認証についてOpenID Connectの仕様としては9. Client Authenticationに書かれている。 参考:OAuth 2.0 クライアント認証 アクセストークンを使ってCalendarList: list を実行する部分 ここまでの実装でアクセストークンを取得する部分まではできたので、ここでいよいよそのアクセストークンを使ってAPIを実行する部分を実装してみる。 Google Calendar APIを実行するためのアクセストークンを取得した(scopeの指定の部分(scope)[#scope]を参照)ので、CalendarList: list を参照して、APIの呼び出しを実装していく。 ソースコードとしては以下(ここではユーザの認可後の認可レスポンスでredirectする先となるエンドポイントで見た画面の「実際にAPIを実行」というボタンをクリックすると呼ばれるREST APIのエンドポイントを実装している)。 server.js app.get('/calendarList', async (req, res) => { // eslint-disable-next-line no-shadow const { AccessToken } = req.app.locals; try { const { data } = await axios.get( 'https://www.googleapis.com/calendar/v3/users/me/calendarList', { headers: { Authorization: `Bearer ${AccessToken.getToken()}` } } ); res.status(200).json(data); } catch (error) { console.log(error); res.status(500).json(error.message); } }); redirect.ejs <!DOCTYPE html> <html lang="ja"> <head> ... </head> <body> <v-app id="app"> ... <v-main> <v-container> <v-row> <v-col cols="8"> ... </v-col> <v-col cols="8" v-if="items"> <v-card> <v-card-title> CalendarList: listの結果 </v-card-title> <v-card-text> <p v-for="item of items"> {{ item.summary }}:{{ item.description }} </p> </v-card-text> </v-card> </v-col> </v-row> </v-container> </v-main> ... </v-app> <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script> <script> new Vue({ el: '#app', vuetify: new Vuetify(), data: () => ({ drawer: false, dumyMenus: ['Foo', 'Bar', 'Fizz', 'Buzz'], items: null }), methods: { async getCalendarList() { try { const response = await fetch('/calendarList', { method: 'GET' }); const { items } = await response.json(); this.items = items; } catch (error) { console.log(error); } } } }); </script> </body> </html> 実行した結果は以下の通り。 実際にGoogle Calendarの方を見てみると、3つのカレンダーが存在しており、それらがAPIで取得できている事が確認できる(グレーで塗りつぶしているものは、primaryカレンダー(googleのアカウントIDに紐づくカレンダー))。 Google Calendarの画面 Google Calendarの設定画面 一部、ソースコードについて補足をする。 AccessToken.getToken() 今回は簡易的にAPIを実行したいだけだったので、単純にAccessTokenClassのインスタンスからアクセストークンを取り出すような実装をしている(ユーザの認可後の認可レスポンスでredirectする先となるエンドポイントの章で取り上げたserver.jsのAccessToken.setToken(camelCaseData.accessToken);でインスタンスのfieldtokenにアクセストークンをsetしている)。 ただこの実装はプロダクトでは絶対にNGなので注意(ユーザ(ブラウザ)をサーバ側で把握する実装になってない)。プロダクトであれば、Redis等にアクセストークンを保存し、CookieにsidのようなsessionIdを渡して、リクエストが来た際にはCookieのsidからユーザ特定をしてアクセストークンを取り出すような実装が良くあるパターンな気がする。 まとめ 前編・後編までに長くなってしまったが、Authorization Code Grant(Flow)について、クライアント側としての動きを体感できた。今回はあえてCSRF対策やPKCE対策をしないでフローを実行していたので、今後その対策の必要性と実際の対策のための実装をやってみたいと思う。
- 投稿日:2022-02-01T09:49:31+09:00
【前編】OAuth2.0の勉強でAuthorization Code Grantをクライアント側として体感してみた
はじめに 認証と認可に関しては、OpenID Connectがデファクトな状況だと思われる。今回はOAuth2.0を拡張した仕様であるOpenID Connectで定義されている、認可のフローであるAuthorization Code Grantについて、認可をもらう側(クライアント側)としてフローを実行する事で、理解を深めてみたいと思う。 流れとしては、 認可エンドポイントへ認可リクエスト 認証(今回はGoogleのOpenID Connectを使うので、Googleの認証)と認可 リダイレクトするので、それをserverで受け取りトークンエンドポイントへアクセストークンをリクエスト の中の、1と3の部分を実際に実装する。 ※今回はあえてCSRF対策やPKCE対策に必要になるパラメータを省略している。CSRF対策やPKCE対策については今後記事を執筆予定。 ※今回、Google Cloud PlatformでGoogle APIsの設定をしてAuthorization Code Flowを体感する。その際料金はかからない想定で本記事は書いている(無料枠があるので)。詳細はGoogle Cloud の無料プログラムを参照。ただし、Authorization Code Flowで取得したアクセストークンを使ってAPIを呼び出すが、その際に選んだAPIによっては課金される場合もあるため注意。本記事の内容を実践して万が一課金されても責任は負えません。Google Cloud Platformの利用にかかる課金やその他の事由については自己責任でお願い致します。) ※本記事中で筆者の理解に誤りがあればご指摘頂けると幸いです。 ※今回、文章量が多くなってしまったため、記事を前編と後編の2つに分割した。前編では、「OpenID Connectとは?という話からAuthorization Code Flowを体感するための事前準備まで」を書いている。後編では、「実際にクライアント側としてAuthorization Code Flowを実装し、アクセストークンを用いてAPIを実行するまで」を書いている。 ソースコード全体は以下。 今回利用するOpenID Provider OAuth2.0?OpenID Connect? 早速だが、記事のタイトルではOAuth2.0の勉強と言いつつ、OpenID Connectという単語が出てきて??となるかもしれないので、ここでOAuth2.0とOpenID Connectについて私が理解している範囲で整理してみたいと思う。 ※ただしOpenID Connectがなぜできたのか?という話については触れない。それについてはOAuth 2.0 + OpenID Connect のフルスクラッチ実装者が知見を語る 認証と認可や単なる OAuth 2.0 を認証に使うと、車が通れるほどのどでかいセキュリティー・ホールができるを参照。 前提として…認証・認可とは? よく混乱しがちな認証と認可の概念ついて、OAuth2.0やOpenID Connectを理解する上では重要になるので、まずは認証と認可の概念について理解する所からスタートしたいと思う。 認証・認可について理解するには、OAuth 2.0 + OpenID Connect のフルスクラッチ実装者が知見を語る 認証と認可を参照頂くと具体的に概念としてどう違うのか?が分かりやすいと思う。端的に言ってしまうと、以下のようになるだろう。 認証とは「誰であるか?」を確かめる仕組みで、本人確認をする時に用いられる 認可とは「誰が誰に何の権限を与えるか?」に関する仕組みで、権限を委譲する時に用いられる(ここでいう委譲は、その相手が人とは限らずアプリケーションである事もある) つまり、認証と認可はそもそも別の概念である。 OAuth2.0とは? ここからはそれぞれ(OAuth2.0とOpenID Connect)について、何者なのか?を見ていく。 一番分かりやすい OAuth の説明の記事を参照するのが分かりやすい。記事を読むと分かるが、OAuth2.0とは、認可の仕組みである。具体的には、「誰が誰に何の権限を与えるか?」に関する仕組みで、権限を委譲する時に用いられる。ここでいう委譲は、その相手が人とは限らずアプリケーションである事もある。 実際の仕様としてはRFC6749にそれが定義されている。 ※認可の仕組みであるというのはRFC6749(3.1. Authorization Endpoint)にもちゃんと明記されており、 The authorization server MUST first verify the identity of the resource owner. The way in which the authorization server authenticates the resource owner (e.g., username and password login, session cookies) is beyond the scope of this specification.(認可サーバーは、最初にリソース所有者の身元を確認しなければならない(MUST)。認可サーバーがリソース所有者を認証する方法(例: ユーザー名とパスワードによるログイン、セッションクッキー)は、この仕様の範囲外である。) と書かれている。 OpenID Connectとは? 一番分かりやすい OpenID Connect の説明の記事を参照するのが分かりやすい。記事を読むと分かるが、OpenID Connectとは、OAuth2.0を拡張したもので、OAuth2.0の認可仕組み(フロー)を流用してIDトークンと呼ばれる認証情報(本人確認されたという事実とそのユーザーの情報)を発行するための仕様を定めたもの。 OpenID ConnectはOAuth2.0を拡張したものなので、 OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol. (Identity, Authentication) + OAuth 2.0 = OpenID Connect というような表現をされ、認証+OAuth2.0=OpenID Connectである。そのためOpenID Connectには認可の仕組みも含むものになっている(認可の仕組みについては、OAuth2.0のRFC6749そのままの部分ももちろんある。具体的にはOpenID Connectのresponse_type=code かつ scopeにopenidを含まないのパターン)。 OpenID Connectの実際の仕様(主要部分)は、OpenID Connect Core 1.0に定義がされている(その他にもOpenID Connect Discovery 1.0などあり、全てがOpenID Connect Core 1.0に書かれているわけではない)。 参考:一番分かりやすい OAuth の説明 参考:一番分かりやすい OpenID Connect の説明 参考:OAuth 2.0 + OpenID Connect のフルスクラッチ実装者が知見を語る 参考:OAuth 2.0 全フローの図解と動画 参考:OpenID Connect 全フロー解説 今回OAuth2.0の勉強なのにOpenID Connectなの?に対する答え 上記で見てきたように、OpenID Connectの仕様で実装されたサーバ(OpenID Provider)を利用すると、そのサーバには認証・認可の両方の機能が備わっており、認可はOAuth2.0の仕様を満たすものなので、OpenID Connectを利用してOAuth2.0の勉強をしていく。 ※OpenID Connectを利用する理由には、今後実際にOpenID ConnectでIDトークンの発行(Implicit Flow)などもやってみて理解を深めていく予定もあるので、単にOAuth2.0の認可サーバではなく、OpenID Connectの認証・認可サーバで勉強するという意図もある。 実際に実装するもののフロー はじめにで少し触れたが、ここでもう一度実装しようとしている、Authorization Code Grant(Flow)のクライアント側の動き( どんなフローなのか?)を図示して整理してみる。 今回は上記の図のフローの中で ①・②:認可リクエストを送る部分 ⑦を受け取るserverのエンドポイント:redirect_uriのエンドポイント ⑧・⑨:トークンエンドポイントへトークンのリクエストを送る部分 を実装する事になる。 ※上記のGoogle認証・認可サーバの部分で、認可エンドポイントのサーバとトークンエンドポイントのサーバという2つを描いているが、これはOpenID Discoveryのエンドポイント https://accounts.google.com/.well-known/openid-configuration を見ると認可エンドポイントとトークンエンドポイントのサーバのホストが違う事が分かったので(2022/01/27時点)、サーバを2つに分けて描いている。 ... "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth", ... "token_endpoint": "https://oauth2.googleapis.com/token", ... この辺りは、OpenID Providerによっては同じサーバに認可エンドポイントもトークンエンドポイントもある場合もあり、Providerによって違うものらしい。Yahoo Japan!のOpenID Providerの方(https://auth.login.yahoo.co.jp/yconnect/v2/.well-known/openid-configuration )はそれぞれ同じサーバである(2022/01/27時点では)。 "authorization_endpoint": "https://auth.login.yahoo.co.jp/yconnect/v2/authorization", "token_endpoint": "https://auth.login.yahoo.co.jp/yconnect/v2/token", 参考:OAuth 2.0 全フローの図解と動画 1. 認可コードフロー 参考:OAuth 2.0 の認可レスポンスとリダイレクトに関する説明 参考:The Discovery document 参考:Yahoo OpenID Configurationエンドポイント 事前準備 Authorization Code Flowについて理解を深めるために、今回はGoogle CalendarのデータをOAuth2.0の仕組みで取得するWebアプリを作成してみる。Google CalendarのAPIを実行できるようにするにはいくつか事前準備が必要なのでその準備を行う。以下で1つずつ見ていく。 ※公式で言うと、Authorizing Requests to the Google Calendar APIに書かれている内容が準備の内容になる。 また、今回はGoogleの認可サーバを利用するが、Googleの認可サーバではクライアントを作成する時に指定する事になるredirect_uriを持つアプリ(今回はWebアプリ)はhttpsである必要がある。さらにIPでの指定はできずパブリックトップレベルドメイン(.com、.org など)にする必要がある。そのための実装・設定も取り上げる。 APIを有効化する まずは使うAPI(CalendarList: list )を有効化する。 Google Cloud PlatformにアクセスしてAPIとサービスのページを開き、APIとサービスの有効化をクリックする。 1 2 検索の部分でcalendarと入力してEnterを押下すると、以下の図のようにGoogle Calendar APIというのが表示されるので、これをクリックして有効にするから有効にする。 3 4 OAuth同意画面を設定する 続いて、認可画面に表示される内容とユーザが認可するscopeの種類を登録する。上記のAPIを有効化するで開いたAPIとサービスのOAuth同意画面のメニューをクリックして、設定を行っていく(ここで設定する内容はユーザが実際に認可を行う際に表示される画面に表示される内容になる)。 外部を選択し、作成を行う。 続けて各Stepで設定を行っていく。 ※今回はAPIとしては、CalendarList: listを呼び出すが、このscopeとしてはCalendarList: list Authorizationに書かれているscopeが必要なのでそれをStep2のスコープで設定する。 ※アプリケーションのホームページ・プライバシーポリシーのリンク・利用規約のリンクなどは設定しなくても問題ない。 1 2 3 4 設定できたらOK。 クライアントを作成する 続いてクライアントを登録する。実際にAuthorization Code Flowを実行する時に必要となる情報(client_idやclient_secret)はここで作成される。 上記のAPIを有効化するで開いたAPIとサービスの認証情報のメニューをクリックして、設定を行っていく。 アプリケーションのHomeとなるURIとリダイレクトURIを設定して作成をすると、以下の図のようにclient_id・client_secretが作成されるのでメモしておく。このclient_id・client_secretは認可エンドポイントへ認可リクエストを行う時、トークンエンドポイントへのリクエストを行う時に使う。また、リダイレクトURIとは、実際に実装するもののフローの章で取り上げた⑥で返ってくる認可レスポンスの302のLocationに設定される。 1 2 ここまで設定できれば準備としては完了になる。 次の章からは実際に実装をしていき、アクセストークンを取得後、そのアクセストークンでCalendarList: list を実行してみる所までみていく。 ※ちなみに、OpenID Connect Dynamic Client Registration 1.0という仕様でクライアントを登録する際の仕様が定められている。 ExpressサーバのHTTPS化 Authorization Code Flowを体感するために、今回はWebアプリをクライアントとして実装するが、Node.jsのExpressを使う。 Googleの認可サーバの仕様に合わせてExpressサーバをhttps化する必要がある(理由は以下の背景についてを参照)のでそれを行う。 背景について HTTPS化する理由については、以下の2点。 GoogleAPIの設定でリダイレクトURIには、httpsのみが設定できるようになっているため (今回は上記で言及したようにOpenID Connectの仕様で実装された認可サーバでAuthorization Code Flowを行うのでOpenID Connectの仕様に従う部分が出てくるが)OpenID Connect Core 1.0 3.1.2.1. Authentication Requestのredirect_uriには、以下のように書かれており、httpも使えるが基本はhttpsを使うべきと書かれているため When using this flow, the Redirection URI SHOULD use the https scheme; however, it MAY use the http scheme, provided that the Client Type is confidential(このフローを使用する場合、Redirection URIはhttpsスキームを使用すべきですが、Client Typeが機密であれば、httpスキームを使用しても構いません(MAY)。) 実際にHTTPS化する HTTPS化するが、ちゃんと証明書を用意するのは手間なので、オレオレ証明書を使ってHTTPS化する。手順としては、Enabling HTTPS on express.jsに書かれている内容が参考になる。 まずはOpenSSLを使って、HTTPS化に必要になるkeyとcrtを生成する。以下のようなopensslコマンドを実行するとCLI上で対話しながらkey・crtを生成できる(質問に対する答えはダミーの内容で実在しないものも含まれている)。 [root@localhost openid-connect]# sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ./server.key -out server.crt Generating a 2048 bit RSA private key ..........................................+++ ..........+++ writing new private key to './server.key' ----- You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [XX]:JP State or Province Name (full name) []:Tokyo Locality Name (eg, city) [Default City]:Tokyo City Organization Name (eg, company) [Default Company Ltd]:Example, Inc. Organizational Unit Name (eg, section) []:Example Unit Common Name (eg, your name or your server's hostname) []:www.example.com Email Address []:dumy@example.com あとは、ここで生成したkeyとcrtを用いて、以下のようにhttpsモジュールを使ってサーバを作成すればいい(expressの公式の内容も参照)。 import express from 'express'; import https from 'https'; import fs from 'fs'; ... const app = express(); const server = callback.protocol === 'https:' ? https.createServer( { key: fs.readFileSync('./ssl/server.key'), cert: fs.readFileSync('./ssl/server.crt') }, app ) : app; ... server.listen(8080); 参考:set-up-SSL-in-nodejs 参考:How To Create a Self-Signed SSL Certificate for Apache in Ubuntu 16.04 参考:app.listen([port[, host[, backlog]]][, callback]) 参考:https.createServer([options][, requestListener]) windowsのhostsファイルを修正して、リダイレクト時のホストをIPに変換する(ローカルのなんちゃってDNSの設定) そもそもwindowsのhostsって何?という話は「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 hostsファイルが分かりやすいと思う。 本題だが、今回はAuthorization Code Grantを体感するために、OpenID Connectの仕様で実装されたGoogle's OAuth 2.0を使っているが、クライアントを登録しているする際に設定するリダイレクトURIにはIPは指定できない仕様になっている。 そのため何らかののドメインを設定する必要があるが、ちゃんとドメインを取得するのも面倒なのでなんちゃってDNSで対応する。 筆者はWindowsなのでhostsファイルを以下のように設定するだけでドメインをローカルのサーバのIPに解決する事ができるようになる。 # Copyright (c) 1993-2009 Microsoft Corp. # # This is a sample HOSTS file used by Microsoft TCP/IP for Windows. # # This file contains the mappings of IP addresses to host names. Each # entry should be kept on an individual line. The IP address should # be placed in the first column followed by the corresponding host name. # The IP address and the host name should be separated by at least one # space. # # Additionally, comments (such as these) may be inserted on individual # lines or following the machine name denoted by a '#' symbol. # 省略 192.168.56.2 example.com ※ちなみに、192.168.56.2はWindows上に立てたVirtualBoxの仮想マシンのIP。仮想マシンの構築についてはWindows上にLinux(CentOS)のWebアプリ開発環境をvirtualboxで構築するを参照。 続きは「【後編】OAuth2.0の勉強でAuthorization Code Grantをクライアント側として体感してみた」で
- 投稿日:2022-02-01T00:14:48+09:00
pnpmが`SKIP_PREFLIGHT_CHECK=true`を求めてくる場合の対処法
⚠️この情報は本記事の投稿時点で有効であったものです。各自エラーメッセージやissueを確認してください。⚠️ エラーメッセージの例 ❯ pnpm build > (プロジェクト名)@0.1.0 build /home/matoruru/Code/(省略) > craco build There might be a problem with the project dependency tree. It is likely not a bug in Create React App, but something you need to fix locally. The react-scripts package provided by Create React App requires a dependency: "webpack": "4.44.2" Don't try to install it manually: your package manager does it automatically. However, a different version of webpack was detected higher up in the tree: /home/matoruru/Code/(省略)/node_modules/.pnpm/node_modules/webpack (version: 4.46.0) Manually installing incompatible versions is known to cause hard-to-debug issues. If you would prefer to ignore this check, add SKIP_PREFLIGHT_CHECK=true to an .env file in your project. That will permanently disable this message but you might encounter other issues. To fix the dependency tree, try following the steps below in the exact order: 1. Delete package-lock.json (not package.json!) and/or yarn.lock in your project folder. 2. Delete node_modules in your project folder. 3. Remove "webpack" from dependencies and/or devDependencies in the package.json file in your project folder. 4. Run npm install or yarn, depending on the package manager you use. In most cases, this should be enough to fix the problem. If this has not helped, there are a few other things you can try: 5. If you used npm, install yarn (http://yarnpkg.com/) and repeat the above steps with it instead. This may help because npm has known issues with package hoisting which may get resolved in future versions. 6. Check if /home/matoruru/Code/(省略)/node_modules/.pnpm/node_modules/webpack is outside your project directory. For example, you might have accidentally installed something in your home folder. 7. Try running npm ls webpack in your project folder. This will tell you which other package (apart from the expected react-scripts) installed webpack. If nothing else helps, add SKIP_PREFLIGHT_CHECK=true to an .env file in your project. That would permanently disable this preflight check in case you want to proceed anyway. P.S. We know this message is long but please read the steps above :-) We hope you find them helpful! ELIFECYCLE Command failed with exit code 1. エラーメッセージが指示してくれている手順を踏んでも解決には至らず・・・。 解決策 package.json内に以下の記述を追加します。 "pnpm": { "overrides": { "webpack": "4.44.2" } }, 参考にしたissue react-scripts does not get the exact webpack version #4076 私のpnpmプロジェクトでも@react/storybookを使っていたので、同じくこの部分が問題になっていたようです。 筆者について フォローしていただけると励みになります! Twitter: https://twitter.com/_matoruru GitHub: https://github.com/matoruru