001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements. See the NOTICE file distributed with this
004     * work for additional information regarding copyright ownership. The ASF
005     * licenses this file to you under the Apache License, Version 2.0 (the
006     * "License"); you may not use this file except in compliance with the License.
007     * You may obtain a copy of the License at
008     * 
009     * http://www.apache.org/licenses/LICENSE-2.0
010     * 
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014     * License for the specific language governing permissions and limitations under
015     * the License.
016     */
017    package org.apache.hadoop.security;
018    
019    import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION;
020    
021    import java.io.IOException;
022    import java.net.InetAddress;
023    import java.net.InetSocketAddress;
024    import java.net.URI;
025    import java.net.URL;
026    import java.net.URLConnection;
027    import java.net.UnknownHostException;
028    import java.security.AccessController;
029    import java.security.PrivilegedAction;
030    import java.security.PrivilegedExceptionAction;
031    import java.util.Arrays;
032    import java.util.List;
033    import java.util.ServiceLoader;
034    import java.util.Set;
035    
036    import javax.security.auth.Subject;
037    import javax.security.auth.kerberos.KerberosPrincipal;
038    import javax.security.auth.kerberos.KerberosTicket;
039    
040    import org.apache.commons.logging.Log;
041    import org.apache.commons.logging.LogFactory;
042    import org.apache.hadoop.classification.InterfaceAudience;
043    import org.apache.hadoop.classification.InterfaceStability;
044    import org.apache.hadoop.conf.Configuration;
045    import org.apache.hadoop.fs.CommonConfigurationKeys;
046    import org.apache.hadoop.http.HttpConfig;
047    import org.apache.hadoop.io.Text;
048    import org.apache.hadoop.net.NetUtils;
049    import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
050    import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
051    import org.apache.hadoop.security.authentication.client.AuthenticationException;
052    import org.apache.hadoop.security.ssl.SSLFactory;
053    import org.apache.hadoop.security.token.Token;
054    import org.apache.hadoop.security.token.TokenInfo;
055    
056    import com.google.common.annotations.VisibleForTesting;
057    
058    //this will need to be replaced someday when there is a suitable replacement
059    import sun.net.dns.ResolverConfiguration;
060    import sun.net.util.IPAddressUtil;
061    
062    @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
063    @InterfaceStability.Evolving
064    public class SecurityUtil {
065      public static final Log LOG = LogFactory.getLog(SecurityUtil.class);
066      public static final String HOSTNAME_PATTERN = "_HOST";
067    
068      // controls whether buildTokenService will use an ip or host/ip as given
069      // by the user
070      @VisibleForTesting
071      static boolean useIpForTokenService;
072      @VisibleForTesting
073      static HostResolver hostResolver;
074    
075      private static SSLFactory sslFactory;
076    
077      static {
078        Configuration conf = new Configuration();
079        boolean useIp = conf.getBoolean(
080          CommonConfigurationKeys.HADOOP_SECURITY_TOKEN_SERVICE_USE_IP,
081          CommonConfigurationKeys.HADOOP_SECURITY_TOKEN_SERVICE_USE_IP_DEFAULT);
082        setTokenServiceUseIp(useIp);
083        if (HttpConfig.isSecure()) {
084          sslFactory = new SSLFactory(SSLFactory.Mode.CLIENT, conf);
085          try {
086            sslFactory.init();
087          } catch (Exception ex) {
088            throw new RuntimeException(ex);
089          }
090        }
091      }
092      
093      /**
094       * For use only by tests and initialization
095       */
096      @InterfaceAudience.Private
097      static void setTokenServiceUseIp(boolean flag) {
098        useIpForTokenService = flag;
099        hostResolver = !useIpForTokenService
100            ? new QualifiedHostResolver()
101            : new StandardHostResolver();
102      }
103      
104      /**
105       * Find the original TGT within the current subject's credentials. Cross-realm
106       * TGT's of the form "krbtgt/[email protected]" may be present.
107       * 
108       * @return The TGT from the current subject
109       * @throws IOException
110       *           if TGT can't be found
111       */
112      private static KerberosTicket getTgtFromSubject() throws IOException {
113        Subject current = Subject.getSubject(AccessController.getContext());
114        if (current == null) {
115          throw new IOException(
116              "Can't get TGT from current Subject, because it is null");
117        }
118        Set<KerberosTicket> tickets = current
119            .getPrivateCredentials(KerberosTicket.class);
120        for (KerberosTicket t : tickets) {
121          if (isOriginalTGT(t))
122            return t;
123        }
124        throw new IOException("Failed to find TGT from current Subject:"+current);
125      }
126      
127      /**
128       * TGS must have the server principal of the form "krbtgt/FOO@FOO".
129       * @param principal
130       * @return true or false
131       */
132      static boolean 
133      isTGSPrincipal(KerberosPrincipal principal) {
134        if (principal == null)
135          return false;
136        if (principal.getName().equals("krbtgt/" + principal.getRealm() + 
137            "@" + principal.getRealm())) {
138          return true;
139        }
140        return false;
141      }
142      
143      /**
144       * Check whether the server principal is the TGS's principal
145       * @param ticket the original TGT (the ticket that is obtained when a 
146       * kinit is done)
147       * @return true or false
148       */
149      protected static boolean isOriginalTGT(KerberosTicket ticket) {
150        return isTGSPrincipal(ticket.getServer());
151      }
152    
153      /**
154       * Convert Kerberos principal name pattern to valid Kerberos principal
155       * names. It replaces hostname pattern with hostname, which should be
156       * fully-qualified domain name. If hostname is null or "0.0.0.0", it uses
157       * dynamically looked-up fqdn of the current host instead.
158       * 
159       * @param principalConfig
160       *          the Kerberos principal name conf value to convert
161       * @param hostname
162       *          the fully-qualified domain name used for substitution
163       * @return converted Kerberos principal name
164       * @throws IOException if the client address cannot be determined
165       */
166      @InterfaceAudience.Public
167      @InterfaceStability.Evolving
168      public static String getServerPrincipal(String principalConfig,
169          String hostname) throws IOException {
170        String[] components = getComponents(principalConfig);
171        if (components == null || components.length != 3
172            || !components[1].equals(HOSTNAME_PATTERN)) {
173          return principalConfig;
174        } else {
175          return replacePattern(components, hostname);
176        }
177      }
178      
179      /**
180       * Convert Kerberos principal name pattern to valid Kerberos principal names.
181       * This method is similar to {@link #getServerPrincipal(String, String)},
182       * except 1) the reverse DNS lookup from addr to hostname is done only when
183       * necessary, 2) param addr can't be null (no default behavior of using local
184       * hostname when addr is null).
185       * 
186       * @param principalConfig
187       *          Kerberos principal name pattern to convert
188       * @param addr
189       *          InetAddress of the host used for substitution
190       * @return converted Kerberos principal name
191       * @throws IOException if the client address cannot be determined
192       */
193      @InterfaceAudience.Public
194      @InterfaceStability.Evolving
195      public static String getServerPrincipal(String principalConfig,
196          InetAddress addr) throws IOException {
197        String[] components = getComponents(principalConfig);
198        if (components == null || components.length != 3
199            || !components[1].equals(HOSTNAME_PATTERN)) {
200          return principalConfig;
201        } else {
202          if (addr == null) {
203            throw new IOException("Can't replace " + HOSTNAME_PATTERN
204                + " pattern since client address is null");
205          }
206          return replacePattern(components, addr.getCanonicalHostName());
207        }
208      }
209      
210      private static String[] getComponents(String principalConfig) {
211        if (principalConfig == null)
212          return null;
213        return principalConfig.split("[/@]");
214      }
215      
216      private static String replacePattern(String[] components, String hostname)
217          throws IOException {
218        String fqdn = hostname;
219        if (fqdn == null || fqdn.equals("") || fqdn.equals("0.0.0.0")) {
220          fqdn = getLocalHostName();
221        }
222        return components[0] + "/" + fqdn.toLowerCase() + "@" + components[2];
223      }
224      
225      static String getLocalHostName() throws UnknownHostException {
226        return InetAddress.getLocalHost().getCanonicalHostName();
227      }
228    
229      /**
230       * Login as a principal specified in config. Substitute $host in
231       * user's Kerberos principal name with a dynamically looked-up fully-qualified
232       * domain name of the current host.
233       * 
234       * @param conf
235       *          conf to use
236       * @param keytabFileKey
237       *          the key to look for keytab file in conf
238       * @param userNameKey
239       *          the key to look for user's Kerberos principal name in conf
240       * @throws IOException if login fails
241       */
242      @InterfaceAudience.Public
243      @InterfaceStability.Evolving
244      public static void login(final Configuration conf,
245          final String keytabFileKey, final String userNameKey) throws IOException {
246        login(conf, keytabFileKey, userNameKey, getLocalHostName());
247      }
248    
249      /**
250       * Login as a principal specified in config. Substitute $host in user's Kerberos principal 
251       * name with hostname. If non-secure mode - return. If no keytab available -
252       * bail out with an exception
253       * 
254       * @param conf
255       *          conf to use
256       * @param keytabFileKey
257       *          the key to look for keytab file in conf
258       * @param userNameKey
259       *          the key to look for user's Kerberos principal name in conf
260       * @param hostname
261       *          hostname to use for substitution
262       * @throws IOException if the config doesn't specify a keytab
263       */
264      @InterfaceAudience.Public
265      @InterfaceStability.Evolving
266      public static void login(final Configuration conf,
267          final String keytabFileKey, final String userNameKey, String hostname)
268          throws IOException {
269        
270        if(! UserGroupInformation.isSecurityEnabled()) 
271          return;
272        
273        String keytabFilename = conf.get(keytabFileKey);
274        if (keytabFilename == null || keytabFilename.length() == 0) {
275          throw new IOException("Running in secure mode, but config doesn't have a keytab");
276        }
277    
278        String principalConfig = conf.get(userNameKey, System
279            .getProperty("user.name"));
280        String principalName = SecurityUtil.getServerPrincipal(principalConfig,
281            hostname);
282        UserGroupInformation.loginUserFromKeytab(principalName, keytabFilename);
283      }
284    
285      /**
286       * create the service name for a Delegation token
287       * @param uri of the service
288       * @param defPort is used if the uri lacks a port
289       * @return the token service, or null if no authority
290       * @see #buildTokenService(InetSocketAddress)
291       */
292      public static String buildDTServiceName(URI uri, int defPort) {
293        String authority = uri.getAuthority();
294        if (authority == null) {
295          return null;
296        }
297        InetSocketAddress addr = NetUtils.createSocketAddr(authority, defPort);
298        return buildTokenService(addr).toString();
299       }
300      
301      /**
302       * Get the host name from the principal name of format <service>/host@realm.
303       * @param principalName principal name of format as described above
304       * @return host name if the the string conforms to the above format, else null
305       */
306      public static String getHostFromPrincipal(String principalName) {
307        return new HadoopKerberosName(principalName).getHostName();
308      }
309    
310      private static ServiceLoader<SecurityInfo> securityInfoProviders = 
311        ServiceLoader.load(SecurityInfo.class);
312      private static SecurityInfo[] testProviders = new SecurityInfo[0];
313    
314      /**
315       * Test setup method to register additional providers.
316       * @param providers a list of high priority providers to use
317       */
318      @InterfaceAudience.Private
319      public static void setSecurityInfoProviders(SecurityInfo... providers) {
320        testProviders = providers;
321      }
322      
323      /**
324       * Look up the KerberosInfo for a given protocol. It searches all known
325       * SecurityInfo providers.
326       * @param protocol the protocol class to get the information for
327       * @param conf configuration object
328       * @return the KerberosInfo or null if it has no KerberosInfo defined
329       */
330      public static KerberosInfo 
331      getKerberosInfo(Class<?> protocol, Configuration conf) {
332        synchronized (testProviders) {
333          for(SecurityInfo provider: testProviders) {
334            KerberosInfo result = provider.getKerberosInfo(protocol, conf);
335            if (result != null) {
336              return result;
337            }
338          }
339        }
340        
341        synchronized (securityInfoProviders) {
342          for(SecurityInfo provider: securityInfoProviders) {
343            KerberosInfo result = provider.getKerberosInfo(protocol, conf);
344            if (result != null) {
345              return result;
346            }
347          }
348        }
349        return null;
350      }
351     
352      /**
353       * Look up the TokenInfo for a given protocol. It searches all known
354       * SecurityInfo providers.
355       * @param protocol The protocol class to get the information for.
356       * @param conf Configuration object
357       * @return the TokenInfo or null if it has no KerberosInfo defined
358       */
359      public static TokenInfo getTokenInfo(Class<?> protocol, Configuration conf) {
360        synchronized (testProviders) {
361          for(SecurityInfo provider: testProviders) {
362            TokenInfo result = provider.getTokenInfo(protocol, conf);
363            if (result != null) {
364              return result;
365            }      
366          }
367        }
368        
369        synchronized (securityInfoProviders) {
370          for(SecurityInfo provider: securityInfoProviders) {
371            TokenInfo result = provider.getTokenInfo(protocol, conf);
372            if (result != null) {
373              return result;
374            }
375          } 
376        }
377        
378        return null;
379      }
380    
381      /**
382       * Decode the given token's service field into an InetAddress
383       * @param token from which to obtain the service
384       * @return InetAddress for the service
385       */
386      public static InetSocketAddress getTokenServiceAddr(Token<?> token) {
387        return NetUtils.createSocketAddr(token.getService().toString());
388      }
389    
390      /**
391       * Set the given token's service to the format expected by the RPC client 
392       * @param token a delegation token
393       * @param addr the socket for the rpc connection
394       */
395      public static void setTokenService(Token<?> token, InetSocketAddress addr) {
396        Text service = buildTokenService(addr);
397        if (token != null) {
398          token.setService(service);
399          if (LOG.isDebugEnabled()) {
400            LOG.debug("Acquired token "+token);  // Token#toString() prints service
401          }
402        } else {
403          LOG.warn("Failed to get token for service "+service);
404        }
405      }
406      
407      /**
408       * Construct the service key for a token
409       * @param addr InetSocketAddress of remote connection with a token
410       * @return "ip:port" or "host:port" depending on the value of
411       *          hadoop.security.token.service.use_ip
412       */
413      public static Text buildTokenService(InetSocketAddress addr) {
414        String host = null;
415        if (useIpForTokenService) {
416          if (addr.isUnresolved()) { // host has no ip address
417            throw new IllegalArgumentException(
418                new UnknownHostException(addr.getHostName())
419            );
420          }
421          host = addr.getAddress().getHostAddress();
422        } else {
423          host = addr.getHostName().toLowerCase();
424        }
425        return new Text(host + ":" + addr.getPort());
426      }
427    
428      /**
429       * Construct the service key for a token
430       * @param uri of remote connection with a token
431       * @return "ip:port" or "host:port" depending on the value of
432       *          hadoop.security.token.service.use_ip
433       */
434      public static Text buildTokenService(URI uri) {
435        return buildTokenService(NetUtils.createSocketAddr(uri.getAuthority()));
436      }
437      
438      /**
439       * Perform the given action as the daemon's login user. If the login
440       * user cannot be determined, this will log a FATAL error and exit
441       * the whole JVM.
442       */
443      public static <T> T doAsLoginUserOrFatal(PrivilegedAction<T> action) { 
444        if (UserGroupInformation.isSecurityEnabled()) {
445          UserGroupInformation ugi = null;
446          try { 
447            ugi = UserGroupInformation.getLoginUser();
448          } catch (IOException e) {
449            LOG.fatal("Exception while getting login user", e);
450            e.printStackTrace();
451            Runtime.getRuntime().exit(-1);
452          }
453          return ugi.doAs(action);
454        } else {
455          return action.run();
456        }
457      }
458      
459      /**
460       * Perform the given action as the daemon's login user. If an
461       * InterruptedException is thrown, it is converted to an IOException.
462       *
463       * @param action the action to perform
464       * @return the result of the action
465       * @throws IOException in the event of error
466       */
467      public static <T> T doAsLoginUser(PrivilegedExceptionAction<T> action)
468          throws IOException {
469        return doAsUser(UserGroupInformation.getLoginUser(), action);
470      }
471    
472      /**
473       * Perform the given action as the daemon's current user. If an
474       * InterruptedException is thrown, it is converted to an IOException.
475       *
476       * @param action the action to perform
477       * @return the result of the action
478       * @throws IOException in the event of error
479       */
480      public static <T> T doAsCurrentUser(PrivilegedExceptionAction<T> action)
481          throws IOException {
482        return doAsUser(UserGroupInformation.getCurrentUser(), action);
483      }
484    
485      private static <T> T doAsUser(UserGroupInformation ugi,
486          PrivilegedExceptionAction<T> action) throws IOException {
487        try {
488          return ugi.doAs(action);
489        } catch (InterruptedException ie) {
490          throw new IOException(ie);
491        }
492      }
493    
494      /**
495       * Open a (if need be) secure connection to a URL in a secure environment
496       * that is using SPNEGO to authenticate its URLs. All Namenode and Secondary
497       * Namenode URLs that are protected via SPNEGO should be accessed via this
498       * method.
499       *
500       * @param url to authenticate via SPNEGO.
501       * @return A connection that has been authenticated via SPNEGO
502       * @throws IOException If unable to authenticate via SPNEGO
503       */
504      public static URLConnection openSecureHttpConnection(URL url) throws IOException {
505        if (!HttpConfig.isSecure() && !UserGroupInformation.isSecurityEnabled()) {
506          return url.openConnection();
507        }
508    
509        AuthenticatedURL.Token token = new AuthenticatedURL.Token();
510        try {
511          return new AuthenticatedURL(null, sslFactory).openConnection(url, token);
512        } catch (AuthenticationException e) {
513          throw new IOException("Exception trying to open authenticated connection to "
514                  + url, e);
515        }
516      }
517    
518      /**
519       * Resolves a host subject to the security requirements determined by
520       * hadoop.security.token.service.use_ip.
521       * 
522       * @param hostname host or ip to resolve
523       * @return a resolved host
524       * @throws UnknownHostException if the host doesn't exist
525       */
526      @InterfaceAudience.Private
527      public static
528      InetAddress getByName(String hostname) throws UnknownHostException {
529        return hostResolver.getByName(hostname);
530      }
531      
532      interface HostResolver {
533        InetAddress getByName(String host) throws UnknownHostException;    
534      }
535      
536      /**
537       * Uses standard java host resolution
538       */
539      static class StandardHostResolver implements HostResolver {
540        @Override
541        public InetAddress getByName(String host) throws UnknownHostException {
542          return InetAddress.getByName(host);
543        }
544      }
545      
546      /**
547       * This an alternate resolver with important properties that the standard
548       * java resolver lacks:
549       * 1) The hostname is fully qualified.  This avoids security issues if not
550       *    all hosts in the cluster do not share the same search domains.  It
551       *    also prevents other hosts from performing unnecessary dns searches.
552       *    In contrast, InetAddress simply returns the host as given.
553       * 2) The InetAddress is instantiated with an exact host and IP to prevent
554       *    further unnecessary lookups.  InetAddress may perform an unnecessary
555       *    reverse lookup for an IP.
556       * 3) A call to getHostName() will always return the qualified hostname, or
557       *    more importantly, the IP if instantiated with an IP.  This avoids
558       *    unnecessary dns timeouts if the host is not resolvable.
559       * 4) Point 3 also ensures that if the host is re-resolved, ex. during a
560       *    connection re-attempt, that a reverse lookup to host and forward
561       *    lookup to IP is not performed since the reverse/forward mappings may
562       *    not always return the same IP.  If the client initiated a connection
563       *    with an IP, then that IP is all that should ever be contacted.
564       *    
565       * NOTE: this resolver is only used if:
566       *       hadoop.security.token.service.use_ip=false 
567       */
568      protected static class QualifiedHostResolver implements HostResolver {
569        @SuppressWarnings("unchecked")
570        private List<String> searchDomains =
571            ResolverConfiguration.open().searchlist();
572        
573        /**
574         * Create an InetAddress with a fully qualified hostname of the given
575         * hostname.  InetAddress does not qualify an incomplete hostname that
576         * is resolved via the domain search list.
577         * {@link InetAddress#getCanonicalHostName()} will fully qualify the
578         * hostname, but it always return the A record whereas the given hostname
579         * may be a CNAME.
580         * 
581         * @param host a hostname or ip address
582         * @return InetAddress with the fully qualified hostname or ip
583         * @throws UnknownHostException if host does not exist
584         */
585        @Override
586        public InetAddress getByName(String host) throws UnknownHostException {
587          InetAddress addr = null;
588    
589          if (IPAddressUtil.isIPv4LiteralAddress(host)) {
590            // use ipv4 address as-is
591            byte[] ip = IPAddressUtil.textToNumericFormatV4(host);
592            addr = InetAddress.getByAddress(host, ip);
593          } else if (IPAddressUtil.isIPv6LiteralAddress(host)) {
594            // use ipv6 address as-is
595            byte[] ip = IPAddressUtil.textToNumericFormatV6(host);
596            addr = InetAddress.getByAddress(host, ip);
597          } else if (host.endsWith(".")) {
598            // a rooted host ends with a dot, ex. "host."
599            // rooted hosts never use the search path, so only try an exact lookup
600            addr = getByExactName(host);
601          } else if (host.contains(".")) {
602            // the host contains a dot (domain), ex. "host.domain"
603            // try an exact host lookup, then fallback to search list
604            addr = getByExactName(host);
605            if (addr == null) {
606              addr = getByNameWithSearch(host);
607            }
608          } else {
609            // it's a simple host with no dots, ex. "host"
610            // try the search list, then fallback to exact host
611            InetAddress loopback = InetAddress.getByName(null);
612            if (host.equalsIgnoreCase(loopback.getHostName())) {
613              addr = InetAddress.getByAddress(host, loopback.getAddress());
614            } else {
615              addr = getByNameWithSearch(host);
616              if (addr == null) {
617                addr = getByExactName(host);
618              }
619            }
620          }
621          // unresolvable!
622          if (addr == null) {
623            throw new UnknownHostException(host);
624          }
625          return addr;
626        }
627    
628        InetAddress getByExactName(String host) {
629          InetAddress addr = null;
630          // InetAddress will use the search list unless the host is rooted
631          // with a trailing dot.  The trailing dot will disable any use of the
632          // search path in a lower level resolver.  See RFC 1535.
633          String fqHost = host;
634          if (!fqHost.endsWith(".")) fqHost += ".";
635          try {
636            addr = getInetAddressByName(fqHost);
637            // can't leave the hostname as rooted or other parts of the system
638            // malfunction, ex. kerberos principals are lacking proper host
639            // equivalence for rooted/non-rooted hostnames
640            addr = InetAddress.getByAddress(host, addr.getAddress());
641          } catch (UnknownHostException e) {
642            // ignore, caller will throw if necessary
643          }
644          return addr;
645        }
646    
647        InetAddress getByNameWithSearch(String host) {
648          InetAddress addr = null;
649          if (host.endsWith(".")) { // already qualified?
650            addr = getByExactName(host); 
651          } else {
652            for (String domain : searchDomains) {
653              String dot = !domain.startsWith(".") ? "." : "";
654              addr = getByExactName(host + dot + domain);
655              if (addr != null) break;
656            }
657          }
658          return addr;
659        }
660    
661        // implemented as a separate method to facilitate unit testing
662        InetAddress getInetAddressByName(String host) throws UnknownHostException {
663          return InetAddress.getByName(host);
664        }
665    
666        void setSearchDomains(String ... domains) {
667          searchDomains = Arrays.asList(domains);
668        }
669      }
670    
671      public static AuthenticationMethod getAuthenticationMethod(Configuration conf) {
672        String value = conf.get(HADOOP_SECURITY_AUTHENTICATION, "simple");
673        try {
674          return Enum.valueOf(AuthenticationMethod.class, value.toUpperCase());
675        } catch (IllegalArgumentException iae) {
676          throw new IllegalArgumentException("Invalid attribute value for " +
677              HADOOP_SECURITY_AUTHENTICATION + " of " + value);
678        }
679      }
680    
681      public static void setAuthenticationMethod(
682          AuthenticationMethod authenticationMethod, Configuration conf) {
683        if (authenticationMethod == null) {
684          authenticationMethod = AuthenticationMethod.SIMPLE;
685        }
686        conf.set(HADOOP_SECURITY_AUTHENTICATION,
687                 authenticationMethod.toString().toLowerCase());
688      }
689    }