Passportを使ってUser Login機能を追加する(認証編)
ここまでを振り返る。
準備編では、Passportのpassport-localという認証方法(ストラテジー)を採用。
パスワードハッシュ編では、新規ユーザー登録の際、入力されたパスワードをハッシュ化してデータベースに保存できるようにした。
前回のSession編では、ログインできた後に必要になるセッションの設定をした。
色々つまづきながら長々とやって来たが、今日はその点と点を結んでいく。
Passportの初期設定
まずはPassportを使用した認証システムの初期設定をするためのコードを22-23行目に追加。
const app = express();
app.use(express.json());
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
store: new PrismaSessionStore(db, {
checkPeriod: 5 * 60 * 1000, // ms
dbRecordIdIsSessionId: true,
dbRecordIdFunction: undefined,
sessionModelName: "userSession"
})
})
);
app.use(passport.initialize()); // Passportの初期化
app.use(passport.session()); // セッションベースの認証有効化
app.post("/api/auth/signup", handleUserSignup);
app.post("/api/auth/login", handleUserLogin);
Passportの認証方法(ストラテジー)設定
今回はpassport-localという認証方法(ストラテジー)を採用することにしているが、まだストラテジーをインポートしただけなので、設定できていない。ストラテジーの設定をする。
先ほどのコードの下に書いてもいいが、今回はconfigurePassport
という関数にまとめて外出しすることにした。
// Initialize Passport and restore authentication state from session
app.use(passport.initialize());
app.use(passport.session());
// Configure Passport authentication strategy
configurePassport();
configurePassport
の中ではユーザー名とパスワードによる認証(LocalStrategy)の具体的な設定を2行目のstrategy
として設定し、41行目で登録している。パスワードハッシュ編の記事でhashPassword
と合わせて作っておいたverifyPassword
をここで使用。
export const configurePassport = function () {
const strategy = new LocalStrategy(async function verify(
username,
password,
done
) {
try {
// データベースから入力されたユーザー名と一致するユーザーを取得
const user = await db.user.findUnique({
where: { username },
select: {
id: true,
name: true,
username: true,
password: true,
},
});
// ユーザーが存在しない場合は、エラーを返すが、
// ブルートフォース攻撃を防ぐためにパスワードが間違っている場合と同じエラーメッセージを返す
if (!user) {
return done(null, false, {
message: "Incorrect username or password.",
});
}
// パスワードが間違っている場合は、エラーを返す
const isValid = await verifyPassword(password, user.password);
if (!isValid) {
return done(null, false, {
message: "Incorrect username or password.",
});
}
// パスワードが正しい場合は、ユーザーを返す
// この際に、パスワードを除外する!
// _は特定のプロパティを除外する。パスワード以外をsafeUserとして受け取ってreturnする
const { password: _, ...safeUser } = user;
return done(null, safeUser);
} catch (error) {
return done(error);
}
});
passport.use(strategy);
// Serialize user for the session
passport.serializeUser((user: any, done) => {
done(null, user.id);
});
// Deserialize user from the session
passport.deserializeUser(async (id: string, done) => {
try {
const user = await db.user.findUnique({
where: { id },
select: {
id: true,
name: true,
username: true,
},
});
done(null, user);
} catch (error) {
done(error);
}
});
};
しかしここに来て、なぜこのタイミングで認証しているのかよく分からなかった。つまりなぜユーザーがログインしようとするタイミングではなく、アプリを起動してpassport.initialize()
をした直後にこのfunctionを呼んでいるのか最初は理由が分からなかった。
configurePassport()は何をしているか
configurePassport
内でやっているのは認証を実行しているのではなく、Passport.jsに、認証方法はこれを使ってね(だからpassport.use(strategy)なのか!)とか、シリアライズや復元が必要なときはこうしてね、などを登録しているだけに過ぎないので実際に実行されるわけでない。Passport.js内部で適切なタイミングで自動的にこれらの関数を呼び出す設計になっているので、こちらとしてはここで登録しておくだけで良い。また、これらはアプリケーションの起動時に一度だけ行われる設定であり、リクエストごとに変わるものではない。
先ほどのコードサンプルのように、ユーザー情報のシリアライズ(セッションへの保存)とデシリアライズ(セッションからの復元)の方法も合わせて登録しておく。
“local”認証
次に、以下の一文を加える。これは先ほど設定したストラテジーを使って認証処理し、認証が成功すればsafeUserがreturnされる(失敗すればエラーが返させる)が、passport.js内部で処理されている部分が見えないため、少し分かりにくい。
export const local = passport.authenticate("local", { session: true });
先に使い方を見ると、以下の9行目のように、login時に認証をさせたいのでここに先ほどの関数(local)を挟む。passport.jsはリクエストに含まれるユーザー名とパスワードをローカルストラテジーで検証し、成功すればセッションを作成するという流れになる。特にユーザー名やパスワードをパラメータとして渡していないように見える。でも大丈夫。ストラテジーごとに決められた名前(”local”の場合はusername
とpassword
)に合わせておけば、デフォルトでreq.body.username
と req.body.password
から値を取得を勝手に取得して認証してくれる。
// Initialize Passport and restore authentication state from session
app.use(passport.initialize());
app.use(passport.session());
// Configure Passport authentication strategy and set up session serialization
configurePassport();
// auth Login
app.post("/api/auth/signup", handleUserSignup);
app.post("/api/auth/login", local, handleUserLogin);
フロント側で以下28行目のように、決められたプロパティ名にしておけばOK。
const userLoginMutation = useMutation({
mutationFn: userLogin,
onSuccess: () => {
queryClient.invalidateQueries(["all-challenges"]);
queryClient.invalidateQueries(["user-challenges"]);
navigate(`/`);
},
onError: (error: { message: string }) => {
setAlert({
message: error.message || "Registration failed",
severity: "error",
});
},
});
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const username = data.get("email-or-username")?.toString();
const password = data.get("password")?.toString();
const errors = {
username: !username ? "Username is required" : "",
password: !password ? "Password is required" : "",
};
setFormErrors(errors);
if (username && password && !errors.username && !errors.password) {
userLoginMutation.mutate({ username, password });
}
};
とりあえずこれで新規ユーザー登録とログインの機能ができた。データベースにはhash化されたパスワードが保存されていることも確認できた。ただ、まだログインしたユーザーの情報をどこにも使っていない(固定のダミーUserで表示している)ので、ユーザー情報の取得やそれに合わせた表示の切り替えなどまだまだやること多い。
今年のお花見は、浅草の人力車から!
当日はあいにくの雨でしたが楽しめました
