2015-08-31

Salesforceのログインフロー試してみた

Salesforceの認証プロセスとして独自のログインフローを構築できるということで、Salesforceが公開しているパッケージを使って以下のチュートリアル(TOTPの2要素認証)に従って検証してみました。 Login-Flows - developer.force.com

ログインフローは以下のスライドも詳しいです! 7 power night2014_kanbayashi

ログインフローについて

ログインフローを利用することで、Flow Designerで作成したフローとログイン時の処理を紐付けることができ、通常の認証処理が完了した後、Flow Designerで作成したフローを走らせることができます。Flow DesignerではフローのプラグインをApexを使って作成でき、このプラグインからTOTP用のQRコード生成やバリデーションをすることが可能なので、独自の多要素認証の認証フローを作成することができます。

パッケージのインストール

まずは以下のパッケージをSalesforce環境にインストールします。 https://login.salesforce.com/packaging/installPackage.apexp?p0=04to0000000WA6J

フローの作成

設定>作成>ワークフローと承認申請>フロー でインストールしたフロー一覧が表示されるので、SF-TOTPの[オープン]をクリックします。

flowdesigner-list

こんな感じでフローが表示されます。

flowdesigner-sf-totp

TOTPPluginのクラスは以下の内容となっています。

global class TOTPPlugin implements Process.Plugin
{    
    global Process.PluginDescribeResult describe()
    {
        Process.PluginDescribeResult result = new Process.PluginDescribeResult();
        result.description='This plug-in handles salesforce standard two factor authentication methods.';
        result.tag='Identity';
        
        result.inputParameters = new List<Process.PluginDescribeResult.InputParameter> {
                new Process.PluginDescribeResult.InputParameter('OTP_INPUT', Process.PluginDescribeResult.ParameterType.STRING, true),
                new Process.PluginDescribeResult.InputParameter('OTP_REGISTRATION_INPUT', Process.PluginDescribeResult.ParameterType.STRING, true),
                new Process.PluginDescribeResult.InputParameter('SECRET_INPUT', Process.PluginDescribeResult.ParameterType.STRING, true)      
            };
        
        result.outputParameters = new List<Process.PluginDescribeResult.OutputParameter> {
            new Process.PluginDescribeResult.OutputParameter('QR_URL_OUTPUT',
                Process.PluginDescribeResult.ParameterType.STRING),
            new Process.PluginDescribeResult.OutputParameter('SECRET_OUTPUT',
                Process.PluginDescribeResult.ParameterType.STRING),
            new Process.PluginDescribeResult.OutputParameter('IsValid_OUTPUT',
                Process.PluginDescribeResult.ParameterType.Boolean)
        };
        
        return result;
    }
    
    global Process.PluginResult invoke(Process.PluginRequest request)
    {   
        Map<String,String> QR;
        String URL; 
        String otp;
        String secret;
        Boolean status = false;
        
        String userid   = UserInfo.getUserId();  
        
        Map<String, Object> result = new Map<String, Object>();
        
        List<TwoFactorInfo> twoFactors = [SELECT UserId, Type FROM TwoFactorInfo where userID = :userid];
        
        secret = (String)request.inputParameters.get('SECRET_INPUT');
        
        if(twoFactors.isEmpty() && secret == null)
        {
            QR = Auth.SessionManagement.getQrCode();      
            URL = QR.get('qrCodeUrl');
            Secret = QR.get('secret');
            
            result.put('QR_URL_OUTPUT', URL);
            result.put('SECRET_OUTPUT', Secret);
            
            return new Process.PluginResult(result);
        }
         
        
        otp = (String)request.inputParameters.get('OTP_REGISTRATION_INPUT');
        
        if(otp == null)
            otp = (String)request.inputParameters.get('OTP_INPUT');
        
        result.put('IsValid_OUTPUT', validate(otp, secret));
           
        return new Process.PluginResult(result);
    }
      
    
    private Boolean validate(String otp, String secret)
    {
        String userid   = UserInfo.getUserId();   
        Boolean status = false;
        
         
        if(secret == null)
        {
            try {
                status = Auth.SessionManagement.validateTotpTokenForUser(otp);
            } 
            catch(Exception e)
            {
                system.debug('The key is invalid or the current user has attempted too many validations');
            } 
            
            return status;
        }
        
        status = Auth.SessionManagement.validateTotpTokenForKey(secret, otp);
        if(status == true)
        {
           TwoFactorInfo TwoFactor = new TwoFactorInfo(UserId=userid, Type='TOTP', SharedKey=secret);

           insert(TwoFactor);
        }
       
        return status;
    }    
}

フローのプラグインを作るにはProcess.Pluginインターフェースを実装する必要があります。実装するメソッドはdescribeとinvokeの2つで、describeメソッドはフローのプラグインの外部仕様、invokeメソッドは実際の処理の内容を定義します。 describeメソッドではProcess.PluginDescribeResultのインスタンスを返します。Process.PluginDescribeResultのインスタンスはフローエレメントの入出力のパラメータを定義することになります。 invokeメソッドではProcess.PluginRequestが引数として渡るので、当インスタンスから入力値を受け取り、Process.PluginResultで出力する値を定義します。invoke内の入出力パラメータはdescribeで定義したものになります。

サンプルのフローの説明をしていきます。 TOTP用のシークレットを発行していない状態では以下の様なフローになります。

  1. 1つ目のTOTPPluginのフローエレメントでQRコード及びTOTP用のシークレットを発行する。
  2. secretが発行されているのでRegistrationのエレメントではRegisterのフローを進む。
  3. Registration Screenの画面では取得したQRコードを表示しつつ、TOTPのテキストボックスを配置する。 →56行目で処理されるOTP_REGISTRATION_INPUTをセット
  4. 2つ目のTOTPPluginでは入力したTOTPと1で発行したsecretからバリデーションを行う →86行目のAuth.SessionManagement.validateTotpTokenForKey(secret, otp)

TOTP用のシークレットが既に発行されている状態では以下の様なフローになります。

  1. 1つめのTOTPPluginのエレメントでは何もせずスルー(状態変数が変わるものの、後続に影響なし)
  2. 1でsecretを発行していないのでRegistrationのエレメントでGet TOTPのフローを進む。
  3. Get Token Screenの画面ではTOTPのテキストボックスを配置する。 →59行目で処理されるOTP_INPUTをセット
  4. 2つ目のTOTPPluginでは入力したTOTPを使ってバリデーションを行う。 →76行目のAuth.SessionManagement.validateTotpTokenForUser(otp)

TwoFactorInfoオブジェクトではユーザの2要素認証の共有秘密鍵を管理していて、Auth.SessionManagement.validateTotpTokenForUserではユーザに割り当てられた秘密鍵を使って入力値の検証を行っています。

ログインフローの作成

セキュリティのコントロール>ログインフローに移動します。

loginflow-list

[新規]を押下して、こんな感じでプロファイルに紐付けて作成すればOK。

loginflow-new

あと、ログインフローを利用する場合は、プロファイルの[ユーザインターフェースログインの 2 要素認証]のチェックを外した方が良いです。私が試した時に2要素認証⇔ログインフローの無限ループになりました。

フローの実行

クレデンシャルによるフォームログイン後、初回はAuth.SessionManagement.getQrCode()で取得したQRコードのURLの画像が埋め込まれたトークンの入力フォームが表示されます。

loginflow-qr-input-first

二回目以降はQRコードの画像は出ません。

loginflow-qr-input

2要素認証で作成した共有秘密鍵はTwoFactorInfoに保持されますが、こちらはdelete文で削除できません。削除するにはユーザの詳細画面で時間ベースのトークンの[削除]リンクをクリックすればOKです。

loginflow-delete-totp-token

まとめ

今回はTOTPの2要素認証のパターンでしたが、フローではメールを飛ばしたり、色んな条件分岐や画面を組み合わせることが出来るため、柔軟にカスタマイズすることが出来ます。Auth.SessionManagementクラスには組織の信頼済みIP範囲内かどうかを確認するメソッドがあったり、実行時の日時等をPlugin内のApexで取得できるので、リスクベース認証的なことも実装できそうな感じです。

このエントリーをはてなブックマークに追加