every Tech Blog

株式会社エブリーのTech Blogです。

STG環境でSSO認証を行う

はじめに

エブリーでデリッシュキッチンの開発をしている本丸です。
デリッシュキッチンのSTG環境のWEBへのアクセスには社内ユーザーからのみという制限があります。 先日、制限をかけるシステムに触れる機会があったので、今回はそのシステムについて紹介しようかと思います。

背景

先日、社外の方にSTG環境にアクセスしてもらうことがあったのですが、Google SSOの認証のページに飛ばされてしまって確認してもらうことができないということがありました。
エブリーではGoogle Workspaceを利用しており、Google WorkspaceのSSOを利用して業務アプリケーションにアクセスすることが多いです。仕組みを調査したところ、このSSOの認証がSTG環境で必須になっており、SSOの認証画面にリダイレクトされていました。

STG環境でSSO認証を行う

以下ではSTG環境でSSO認証を行う仕組みについて述べますが、SSO認証に用いるIdP(Identity Provider)の詳細や設定方法については記事の範囲外とさせていただきます。
また、デリッシュキッチンのWebはアプリケーションが動作しているECSの前段にELBやCloudFrontが存在しており、今回の記事の内容では通信がCloudFrontを経由していることが前提となります。
STG環境でSSO認証を行うためにLambda@EdgeとGoogleのIdPを利用します。
おおよその流れは図のようになりますが、後で疑似コードで実際にどのように実装しているかを説明します。

Lambda@Edge

AWSの公式ドキュメント Lambda@Edge を使用してエッジでカスタマイズする - Amazon CloudFrontによると、

Lambda@Edge は AWS Lambda の拡張です。Lambda@Edge は、Amazon CloudFront が配信するコンテンツをカスタマイズする関数を実行できるコンピューティングサービスです。

となっています。
Lambda@Edgeを使うことによってCloudFrontから配信するコンテンツをカスタマイズすることができるので、今回はこのLambda@Edgeを使ってSTG環境へのアクセスにSSO認証をかけます。
Lambda@Edgeは下記の4つのイベントにトリガーできるのですが、ビューワーリクエストをトリガーにして認証を行なっていくことになります。

  • ビューワーリクエスト: ユーザーからCloudFrontへのリクエスト
  • オリジンリクエスト: CloudFrontからオリジン(コンテンツ)へのリクエスト
  • オリジンレスポンス: オリジン(コンテンツ)からCloudFrontへのレスポンス
  • ビューワーレスポンス: CloudFrontからユーザーへのレスポンス

Lambda@Edgeで何をしているか

以下では、疑似コードを載せていますが、エラーハンドリングを無視していたりと実際に動くものではないことはご留意いただければと思います。

STG環境でSSO認証を行うためにLambda@Edgeで大きく分けて2つの処理を行っています。

  1. 特定条件の時に認証のスキップ
  2. SSO認証を行う

1. 特定条件の時に認証のスキップ

exports.handler = async (event, _, callback) => {
  const request = event.request;
  // 指定されたIPアドレスから指定されたpathへのアクセスであれば、認証を無視する
  if (allowedPath(request.uri) && await allowedIP(request.clientIp)) {
    // requestを継続してcontentsにアクセスする
    return callback(null, request);
  }

  const headers = request.headers;
  // SSO認証を行う
  return await sso.main(request, headers, callback);  
};

ここでは、今回この記事を書く背景となった外部の方にSTG環境にアクセスしてもらう場合など、SSO認証が行えないがアクセスさせたい場合に例外的にアクセスを許可しています。認証をスキップする条件に縛りはないのですが、疑似コードではIPアドレスとアクセスするコンテンツのpathで条件をかけています。
もし条件に一致しない場合はSSO認証を行うことになります。

2. SSO認証を行う

exports.main = async (request, headers, callback) => {
  try {
    // ④ トークンがsetされている場合
    if (hasToken(headers)) {
      // トークンの検証を行い、結果を返す
      return validateToken(request, headers, callback);
    }

    // ② 認可コードを渡すcallbackとして呼び出された場合
    if (request.uri.startsWith(config.CALLBACK_PATH)) {
      const queryDict = qs.parse(request.querystring);
      // 認可コードとトークンの交換をIdPにリクエストする
      const response = await requestToken(queryDict);
      return setToken(request, headers, response, queryDict, callback);
    }

    // ① SSO認証のためにIdPにリダイレクトする
    redirectToIdP(request, callback);
  } catch (error) {
    console.error(error);
    throw(error);
  }
};

function setToken(request, headers, response, queryDict, callback) {
  const decodedData = jwt.decode(response.id_token);

  // JWTの確認を行う
  jwt.verify(decodedData);
  
  // ③ CookieにトークンをsetするためにSet-Cookieを指定してresponseを返す
  const response = getResponseForSetToken(queryDict, config, headers, decodedData);
  callback(null, response);
}

function validateToken(request, headers, callback) {
  // JWTの確認を行う
 jwt.verify();

  // requestを継続してcontentsにアクセスする
 callback(null, request);
}

疑似コードを見ていただければ、どのような処理を行っているのか理解していただけるかもしれませんが、簡単に説明していきます。

始めに、④や②に当てはまらなかった場合、つまり、SSO認証の結果のトークンがsetされておらずcallbackでリダイレクトされたpathでもない場合は、認証が完了していないため①でIdPの認証ページにリダイレクトさせます。

次に、IdPで認証が成功した場合はcallbackで規定のpathにリダイレクトされます。callbackでリダイレクトされている場合は認可コードを受け取っているはずなので、この認可コードを使ってIdPにトークンをリクエストします。無事にIdPからトークンを取得できた場合は③でトークンをCookieにセットした後に元のリクエストページへリダイレクトさせます。

最後に、元のリクエストにリダイレクトされた際にSSO認証の結果のトークンがsetされているはずなので④で確認を行います。ここでトークンがsetされている場合はトークンの正当性を確認します。トークンの正当性が確認できた場合は、SSO認証済みのはずなのでコンテンツにアクセスするためのrequestを継続します。

まとめ

今までLambda@Edgeを触ったことがなかったので勉強する良い機会になりました。
この手のシステムは一度開発が完了するとなかなか触れる機会がないことも多いかと思いますが、積極的に触ってみることも大切だと感じました。