16.5. EJB 認証のために追加セキュリティーを提供する

概要

デフォルトでは、アプリケーションサーバーにデプロイされた EJB にリモートコールを行う場合は、サーバーへの接続が認証され、この接続を介して受信されたすべての要求が、接続を認証したクレデンシャルを使用して実行されます。接続レベルでの認証は、基礎となる SASL (Simple Authentication and Security Layer) の機能に依存します。カスタム SASL メカニズムを記述する代わりに、サーバーに対する接続を開いて認証し、EJB を呼び出す前にセキュリティートークンを追加できます。このトピックでは、EJB 認証のために既存のクライアント接続で追加情報を渡す方法について説明します。

以下のコード例は、デモ目的専用です。これらのコード例は 1 つの方法のみを示し、アプリケーションのニーズに応じてカスタマイズする必要があります。パスワードは、SASL メカニズムを使用して交換されます。SASL DIGEST-MD5 認証が使用される場合、パスワードはチャンレンジ値でハッシュ化され、平文で送信されません。ただし、残りのトークンは平文で送信されます。これらのトークンに機密情報が含まれる場合は、接続の暗号化を有効にできます。

手順16.3 EJB 認証のためにセキュリティー情報を渡す

認証された接続に追加セキュリティーを提供するには、以下の 3 つのコンポーネントを作成する必要があります。
  1. クライアントサイドインターセプターを作成する

    このインターセプターは、org.jboss.ejb.client.EJBClientInterceptor を実装する必要があります。インターセプターは、コンテキストデータマップを介して追加セキュリティートークンを渡すことが期待されます。このコンテキストデータマップは、EJBClientInvocationContext.getContextData() への呼び出しを介して取得できます。追加セキュリティートークンを作成するクライアントサイドインターセプターコードの例は、以下のとおりです。
    public class ClientSecurityInterceptor implements EJBClientInterceptor {
    
        public void handleInvocation(EJBClientInvocationContext context) throws Exception {
            Object credential = SecurityActions.securityContextGetCredential();
    
            if (credential != null && credential instanceof PasswordPlusCredential) {
                PasswordPlusCredential ppCredential = (PasswordPlusCredential) credential;
                Map<String, Object> contextData = context.getContextData();
                contextData.put(ServerSecurityInterceptor.SECURITY_TOKEN_KEY, 
                                  ppCredential.getAuthToken());
            }
            context.sendRequest();
        }
    
        public Object handleInvocationResult(EJBClientInvocationContext context) 
                throws Exception {
            return context.getResult();
        }
    }
    
    
    クライアントインターセプターをアプリケーションに接続する方法については、 「アプリケーションでのクライアントサイドインターセプターの使用」を参照してください。
  2. サーバーサイドコンテナーインターセプターを作成および設定する

    コンテナーインターセプタークラスは、単純な Plain Old Java Object (POJO) です。@javax.annotation.AroundInvoke を使用して、Bean での呼び出し中に呼び出されるメソッドを指定します。コンテナーインターセプターの詳細については、 「コンテナーインターセプターについて」を参照してください。
    1. コンテナーインターセプターを作成する

      このインターセプターは、コンテキストからセキュリティー認証トークンを取得し、認証のために JAAS (Java Authentication and Authorization Service) ドメインに渡します。コンテナーインターセプターコードの例は以下のとおりです。
      public class ServerSecurityInterceptor {
      
          private static final Logger logger = Logger.getLogger(ServerSecurityInterceptor.class);
          static final String SECURITY_TOKEN_KEY = ServerSecurityInterceptor.class.getName() + ".SecurityToken";
      
          @AroundInvoke
          public Object aroundInvoke(final InvocationContext invocationContext) throws Exception {
              Principal userPrincipal = null;
              RealmUser connectionUser = null;
              String authToken = null;
      
              Map<String, Object> contextData = invocationContext.getContextData();
              if (contextData.containsKey(SECURITY_TOKEN_KEY)) {
                  authToken = (String) contextData.get(SECURITY_TOKEN_KEY);
      
                  Connection con = SecurityActions.remotingContextGetConnection();
      
                  if (con != null) {
                      UserInfo userInfo = con.getUserInfo();
                      if (userInfo instanceof SubjectUserInfo) {
                          SubjectUserInfo sinfo = (SubjectUserInfo) userInfo;
                          for (Principal current : sinfo.getPrincipals()) {
                              if (current instanceof RealmUser) {
                                  connectionUser = (RealmUser) current;
                                  break;
                              }
                          }
                      }
                      userPrincipal = new SimplePrincipal(connectionUser.getName());
      
                  } else {
                      throw new IllegalStateException("Token authentication requested but no user on connection found.");
                  }
              }
      
              SecurityContext cachedSecurityContext = null;
              boolean contextSet = false;
              try {
                  if (userPrincipal != null && connectionUser != null && authToken != null) {
                      try {
                          // We have been requested to use an authentication token
                          // so now we attempt the switch.
                          cachedSecurityContext = SecurityActions.securityContextSetPrincipalCredential(userPrincipal,
                                  new OuterUserPlusCredential(connectionUser, authToken));
                          // keep track that we switched the security context
                          contextSet = true;
                          SecurityActions.remotingContextClear();
                      } catch (Exception e) {
                          logger.error("Failed to switch security context for user", e);
                          // Don't propagate the exception stacktrace back to the client for security reasons
                          throw new EJBAccessException("Unable to attempt switching of user.");
                      }
                  }
      
                  return invocationContext.proceed();
              } finally {
                  // switch back to original security context
                  if (contextSet) {
                      SecurityActions.securityContextSet(cachedSecurityContext);
                  }
              }
          }
      }
      
      
    2. コンテナーインターセプターを設定する

      サーバーサイドコンテナーインターセプターの設定方法については、 「コンテナーインターセプターの設定」を参照してください。
  3. JAAS LoginModule を作成する

    このカスタムモジュールは、既存の認証済み接続情報と追加セキュリティートークンを使用して認証を実行します。追加セキュリティートークンを使用し、認証を実行するコードの例は以下のとおりです。
    public class SaslPlusLoginModule extends AbstractServerLoginModule {
    
        private static final String ADDITIONAL_SECRET_PROPERTIES = "additionalSecretProperties";
        private static final String DEFAULT_AS_PROPERTIES = "additional-secret.properties";
        private Properties additionalSecrets;
        private Principal identity;
    
        @Override
        public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
            addValidOptions(new String[] { ADDITIONAL_SECRET_PROPERTIES });
            super.initialize(subject, callbackHandler, sharedState, options);
            
            // Load the properties that contain the additional security tokens
            String propertiesName;
            if (options.containsKey(ADDITIONAL_SECRET_PROPERTIES)) {
                propertiesName = (String) options.get(ADDITIONAL_SECRET_PROPERTIES);
            } else {
                propertiesName = DEFAULT_AS_PROPERTIES;
            }
            try {
                additionalSecrets = SecurityActions.loadProperties(propertiesName);
            } catch (IOException e) {
                throw new IllegalArgumentException(String.format("Unable to load properties '%s'", propertiesName), e);
            }
        }
    
        @Override
        public boolean login() throws LoginException {
            if (super.login() == true) {
                log.debug("super.login()==true");
                return true;
            }
    
            // Time to see if this is a delegation request.
            NameCallback ncb = new NameCallback("Username:");
            ObjectCallback ocb = new ObjectCallback("Password:");
    
            try {
                callbackHandler.handle(new Callback[] { ncb, ocb });
            } catch (Exception e) {
                if (e instanceof RuntimeException) {
                    throw (RuntimeException) e;
                }
                return false; // If the CallbackHandler can not handle the required callbacks then no chance.
            }
    
            String name = ncb.getName();
            Object credential = ocb.getCredential();
    
            if (credential instanceof OuterUserPlusCredential) {
                OuterUserPlusCredential oupc = (OuterUserPlusCredential) credential;
                if (verify(name, oupc.getName(), oupc.getAuthToken())) {
                    identity = new SimplePrincipal(name);
                    if (getUseFirstPass()) {
                        String userName = identity.getName();
                        if (log.isDebugEnabled())
                            log.debug("Storing username '" + userName + "' and empty password");
                        // Add the username and an empty password to the shared state map
                        sharedState.put("javax.security.auth.login.name", identity);
                        sharedState.put("javax.security.auth.login.password", oupc);
                    }
                    loginOk = true;
                    return true;
                }
            }
    
            return false; // Attempted login but not successful.
        }
    
        private boolean verify(final String authName, final String connectionUser, final String authToken) {
            // For the purpose of this quick start we are not supporting switching users, this login module is validation an
            // additional security token for a user that has already passed the sasl process.
            return authName.equals(connectionUser) && authToken.equals(additionalSecrets.getProperty(authName));
        }
    
        @Override
        protected Principal getIdentity() {
            return identity;
        }
    
        @Override
        protected Group[] getRoleSets() throws LoginException {
            Group roles = new SimpleGroup("Roles");
            Group callerPrincipal = new SimpleGroup("CallerPrincipal");
            Group[] groups = { roles, callerPrincipal };
            callerPrincipal.addMember(getIdentity());
            return groups;
        }
    }
    
    
  4. カスタム LoginModule をチェーンに追加する

    新しいカスタム LoginModule はチェーンの正しい場所に追加して正しい順序で呼び出されるようにする必要があります。この例では、SaslPlusLoginModule は、password-stacking オプションセットでロールをロードする LoginModule の前にチェーンする必要があります。
    • 管理 CLI を使用して LoginModule 順序を設定する

      password-stacking オプションを設定する RealmDirect LoginModule の前にカスタム SaslPlusLoginModule をチェーンする管理 CLI コマンドの例は以下のとおりです。
      [standalone@localhost:9999 /] ./subsystem=security/security-domain=quickstart-domain:add(cache-type=default)[standalone@localhost:9999 /] ./subsystem=security/security-domain=quickstart-domain/authentication=classic:add[standalone@localhost:9999 /] ./subsystem=security/security-domain=quickstart-domain/authentication=classic/login-module=DelegationLoginModule:add(code=org.jboss.as.quickstarts.ejb_security_plus.SaslPlusLoginModule,flag=optional,module-options={password-stacking=useFirstPass})[standalone@localhost:9999 /] ./subsystem=security/security-domain=quickstart-domain/authentication=classic/login-module=RealmDirect:add(code=RealmDirect,flag=required,module-options={password-stacking=useFirstPass})
      
      
      
      
      管理 CLI の詳細については、カスタマーポータル (https://access.redhat.com/site/documentation/JBoss_Enterprise_Application_Platform/) にある JBoss Enterprise Application Platform 6 向け 『管理および設定ガイド』 の章「『管理インターフェース』」を参照してください。
    • LoginModule 順序を手動で設定する

      以下に、サーバー設定ファイルの security サブシステムで LoginModule 順序を設定する XML の例を示します。カスタム SaslPlusLoginModuleRealmDirect LoginModule より前に指定してユーザーロールがロードされ、password-stacking オプションが設定される前にリモートユーザーを確認できるようにする必要があります。
      <security-domain name="quickstart-domain" cache-type="default">
          <authentication>
              <login-module code="org.jboss.as.quickstarts.ejb_security_plus.SaslPlusLoginModule" flag="required">
                  <module-option name="password-stacking" value="useFirstPass"/>
              </login-module>
              <login-module code="RealmDirect" flag="required">
                  <module-option name="password-stacking" value="useFirstPass"/>
              </login-module>
          </authentication>
      </security-domain>
      
      
  5. リモートクライアントを作成する

    以下のコード例では、上記の JAAS LoginModule によりアクセスされる additional-secret.properties ファイルに以下のプロパティーが含まれることを前提とします。
    quickstartUser=7f5cc521-5061-4a5b-b814-bdc37f021acc
    
    
    以下のコードは、EJB 呼び出しの前にセキュリティートークンを作成し、設定する方法を示しています。シークレットトークンはデモ目的のためにのみハードコーディングされています。このクライアントは、単に結果をコンソールに出力します。
    import static org.jboss.as.quickstarts.ejb_security_plus.EJBUtil.lookupSecuredEJB;
    
    public class RemoteClient {
    
        /**
         * @param args
         */
        public static void main(String[] args) throws Exception {
            SimplePrincipal principal = new SimplePrincipal("quickstartUser");
            Object credential = new PasswordPlusCredential("quickstartPwd1!".toCharArray(), "7f5cc521-5061-4a5b-b814-bdc37f021acc");
    
            SecurityActions.securityContextSetPrincipalCredential(principal, credential);
            SecuredEJBRemote secured = lookupSecuredEJB();
    
            System.out.println(secured.getPrincipalInformation());
        }
    }