LINE WORKS × AWS Cognitoを使ったSAML,OpenID ConnectのSSO認証について

2023/12/28に公開

LINE WORKS × AWS Cognitoを使ったSAML,OpenID ConnectのSSO認証について

はじめに

初めまして。ファストドクター株式会社でソフトウェアエンジニアをしている今泉と申します。主にAWSを使ったインフラ構築とTypeScriptを使ったアプリケーション開発を行っています。

早速ですが、LINE WORKSとAmazon Cognito を、繋げたことありますか?

弊社でもCognitoベースのSSO認証を導入しています、とあるシステムでは LINE WORKSとつなげる必要があり、私が担当することになりました。Cognito/SSO もそこまで知識があったわけではない上に、LINE WORKS は簡単につながらず、特殊なことをしないといけなく、結構ハマってしまいました。

その時に、やったこと、注意点やノウハウを、ぜひ、みなさまに共有させてください。

だれに見て欲しいか

  • SAMLのデバッグ方法を知りたい人
  • LINE WORKSとAWS CognitoによるSSO認証を使ってアプリケーションにログイン機能を作りたい人

先にLINE WORKS × AWS CognitoのSSO認証の結論から

LINE WORKS(IdP側)とAWS Cognito(SP側)のSSO認証はできなくはないが、通常とは違うSSO認証でしか連携ができない。

実現する方法としては

  • LINE WORKS(OAuth) × OAuth-OIDC-wrapper × AWS Cognito(OIDC)

のように、OAuthとOpenID Connect(OIDCと略します)を変換するWrapperを用意するしかAWS Cognitoでは動かないので

  • セキュリティ要件を精査した上でwrapper構成を使う
  • AWS Cognitoは使わずLINE WORKSのSAML要件にあう別のものにする
  • LINE WORKSのSSO認証要件を諦める

この3つの選択肢をとる必要があります。

試したこと

LINE WORKS(SAML) × AWS Cognito(SAML)

LINE WORKS公式ドキュメントにはLINE WORKSをIdPとした場合のSSO認証はSAMLしかありません。

IdP側でよくある機能として、SP側に連携するためのxmlファイルのメタデータドキュメントをダウンロードして、それをSPにアップロードしてIdP ⇄ SPのSAMLの通信を規定します。

しかし、LINE WORKS側にはメタデータドキュメントを提供する機能がなく、自分でSAMLの要件を確認しながらメタデータファイルをXMLで記述します。

メタデータファイルの一例

<?xml version="1.0"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" validUntil="2023-02-18T04:59:54Z" cacheDuration="PT1677128394S"entityID="xxxxxxxxxxxxxxx">
  <md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
    <md:KeyDescriptor use="signing">
      <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <ds:X509Data>
          <ds:X509Certificate>xxxxxxxxxx</ds:X509Certificate>
        </ds:X509Data>
      </ds:KeyInfo>
    </md:KeyDescriptor>
    <md:KeyDescriptor use="encryption">
      <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <ds:X509Data>
          <ds:X509Certificate>xxxxxxxxxx</ds:X509Certificate>
        </ds:X509Data>
      </ds:KeyInfo>
    </md:KeyDescriptor>
    <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
    <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="xxxxxxxxxxxxx"/>
  </md:IDPSSODescriptor>
</md:EntityDescriptor>

そして出来上がったら実際に開発しているログイン画面から認証をしてみます。

その際、Google拡張機能の「SAML Chrome Panel」を開いておきます。この拡張機能はSAMLが使われている時には、SAML RequestとSAML Responseを可視化することができるので、SAMLのデバッグに便利です。
https://chromewebstore.google.com/detail/saml-chrome-panel/paijfdbeoenhembfhkhllainmocckace?hl=ja

しかし、メタデータドキュメントを色々変えても、SAML Requestが下記2点においてLINE WORKS推奨のものにならずエラーになってしまいます。

① protcolbindingが表示されない
② Issuerに意図しないFormat指定がある

Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"

LINE WORKS推奨 SAML Request

<?xml version="1.0" encoding="UTF-8"?>
<saml2p:AuthnRequest
    xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"
    AssertionConsumerServiceURL="https://example.com/acs/vendor.com"
    ID="fiokocckbjonklcjiepfejmoehpebebmholeoibp"
    IssueInstant="2018-02-25T07:42:35Z"
    ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
    ProviderName="example.com"
    Version="2.0">
    <saml2:Issuer
        xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">SPIssuer</saml2:Issuer>
    <saml2p:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameidformat:unspecified"/>
</saml2p:AuthnRequest>

AWS Cognitoにメタデータを登録した結果のSAML Request

<?xml version="1.0" encoding="UTF-8"?>
<saml2p:AuthnRequest 
    xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" 
    AssertionConsumerServiceURL="https://auth.dev.iwith.xyz/saml2/idpresponse" 
    Destination="https://auth.worksmobile.com/saml2/idp/works-506217" 
    ID="_3e9f7504-14d1-42df-a130-172624d6935e"
    IssueInstant="2023-02-08T06:08:26.814Z" Version="2.0">
     <saml2:Issuer
         xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">urn:amazon:cognito:sp:xxxxxxxxxxxxxxx</saml2:Issuer>
</saml2p:AuthnRequest>

SAML公式ドキュメントであるSAML Wikiを見ながらメタデータを色々調整したがうまくいきませんでした。
https://wiki.oasis-open.org/security/FrontPage

次に、IdPやSPをDockerでKeycloakを立てて実際に処理を見立ててデバッグしていきました。

余談ですが、KeycloakはOIDC,SAML,OAuthなどの認証・認可の標準仕様を押さえているOSSで、認証認可の開発をする方には是非抑えていただきたいOSSです。

詳しいことを書くとそれだけで一つの記事になりますので、詳細は下記をご覧ください。
https://www.keycloak.org/getting-started/getting-started-docker

しかしそれでもデバックは進まず最終的にAWSのサポート窓口に確認して
AWS Cognitoの仕様上、LINE WORKSの推奨のSAML Requestは送れないと結論がでました。

LINE WORKS(OAuth) × OAuth-OIDC-wrapper × AWS Cognito(OIDC)

調べている中でOAuth × OIDCをwrapperで疎通させる方法があり、試したところ認証はうまくいきました。

概要

  • LINE WORKS側をOAuth2.0のAccount認証(OAuth)
  • AWS Cognito側はOIDC
  • 上記はそのままだとプロトコルが違うので、OAuth ⇄ OIDC を変換するwrapperをAWS Lambdaで立てる

シーケンス図

黒線はAWS Cognitoの処理部分、赤線はそれ以外で分けて表示しています。

やったこと

元々GitHubとAWS CognitoのOIDC ⇄ OAuthのラッパーでしたが、これをLINE WORKS用に修正し、AWS SAMでLambdaをデプロイしました。

Lambdaが4つできますので、API GatewayのエンドポイントをAWS CognitoのOIDCの設定で

  • 承認エンドポイント
  • トークンエンドポイント
  • UserInfoエンドポイント
  • Jwks_uriエンドポイント

に、それぞれ対応するエンドポイントを入れて、許可されたスコープを

  • openid
  • user

に設定してあげます。

LINE WORKS側のOAuthについてはUser Account認証を使い実装します。
https://developers.worksmobile.com/jp/docs/auth-oauth

自身のLINE WORKSプロフィールの取得方法は公式ドキュメントにはあまり記載がないですが、メールアドレスがあれば

GET https://www.worksapis.com/v1.0/users/me

としてあげると個別のLINE WORKSプロフィールの情報が取得できます。

AWS CognitoのJWT検証は、AWS公式ライブラリのaws-jwt-verifyを使って検証をするためのLambdaを別途で立て、ログインが必要なページではSessionStorageに保存されているJWTの検証を行い、問題なければ表示する形にしてます。

https://github.com/awslabs/aws-jwt-verify

aws-jwt-verifyのLambdaコードサンプル例

import { CognitoJwtVerifier } from "aws-jwt-verify";

async function verifyJwt(jwt) {
  const verifier = CognitoJwtVerifier.create({
    userPoolId: "xxxxxxxxxxxxxx",
    tokenUse: "id", // アクセストークンの時は access
    clientId: "xxxxxxxxxxxxxx",
  });
  try {
    const payload = await verifier.verify(jwt);
    console.log("Token is valid. Payload: ", payload);
    return payload;
  } catch (err) {
    console.error(err);
    console.log("Token not valid!");
    return { message: "Token not valid" };
  }
}

export const handler = async (event) => {
 
  console.info("received:", event);
  
  const jwt = event.headers.authorization.split(" ")[1];
  
  const payload = await verifyJwt(jwt);

  const response = {
    statusCode: 200,
    body: JSON.stringify(payload),
  };

  console.info(
    `response from: ${event.path} statusCode: ${response.statusCode} body: ${response.body}`
  );
  return response;
};

まとめ

今回はLINE WORKSとAWS CognitoをWrapperでOIDCを使ってSSOを連携しました。
IdPやSPがSAMLやOIDCに対応しているとドキュメントに記載されているから必ず実装できるわけでなく、個々のIdPやSPのサービスの要件をちゃんと確認して実際に実装できるかどうかまで確認をした方が余計な時間をかけなくて済むので、皆さんも是非この点を覚えていただければと思います。ここまで閲覧ありがとうございました。

参考URL

免責

こちらの記事を元にSSO認証を実装される際にはセキュリティ観点等を踏まえて自己責任でお願いいたします。

Discussion