見出し画像

自作アプリケーションにOneLogin認証を導入する方法

こんにちは、ズッ友です。ナビタイムジャパンでBFF開発環境・運用改善業務を担当しています。

先日、社内ツールに対してOneLogin認証を取り入れました。
この記事では、その際に得た認証の仕組みと開発方法の知見についてご紹介させていただきます。
紹介する対象言語はKotlinとなりますが、他の言語でも認証全体の仕組みは同じです。
これからOneLogin認証によりアカウント連携を簡易化したい方々の参考になれば幸いです。

OneLogin認証とは

OneLogin, Incが提供するシングルサインオン(SSO)サービスです。
一度のユーザー認証処理によって、あらかじめ連携設定しておいた複数のサービスが追加認証なしで利用できます。
都度IDやパスワードを入力するといった作業が不要になります。

ナビタイムジャパンにおけるOneLogin認証

ナビタイムジャパンで利用している様々なWebサービス、例えばSlackSentryなどへの認証もOneLoginを利用しています。

今回作成した社内ツールにおいても独自の認証ではなくOneLoginを利用して認証処理を簡易化しました。

Security Assertion Markup Language(SAML)

OneLogin認証にはSAMLを利用しています。
そのため、自作アプリケーションへの導入ではSAMLを実装していくことになります。
ここではSAMLとはなにかを簡単に説明します。

XML関連の標準化団体OASISによって策定された、異なるインターネットドメイン間でユーザー認証を行うためのXMLをベースにした標準規格です。
認証情報を提供する側をIdentity Provider(IdP)、認証情報を利用する側をService Provider(SP)と言います。
OneLoginがIdP、アプリケーションがSPにあたります。

SAML認証の流れ

以下の流れで認証が行われます。

認証の流れ
  1. ユーザーがログインするためSPへアクセス

  2. SPがSAML認証要求を作成

  3. SPが2で作成した認証要求とユーザーアクセスとともにIdPへリダイレクト

  4. IdPは渡された認証情報を検証

  5. IdPはユーザーの認証を実施

    1. ユーザー認証が切れている場合、ユーザーはID・パスワードを入力し再認証

  6. IdPはSAML認証応答を作成し

  7. 認証応答とともにSPへリダイレクト

  8. SPはSAML認証応答を検証し、ユーザーログインを許可/拒否

導入方法

OneLogin認証を導入する方法を説明します。
連携済み・自作それぞれ導入方法が異なるため、その概要を記載します。

連携済みサービスへの導入

OneLoginと連携済みのサービスを利用する場合は簡単です。
OneLoginAppを作成する際に連携対象のサービスを選択することで、設定に必要な項目がわかります。
公式サイトには G Suite, Salesforceの連携方法が載っています。
連携したい対象サービスの公式サイトに連携方法が記載されていることが多いため、そちらをご確認ください。

自作アプリケーションへの導入

本記事の主題です。
前述したように、自作アプリケーションへの導入の場合はSAML認証を実装する必要があります。
実装に際して、OneLoginからOpen SourceのSAML Toolkitが提供されているためそちらを利用します。
サポート言語は下記です。

  • PHP

  • Java

  • Python

  • Ruby

  • .NET

開発ドキュメントがありますのでそちらもご覧ください。

SAML Toolkitを利用した導入手順

導入対象

導入したツールの概要は下記です。
※OneLogin連携に関係するものだけ記載しています。

  • Webアプリケーション

  • 言語:Kotlin

  • フレームワーク:Jersey

  • プロジェクト管理:Maven

ディレクトリ構成

- src
    - main.kotlin.tool
        - filter
            - RequestFilter.kt          # 認証処理を行うリクエストフィルター
        - infra                         # 後述するフレームワーク仕様吸収用のインフラ層
            - OneloginAuth
            - OneloginLogoutRequest
            - OneloginLogoutResponse
        - resource                      
            - AuthResource              # OneLogin用エントリーポイント
            - TopResource               # ツールトップ画面
- resources
    - onelogin.saml.properties          # OneLogin認証設定
- webapp
    - top.jsp                           # ツールトップ画面
- pom.xml

連携設定:OneLoginAppの作成

公式サイトの手順に従い作成します。

  1. Go to Apps > Add Apps.

  2. Search for SAML Test Connector.

  3. Select the SAML Test Connector (IdP w/ attr) app.

連携設定:IdP設定

作成したアプリケーション → OneLoginの接続設定を行います。
作成したOneLoginAppに記載されている情報をonelogin.saml.propertiesに記載します。

パラメータ対応表

onelogin.saml.propertiesは下記となります。

onelogin.saml2.idp.entityid=https://app.onelogin.com/saml/metadata/XXXXX
onelogin.saml2.idp.single_sign_on_service.url=https://ntj.onelogin.com/trust/saml2/http-post/sso/XXXXX
onelogin.saml2.idp.single_logout_service.url=https://ntj.onelogin.com/trust/saml2/http-redirect/slo/XXXXX
onelogin.saml2.idp.x509cert=-----BEGIN CERTIFICATE-----\
XXXXXX
-----END CERTIFICATE-----

連携設定:SP設定

OneLogin → 作成したアプリケーションの接続設定を行います。
アプリケーション側に作成したエンドポイントの設定をonelogin.saml.propertiesとOneLogin両者に設定します。

パラメータ対応表

onelogin.saml.propertiesは下記となります。

# 2で記載したものに追記

onelogin.saml2.sp.entityid=https://example.com/auth/metadata
onelogin.saml2.sp.assertion_consumer_service.url=https://example.com/auth/acs
onelogin.saml2.sp.single_logout_service.url=https://example.com/auth/sls

以上でアプリケーション側の設定は完了です。

実装:Toolkitのインポート

mavenライブラリとして提供されているため、pom.xmlに記載するだけで利用可能です。

<dependency>
    <groupId>com.onelogin</groupId>
    <artifactId>java-saml</artifactId>
    <version>2.9.0</version>
</dependency>

実装:フィルター層

アプリケーションにリクエストした際にOneLogin認証をかけたいため、共通で通過するリクエストフィルターで認証処理を実施します。
フィルターの実装は下記です。

@Provider
class RequestFilter: ContainerRequestFilter {

    @Context
    private lateinit var req: HttpServletRequest

    @Context
    private lateinit var resp: HttpServletResponse

    override fun filter(reqCtx: ContainerRequestContext?) {
        // 認証を実施するリクエストを判別します①       
        val needAuthPath = PageConstant.needAuth(req.servletPath)
            ?:throw NotFoundException()

        // 認証済みのリクエストか判別します②
        val hasSession = req.session.getAttribute("nameId")?.toString()?.isNotEmpty()?:false
        if (!needAuthPath || hasSession) return

        // ログイン処理を実施します③
        val auth = Auth(req, resp)
        auth.login(req.requestURI)
        reqCtx?.abortWith(Response.ok().build())
    }
}

認証を実施するリクエストを判別します①
ユーザー操作ではないヘルスチェック用のリクエストは除外します。
後述するOneLogin認証に利用されるリクエストは除外します。

認証済みのリクエストか判別します②
ログインに成功した場合、セッションにnameIdを入れて管理します。
そのためnameIdを保持していれば再認証不要と判断しています。

ログイン処理を実施します③
Toolkit提供のAuthクラスのインスタンスを生成します。
loginメソッドを呼び出します。
引数にリクエストされたURL情報を渡すことで、認証→ログイン成功後に指定URLにリダイレクトしてくれます。

実装:API

OneLoginからの応答を受け取るAPI3つと、ログアウト処理実行用API1つを作成します。

ログイン応答API (/acs)
フィルター層でOneLoginへログイン要求した後の結果受付用APIです。
acsはassertion consumer serviceの略で、onelogin.saml.propetiesに本APIのURLを記載します。

重要な処理にはコメントで説明を記載しています。

@POST
@Path("/acs")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_HTML)
@Template(name = "/top.jsp")
fun acs(
    // OneLoginから2つのパラメータを受け付けます。これは後述するフレームワーク用の対応です。
    @FormParam("SAMLResponse") samlResponse: String?,
    @FormParam("RelayState") relayState: String?): Response {

    // ログイン結果の確認を実施します
    val auth = OneloginAuth(request, response)
    auth.processResponse(null, samlResponse)
    // 認証失敗している場合のハンドリング
    if (!auth.isAuthenticated()) {
        throw RuntimeException("Not authenticated")
    }

    val errors = auth.getErrors()
    if (errors.isNotEmpty()) {
        if (auth.isDebugActive()) {
            auth.getLastErrorReason()?.let { println(it) }
        }
        throw RuntimeException("Error Authentication. Please check log.")
    } else {
        // 認証成功しエラーもない場合はセッションに認証情報を付与します
        val attributes = auth.getAttributes()
        request.session.setAttribute("attributes", attributes);
        request.session.setAttribute("nameId", auth.getNameId())
        request.session.setAttribute("nameIdFormat", auth.getNameIdFormat())
        request.session.setAttribute("sessionIndex", auth.getSessionIndex())
        request.session.setAttribute("nameidNameQualifier", auth.getNameIdNameQualifier())
        request.session.setAttribute("nameidSPNameQualifier", auth.getNameIdSPNameQualifier())

        // 指定したリダイレクト先が存在する場合はリダイレクトします
        return if (relayState != null && relayState != ServletUtils.getSelfRoutedURLNoQuery(request)) {
            Response.seeOther(URI(relayState)).build()
        } else {
            Response.ok().entity("status" to "ok").build()
        }
    }
}

ログアウト応答API(/sls)
OneLoginへログアウト要求を行った後の結果受付用APIです。
slsはsingle logout serviceの略で、onelogin.saml.propetiesに本APIのURLを記載します。

重要な処理にはコメントで説明を記載しています。

@GET
@Path("/sls")
@Produces(MediaType.TEXT_HTML)
@Template(name = "/auth/logout.jsp")
fun sls(): Response {
    // ログアウト結果を確認します
    val auth = OneloginAuth(request, response)
    auth.processSLO(false, null, false)

    // エラー確認。本実装ではエラーは画面に結果のみ表示する形式としています。
    return Response.ok()
        .entity(mapOf("success" to auth.getErrors().isEmpty()))
        .build()
}

メタデータ受信API(/metadata)
メタデータ受信APIです。設定情報を確認することができます。
本APIはOneLoginからの認証照合に利用されるAPIのため、サービス上で表示することはありません。
onelogin.saml.propetiesに本APIのURLを記載します。

重要な処理にはコメントで説明を記載しています。

@POST
@Path("/metadata")
@Produces(MediaType.APPLICATION_XHTML_XML)
fun metadata(): Response {
    val auth = Auth()
    val settings = auth.settings
    val metadata = settings.spMetadata

    // メタデータ情報のバリデーションを実施します
    val errors = Saml2Settings.validateMetadata(metadata)

    // バリデーション結果に問題があるか確認します
    if (errors.isEmpty()) {
        return Response.ok().entity(metadata).build()
    } else {
        throw RuntimeException(errors.joinToString("\n"))
    }
}

ログアウト実行API(/logout)
画面に配置したログアウトボタンを押した時などにリクエストされるAPIです。
OneLoginへログアウト要求を送信します。

重要な処理にはコメントで説明を記載しています。

@GET
@Path("/logout")
fun logout(): Response {
    val auth = Auth(request, response)

    // ログアウト対象の情報を設定します。セッションに保持している情報を利用します。
    val logoutParam = LogoutRequestParams(
        request.session.getAttribute("sessionIndex")?.toString(),
        request.session.getAttribute("nameId")?.toString(),
        request.session.getAttribute("nameIdFormat")?.toString(),
        request.session.getAttribute("nameidNameQualifier")?.toString(),
        request.session.getAttribute("nameidSPNameQualifier")?.toString(),
    )

    // ログアウト要求を実施します
    auth.logout(null, logoutParam)
    return Response.ok().build()
}

以上が必要な実装となります。

フレームワーク仕様吸収

前述したAPI実装において、OneloginAuth はライブラリが持っているクラスをコピーしてカスタマイズしたものです。

ライブラリの実装ではHttpServletRequest.getParameter()を用いてPOSTのフォームパラメータを取得しています。
しかし、Jerseyを利用した場合、フレームワーク側でラップされてしまい、上記関数では取得ができなくなります。
そのため、@FormParamアノテーションで変数に格納したものを利用して処理が行えるものを作成しています。

下記が対応例です。

// ライブラリのAuthクラス。
// 記述はJavaです。
public void processResponse(String requestId) throws Exception {
    this.authenticated = false;
    HttpRequest httpRequest = ServletUtils.makeHttpRequest(this.request);
    String samlResponseParameter = httpRequest.getParameter("SAMLResponse");
    if (samlResponseParameter == null) {
        this.errors.add("invalid_binding");
        String errorMsg = "SAML Response not found, Only supported HTTP_POST Binding";
        LOGGER.error("processResponse error." + errorMsg);
        throw new Error(errorMsg, 3);
    } else {
        SamlResponse samlResponse = this.samlMessageFactory.createSamlResponse(this.settings, httpRequest);


// 自作したOneloginAuthクラス。samlResponseParameterを外から渡せるようにしています。
// 記述はKotlinに変更しています。
fun processResponse(requestId: String?, samlResponseParameter: String?) {
    this.authenticated = false
    val httpRequest = ServletUtils.makeHttpRequest(this.request)

    if (samlResponseParameter.isNullOrEmpty()) {
        this.errors.plus("invalid_binding")
        val errorMsg = "SAML Response not found, Only supported HTTP_POST Binding"
        LOGGER.error("processResponse error.$errorMsg")
        throw Error(errorMsg, 3)
    } else {
        val samlResponse = SamlResponse(this.settings, parseHttpToHttps(httpRequest.requestURL), samlResponseParameter)

さいごに

今回はOneLogin認証を自作アプリケーションに導入する方法を、認証方式・実装を含めた手順とともに紹介させていただきました。

作成した社内ツールにOneLogin認証を取り入れたことで、新しく入った方でも専用アカウントを作る手間なく利用できるようになりました。
また、ツール利用者の管理も一元化されました。

本記事が今後OneLogin連携に取り組まれる方の参考になれば幸いです。

最後までお読みいただき、ありがとうございました。