セッションベースの認証をGoFiberに追加


今回サーバにセッションベースのユーザ認証を追加した内容をまとめます.
セッションベースのユーザー認証
JSON Web Tokenベースのユーザー認証が好きでした.JWTを書くときにクライアントが格納するトークン値は常に不安定であるため、「secure storageforJWT」というキーワードを使って検索することが多く、次のような文章が発見されます.
GoFiber
Stateless JWT tokens cannot be invalidated or updated, and will introduce either size issues or security issues depending on where you store them. Stateful JWT tokens are functionally the same as session cookies, but without the battle-tested and well-reviewed implementations or client support.
Unless you work on a Reddit-scale application, there's no reason to be using JWT tokens as a session mechanism. Just use sessions.
この文章にかなりの共感があったため,新編のサービスはセッションに基づいてユーザ認証を行うことにした.
セッション・ベースのユーザー認証とは、ユーザーの「セッション」をサーバ上のデータベースに格納し、クライアントはそのセッションを見つけることができるセッション・キーのみを格納します.クライアントがセッションキーを使用してAPIを呼び出すと、サーバはセッションキーを使用してセッションデータベースにアクセスし、セッションが存在するかどうか、およびそのユーザが誰であるかを決定します.通常、「セッション」は、ユーザIDのサイズを記憶する.
セッションはサーバ側で保存および管理されるため、ユーザーがサーバ上でログイン/ログアウトを決定したり、セッションキーが漏洩または不正に漏洩した場合、すぐにサーバ上でセッションを無効にすることができるという利点があります.その欠点は、ユーザセッションを管理するために中央データベースが必要であり、多くのユーザが同時に接続するサービス(上記の例ではReddit)と、セッションを管理するデータベースのスケールの問題である.したがって,バックエンド構造が分散したマイクロサービスアーキテクチャなどの面では使いにくい.
セッション・ベースのユーザー認証コンポーネント
セッション・ベースのユーザー認証を実現するには、次の決定が必要です.
  • データベース
  • クライアントリポジトリ
  • つまり、これはストレージの問題であり、通常は次のように選択されます.
  • セッションを格納するために使用されるデータベース:通常、NoSQLまたはキー値データベースが使用されます.なぜなら、セッションには高速I/Oが必要であり、リレーションシップは必要ありません.通常、Redisなどのメモリベースの高性能NoSQLデータベースが使用されます.
  • セッションキーを格納するクライアント・リポジトリ:通常、クッキーが使用されます.クライアントがfetchなどの関数を使用してAPIを呼び出すと、Originが同じである場合、Cookieは自動的にサーバに含めて送信されます.Cookieは小さくて軽いですが、その「自動」の特性のため、安全性の面で多くの弱点があり、数十年来様々な方法で改善されてきました.セッションキーの保存には、通常、httpOnlyおよびsecureタグが使用されます.
  • GoFiberでのセッションベースのユーザー認証の構成
    セッションベースのユーザ認証機能を実装する際に使用されるクッキーには、httpOnlyおよびsecureのオプションが含まれる.このうちhttpOnlyとは、CookieがクライアントのJavaScriptコードにアクセスできないことを意味する.たとえば、元のフロントエンドのJavaScriptコードから、document.cookieAPIで保存されているCookieを読み込み、書き込み、変更できますが、httpOnlyCookieはこのAPIからアクセスできません.ブラウザは、HTTP呼び出しに対してのみ自動的に含まれます.そのためJavaScriptを挿入することでCookieに対して攻撃をブロックするなど安全です.
    2番目のsecureオプションは、SSL/TLS呼び出しのみに対応するCookieが含まれることを意味する.安全のための最も基本的な措置といえる.また、secureオプションは、フロントエンドサーバとバックエンドサーバのホストが異なる場合、Cross-Origin構成にも必要です.
    CORS設定
    Cross-Origin CookieをhttpOnlyおよびsecureオプションとして使用するには、CORSにいくつかの制限があります.
  • Access-Control-Allow-Credentialstrueでなければなりません.
  • Access-Control-Allow-Originおよび Access-Control-Allow-Headersワイルドカード(*)の値は割り当てられません.どのOriginとHeadersが利用可能かを明確に定義する必要があります.
  • したがって、CORSに関連するGoFiberアプリケーションコードを以下のように変更します.
    app := fiber.New()
    ...
    app.Use(cors.New(cors.Config{
    	// TODO: production 에서 수정
    	AllowOrigins: "https://localhost:3000", 
    	AllowMethods: strings.Join([]string{
    		fiber.MethodGet,
    		fiber.MethodPost,
    		fiber.MethodDelete,
    		fiber.MethodPatch,
    	}, ","),
    	AllowCredentials: true,
    }))
    セッション・リポジトリの設定
    セッションを保存するためにデータベースを設定する必要があります.GoFiberはStop using JWT for sessionsを事前に定義した.私はすでにMongoDBを書いているので、まずMongoDBにセッションを追加しました.
    import (
    	...
    	"github.com/gofiber/fiber/v2/middleware/session"
    	"github.com/gofiber/storage/mongodb"
        ...
    )
    
    // 전역변수로 선언해서 사용해도 좋다
    var store *session.Store
    
    func NewSessionStore() {
    	storage := mongodb.New(mongodb.Config{
        	ConnectionURI: "mongodb://...",
        })
        
        // Session 저장소 생성
        store = session.New(session.Config{
        	Storage: storage,
            ...        
        })
    }
    ここには複数のオプションがありますが、デフォルトではオプションsecurehttpOnlysame-siteが調整されます.
    func NewSessionStore() {
        ...
        // Session 저장소 생성
        store = session.New(session.Config{
            ...       
            CookieSecure:   true,
            CookieHTTPOnly: true,
            CookieSameSite: "None", // For cross-origin
        })
    }
    では、Appの作成時にNewSessionStore関数を呼び出します.
    func New() *fiber.App {
        ...
        NewSessionStore()
        
        ...
        app := fiber.New()
        ...
        
        return app
    }
    GoFiberの公式サイトはMiddlewareセクションでセッションに関するコードを紹介していますが、厳密にはミドルウェアではありません.データベース設定に近い.データベース設定コードの近くで一緒に設定すればいいのです
    セッション保存値の読み取りと書き込み
    セッション・リポジトリとして、書き込み、読み取り、消去を行います.ユーザログイン時に書き込み、ユーザログアウト時に消去し、検証が必要なAPIで読み出す.
    書き込み
    
    var store *session.Store
    ...
    func SetSession(userID string, c *fiber.Ctx) error {
        // Fiber Context 에 맞춰 세션 저장소를 불러온다.
        sess, err := store.Get(c)
        if err != nil {
        	return err
        }
        
        sess.Set("user", userID)
        
        // set-cookie 헤더에 자동으로 세션키를 포함한다
        if err := sess.Save(); err != nil {
        	return err
        }
    }
    読み取り
    var store *session.Store
    ...
    func GetUserIDFromSession(userID string, c *fiber.Ctx) (string, error) {
        // Fiber Context 에 맞춰 세션 저장소를 불러온다.
        sess, err := store.Get(c)
        if err != nil {
        	return err
        }
        
        raw := sess.Get("user")
        if raw == nil {
        	return "", errors.New("user not logged in")
        }
        
        userID, ok := raw.(string)
        if !ok {
        	return "", errors.New("malformed session")
        }
    
        return userID, nil
    }
    クリア
    
    func RemoveAuthenticatedUserID(c *fiber.Ctx) error {
        // Fiber Context 에 맞춰 세션 저장소를 불러온다.
        sess, err := store.Get(c)
        if err != nil {
        	return errors.WithStack(err)
        }
    
        if err := sess.Destroy(); err != nil {
        	return errors.WithStack(err)
        }
    
        return nil
    }
    sess.Destroy()関数はクライアントCookieを削除することもあります.
    フロントエンドでの使用
    フロントエンドコードでは、fetch関数を呼び出すときにcredentials: 'include'値を追加することが重要です.
    await fetch('https://...', {
      method: 'POST',
      ...
      credentials: 'include'
    })
    Set-CookieHeaderは応答の呼び出し、すなわちログインAPI呼び出しとして、credentialsを追加する必要がありますので、ご注意ください.