もう一度passport.jsについて

最近、彼らはexpress.jsのプロジェクトをサポートしてくれました。 プロジェクトコードを調べると、99.999%のケースのようにpassport.jsライブラリに基づいた認証/承認の混乱を招く作業が見つかりました。 このコードは機能し、「作業-触らない」という原則に従って、そのままにしておきました。 数日後、さらに2つの承認戦略を追加するタスクが与えられました。 そして、私はすでに同様の仕事をしていたことを思い出し始め、それは数行のコードを取りました。 passport.jsのドキュメントに目を通しましたが、何をどのようにすればよいのかを理解するのに苦労しませんでした。 そこで、彼らは厳密に1つの戦略が使用される場合を考慮しました。そして、それぞれ個別に例が与えられます。 しかし、いくつかの戦略を組み合わせる方法、logIn()メソッドを使用する必要がある理由(これはlogin()と同じです)-まだ明確にされていません。 そのため、今すぐ理解し、同じ検索を何度も繰り返さないために、これらのメモを自分用に編集しました。



少し歴史。 最初、Webアプリケーションは2つのタイプの認証/承認を使用しました。1)基本および2)Cookieを使用するセッションを使用します。 基本認証/許可では、ヘッダーが各リクエストで送信されるため、クライアント認証は各リクエストで実行されます。 セッションを使用する場合、クライアント認証は1回だけ実行されます(メソッドは、Basic、フォームで送信される名前とパスワード、およびpassport.jsの戦略と呼ばれる他の何千ものメソッドなど、非常に異なる場合があります)。 主なことは、認証後、クライアントがCookie(または一部の実装ではセッションデータ)のセッション識別子を識別し、ユーザー識別子がセッションデータに格納されることです。



最初に、認証/承認のためにアプリケーションでセッションを使用するかどうかを決定する必要があります。 モバイルアプリケーションのバックエンドを開発している場合、ほとんどの場合そうではありません。 これがWebアプリケーションである場合、ほとんどの場合はyesです。 セッションを使用するには、Cookieパーサー、セッションミドルウェアをアクティブにし、セッションを初期化する必要があります。



const app = express(); const sessionMiddleware = session({ store: new RedisStore({client: redisClient}), secret, resave: true, rolling: true, saveUninitialized: false, cookie: { maxAge: 10 * 60 * 1000, httpOnly: false, }, }); app.use(cookieParser()); app.use(sessionMiddleware); app.use(passport.initialize()); app.use(passport.session());
      
      





ここで、いくつかの重要な説明をする必要があります。 数年でredisがすべてのRAMを使いたくない場合は、セッションデータのタイムリーな削除に注意する必要があります。 maxAgeパラメーターがこれを担当します。これにより、Cookieとredisに保存された値にこの値が等しく設定されます。 resave:true、rolling:trueの値を設定すると、新しいリクエストごとに、指定されたmaxAge値で有効期間が延長されます(必要な場合)。 それ以外の場合、クライアントセッションは定期的に中断されます。 最後に、saveUninitialized:falseパラメーターは空のセッションをredisに配置しません。 これにより、不要なデータでredisを詰まらせることなく、アプリケーションレベルでセッションとpassport.jsの初期化を行うことができます。 ルートレベルでは、passport.initialize()メソッドを異なるパラメーターで呼び出す必要がある場合にのみ初期化を行うのが理にかなっています。



セッションを使用しない場合、初期化は大幅に削減されます。



 app.use(passport.initialize());
      
      







次に、戦略オブジェクトを作成する必要があります(passport.jsは用語で認証方法を呼び出します)。 各戦略には独自の構成機能があります。 変更されない唯一のことは、コールバック関数がストラテジーコンストラクターに渡されることです。ストラテジーコンストラクターは、次のミドルウェアキューのrequest.userとしてアクセス可能なユーザーオブジェクトを形成します。



 const jwtStrategy = new JwtStrategy(params, (payload, done) => UserModel.findOne({where: {id: payload.userId}}) .then((user = null) => { done(null, user); }) .catch((error) => { done(error, null); }) );
      
      







セッションを使用しない場合、保護されたリソースにアクセスするたびにこのメソッドが呼び出され、データベースへのクエリ(例のように)がアプリケーションのパフォーマンスに大きく影響することに注意する必要があります。



次に、戦略を使用するコマンドを与える必要があります。 各戦略にはデフォルト名があります。 ただし、明示的に設定することもできます。これにより、異なるパラメーターとコールバック関数のロジックで1つの戦略を使用できます。



 passport.use('jwt', jwtStrategy); passport.use('simple-jwt', simpleJwtStrategy);
      
      







次に、保護されたルートに対して、認証戦略と重要なセッションパラメータを設定する必要があります(デフォルトはtrueです)。



 const authenticate = passport.authenticate('jwt', {session: false}); router.use('/hello', authenticate, (req, res) => { res.send('hello'); });
      
      







セッションが使用されない場合、すべての制限されたアクセスルートで認証を保護する必要があります。 セッションが使用される場合、認証が1回発生し、これに対して特別なルート、たとえばログインが設定されます。



 const authenticate = passport.authenticate('local', {session: true}); router.post('/login', authenticate, (req, res) => { res.send({}) ; }); router.post('/logout', mustAuthenticated, (req, res) => { req.logOut(); res.send({}); });
      
      







保護されたルートでセッションを使用する場合、原則として、非常に簡潔なミドルウェア(何らかの理由でpassport.jsライブラリに含まれていない)が使用されます。



 function mustAuthenticated(req, res, next) { if (!req.isAuthenticated()) { return res.status(HTTPStatus.UNAUTHORIZED).send({}); } next(); }
      
      







そのため、最後の瞬間がありました-セッションへの/からのrequest.userオブジェクトのシリアライゼーションとデシリアライゼーション:



 passport.serializeUser((user, done) => { done(null, user.id); }); passport.deserializeUser((id, done) => { UserModel.findOne({where: {id}}).then((user) => { done(null, user); return null; }); });
      
      







シリアル化と逆シリアル化は{session:true}属性が設定された戦略でのみ機能することをもう一度強調したいと思います。 シリアル化は、認証直後に一度だけ実行されます。 したがって、セッションに保存されているデータの更新は、ユーザーID(変更されない)のみが保存されることに関連して、非常に問題となります。 シリアル化解除は、セキュリティで保護されたルートへのすべての要求で実行されます。 これに関連して、データベースへのクエリ(例のように)は、アプリケーションのパフォーマンスに大きく影響します。



発言。 複数の戦略を同時に使用する場合、これらのすべての戦略に対して同じシリアル化/逆シリアル化コードが機能します。 たとえば、認証が実行された戦略を説明するために、ユーザーオブジェクトに戦略属性を含めることができます。 また、異なる値でinitialize()メソッドを複数回呼び出すことは意味がありません。 前回の呼び出しの値で書き換えられます。



これで話は終わりかもしれません。 なぜなら すでに言われたこと以外は、実際には、他に何も必要ありません。 ただし、フロントエンド開発者の要求で、エラーの説明を含むオブジェクトを401応答に追加する必要がありました(デフォルトでは、これはUnauthorized行です)。 そして、これが判明したように、これを簡単に行うことはできません。 そのような場合は、ライブラリのコアを少し深くする必要がありますが、あまり良くありません。 passport.authenticateメソッドには、3番目のオプションパラメーターがあります:署名関数(エラー、ユーザー、情報)を持つコールバック関数。 小さな問題は、応答オブジェクトもdone()/ next()などの関数もこの関数に渡されないため、自分でミドルウェアに変換する必要があることです。



 route.post('/hello', authenticate('jwt', {session: false}), (req, res) => { res.send({}) ; }); function authenticate(strategy, options) { return function (req, res, next) { passport.authenticate(strategy, options, (error, user , info) => { if (error) { return next(error); } if (!user) { return next(new TranslatableError('unauthorised', HTTPStatus.UNAUTHORIZED)); } if (options.session) { return req.logIn(user, (err) => { if (err) { return next(err); } return next(); }); } req.user = user; next(); })(req, res, next); }; }
      
      







便利なリンク:



1) toon.io/understanding-passportjs-authentication-flow

2) habr.com/post/201206

3) habr.com/company/ruvds/blog/335434

4) habr.com/post/262979

5) habr.com/company/Voximplant/blog/323160

6) habr.com/company/dataart/blog/262817

7) tools.ietf.org/html/draft-ietf-oauth-pop-architecture-08

8) oauth.net/articles/authentication



apapacy@gmail.com

2019年1月4日



All Articles