Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle multiple Authenticator

Tags:

android

To authenticate a request, I use Authenticator.setDefault which is VM wide...
What If I want to separate different webservices and each one are aware of their authentication credentials.
Do I need to Authenticator.setDefault for each request ?
This may not work if there are concurrent connection with mixed webservices...

like image 234
CiNN Avatar asked Mar 02 '11 17:03

CiNN


1 Answers

Building on Mike's response above I have the following solution, because while I much appreciate the general idea (that's why I've copied it ;-) , I see a few problems with it:

  • Mike's solution will throw a NullPointerException if the JDK requests the authentication via one of the two static request methods in java.net.Authenticator that do not pass the URL (then getRequestingURL() will return null).
  • It requires you to pass in an external regex pattern that deconstructs the URL. This is (very) easy to get wrong, and the URL class in the JDK implements this parsing, so I prefer to use that.
  • It requires that some external class builds the map of PasswordAuthentication objects, and then sets it. It does not implement a registration mechanism that other components in your system can use. I've also turned it into a singleton.
  • More of a style thing: I don't recommend duplicating class names (Authenticator), so I've renamed it DefaultAuthenticator.

Below solution I think solves these issues.

    /**
     * Authenticator which keeps credentials to be passed to the requestor based on authority of the requesting URL. The
     * authority is <pre>user:password@host:port</pre>, where all parts are optional except the host.
     * <p>
     * If the configured credentials are not found, the Authenticator will use the credentials embedded in the URL, if
     * present. Embedded credentials are in the form of <pre>user:password@host:port</pre>
     *   
     * @author Michael Fortin 2011-09-23
     */
    public final class DefaultAuthenticator extends Authenticator {

        private static final Logger LOG = Logger.getLogger(DefaultAuthenticator.class.getName());
        private static DefaultAuthenticator instance;

        private Map<String, PasswordAuthentication> authInfo = new HashMap<String, PasswordAuthentication>();

        private DefaultAuthenticator() {
        }

        public static synchronized DefaultAuthenticator getInstance() {
            if (instance == null) {
                instance = new DefaultAuthenticator();
                Authenticator.setDefault(instance);
            }
            return instance;
        }

        // unit testing
        static void reset() {
            instance = null;
            Authenticator.setDefault(null);        
        }

        @Override
        protected PasswordAuthentication getPasswordAuthentication() {

            String requestorInfo = getRequestorInfo();
            LOG.info(getRequestorType() + " at \"" + getRequestingPrompt() + "\" is requesting " + getRequestingScheme()
                    + " password authentication for \"" + requestorInfo + "\"");

            if (authInfo.containsKey(requestorInfo)) {
                return authInfo.get(requestorInfo);
            } else {
                PasswordAuthentication pa = getEmbeddedCredentials(getRequestingURL());
                if (pa == null) {
                    LOG.warning("No authentication information");
                }
                return pa;
            }

        }

        /**
         * Register the authentication information for a given URL.
         * 
         * @param url - the URL that will request authorization
         * @param auth - the {@link PasswordAuthentication} for this URL providing the credentials
         */
        public void register(URL url, PasswordAuthentication auth) {
            String requestorInfo = getRequestorInfo(url.getHost(), url.getPort()); 
            authInfo.put(requestorInfo, auth);
        }

        /**
         * Get the requestor info based on info provided.
         * 
         * @param host - hostname of requestor
         * @param port - TCP/IP port
         * @return requestor info string
         */
        private String getRequestorInfo(String host, int port) {

            String fullHostname;
            try {
                InetAddress addr = InetAddress.getByName(host);
                fullHostname = addr.getCanonicalHostName();
            } catch (UnknownHostException e) {
                fullHostname = host;
            }

            if (port == -1) {
                return fullHostname;
            } else {
                return fullHostname + ":" + port;
            }
        }

        /**
         * Get the requestor info for the request currently being processed by this Authenticator.
         * 
         * @return requestor info string for current request
         */
        private String getRequestorInfo() {

            String host;
            InetAddress addr = getRequestingSite();
            if (addr == null) {
                host = getRequestingHost();
            } else {
                host = addr.getCanonicalHostName();
            }
            return getRequestorInfo(host, getRequestingPort());
        }

        /**
         * Get the credentials from the requesting URL.
         * 
         * @param url - URL to get the credentials from (can be null, method will return null)
         * @return PasswordAuthentication with credentials from URL or null if URL contains no credentials or if URL is
         * null itself
         */
        PasswordAuthentication getEmbeddedCredentials(URL url) {

            if (url == null) {
                return null;
            }

            String userInfo = url.getUserInfo();
            int colon = userInfo == null ? -1 : userInfo.indexOf(":");
            if (colon == -1) {
                return null;
            } else {
                String userName = userInfo.substring(0, colon);
                String pass = userInfo.substring(colon + 1);
                return new PasswordAuthentication(userName, pass.toCharArray());
            }
        }
    }

While I'm at it, let me give you my unit tests (JUnit 4).

    /**
     * @author Paul Balm - May 10 2012
     */
    public class DefaultAuthenticatorTest {

        private static final Logger LOG = Logger.getLogger(DefaultAuthenticatorTest.class.getName());

        @Before
        public void setUp() throws Exception {
            DefaultAuthenticator.reset();
            DefaultAuthenticator.getInstance();
        }

        @After
        public void tearDown() {
            DefaultAuthenticator.reset();
        }

        @Test
        public void testRequestAuthenticationFromURL() throws MalformedURLException, UnknownHostException {

            Map<String, String[]> urls = generateURLs();

            for (String urlStr : urls.keySet()) {
                String[] userInfo = urls.get(urlStr);
                LOG.info("Testing: " + urlStr);
                URL url = new URL(urlStr);
                request(userInfo[1], userInfo[2], url, true);
            }

        }

        @Test
        public void testRequestAuthenticationRegistered() throws UnknownHostException, MalformedURLException {

            Map<String, String[]> urls = generateURLs();

            for (String urlStr : urls.keySet()) {
                String[] userInfo = urls.get(urlStr);
                LOG.info("Testing: " + urlStr);
                URL url = new URL(urlStr);

                DefaultAuthenticator.reset();
                DefaultAuthenticator auth = DefaultAuthenticator.getInstance();

                String userName = userInfo[1];
                String password = userInfo[2];

                if (password != null) {
                    // You can't register a null password
                    auth.register(url, new PasswordAuthentication(userName, password.toCharArray()));
                }

                request(userName, password, url, false);
            }

        }

        /**
         *  Generate a bunch of URLs mapped to String array. The String array has the following elements:
         *  - user info part of URL, 
         *  - expected user, 
         *  - expected password
         *  
         *  Note that the keys of the maps must be strings and not URL objects, because of the way URL.equals is
         *  implemented. This method does not consider the credentials.
         *  
         * @throws MalformedURLException 
         */
        Map<String, String[]> generateURLs() {
            String[] hosts = new String[]{ "127.0.0.1", "localhost.localdomain"};

            List<String[]> userData = new ArrayList<String[]>();

            // normal cases
            userData.add(new String[] { "user:pass@", "user", "pass" }); // results in: http://user:pass@[host]
            userData.add(new String[] { "", null, null });
            // unexpected cases
            userData.add(new String[] { "@", null, null });
            userData.add(new String[] { ":@", "", "" });
            userData.add(new String[] { "user:@", "user", "" });
            userData.add(new String[] { ":pass@", "", "pass" });

            Map<String, String[]> urls = new HashMap<String, String[]>();

            for (String[] userInfo : userData) {
                for (String host : hosts) {
                    String s = "http://" + userInfo[0] + host;
                    urls.put(s, userInfo);
                }
            }

            LOG.info("" + urls.size() + " URLs prepared");

            return urls;
        }

        private void request(String expectedUser, String expectedPass, URL url, boolean inURL)
        throws UnknownHostException {        

            String host = url.getHost();
            InetAddress addr = InetAddress.getAllByName(host)[0];
            int port = url.getPort();
            String protocol = url.getProtocol();
            String prompt = ""; // prompt for the user when asking for the credentials
            String scheme = "basic"; // or digest
            RequestorType reqType = RequestorType.SERVER;

            PasswordAuthentication credentials =
                Authenticator.requestPasswordAuthentication(addr, port, protocol, prompt, scheme);
            // If the credentials are in the URL, you can't find them using this method because we're not passing the URL
            checkCredentials(url, inURL ? null : expectedUser, inURL ? null : expectedPass, credentials); 

            credentials = Authenticator.requestPasswordAuthentication(host, addr, port, protocol, prompt, scheme);
            // If the credentials are in the URL, you can't find them using this method because we're not passing the URL
            checkCredentials(url, inURL ? null : expectedUser, inURL ? null : expectedPass, credentials); 

            credentials = Authenticator.requestPasswordAuthentication(host, addr, port, protocol, prompt, scheme, url, reqType);
            checkCredentials(url, expectedUser, expectedPass, credentials);
        }

        private void checkCredentials(URL url, String expectedUser, String expectedPass, PasswordAuthentication credentials) {
            if (expectedUser == null) {
                Assert.assertNull(url.toString(), credentials);
            } else {
                Assert.assertNotNull(url.toString(), credentials);
                Assert.assertEquals(url.toString(), expectedUser, credentials.getUserName());

                if (expectedPass == null) {
                    Assert.assertNull(url.toString(), credentials.getPassword());
                } else {
                    Assert.assertArrayEquals(url.toString(), expectedPass.toCharArray(), credentials.getPassword());
                }
            }
        }

    }
like image 138
Paul Avatar answered Oct 03 '22 02:10

Paul