私たちのほとんどは、少なくとも一度はアカウント回復プロセスを経験したことがあります。パスワードを忘れた場合、新しいパスワードを作成してシステムへのアクセスを取り戻す手順が必要になります。この記事では、Node.js、Knex、およびいくつかの非公開ツールを Express とともに使用してルートを処理し、必要な操作を実行するこのようなプロセスの実装に焦点を当てます。
ルーターの実装、URL パラメーターの処理、証明として電子メールまたは電話番号しか利用できない場合にユーザーに送信する内容の決定、電子メール送信の管理、セキュリティ上の懸念への対処について説明します。
コーディングに入る前に、同じコードベースを使って作業していることを確認したいと思います。コードベースには私のパブリックからアクセスできます。
次に、パスワードを忘れた場合のフローのスキーマを見てみましょう。
サーバーは、パスワードをリセットするための有効なリンクを含む電子メールをユーザーのメールボックスに送信する責任を負い、トークンとユーザーの存在も検証します。
電子メール サービスの利用を開始し、Node.js で電子メールを送信するには、既存の依存関係に加えて次のパッケージをインストールする必要があります。
npm i --save nodemailer handlebars
Nodemailer : SMTP またはその他のトランスポート メカニズムを使用して電子メールを簡単に送信できる強力なモジュール。
Handlebars : Handlebars は、JavaScript 用の人気のあるテンプレート エンジンです。これにより、レンダリング時にデータを入力できるプレースホルダーを含むテンプレートを定義できるようになります。
ここで、移行を作成する必要があるため、私の場合は、新しい列forgot_password_token
users
テーブルに追加する必要があります。
knex migrate:make add_field_forgot_password_token -x ts
生成されたファイルにコードを設定します。
import type { Knex } from 'knex'; export async function up(knex: Knex): Promise<void> { return knex.schema.alterTable('users', table => { table.string('forgot_password_token').unique(); }); } export async function down(knex: Knex): Promise<void> { return knex.schema.alterTable('users', table => { table.dropColumn('forgot_password_token'); }); }
「ユーザー」テーブルのパスワードを忘れた場合のトークンの移行
そして最新のファイルを移行します。
knex migrate:knex
これで、 users
テーブルに、 forgot_password_token
設定できるようになりました。
パスワード忘れとリセットのロジックを処理するコントローラーを管理するには、2 つのルートを確立する必要があります。最初のルートはパスワードを忘れた場合のプロセスを開始し、2 番目のルートは検証のために URL 内のトークン パラメータを期待してリセット プロセスを処理します。これを実装するには、 src/routes/
ディレクトリ内に、 forgotPasswordRouter.ts
という名前のファイルを作成し、次のコードを挿入します。
import { Router } from 'express'; import { forgotPasswordController } from 'src/controllers/forgotPasswordController'; import { resetPasswordController } from 'src/controllers/resetPasswordController'; export const forgotPasswordRouter = Router(); forgotPasswordRouter.post('/', forgotPasswordController); forgotPasswordRouter.post('/reset/:token', resetPasswordController);
パスワードを忘れたルーター
2 つのコントローラーが電子メールの送信とパスワードのリセットのロジックを管理します。
クライアントがパスワードを忘れると、セッションが失われます。これは、電子メールやその他のセキュリティ識別子以外のユーザー データを取得できないことを意味します。私たちの場合は、パスワードのリセットを処理するために電子メールを送信しています。このロジックをコントローラーに設定します。
forgotPasswordRouter.post('/', forgotPasswordController);
「パスワードを忘れた場合」を覚えていますか?ログイン フォームの下のリンクは通常、ログイン フォーム内のクライアントの UI にありますか?それをクリックすると、パスワードのリセットをリクエストできるビューに移動します。メールアドレスを入力するだけで、コントローラーが必要な手続きをすべて処理します。次のコードを調べてみましょう。
import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; import { TokenService } from 'src/services/TokenService'; import { EmailService } from 'src/services/EmailService'; export const forgotPasswordController = async (req: Request, res: Response) => { try { const { email, }: { email: string; } = req.body; const user = await UserModel.findByEmail(email); if (user) { const token = await TokenService.sign( { id: user.id, }, { expiresIn: '1 day', } ); await user.context.update({ forgot_password_token: token }); await EmailService.sendPasswordResetEmail(email, token); } return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } };
パスワードを忘れた場合のコントローラー
本文から電子メールを取得し、 UserModel.findByEmail
を使用してユーザーを検索します。ユーザーが存在する場合、 TokenService.sign
を使用して JWT トークンを作成し、そのトークンを 1 日の有効期限でユーザーforgot_password_token
に保存します。次に、ユーザーがパスワードを変更できるトークンと適切なリンクを含むメッセージを電子メールに送信します。
電子メールを送信できるようにするには、送信者となる新しい電子メール アドレスを作成する必要があります。
Google にアクセスして新しいメール アカウントを作成し、アカウントが作成されたら、 [Google アカウントの管理]リンクに進みます。右上のアバターをクリックすると見つかります。次に、左側のメニューで[セキュリティ]項目をクリックし、 [2 段階認証]を押します。以下に「アプリのパスワード」セクションがあり、矢印をクリックします。
使用する必要がある名前を入力します。私の場合は、 Nodemailer
設定してCreateを押しました。
生成されたパスワードをコピーし、 .env
ファイルに設定します。ファイルに 2 つの変数を設定する必要があります。
MAIL_USER="[email protected]" MAIL_PASSWORD="vyew hzek avty iwst"
もちろん、 info@company_name.com
のような適切なメールを設定するには、Google Workspace または AWS Amazon WorkMail を AWS SES またはその他のサービスと一緒に設定する必要があります。ただし、私たちの場合は、シンプルな Gmail アカウントを無料で使用しています。
.env
ファイルが準備できたら、電子メールを送信するためのサービスを設定する準備が整いました。コントローラーは、生成されたトークンとメッセージの受信者の電子メール アドレスを使用してサービスを使用します。
await EmailService.sendPasswordResetEmail(email, token);
src/services/EmailService.ts
を作成し、サービスのクラスを定義しましょう。
export class EmailService {}
そして今、初期データとして、 nodemailer
で使用する環境を取得する必要があります。
import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; dotenv.config(); export class EmailService { private static transporter: nodemailer.Transporter; private static env = { USER: process.env.MAIL_USER, PASS: process.env.MAIL_PASSWORD, }; }
メールサービス
サービスの初期化を行う必要があります。前回の記事でも書きましたが、
import { TokenService } from 'src/services/TokenService'; import { RedisService } from 'src/services/RedisService'; import { EmailService } from 'src/services/EmailService'; export const initialize = async () => { await RedisService.initialize(); TokenService.initialize(); EmailService.initialize(); };
サービスの初期化
ここで、 EmailService
クラス内で初期化の作成に進みましょう。
import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; dotenv.config(); export class EmailService { private static transporter: nodemailer.Transporter; private static env = { USER: process.env.MAIL_USER, PASS: process.env.MAIL_PASSWORD, }; public static initialize() { try { EmailService.transporter = nodemailer.createTransport({ service: 'gmail', auth: { user: this.env.USER, pass: this.env.PASS, }, }); } catch (error) { console.error('Error initializing email service'); throw error; } } }
電子メールサービスの初期化
nodemailer
ライブラリによって提供されるメソッドである初期化nodemailer.createTransport()
があります。電子メールの送信に使用されるトランスポーター オブジェクトを作成します。このメソッドは、トランスポーターの構成の詳細を指定するオプション オブジェクトを引数として受け取ります。
Google を使用しています: service: 'gmail'
電子メール サービス プロバイダーを指定します。 Nodemailer はさまざまな電子メール サービス プロバイダーの組み込みサポートを提供し、 gmail
はトランスポーターが Gmail の SMTP サーバーと連携するように構成されていることを示します。
認証auth
の場合、電子メール サービス プロバイダーの SMTP サーバーにアクセスするために必要な資格情報を設定する必要があります。
user
の場合は、メールの送信元のメール アドレスに設定する必要があり、そのパスワードはアプリ パスワードから Google アカウントで生成されています。
次に、サービスの最後の部分を設定しましょう。
import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; import { generateAttachments } from 'src/helpers/generateAttachments'; import { generateTemplate } from 'src/helpers/generateTemplate'; import { getHost } from 'src/helpers/getHost'; dotenv.config(); export class EmailService { // ...rest code public static async sendPasswordResetEmail(email: string, token: string) { try { const host = getHost(); const template = generateTemplate<{ token: string; host: string; }>('passwordResetTemplate', { token, host }); const attachments = generateAttachments([{ name: 'email_logo' }]); const info = await EmailService.transporter.sendMail({ from: this.env.USER, to: email, subject: 'Password Reset', html: template, attachments, }); console.log('Message sent: %s', info.messageId); } catch (error) { console.error('Error sending email: ', error); } } }
パスワードリセットメールを送信する
先に進む前に、クライアントが電子メールを受信するときに適切なホストを決定することが重要です。電子メール本文にトークンを使用したリンクを確立することが不可欠です。
import * as dotenv from 'dotenv'; import process from 'process'; dotenv.config(); export const getHost = (): string => { const isProduction = process.env.NODE_ENV === 'production'; const protocol = isProduction ? 'https' : 'http'; const port = isProduction ? '' : `:${process.env.CLIENT_PORT}`; return `${protocol}://${process.env.WEB_HOST}${port}`; };
ホストの取得
テンプレートの場合、 handlebars
を使用しているため、 src/temlates/passwordResetTemplate.hbs
に最初の HTML テンプレートを作成する必要があります。
<!-- passwordResetTemplate.hbs --> <html lang='en'> <head> <style> a { color: #372aff; } .token { font-weight: bold; } </style> <title>Forgot Password</title> </head> <body> <p>You requested a password reset. Please use the following link to reset your password:</p> <a class='token' href="{{ host }}/reset-password/{{ token }}">Reset Password</a> <p>If you did not request a password reset, please ignore this email.</p> <img src="cid:email_logo" alt="Email Logo"/> </body> </html>
パスワードリセットテンプレート
これで、このテンプレートをヘルパーで再利用できるようになりました。
import path from 'path'; import fs from 'fs'; import handlebars from 'handlebars'; export const generateTemplate = <T>(name: string, props: T): string => { const templatePath = path.join(__dirname, '..', 'src/templates', `${name}.hbs`); const templateSource = fs.readFileSync(templatePath, 'utf8'); const template = handlebars.compile(templateSource); return template(props); };
テンプレートヘルパーの生成
電子メールを強化するために、添付ファイルを含めることもできます。これを行うには、 email_logo.png
ファイルをsrc/assets
フォルダーに追加します。次に、次のヘルパー関数を使用して、この画像を電子メール内でレンダリングできます。
import path from 'path'; import { Extension } from 'src/@types/enums'; type AttachmentFile = { name: string; ext?: Extension; cid?: string; }; export const generateAttachments = (files: AttachmentFile[] = []) => files.map(file => { const ext = file.ext || Extension.png; const filename = `${file.name}.${ext}`; const imagePath = path.join(__dirname, '..', 'src/assets', filename); return { filename, path: imagePath, cid: file.cid || file.name, }; });
添付ファイルの生成ヘルパー
これらのヘルパーをすべて収集したら、次を使用して電子メールを送信できるようにする必要があります。
const info = await EmailService.transporter.sendMail({ from: this.env.USER, to: email, subject: 'Password Reset', html: template, attachments, });
このアプローチは適切なスケーラビリティを提供し、サービスがさまざまなコンテンツを含む電子メールを送信するためにさまざまな方法を使用できるようにします。
次に、ルーターでコントローラーをトリガーし、電子メールを送信してみましょう。そのために私が使っているのは、
コンソールには、メッセージが送信されたことが表示されます。
Message sent: <1k96ah55-c09t-p9k2–[email protected]>
受信箱内の新しいメッセージを確認します。
パスワードをリセットするリンクには、トークンとホストが含まれている必要があります。
http://localhost:3000/reset-password/<token>
このメッセージは開発プロセスに関係しているため、ここではポート3000
が指定されています。これは、パスワード リセットのフォームを処理するクライアントも開発環境内で動作することを示しています。
トークンは、電子メールを送信したユーザーを取得できる TokenService を使用してコントローラー側で検証する必要があります。トークンを使用するルーターを回復しましょう。
forgotPasswordRouter.post('/reset/:token', resetPasswordController);
コントローラーは、トークンが有効で有効期限が切れていない場合にのみ、有効期限が 1 時間に設定されているようにパスワードを更新します。この機能を実装するには、 src/controllers/
フォルダーに移動し、次のコードを含むresetPasswordController.ts
という名前のファイルを作成します。
import bcrypt from 'bcrypt'; import { Request, Response } from 'express'; import { TokenService } from 'src/services/TokenService'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; export const resetPasswordController = async (req: Request, res: Response) => { try { const token = req.params.token; if (!token) { return res.sendStatus(400); } const userData = await TokenService.verify<{ id: number }>(token); const user = await UserModel.findOneById<User>(userData.id); if (!user) { return res.sendStatus(400); } const newPassword = req.body.password; if (!newPassword) { return res.sendStatus(400); } const hashedPassword = await bcrypt.hash(newPassword, 10); await UserModel.updateById(user.id, { password: hashedPassword, passwordResetToken: null }); return res.sendStatus(200); } catch (error) { const errors = ['jwt malformed', 'TokenExpiredError', 'invalid token']; if (errors.includes(error.message)) { return res.sendStatus(400); } return res.sendStatus(500); } };
パスワードコントローラーのリセット
このコントローラーはトークンを受信して検証し、復号化されたデータからユーザー ID を抽出し、対応するユーザーを取得し、リクエスト本文でクライアントから送信された新しいパスワードを取得して、データベース内のパスワードの更新を開始します。最終的に、これによりクライアントは新しいパスワードを使用してログインできるようになります。
電子メール サービスのスケーラビリティは、パスワードの更新を示し、その後のログインを可能にするような確認メッセージや成功メッセージの送信など、さまざまなアプローチを通じて実証されます。ただし、特にアプリケーションのセキュリティを強化することが不可欠な場合、パスワードの管理は大きな課題となります。
セキュリティを強化するために利用できるオプションが多数あり、トークンの比較、電子メール、パスワードの検証など、パスワードの変更を許可する前の追加のチェックが含まれます。
もう 1 つのオプションは、サーバー側で検証するためにコードがユーザーの電子メールに送信される PIN コード システムを実装することです。これらの各対策では、電子メール送信機能を利用する必要があります。
実装されたすべてのコードは、
このビルドで自由に実験を行って、このトピックについてどのような点が評価できるかについてフィードバックを共有してください。どうもありがとう。
この記事で使用したいくつかの参考資料を次に示します。
ここでも公開されています