/** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. See accompanying LICENSE file. */ package org.apache.hadoop.security.authentication.server; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.util.Arrays; import java.util.List; import java.util.ArrayList; import java.util.Properties; import java.util.Vector; import java.util.Date; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.hadoop.minikdc.KerberosSecurityTestcase; import org.apache.hadoop.security.authentication.KerberosTestUtils; import org.apache.hadoop.security.authentication.client.AuthenticationException; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import com.nimbusds.jose.*; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jose.crypto.RSASSASigner; import com.nimbusds.jose.crypto.RSASSAVerifier; import com.nimbusds.jose.util.Base64URL; public class TestJWTRedirectAuthentictionHandler extends KerberosSecurityTestcase { private static final String SERVICE_URL = "https://localhost:8888/resource"; private static final String REDIRECT_LOCATION = "https://localhost:8443/authserver?originalUrl=" + SERVICE_URL; RSAPublicKey publicKey = null; RSAPrivateKey privateKey = null; JWTRedirectAuthenticationHandler handler = null; @Test public void testNoPublicKeyJWT() throws Exception { try { Properties props = getProperties(); handler.init(props); SignedJWT jwt = getJWT("bob", new Date(new Date().getTime() + 5000), privateKey); Cookie cookie = new Cookie("hadoop-jwt", jwt.serialize()); HttpServletRequest request = Mockito.mock(HttpServletRequest.class); Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie }); Mockito.when(request.getRequestURL()).thenReturn( new StringBuffer(SERVICE_URL)); HttpServletResponse response = Mockito.mock(HttpServletResponse.class); Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn( SERVICE_URL); AuthenticationToken token = handler.alternateAuthenticate(request, response); fail("alternateAuthentication should have thrown a ServletException"); } catch (ServletException se) { assertTrue(se.getMessage().contains( "Public key for signature validation must be provisioned")); } catch (AuthenticationException ae) { fail("alternateAuthentication should NOT have thrown a AuthenticationException"); } } @Test public void testCustomCookieNameJWT() throws Exception { try { handler.setPublicKey(publicKey); Properties props = getProperties(); props.put(JWTRedirectAuthenticationHandler.JWT_COOKIE_NAME, "jowt"); handler.init(props); SignedJWT jwt = getJWT("bob", new Date(new Date().getTime() + 5000), privateKey); Cookie cookie = new Cookie("jowt", jwt.serialize()); HttpServletRequest request = Mockito.mock(HttpServletRequest.class); Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie }); Mockito.when(request.getRequestURL()).thenReturn( new StringBuffer(SERVICE_URL)); HttpServletResponse response = Mockito.mock(HttpServletResponse.class); Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn( SERVICE_URL); AuthenticationToken token = handler.alternateAuthenticate(request, response); Assert.assertEquals("bob", token.getUserName()); } catch (ServletException se) { fail("alternateAuthentication should NOT have thrown a ServletException: " + se.getMessage()); } catch (AuthenticationException ae) { fail("alternateAuthentication should NOT have thrown a AuthenticationException"); } } @Test public void testNoProviderURLJWT() throws Exception { try { handler.setPublicKey(publicKey); Properties props = getProperties(); props .remove(JWTRedirectAuthenticationHandler.AUTHENTICATION_PROVIDER_URL); handler.init(props); SignedJWT jwt = getJWT("bob", new Date(new Date().getTime() + 5000), privateKey); Cookie cookie = new Cookie("hadoop-jwt", jwt.serialize()); HttpServletRequest request = Mockito.mock(HttpServletRequest.class); Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie }); Mockito.when(request.getRequestURL()).thenReturn( new StringBuffer(SERVICE_URL)); HttpServletResponse response = Mockito.mock(HttpServletResponse.class); Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn( SERVICE_URL); AuthenticationToken token = handler.alternateAuthenticate(request, response); fail("alternateAuthentication should have thrown an AuthenticationException"); } catch (ServletException se) { assertTrue(se.getMessage().contains( "Authentication provider URL must not be null")); } catch (AuthenticationException ae) { fail("alternateAuthentication should NOT have thrown a AuthenticationException"); } } @Test public void testUnableToParseJWT() throws Exception { try { KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(2048); KeyPair kp = kpg.genKeyPair(); RSAPublicKey publicKey = (RSAPublicKey) kp.getPublic(); handler.setPublicKey(publicKey); Properties props = getProperties(); handler.init(props); SignedJWT jwt = getJWT("bob", new Date(new Date().getTime() + 5000), privateKey); Cookie cookie = new Cookie("hadoop-jwt", "ljm" + jwt.serialize()); HttpServletRequest request = Mockito.mock(HttpServletRequest.class); Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie }); Mockito.when(request.getRequestURL()).thenReturn( new StringBuffer(SERVICE_URL)); HttpServletResponse response = Mockito.mock(HttpServletResponse.class); Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn( SERVICE_URL); AuthenticationToken token = handler.alternateAuthenticate(request, response); Mockito.verify(response).sendRedirect(REDIRECT_LOCATION); } catch (ServletException se) { fail("alternateAuthentication should NOT have thrown a ServletException"); } catch (AuthenticationException ae) { fail("alternateAuthentication should NOT have thrown a AuthenticationException"); } } @Test public void testFailedSignatureValidationJWT() throws Exception { try { // Create a public key that doesn't match the one needed to // verify the signature - in order to make it fail verification... KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(2048); KeyPair kp = kpg.genKeyPair(); RSAPublicKey publicKey = (RSAPublicKey) kp.getPublic(); handler.setPublicKey(publicKey); Properties props = getProperties(); handler.init(props); SignedJWT jwt = getJWT("bob", new Date(new Date().getTime() + 5000), privateKey); Cookie cookie = new Cookie("hadoop-jwt", jwt.serialize()); HttpServletRequest request = Mockito.mock(HttpServletRequest.class); Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie }); Mockito.when(request.getRequestURL()).thenReturn( new StringBuffer(SERVICE_URL)); HttpServletResponse response = Mockito.mock(HttpServletResponse.class); Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn( SERVICE_URL); AuthenticationToken token = handler.alternateAuthenticate(request, response); Mockito.verify(response).sendRedirect(REDIRECT_LOCATION); } catch (ServletException se) { fail("alternateAuthentication should NOT have thrown a ServletException"); } catch (AuthenticationException ae) { fail("alternateAuthentication should NOT have thrown a AuthenticationException"); } } @Test public void testExpiredJWT() throws Exception { try { handler.setPublicKey(publicKey); Properties props = getProperties(); handler.init(props); SignedJWT jwt = getJWT("bob", new Date(new Date().getTime() - 1000), privateKey); Cookie cookie = new Cookie("hadoop-jwt", jwt.serialize()); HttpServletRequest request = Mockito.mock(HttpServletRequest.class); Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie }); Mockito.when(request.getRequestURL()).thenReturn( new StringBuffer(SERVICE_URL)); HttpServletResponse response = Mockito.mock(HttpServletResponse.class); Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn( SERVICE_URL); AuthenticationToken token = handler.alternateAuthenticate(request, response); Mockito.verify(response).sendRedirect(REDIRECT_LOCATION); } catch (ServletException se) { fail("alternateAuthentication should NOT have thrown a ServletException"); } catch (AuthenticationException ae) { fail("alternateAuthentication should NOT have thrown a AuthenticationException"); } } @Test public void testInvalidAudienceJWT() throws Exception { try { handler.setPublicKey(publicKey); Properties props = getProperties(); props .put(JWTRedirectAuthenticationHandler.EXPECTED_JWT_AUDIENCES, "foo"); handler.init(props); SignedJWT jwt = getJWT("bob", new Date(new Date().getTime() + 5000), privateKey); Cookie cookie = new Cookie("hadoop-jwt", jwt.serialize()); HttpServletRequest request = Mockito.mock(HttpServletRequest.class); Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie }); Mockito.when(request.getRequestURL()).thenReturn( new StringBuffer(SERVICE_URL)); HttpServletResponse response = Mockito.mock(HttpServletResponse.class); Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn( SERVICE_URL); AuthenticationToken token = handler.alternateAuthenticate(request, response); Mockito.verify(response).sendRedirect(REDIRECT_LOCATION); } catch (ServletException se) { fail("alternateAuthentication should NOT have thrown a ServletException"); } catch (AuthenticationException ae) { fail("alternateAuthentication should NOT have thrown a AuthenticationException"); } } @Test public void testValidAudienceJWT() throws Exception { try { handler.setPublicKey(publicKey); Properties props = getProperties(); props .put(JWTRedirectAuthenticationHandler.EXPECTED_JWT_AUDIENCES, "bar"); handler.init(props); SignedJWT jwt = getJWT("bob", new Date(new Date().getTime() + 5000), privateKey); Cookie cookie = new Cookie("hadoop-jwt", jwt.serialize()); HttpServletRequest request = Mockito.mock(HttpServletRequest.class); Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie }); Mockito.when(request.getRequestURL()).thenReturn( new StringBuffer(SERVICE_URL)); HttpServletResponse response = Mockito.mock(HttpServletResponse.class); Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn( SERVICE_URL); AuthenticationToken token = handler.alternateAuthenticate(request, response); Assert.assertEquals("bob", token.getUserName()); } catch (ServletException se) { fail("alternateAuthentication should NOT have thrown a ServletException"); } catch (AuthenticationException ae) { fail("alternateAuthentication should NOT have thrown an AuthenticationException"); } } @Test public void testValidJWT() throws Exception { try { handler.setPublicKey(publicKey); Properties props = getProperties(); handler.init(props); SignedJWT jwt = getJWT("alice", new Date(new Date().getTime() + 5000), privateKey); Cookie cookie = new Cookie("hadoop-jwt", jwt.serialize()); HttpServletRequest request = Mockito.mock(HttpServletRequest.class); Mockito.when(request.getCookies()).thenReturn(new Cookie[] { cookie }); Mockito.when(request.getRequestURL()).thenReturn( new StringBuffer(SERVICE_URL)); HttpServletResponse response = Mockito.mock(HttpServletResponse.class); Mockito.when(response.encodeRedirectURL(SERVICE_URL)).thenReturn( SERVICE_URL); AuthenticationToken token = handler.alternateAuthenticate(request, response); Assert.assertNotNull("Token should not be null.", token); Assert.assertEquals("alice", token.getUserName()); } catch (ServletException se) { fail("alternateAuthentication should NOT have thrown a ServletException."); } catch (AuthenticationException ae) { fail("alternateAuthentication should NOT have thrown an AuthenticationException"); } } @Before public void setup() throws Exception, NoSuchAlgorithmException { setupKerberosRequirements(); KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(2048); KeyPair kp = kpg.genKeyPair(); publicKey = (RSAPublicKey) kp.getPublic(); privateKey = (RSAPrivateKey) kp.getPrivate(); handler = new JWTRedirectAuthenticationHandler(); } protected void setupKerberosRequirements() throws Exception { String[] keytabUsers = new String[] { "HTTP/host1", "HTTP/host2", "HTTP2/host1", "XHTTP/host" }; String keytab = KerberosTestUtils.getKeytabFile(); getKdc().createPrincipal(new File(keytab), keytabUsers); } @After public void teardown() throws Exception { handler.destroy(); } protected Properties getProperties() { Properties props = new Properties(); props.setProperty( JWTRedirectAuthenticationHandler.AUTHENTICATION_PROVIDER_URL, "https://localhost:8443/authserver"); props.setProperty("kerberos.principal", KerberosTestUtils.getServerPrincipal()); props.setProperty("kerberos.keytab", KerberosTestUtils.getKeytabFile()); return props; } protected SignedJWT getJWT(String sub, Date expires, RSAPrivateKey privateKey) throws Exception { JWTClaimsSet claimsSet = new JWTClaimsSet(); claimsSet.setSubject(sub); claimsSet.setIssueTime(new Date(new Date().getTime())); claimsSet.setIssuer("https://c2id.com"); claimsSet.setCustomClaim("scope", "openid"); claimsSet.setExpirationTime(expires); List<String> aud = new ArrayList<String>(); aud.add("bar"); claimsSet.setAudience("bar"); JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256).build(); SignedJWT signedJWT = new SignedJWT(header, claimsSet); Base64URL sigInput = Base64URL.encode(signedJWT.getSigningInput()); JWSSigner signer = new RSASSASigner(privateKey); signedJWT.sign(signer); return signedJWT; } }