blob: bf0626978bfc53a9c3e819c36568612a2ba3db8b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package org.apache.knox.gateway.service.knoxtoken;
import static org.apache.knox.gateway.config.impl.GatewayConfigImpl.KNOX_TOKEN_USER_LIMIT;
import static org.apache.knox.gateway.config.impl.GatewayConfigImpl.KNOX_TOKEN_USER_LIMIT_DEFAULT;
import static org.apache.knox.gateway.service.knoxtoken.TokenResource.KNOX_TOKEN_ISSUER;
import static org.apache.knox.gateway.service.knoxtoken.TokenResource.KNOX_TOKEN_USER_LIMIT_EXCEEDED_ACTION;
import static org.apache.knox.gateway.service.knoxtoken.TokenResource.TOKEN_INCLUDE_GROUPS_IN_JWT_ALLOWED;
import static org.apache.knox.gateway.services.security.token.JWTokenAttributes.DEFAULT_ISSUER;
import static org.apache.knox.gateway.services.security.token.impl.JWTToken.KNOX_GROUPS_CLAIM;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Predicate;
import javax.security.auth.Subject;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.KeyLengthException;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.lang3.StringUtils;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.security.PrimaryPrincipal;
import org.apache.knox.gateway.services.GatewayServices;
import org.apache.knox.gateway.services.ServiceLifecycleException;
import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.security.AliasService;
import org.apache.knox.gateway.services.security.token.JWTokenAttributes;
import org.apache.knox.gateway.services.security.token.JWTokenAuthority;
import org.apache.knox.gateway.services.security.token.KnoxToken;
import org.apache.knox.gateway.services.security.token.PersistentTokenStateService;
import org.apache.knox.gateway.services.security.token.TokenMetadata;
import org.apache.knox.gateway.services.security.token.TokenStateService;
import org.apache.knox.gateway.services.security.token.TokenUtils;
import org.apache.knox.gateway.services.security.token.UnknownTokenException;
import org.apache.knox.gateway.services.security.token.impl.JWT;
import org.apache.knox.gateway.services.security.token.impl.JWTToken;
import org.apache.knox.gateway.services.security.token.impl.TokenMAC;
import org.apache.knox.gateway.services.token.impl.JDBCTokenStateService;
import org.apache.knox.gateway.util.JsonUtils;
import org.easymock.EasyMock;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Some tests for the token service
*/
public class TokenServiceResourceTest {
private static RSAPublicKey publicKey;
private static RSAPrivateKey privateKey;
private static final String TOKEN_API_PATH = "https://gateway-host:8443/gateway/sandbox/knoxtoken/api/v1";
private static final String TOKEN_PATH = "/token";
private static final String JKWS_PATH = "/jwks.json";
private static final String USER_NAME = "alice";
private ServletContext context;
private HttpServletRequest request;
private JWTokenAuthority authority;
private TestTokenStateService tss = new TestTokenStateService();
private char[] hmacSecret;
private enum TokenLifecycleOperation {
Renew,
Revoke
}
@BeforeClass
public static void setUpBeforeClass() throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair KPair = kpg.generateKeyPair();
publicKey = (RSAPublicKey) KPair.getPublic();
privateKey = (RSAPrivateKey) KPair.getPrivate();
}
private void configureCommonExpectations(Map<String, String> contextExpectations) throws Exception {
configureCommonExpectations(contextExpectations, null, null);
}
private void configureCommonExpectations(Map<String, String> contextExpectations, String expectedSubjectDN) throws Exception {
configureCommonExpectations(contextExpectations, expectedSubjectDN, null);
}
private void configureCommonExpectations(Map<String, String> contextExpectations, Boolean serverManagedTssEnabled) throws Exception {
configureCommonExpectations(contextExpectations, null, serverManagedTssEnabled);
}
private void configureCommonExpectations(Map<String, String> contextExpectations, String expectedSubjectDN, Boolean serverManagedTssEnabled) throws Exception {
context = EasyMock.createNiceMock(ServletContext.class);
contextExpectations.forEach((key, value) -> EasyMock.expect(context.getInitParameter(key)).andReturn(value).anyTimes());
EasyMock.expect(context.getInitParameterNames()).andReturn(Collections.enumeration(contextExpectations.keySet())).anyTimes();
request = EasyMock.createNiceMock(HttpServletRequest.class);
EasyMock.expect(request.getServletContext()).andReturn(context).anyTimes();
Principal principal = EasyMock.createNiceMock(Principal.class);
EasyMock.expect(principal.getName()).andReturn(USER_NAME).anyTimes();
EasyMock.expect(request.getUserPrincipal()).andReturn(principal).anyTimes();
EasyMock.expect(request.getRequestURL()).andReturn(new StringBuffer(TOKEN_API_PATH+TOKEN_PATH)).anyTimes();
if (contextExpectations.containsKey(TokenResource.LIFESPAN)) {
EasyMock.expect(request.getParameter(TokenResource.LIFESPAN)).andReturn(contextExpectations.get(TokenResource.LIFESPAN)).anyTimes();
}
if (contextExpectations.containsKey(TokenResource.KNOX_TOKEN_INCLUDE_GROUPS)) {
EasyMock.expect(request.getParameter(TokenResource.KNOX_TOKEN_INCLUDE_GROUPS)).andReturn(contextExpectations.get(TokenResource.KNOX_TOKEN_INCLUDE_GROUPS)).anyTimes();
}
if (contextExpectations.containsKey(TokenResource.QUERY_PARAMETER_DOAS)) {
EasyMock.expect(request.getParameter(TokenResource.QUERY_PARAMETER_DOAS)).andReturn(contextExpectations.get(TokenResource.QUERY_PARAMETER_DOAS)).anyTimes();
}
if (contextExpectations.containsKey(TokenResource.IMPERSONATION_ENABLED_PARAM)) {
EasyMock.expect(request.getParameter(TokenResource.IMPERSONATION_ENABLED_PARAM)).andReturn(contextExpectations.get(TokenResource.IMPERSONATION_ENABLED_PARAM)).anyTimes();
}
EasyMock.expect(request.getParameterNames()).andReturn(Collections.emptyEnumeration()).anyTimes();
GatewayServices services = EasyMock.createNiceMock(GatewayServices.class);
EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(services).anyTimes();
GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
EasyMock.expect(context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(config).anyTimes();
EasyMock.expect(config.getSigningKeystoreName()).andReturn(null).anyTimes();
if (serverManagedTssEnabled != null) {
if (serverManagedTssEnabled) {
EasyMock.expect(config.isServerManagedTokenStateEnabled()).andReturn(true).anyTimes();
}
}
final String tokenStateServiceType = ServiceType.TOKEN_STATE_SERVICE.getShortName();
if (contextExpectations.containsKey(tokenStateServiceType)) {
EasyMock.expect(config.getServiceParameter(tokenStateServiceType, "impl")).andReturn(contextExpectations.get(tokenStateServiceType)).anyTimes();
}
EasyMock.expect(config.getKnoxTokenHashAlgorithm()).andReturn(HmacAlgorithms.HMAC_SHA_256.getName()).anyTimes();
EasyMock.expect(config.getMaximumNumberOfTokensPerUser())
.andReturn(contextExpectations.containsKey(KNOX_TOKEN_USER_LIMIT) ? Integer.parseInt(contextExpectations.get(KNOX_TOKEN_USER_LIMIT)) : -1).anyTimes();
EasyMock.expect(services.getService(ServiceType.TOKEN_STATE_SERVICE)).andReturn(tss).anyTimes();
AliasService aliasService = EasyMock.createNiceMock(AliasService.class);
EasyMock.expect(services.getService(ServiceType.ALIAS_SERVICE)).andReturn(aliasService).anyTimes();
EasyMock.expect(aliasService.getPasswordFromAliasForGateway(TokenUtils.SIGNING_HMAC_SECRET_ALIAS)).andReturn(hmacSecret).anyTimes();
EasyMock.expect(aliasService.getPasswordFromAliasForGateway(TokenMAC.KNOX_TOKEN_HASH_KEY_ALIAS_NAME)).andReturn("sPj8FCgQhCEi6G18kBfpswxYSki33plbelGLs0hMSbk".toCharArray()).anyTimes();
authority = new TestJWTokenAuthority(publicKey, privateKey);
EasyMock.expect(services.getService(ServiceType.TOKEN_SERVICE)).andReturn(authority).anyTimes();
if (StringUtils.isNotBlank(expectedSubjectDN)) {
X509Certificate trustedCertMock = EasyMock.createMock(X509Certificate.class);
EasyMock.expect(trustedCertMock.getSubjectDN()).andReturn(new PrimaryPrincipal(expectedSubjectDN)).anyTimes();
ArrayList<X509Certificate> certArrayList = new ArrayList<>();
certArrayList.add(trustedCertMock);
X509Certificate[] certs = {};
EasyMock.expect(request.getAttribute("javax.servlet.request.X509Certificate")).andReturn(certArrayList.toArray(certs)).anyTimes();
EasyMock.replay(trustedCertMock);
}
else {
EasyMock.expect(request.getAttribute("javax.servlet.request.X509Certificate")).andReturn(null).anyTimes();
}
EasyMock.replay(principal, services, context, request, aliasService, config);
}
@Test(expected = KeyLengthException.class)
public void testInvalidHmacSecretThrowsException() throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
hmacSecret = "1234".toCharArray();
contextExpectations.put("knox.token.sigalg", JWSAlgorithm.HS256.getName());
configureCommonExpectations(contextExpectations);
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
}
@Test
public void testValidHmacSecretNoException() throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
hmacSecret = "12345678123456781234567812345678".toCharArray();
contextExpectations.put("knox.token.sigalg", JWSAlgorithm.HS256.getName());
configureCommonExpectations(contextExpectations);
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
}
@Test
public void testClientData() {
TokenResource tr = new TokenResource();
Map<String,Object> clientDataMap = new HashMap<>();
tr.addClientDataToMap("cookie.name=hadoop-jwt,test=value".split(","), clientDataMap);
Assert.assertEquals(2, clientDataMap.size());
clientDataMap = new HashMap<>();
tr.addClientDataToMap("cookie.name=hadoop-jwt".split(","), clientDataMap);
Assert.assertEquals(1, clientDataMap.size());
clientDataMap = new HashMap<>();
tr.addClientDataToMap("".split(","), clientDataMap);
Assert.assertEquals(0, clientDataMap.size());
}
@Test
public void testGetToken() throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put("org.apache.knox.gateway.gateway.cluster", "test");
contextExpectations.put(TokenResource.TOKEN_CLIENT_DATA, "sampleClientData=param1=value1&param2=value2");
configureCommonExpectations(contextExpectations, Boolean.TRUE);
TokenResource tr = new TokenResource();
tr.context = context;
tr.request = request;
tr.init();
// Issue a token
Response retResponse = tr.doGet();
assertEquals(200, retResponse.getStatus());
// Parse the response
String retString = retResponse.getEntity().toString();
String accessToken = getTagValue(retString, "access_token");
assertNotNull(accessToken);
String expiry = getTagValue(retString, "expires_in");
assertNotNull(expiry);
assertNotNull(getTagValue(retString, "token_id"));
assertTrue(Boolean.parseBoolean(getTagValue(retString, "managed")));
assertEquals(getTagValue(retString, "sampleClientData"), "param1=value1&param2=value2");
// Verify the token
JWT parsedToken = new JWTToken(accessToken);
assertEquals("alice", parsedToken.getSubject());
assertTrue(authority.verifyToken(parsedToken));
}
/*
* KNOX-2266
*/
@Test
public void testConcurrentGetToken() throws Exception {
configureCommonExpectations(Collections.singletonMap("org.apache.knox.gateway.gateway.cluster", "test"));
final TokenResource tr = new TokenResource();
tr.context = context;
tr.request = request;
tr.init();
// Request two tokens concurrently
Callable<Response> task = tr::doGet;
List<Callable<Response>> tasks = Collections.nCopies(2, task);
ExecutorService executorService = Executors.newFixedThreadPool(2);
List<Future<Response>> futures = executorService.invokeAll(tasks);
List<Response> responses = new ArrayList<>(futures.size());
for (Future<Response> f : futures) {
responses.add(f.get());
}
// Parse the responses
String accessToken1 = getTagValue(responses.get(0).getEntity().toString(), "access_token");
assertNotNull(accessToken1);
JWT jwt1 = new JWTToken(accessToken1);
String accessToken2 = getTagValue(responses.get(1).getEntity().toString(), "access_token");
assertNotNull(accessToken1);
JWT jwt2 = new JWTToken(accessToken2);
// Verify the tokens
assertNotEquals("Access tokens should be different.", accessToken1, accessToken2);
assertEquals("The token expirations should be the same.", jwt1.getExpires(), jwt2.getExpires());
assertNotEquals("Tokens should have unique IDs.", TokenUtils.getTokenId(jwt1), TokenUtils.getTokenId(jwt2));
}
@Test
public void testAudiences() throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put("knox.token.audiences", "recipient1,recipient2");
configureCommonExpectations(contextExpectations);
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
// Issue a token
Response retResponse = tr.doGet();
assertEquals(200, retResponse.getStatus());
// Parse the response
String retString = retResponse.getEntity().toString();
String accessToken = getTagValue(retString, "access_token");
assertNotNull(accessToken);
String expiry = getTagValue(retString, "expires_in");
assertNotNull(expiry);
// Verify the token
JWT parsedToken = new JWTToken(accessToken);
assertEquals("alice", parsedToken.getSubject());
assertTrue(authority.verifyToken(parsedToken));
// Verify the audiences
List<String> audiences = Arrays.asList(parsedToken.getAudienceClaims());
assertEquals(2, audiences.size());
assertTrue(audiences.contains("recipient1"));
assertTrue(audiences.contains("recipient2"));
}
@Test
public void testAudiencesWhitespace() throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put("knox.token.audiences", " recipient1,recipient2 ");
configureCommonExpectations(contextExpectations);
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
// Issue a token
Response retResponse = tr.doGet();
assertEquals(200, retResponse.getStatus());
// Parse the response
String retString = retResponse.getEntity().toString();
String accessToken = getTagValue(retString, "access_token");
assertNotNull(accessToken);
String expiry = getTagValue(retString, "expires_in");
assertNotNull(expiry);
// Verify the token
JWT parsedToken = new JWTToken(accessToken);
assertEquals("alice", parsedToken.getSubject());
assertTrue(authority.verifyToken(parsedToken));
// Verify the audiences
List<String> audiences = Arrays.asList(parsedToken.getAudienceClaims());
assertEquals(2, audiences.size());
assertTrue(audiences.contains("recipient1"));
assertTrue(audiences.contains("recipient2"));
}
@Test
public void testValidClientCert() throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put("knox.token.client.cert.required", "true");
contextExpectations.put("knox.token.allowed.principals", "CN=localhost, OU=Test, O=Hadoop, L=Test, ST=Test, C=US");
configureCommonExpectations(contextExpectations, "CN=localhost,OU=Test, O=Hadoop, L=Test, ST=Test, C=US");
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
// Issue a token
Response retResponse = tr.doGet();
assertEquals(200, retResponse.getStatus());
// Parse the response
String retString = retResponse.getEntity().toString();
String accessToken = getTagValue(retString, "access_token");
assertNotNull(accessToken);
String expiry = getTagValue(retString, "expires_in");
assertNotNull(expiry);
// Verify the token
JWT parsedToken = new JWTToken(accessToken);
assertEquals("alice", parsedToken.getSubject());
assertTrue(authority.verifyToken(parsedToken));
}
@Test
public void testValidClientCertWrongUser() throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put("knox.token.client.cert.required", "true");
contextExpectations.put("knox.token.allowed.principals", "CN=remotehost, OU=Test, O=Hadoop, L=Test, ST=Test, C=US");
configureCommonExpectations(contextExpectations, "CN=localhost,OU=Test, O=Hadoop, L=Test, ST=Test, C=US");
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
// Issue a token
Response retResponse = tr.doGet();
assertEquals(403, retResponse.getStatus());
}
@Test
public void testMissingClientCert() throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put("knox.token.client.cert.required", "true");
contextExpectations.put("knox.token.allowed.principals", "CN=remotehost, OU=Test, O=Hadoop, L=Test, ST=Test, C=US");
configureCommonExpectations(contextExpectations, (String) null);
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
// Issue a token
Response retResponse = tr.doGet();
assertEquals(403, retResponse.getStatus());
}
@Test
public void testSignatureAlgorithm() throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put("knox.token.sigalg", "RS512");
configureCommonExpectations(contextExpectations);
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
// Issue a token
Response retResponse = tr.doGet();
assertEquals(200, retResponse.getStatus());
// Parse the response
String retString = retResponse.getEntity().toString();
String accessToken = getTagValue(retString, "access_token");
assertNotNull(accessToken);
String expiry = getTagValue(retString, "expires_in");
assertNotNull(expiry);
// Verify the token
JWT parsedToken = new JWTToken(accessToken);
assertEquals("alice", parsedToken.getSubject());
assertTrue(authority.verifyToken(parsedToken));
assertTrue(parsedToken.getHeader().contains("RS512"));
}
@Test
public void testDefaultTTL() throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put("knox.token.ttl", null);
configureCommonExpectations(contextExpectations);
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
// Issue a token
Response retResponse = tr.doGet();
assertEquals(200, retResponse.getStatus());
// Parse the response
String retString = retResponse.getEntity().toString();
String accessToken = getTagValue(retString, "access_token");
assertNotNull(accessToken);
String expiry = getTagValue(retString, "expires_in");
assertNotNull(expiry);
// Verify the token
JWT parsedToken = new JWTToken(accessToken);
assertEquals("alice", parsedToken.getSubject());
assertTrue(authority.verifyToken(parsedToken));
Date expiresDate = parsedToken.getExpiresDate();
Date now = new Date();
assertTrue(expiresDate.after(now));
assertTrue((expiresDate.getTime() - now.getTime()) < 30000L);
}
@Test
public void testCustomTTL() throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put("knox.token.ttl", "60000");
configureCommonExpectations(contextExpectations);
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
// Issue a token
Response retResponse = tr.doGet();
assertEquals(200, retResponse.getStatus());
// Parse the response
String retString = retResponse.getEntity().toString();
String accessToken = getTagValue(retString, "access_token");
assertNotNull(accessToken);
String expiry = getTagValue(retString, "expires_in");
assertNotNull(expiry);
// Verify the token
JWT parsedToken = new JWTToken(accessToken);
assertEquals("alice", parsedToken.getSubject());
assertTrue(authority.verifyToken(parsedToken));
Date expiresDate = parsedToken.getExpiresDate();
Date now = new Date();
assertTrue(expiresDate.after(now));
long diff = expiresDate.getTime() - now.getTime();
assertTrue(diff < 60000L && diff > 30000L);
}
@Test
public void testNegativeTTL() throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put("knox.token.ttl", "-60000");
configureCommonExpectations(contextExpectations);
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
// Issue a token
Response retResponse = tr.doGet();
assertEquals(200, retResponse.getStatus());
// Parse the response
String retString = retResponse.getEntity().toString();
String accessToken = getTagValue(retString, "access_token");
assertNotNull(accessToken);
String expiry = getTagValue(retString, "expires_in");
assertNotNull(expiry);
// Verify the token
JWT parsedToken = new JWTToken(accessToken);
assertEquals("alice", parsedToken.getSubject());
assertTrue(authority.verifyToken(parsedToken));
Date expiresDate = parsedToken.getExpiresDate();
Date now = new Date();
assertTrue(expiresDate.after(now));
assertTrue((expiresDate.getTime() - now.getTime()) < 30000L);
}
@Test
public void testOverflowTTL() throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put("knox.token.ttl", String.valueOf(Long.MAX_VALUE));
configureCommonExpectations(contextExpectations);
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
// Issue a token
Response retResponse = tr.doGet();
assertEquals(200, retResponse.getStatus());
// Parse the response
String retString = retResponse.getEntity().toString();
String accessToken = getTagValue(retString, "access_token");
assertNotNull(accessToken);
String expiry = getTagValue(retString, "expires_in");
assertNotNull(expiry);
// Verify the token
JWT parsedToken = new JWTToken(accessToken);
assertEquals("alice", parsedToken.getSubject());
assertTrue(authority.verifyToken(parsedToken));
Date expiresDate = parsedToken.getExpiresDate();
Date now = new Date();
assertTrue(expiresDate.after(now));
assertTrue((expiresDate.getTime() - now.getTime()) < 30000L);
}
@Test
public void testTokenRenewal_ServerManagedStateConfiguredAtGatewayOnly() throws Exception {
final String caller = "yarn";
Response renewalResponse = doTestTokenRenewal(null, true, caller, null, createTestSubject(caller)).getValue();
validateSuccessfulRenewalResponse(renewalResponse);
}
@Test
public void testTokenRenewal_ServerManagedStateDisabledAtGatewayWithServiceOverride() throws Exception {
final String caller = "yarn";
Response renewalResponse = doTestTokenRenewal(true, false, caller, null, createTestSubject(caller)).getValue();
validateSuccessfulRenewalResponse(renewalResponse);
}
@Test
public void testTokenRenewal_ServerManagedStateEnabledAtGatewayWithServiceOverride() throws Exception {
final String caller = "yarn";
Map.Entry<TestTokenStateService, Response> result =
doTestTokenRenewal(false, true, caller, null, createTestSubject(caller));
// Make sure the expiration was not recorded by the TokenStateService, since it is disabled for this test
TestTokenStateService tss = result.getKey();
assertEquals("TokenStateService should be disabled for this test.", 0, tss.expirationData.size());
Response renewalResponse = result.getValue();
validateSuccessfulRenewalResponse(renewalResponse);
String responseContent = (String) renewalResponse.getEntity();
assertNotNull(responseContent);
Map<String, Object> json = parseJSONResponse(responseContent);
assertTrue(Boolean.parseBoolean((String)json.get("renewed")));
assertNotNull(json.get("expires")); // Should get back the original expiration from the token itself
}
@Test
public void testTokenRenewal_ServerManagedStateNotConfiguredAtAll() throws Exception {
Map.Entry<TestTokenStateService, Response> result = doTestTokenRenewal(null, null, null, null, null);
// Make sure the expiration was not recorded by the TokenStateService, since it is disabled for this test
TestTokenStateService tss = result.getKey();
assertEquals("TokenStateService should be disabled for this test.", 0, tss.expirationData.size());
Response renewalResponse = result.getValue();
validateSuccessfulRenewalResponse(renewalResponse);
String responseContent = (String) renewalResponse.getEntity();
assertNotNull(responseContent);
Map<String, Object> json = parseJSONResponse(responseContent);
assertTrue(Boolean.parseBoolean((String)json.get("renewed")));
assertNotNull(json.get("expires")); // Should get back the original expiration from the token itself
}
@Test
public void testTokenRenewal_Disabled() throws Exception {
Map.Entry<TestTokenStateService, Response> result = doTestTokenRenewal(false, null, null, null);
// Make sure the expiration was not recorded by the TokenStateService, since it is disabled for this test
TestTokenStateService tss = result.getKey();
assertEquals("TokenStateService should be disabled for this test.", 0, tss.expirationData.size());
Response renewalResponse = result.getValue();
validateSuccessfulRenewalResponse(renewalResponse);
String responseContent = (String) renewalResponse.getEntity();
assertNotNull(responseContent);
Map<String, Object> json = parseJSONResponse(responseContent);
assertTrue(Boolean.parseBoolean((String)json.get("renewed")));
assertNotNull(json.get("expires")); // Should get back the original expiration from the token itself
}
@Test
public void testTokenRenewal_Enabled_NoRenewersNoSubject() throws Exception {
Response renewalResponse = doTestTokenRenewal(true, null, null);
validateRenewalResponse(renewalResponse, 403, false, "Caller (null) not authorized to renew tokens.", TokenResource.ErrorCode.UNAUTHORIZED);
}
@Test
public void testTokenRenewal_Enabled_NoRenewersWithSubject() throws Exception {
final String caller = "yarn";
Response renewalResponse = doTestTokenRenewal(true, null, createTestSubject(caller));
validateRenewalResponse(renewalResponse,
403,
false,
"Caller (" + caller + ") not authorized to renew tokens.", TokenResource.ErrorCode.UNAUTHORIZED);
}
@Test
public void testTokenRenewal_Enabled_WithRenewersNoSubject() throws Exception {
Response renewalResponse = doTestTokenRenewal(true, "larry, moe, curly ", null);
validateRenewalResponse(renewalResponse,
403,
false,
"Caller (null) not authorized to renew tokens.", TokenResource.ErrorCode.UNAUTHORIZED);
}
@Test
public void testTokenRenewal_Enabled_WithRenewersWithInvalidSubject() throws Exception {
final String caller = "shemp";
Response renewalResponse = doTestTokenRenewal(true, "larry, moe, curly ", createTestSubject(caller));
validateRenewalResponse(renewalResponse,
403,
false,
"Caller (" + caller + ") not authorized to renew tokens.", TokenResource.ErrorCode.UNAUTHORIZED);
}
@Test
public void testTokenRenewal_Enabled_WithRenewersWithValidSubject() throws Exception {
final String caller = "shemp";
Response renewalResponse =
doTestTokenRenewal(true, ("larry, moe, curly ," + caller), createTestSubject(caller));
validateSuccessfulRenewalResponse(renewalResponse);
}
@Test
public void testTokenRenewal_Enabled_WithDefaultMaxTokenLifetime() throws Exception {
final String caller = "yarn";
// Max lifetime duration is 10ms
Map.Entry<TestTokenStateService, Response> testResult =
doTestTokenRenewal(true, caller, null, createTestSubject(caller));
TestTokenStateService tss = testResult.getKey();
assertEquals(1, tss.issueTimes.size());
String token = tss.issueTimes.keySet().iterator().next();
// Verify that the configured max lifetime was honored
assertEquals(tss.getDefaultMaxLifetimeDuration(), tss.getMaxLifetime(token) - tss.getIssueTime(token));
}
@Test
public void testTokenRenewal_Enabled_WithConfigurableMaxTokenLifetime() throws Exception {
final String caller = "yarn";
// Max lifetime duration is 10ms
Map.Entry<TestTokenStateService, Response> testResult =
doTestTokenRenewal(true, caller, 10L, createTestSubject(caller));
TestTokenStateService tss = testResult.getKey();
assertEquals(1, tss.issueTimes.size());
String token = tss.issueTimes.keySet().iterator().next();
// Verify that the configured max lifetime was honored
assertEquals(10L, tss.getMaxLifetime(token) - tss.getIssueTime(token));
}
@Test
public void testTokenRevocation_ServerManagedStateNotConfigured() throws Exception {
Response renewalResponse = doTestTokenRevocation(null, null, null);
validateRevocationResponse(renewalResponse,
400,
false,
"Token revocation support is not configured", TokenResource.ErrorCode.CONFIGURATION_ERROR);
}
@Test
public void testTokenRevocation_Disabled() throws Exception {
Response renewalResponse = doTestTokenRevocation(false, null, null);
validateRevocationResponse(renewalResponse,
400,
false,
"Token revocation support is not configured", TokenResource.ErrorCode.CONFIGURATION_ERROR);
}
@Test
public void testTokenRevocation_Enabled_NoRenewersNoSubject() throws Exception {
Response renewalResponse = doTestTokenRevocation(true, null, null);
validateRevocationResponse(renewalResponse,
403,
false,
"Caller (null) not authorized to revoke tokens.", TokenResource.ErrorCode.UNAUTHORIZED);
}
@Test
public void testTokenRevocation_Enabled_NoRenewersWithSubject() throws Exception {
final String caller = "yarn";
Response renewalResponse = doTestTokenRevocation(true, null, createTestSubject(caller));
validateRevocationResponse(renewalResponse,
403,
false,
"Caller (" + caller + ") not authorized to revoke tokens.", TokenResource.ErrorCode.UNAUTHORIZED);
}
@Test
public void testTokenRevocation_Enabled_WithRenewersNoSubject() throws Exception {
Response renewalResponse = doTestTokenRevocation(true, "larry, moe, curly ", null);
validateRevocationResponse(renewalResponse,
403,
false,
"Caller (null) not authorized to revoke tokens.", TokenResource.ErrorCode.UNAUTHORIZED);
}
@Test
public void testTokenRevocation_Enabled_WithRenewersWithInvalidSubject() throws Exception {
final String caller = "shemp";
Response renewalResponse = doTestTokenRevocation(true, "larry, moe, curly ", createTestSubject(caller));
validateRevocationResponse(renewalResponse,
403,
false,
"Caller (" + caller + ") not authorized to revoke tokens.", TokenResource.ErrorCode.UNAUTHORIZED);
}
@Test
public void testTokenRevocation_Enabled_WithRenewersWithValidSubject() throws Exception {
final String caller = "shemp";
Response renewalResponse =
doTestTokenRevocation(true, ("larry, moe, curly ," + caller), createTestSubject(caller));
validateSuccessfulRevocationResponse(renewalResponse);
}
@Test
public void testTokenRevocation_Enabled_RevokeOwnToken() throws Exception {
final Response renewalResponse = doTestTokenRevocation(true, null, createTestSubject(USER_NAME));
validateSuccessfulRevocationResponse(renewalResponse);
}
@Test
public void testKidJkuClaims() throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put("knox.token.ttl", "60000");
configureCommonExpectations(contextExpectations);
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
// Issue a token
Response retResponse = tr.doGet();
assertEquals(200, retResponse.getStatus());
// Parse the response
final String retString = retResponse.getEntity().toString();
final String accessToken = getTagValue(retString, "access_token");
assertNotNull(accessToken);
// Verify the token
final JWT parsedToken = new JWTToken(accessToken);
assertEquals("alice", parsedToken.getSubject());
assertTrue(authority.verifyToken(parsedToken));
assertNotNull(parsedToken.getClaim("kid"));
assertEquals(TOKEN_API_PATH+JKWS_PATH, parsedToken.getClaim("jku"));
}
@Test
public void testGetTokenStateStatusTokenStateServiceNotEnabled() throws Exception {
testGetTokenStateStatus(Collections.singletonMap(TokenStateService.CONFIG_SERVER_MANAGED, "false"), "false", null, null, null);
}
@Test
public void testGetTokenStateStatusTokenStateServiceConfiguredProperly() throws Exception {
final Map<String, String> expectations = new HashMap<>();
expectations.put(TokenStateService.CONFIG_SERVER_MANAGED, "true");
expectations.put(ServiceType.TOKEN_STATE_SERVICE.getShortName(), TestTokenStateService.class.getCanonicalName());
testGetTokenStateStatus(expectations, "true", "TestTokenStateService", "TestTokenStateService", "false");
}
@Test
public void testGetTokenStateStatusTokenStateServiceIsMisconfigured() throws Exception {
final Map<String, String> expectations = new HashMap<>();
expectations.put(TokenStateService.CONFIG_SERVER_MANAGED, "true");
expectations.put(ServiceType.TOKEN_STATE_SERVICE.getShortName(), JDBCTokenStateService.class.getCanonicalName());
testGetTokenStateStatus(expectations, "true", "JDBCTokenStateService", "TestTokenStateService", "false");
}
private void testGetTokenStateStatus(Map<String, String> expectations, String expectedTokenManagementFlag, String expectedConfiguredTssBackend, String expectedActualTssBackend,
String expectedAllowedTssFlag) throws Exception {
configureCommonExpectations(expectations);
final TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
final Response response = tr.getTokenStateServiceStatus();
assertEquals(200, response.getStatus());
final String statusJson = response.getEntity().toString();
final Map<String, String> statusMap = JsonUtils.getMapFromJsonString(statusJson);
if (expectedTokenManagementFlag != null) {
assertEquals(statusMap.get("tokenManagementEnabled"), expectedTokenManagementFlag);
}
if (expectedConfiguredTssBackend != null) {
assertEquals(statusMap.get("configuredTssBackend"), expectedConfiguredTssBackend);
}
if (expectedActualTssBackend != null) {
assertEquals(statusMap.get("actualTssBackend"), expectedActualTssBackend);
}
if (expectedAllowedTssFlag != null) {
assertEquals(statusMap.get("allowedTssForTokengen"), expectedAllowedTssFlag);
}
assertTrue(Boolean.parseBoolean(statusMap.get("lifespanInputEnabled")));
}
@Test
public void testGettingTokenWithLifespanLessThanConfiguredTTL() throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put("knox.token.ttl", "172800000"); // 2 days
contextExpectations.put(TokenResource.LIFESPAN, "P1DT0H0M"); // 1 day 0 hour 0 minute
configureCommonExpectations(contextExpectations);
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
// Issue a token
final Response retResponse = tr.doGet();
final String retString = retResponse.getEntity().toString();
String accessToken = getTagValue(retString, "access_token");
assertNotNull(accessToken);
// Verify the token
final JWT parsedToken = new JWTToken(accessToken);
assertTrue(authority.verifyToken(parsedToken));
final Date expiresDate = parsedToken.getExpiresDate();
final Calendar nowPlus23Hours = Calendar.getInstance(TimeZone.getDefault(), Locale.getDefault());
nowPlus23Hours.add(Calendar.HOUR_OF_DAY, 23);
final Calendar tomorrowPlus23Hours = Calendar.getInstance(TimeZone.getDefault(), Locale.getDefault());
tomorrowPlus23Hours.add(Calendar.HOUR_OF_DAY, 47);
final long oneHourInMills = 3600000L;
assertTrue(expiresDate.after(nowPlus23Hours.getTime()));
assertTrue(expiresDate.before(tomorrowPlus23Hours.getTime())); // make sure the supplied lifespan was used and not the configured TTL
assertTrue((expiresDate.getTime() - nowPlus23Hours.getTime().getTime()) < oneHourInMills);
}
@Test
public void testGettingTokenWithLifespanGreaterThanConfiguredTTL() throws Exception {
testGettingTokenWithConfiguredTTL("P1D");
}
@Test
public void testGettingTokenWithConfiguredTTLIfLifespanIsInvalid() throws Exception {
testGettingTokenWithConfiguredTTL("InvalidLifespanPattern");
}
private void testGettingTokenWithConfiguredTTL(String lifespan) throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
final long oneMinute = 60000L;
contextExpectations.put("knox.token.ttl", String.valueOf(oneMinute));
contextExpectations.put(TokenResource.LIFESPAN, lifespan); // 1 day
configureCommonExpectations(contextExpectations);
final TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
// Issue a token
final Response retResponse = tr.doGet();
final String retString = retResponse.getEntity().toString();
String accessToken = getTagValue(retString, "access_token");
assertNotNull(accessToken);
// Verify the token
final JWT parsedToken = new JWTToken(accessToken);
assertTrue(authority.verifyToken(parsedToken));
final Date expiresDate = parsedToken.getExpiresDate();
final Date now = new Date();
assertTrue(expiresDate.after(now));
assertTrue((expiresDate.getTime() - now.getTime()) < oneMinute); // the configured TTL was used even if lifespan was supplied
}
@Test
public void testConfiguredTokenLimitPerUser() throws Exception {
testLimitingTokensPerUser(KNOX_TOKEN_USER_LIMIT_DEFAULT, KNOX_TOKEN_USER_LIMIT_DEFAULT);
}
@Test
public void testUnlimitedTokensPerUser() throws Exception {
testLimitingTokensPerUser(-1, 100);
}
@Test
public void testTokenLimitChangeAfterAlreadyHavingTokens() throws Exception {
Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put(KNOX_TOKEN_USER_LIMIT, "-1");
configureCommonExpectations(contextExpectations, Boolean.TRUE);
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
// already have N tokens
int numberOfPreExistingTokens = 5;
for (int i = 0; i < numberOfPreExistingTokens; i++) {
tr.doGet();
}
Response getKnoxTokensResponse = getUserTokensResponse(tr);
Collection<String> tokens = ((Map<String, Collection<String>>) JsonUtils.getObjectFromJsonString(getKnoxTokensResponse.getEntity().toString()))
.get("tokens");
assertEquals(tokens.size(), numberOfPreExistingTokens);
// change the limit and try generate one more
contextExpectations.put(KNOX_TOKEN_USER_LIMIT, Integer.toString(numberOfPreExistingTokens -1));
configureCommonExpectations(contextExpectations, Boolean.TRUE);
tr.request = request;
tr.context = context;
tr.init();
Response response = tr.doGet();
assertTrue(response.getEntity().toString().contains("Unable to get token - token limit exceeded."));
}
private Response getUserTokensResponse(TokenResource tokenResource) {
return getUserTokensResponse(tokenResource, false);
}
private Response getUserTokensResponse(TokenResource tokenResource, boolean createdBy) {
final MultivaluedMap<String, String> queryParameters = new MultivaluedHashMap<>();
queryParameters.put(createdBy ? "createdBy" : "userName", Arrays.asList(USER_NAME));
final UriInfo uriInfo = EasyMock.createNiceMock(UriInfo.class);
EasyMock.expect(uriInfo.getQueryParameters()).andReturn(queryParameters).anyTimes();
EasyMock.replay(uriInfo);
return tokenResource.getUserTokens(uriInfo);
}
@Test
public void testTokenLimitPerUserExceeded() throws Exception {
try {
testLimitingTokensPerUser(10, 11);
fail("Exception should have been thrown");
} catch (Exception e) {
assertTrue(e.getMessage().contains("Unable to get token - token limit exceeded."));
}
}
@Test
public void testTokenLimitPerUserExceededShouldRevokeOldestToken() throws Exception {
try {
testLimitingTokensPerUser(10, 11, true);
} catch (Exception e) {
fail("Exception should NOT have been thrown");
}
}
private void testLimitingTokensPerUser(int configuredLimit, int numberOfTokens) throws Exception {
testLimitingTokensPerUser(configuredLimit, numberOfTokens, false);
}
private void testLimitingTokensPerUser(int configuredLimit, int numberOfTokens, boolean revokeOldestToken) throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put(KNOX_TOKEN_USER_LIMIT, String.valueOf(configuredLimit));
if (revokeOldestToken) {
contextExpectations.put(KNOX_TOKEN_USER_LIMIT_EXCEEDED_ACTION, TokenResource.UserLimitExceededAction.REMOVE_OLDEST.name());
}
configureCommonExpectations(contextExpectations, Boolean.TRUE);
final TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
for (int i = 0; i < numberOfTokens; i++) {
final Response getTokenResponse = Subject.doAs(createTestSubject(USER_NAME), (PrivilegedAction<Response>) () -> tr.doGet());
if (getTokenResponse.getStatus() != Response.Status.OK.getStatusCode()) {
throw new Exception(getTokenResponse.getEntity().toString());
}
}
final Response getKnoxTokensResponse = getUserTokensResponse(tr);
final Collection<String> tokens = ((Map<String, Collection<String>>) JsonUtils.getObjectFromJsonString(getKnoxTokensResponse.getEntity().toString()))
.get("tokens");
assertEquals(tokens.size(), revokeOldestToken ? configuredLimit : numberOfTokens);
}
@Test
public void testCreateImpersonatedToken() throws Exception {
testCreateImpersonatedToken(true);
}
@Test
public void testImpersonationDisabled() throws Exception {
testCreateImpersonatedToken(false);
}
private void testCreateImpersonatedToken(boolean enableImpersonation) throws Exception {
final String impersonatedUser = "testUser";
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put(TokenResource.QUERY_PARAMETER_DOAS, impersonatedUser);
contextExpectations.put(TokenResource.PROXYUSER_PREFIX + "." + USER_NAME + ".users", impersonatedUser);
contextExpectations.put(TokenResource.PROXYUSER_PREFIX + "." + USER_NAME + ".hosts", "*");
contextExpectations.put(TokenResource.IMPERSONATION_ENABLED_PARAM, Boolean.toString(enableImpersonation));
configureCommonExpectations(contextExpectations, Boolean.TRUE);
final TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
tr.doGet();
final Response getKnoxTokensResponse = getUserTokensResponse(tr, enableImpersonation);
final Collection<LinkedHashMap<String, Object>> tokens = ((Map<String, Collection<LinkedHashMap<String, Object>>>) JsonUtils
.getObjectFromJsonString(getKnoxTokensResponse.getEntity().toString())).get("tokens");
final LinkedHashMap<String, Object> knoxToken = tokens.iterator().next();
final Map<String, String> metadata = (Map<String, String>) knoxToken.get("metadata");
if (enableImpersonation) {
assertEquals(metadata.get("createdBy"), USER_NAME);
assertEquals(metadata.get("userName"), impersonatedUser);
} else {
assertNull(metadata.get("createdBy"));
assertEquals(USER_NAME, metadata.get("userName"));
}
}
@Test
public void testDefaultIssuer() throws Exception {
Map<String, String> contextExpectations = new HashMap<>();
configureCommonExpectations(contextExpectations, Boolean.TRUE);
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
Response response = tr.doGet();
assertEquals(200, response.getStatus());
String accessToken = getTagValue(response.getEntity().toString(), "access_token");
Map<String, Object> payload = parseJSONResponse(JWTToken.parseToken(accessToken).getPayload());
assertEquals(DEFAULT_ISSUER, payload.get("iss"));
}
@Test
public void testConfiguredIssuer() throws Exception {
Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put(KNOX_TOKEN_ISSUER, "test issuer");
configureCommonExpectations(contextExpectations, Boolean.TRUE);
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
Response response = tr.doGet();
assertEquals(200, response.getStatus());
String accessToken = getTagValue(response.getEntity().toString(), "access_token");
Map<String, Object> payload = parseJSONResponse(JWTToken.parseToken(accessToken).getPayload());
assertEquals("test issuer", payload.get("iss"));
}
@Test
public void testGroupsAddedToToken() throws Exception {
Set<String> groups = new HashSet<>(Arrays.asList("group1", "group2"));
Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put(TOKEN_INCLUDE_GROUPS_IN_JWT_ALLOWED, "true");
contextExpectations.put(TokenResource.KNOX_TOKEN_INCLUDE_GROUPS, "true");
configureCommonExpectations(contextExpectations, Boolean.TRUE);
TokenResource tr = new TokenResource() {
@Override
protected Set<String> groups() {
return groups;
}
};
tr.request = request;
tr.context = context;
tr.init();
Response response = tr.doGet();
assertEquals(200, response.getStatus());
String accessToken = getTagValue(response.getEntity().toString(), "access_token");
Map<String, Object> payload = parseJSONResponse(JWTToken.parseToken(accessToken).getPayload());
assertEquals(new ArrayList<>(groups), payload.get(KNOX_GROUPS_CLAIM));
}
@Test
public void testNoGroupsAddedToTokenByDefault() throws Exception {
Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put(TOKEN_INCLUDE_GROUPS_IN_JWT_ALLOWED, "true");
configureCommonExpectations(contextExpectations, Boolean.TRUE);
TokenResource tr = new TokenResource() {
@Override
protected Set<String> groups() {
return new HashSet<>(Arrays.asList("group1", "group2"));
}
};
tr.request = request;
tr.context = context;
tr.init();
Response response = tr.doGet();
assertEquals(200, response.getStatus());
String accessToken = getTagValue(response.getEntity().toString(), "access_token");
Map<String, Object> payload = parseJSONResponse(JWTToken.parseToken(accessToken).getPayload());
assertFalse(payload.containsKey(KNOX_GROUPS_CLAIM));
}
@Test
public void testBadRequestWhenGroupsAreRequestedToBeIncludedInTokenButItIsDisabledByServer() throws Exception {
Set<String> groups = new HashSet<>(Arrays.asList("group1", "group2"));
Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put(TOKEN_INCLUDE_GROUPS_IN_JWT_ALLOWED, "false");
contextExpectations.put(TokenResource.KNOX_TOKEN_INCLUDE_GROUPS, "true");
configureCommonExpectations(contextExpectations, Boolean.TRUE);
TokenResource tr = new TokenResource() {
@Override
protected Set<String> groups() {
return groups;
}
};
tr.request = request;
tr.context = context;
tr.init();
Response response = tr.doGet();
assertEquals(400, response.getStatus());
}
@Test
public void passcodeShouldNotBeInResponseIfTokenStateServiceIsDisabled() throws Exception {
testPasscodeToken(false, false, false);
}
@Test
public void passcodeShouldNotBeInResponseIfTokenStateServiceIsNotPersistent() throws Exception {
testPasscodeToken(true, false, false);
}
@Test
public void passcodeShouldBeInResponseIfTokenStateServiceIsEnabledAndPersistent() throws Exception {
testPasscodeToken(true, true, true);
}
private void testPasscodeToken(boolean serverManagedTssEnabled, boolean usePersistentTokenStore, boolean expectPasscodeInResponse) throws Exception {
try {
if (usePersistentTokenStore) {
tss = new PersistentTestTokenStateService();
}
configureCommonExpectations(new HashMap<>(), serverManagedTssEnabled);
final TokenResource tr = new TokenResource();
tr.context = context;
tr.request = request;
tr.init();
// Issue a token
final Response response = tr.doGet();
assertEquals(200, response.getStatus());
final String retString = response.getEntity().toString();
final String passcode = getTagValue(retString, TokenResource.PASSCODE);
if (expectPasscodeInResponse) {
assertNotNull(passcode);
} else {
assertNull(passcode);
}
} finally {
tss = new TestTokenStateService();
}
}
/**
*
* @param isTokenStateServerManaged true, if server-side token state management should be enabled; Otherwise, false or null.
* @param renewers A comma-delimited list of permitted renewer user names
* @param caller The user name making the request
*
* @return The Response from the token renewal request
*
* @throws Exception
*/
private Response doTestTokenRenewal(final Boolean isTokenStateServerManaged,
final String renewers,
final Subject caller) throws Exception {
return doTestTokenRenewal(isTokenStateServerManaged, renewers, null, caller).getValue();
}
/**
*
* @param isTokenStateServerManaged true, if server-side token state management should be enabled; Otherwise, false or null.
* @param renewers A comma-delimited list of permitted renewer user names
* @param maxTokenLifetime The maximum duration (milliseconds) for a token's lifetime
* @param caller The user name making the request
*
* @return The Response from the token renewal request
*
* @throws Exception
*/
private Map.Entry<TestTokenStateService, Response> doTestTokenRenewal(final Boolean isTokenStateServerManaged,
final String renewers,
final Long maxTokenLifetime,
final Subject caller) throws Exception {
return doTestTokenRenewal(isTokenStateServerManaged,
null,
renewers,
maxTokenLifetime,
caller);
}
/**
*
* @param serviceLevelConfig true, if server-side token state management should be enabled; Otherwise, false or null.
* @param gatewayLevelConfig true, if server-side token state management should be enabled; Otherwise, false or null.
* @param renewers A comma-delimited list of permitted renewer user names
* @param maxTokenLifetime The maximum duration (milliseconds) for a token's lifetime
* @param caller The user name making the request
*
* @return The Response from the token renewal request
*
* @throws Exception
*/
private Map.Entry<TestTokenStateService, Response> doTestTokenRenewal(final Boolean serviceLevelConfig,
final Boolean gatewayLevelConfig,
final String renewers,
final Long maxTokenLifetime,
final Subject caller) throws Exception {
return doTestTokenLifecyle(TokenLifecycleOperation.Renew,
serviceLevelConfig,
gatewayLevelConfig,
renewers,
maxTokenLifetime,
caller);
}
/**
*
* @param isTokenStateServerManaged true, if server-side token state management should be enabled; Otherwise, false or null.
* @param renewers A comma-delimited list of permitted renewer user names
* @param caller The user name making the request
*
* @return The Response from the token revocation request
*
* @throws Exception
*/
private Response doTestTokenRevocation(final Boolean isTokenStateServerManaged,
final String renewers,
final Subject caller) throws Exception {
return doTestTokenLifecyle(TokenLifecycleOperation.Revoke, isTokenStateServerManaged, renewers, caller);
}
/**
* @param operation A TokenLifecycleOperation
* @param serverManaged true, if server-side token state management should be enabled; Otherwise, false or null.
* @param renewers A comma-delimited list of permitted renewer user names
* @param caller The user name making the request
*
* @return The Response from the token revocation request
*
* @throws Exception
*/
private Response doTestTokenLifecyle(final TokenLifecycleOperation operation,
final Boolean serverManaged,
final String renewers,
final Subject caller) throws Exception {
return doTestTokenLifecyle(operation, serverManaged, renewers, null, caller).getValue();
}
/**
* @param operation A TokenLifecycleOperation
* @param serviceLevelConfig true, if server-side token state management should be enabled at the service level;
* Otherwise, false or null.
* @param renewers A comma-delimited list of permitted renewer user names
* @param maxTokenLifetime The maximum lifetime duration for a token.
* @param caller The user name making the request
*
* @return The Response from the token revocation request
*
* @throws Exception
*/
private Map.Entry<TestTokenStateService, Response> doTestTokenLifecyle(final TokenLifecycleOperation operation,
final Boolean serviceLevelConfig,
final String renewers,
final Long maxTokenLifetime,
final Subject caller) throws Exception {
return doTestTokenLifecyle(operation, serviceLevelConfig, null, renewers, maxTokenLifetime, caller);
}
/**
* @param operation A TokenLifecycleOperation
* @param serviceLevelConfig true, if server-side token state management should be enabled at the service level;
* Otherwise, false or null.
* @param gatewayLevelConfig true, if server-side token state management should be enabled at the gateway level;
* Otherwise, false or null.
* @param renewers A comma-delimited list of permitted renewer user names
* @param maxTokenLifetime The maximum lifetime duration for a token.
* @param caller The user name making the request
*
* @return The Response from the token revocation request
*
* @throws Exception
*/
private Map.Entry<TestTokenStateService, Response> doTestTokenLifecyle(final TokenLifecycleOperation operation,
final Boolean serviceLevelConfig,
final Boolean gatewayLevelConfig,
final String renewers,
final Long maxTokenLifetime,
final Subject caller) throws Exception {
final Map<String, String> contextExpectations = new HashMap<>();
contextExpectations.put("knox.token.audiences", "recipient1,recipient2");
contextExpectations.put("knox.token.ttl", String.valueOf(Long.MAX_VALUE));
if (serviceLevelConfig != null) {
contextExpectations.put("knox.token.exp.server-managed", String.valueOf(serviceLevelConfig));
if (maxTokenLifetime != null) {
contextExpectations.put("knox.token.exp.renew-interval", String.valueOf(maxTokenLifetime / 2));
contextExpectations.put("knox.token.exp.max-lifetime", maxTokenLifetime.toString());
}
}
contextExpectations.put("knox.token.renewer.whitelist", renewers);
configureCommonExpectations(contextExpectations, gatewayLevelConfig);
TokenResource tr = new TokenResource();
tr.request = request;
tr.context = context;
tr.init();
// Request a token
Response retResponse = tr.doGet();
assertEquals(200, retResponse.getStatus());
// Parse the response
String retString = retResponse.getEntity().toString();
String accessToken = getTagValue(retString, "access_token");
assertNotNull(accessToken);
Response response;
switch (operation) {
case Renew:
response = requestTokenRenewal(tr, accessToken, caller);
break;
case Revoke:
response = requestTokenRevocation(tr, accessToken, caller);
break;
default:
throw new Exception("Invalid operation: " + operation);
}
return new AbstractMap.SimpleEntry<>(tss, response);
}
private static Response requestTokenRenewal(final TokenResource tr, final String tokenData, final Subject caller) {
Response response;
if (caller != null) {
response = Subject.doAs(caller, (PrivilegedAction<Response>) () -> tr.renew(tokenData));
} else {
response = tr.renew(tokenData);
}
return response;
}
private static Response requestTokenRevocation(final TokenResource tr, final String tokenData, final Subject caller) {
Response response;
if (caller != null) {
response = Subject.doAs(caller, (PrivilegedAction<Response>) () -> tr.revoke(tokenData));
} else {
response = tr.revoke(tokenData);
}
return response;
}
private static void validateSuccessfulRenewalResponse(final Response response) throws IOException {
validateRenewalResponse(response, 200, true, null, null);
}
private static void validateRenewalResponse(final Response response,
final int expectedStatusCode,
final boolean expectedResult,
final String expectedMessage,
final TokenResource.ErrorCode expectedCode) throws IOException {
assertEquals(expectedStatusCode, response.getStatus());
assertTrue(response.hasEntity());
String responseContent = (String) response.getEntity();
assertNotNull(responseContent);
assertFalse(responseContent.isEmpty());
Map<String, Object> json = parseJSONResponse(responseContent);
boolean result = Boolean.valueOf((String)json.get("renewed"));
assertEquals(expectedResult, result);
assertEquals(expectedMessage, json.get("error"));
if (expectedCode != null) {
assertEquals(expectedCode.toInt(), json.get("code"));
}
}
private static void validateSuccessfulRevocationResponse(final Response response) throws IOException {
validateRevocationResponse(response, 200, true, null, null);
}
private static void validateRevocationResponse(final Response response,
final int expectedStatusCode,
final boolean expectedResult,
final String expectedMessage,
final TokenResource.ErrorCode expectedCode) throws IOException {
assertEquals(expectedStatusCode, response.getStatus());
assertTrue(response.hasEntity());
String responseContent = (String) response.getEntity();
assertNotNull(responseContent);
assertFalse(responseContent.isEmpty());
Map<String, Object> json = parseJSONResponse(responseContent);
boolean result = Boolean.valueOf((String)json.get("revoked"));
assertEquals(expectedResult, result);
assertEquals(expectedMessage, json.get("error"));
if (expectedCode != null) {
assertEquals(expectedCode.toInt(), json.get("code"));
}
}
private String getTagValue(String token, String tagName) {
if (!token.contains(tagName)) {
return null;
}
String searchString = tagName + "\":";
String value = token.substring(token.indexOf(searchString) + searchString.length());
if (value.startsWith("\"")) {
value = value.substring(1);
}
if (value.contains("\"")) {
return value.substring(0, value.indexOf('\"'));
} else if (value.contains(",")) {
return value.substring(0, value.indexOf(','));
} else {
return value.substring(0, value.length() - 1);
}
}
/**
* Create a Subject for testing.
*
* @param username The user identifier
*
* @return A Subject
*/
private Subject createTestSubject(final String username) {
Subject s = new Subject();
Set<Principal> principals = s.getPrincipals();
principals.add(new PrimaryPrincipal(username));
return s;
}
private static Map<String, Object> parseJSONResponse(final String response) throws IOException {
return (new ObjectMapper()).readValue(response, new TypeReference<Map<String, Object>>(){});
}
private static class TestTokenStateService implements TokenStateService {
private Map<String, Long> expirationData = new HashMap<>();
private Map<String, Long> issueTimes = new HashMap<>();
private Map<String, Long> maxLifetimes = new HashMap<>();
private final Map<String, TokenMetadata> tokenMetadata = new ConcurrentHashMap<>();
long getIssueTime(final String token) {
return issueTimes.get(token);
}
long getMaxLifetime(final String token) {
return maxLifetimes.get(token);
}
@Override
public long getTokenIssueTime(String tokenId) throws UnknownTokenException {
return issueTimes.getOrDefault(tokenId, 0L);
}
@Override
public void addToken(JWTToken token, long issueTime) {
addToken(TokenUtils.getTokenId(token), issueTime, token.getExpiresDate().getTime());
}
@Override
public long getDefaultRenewInterval() {
return 250;
}
@Override
public long getDefaultMaxLifetimeDuration() {
return 500;
}
@Override
public void addToken(String tokenId, long issueTime, long expiration) {
addToken(tokenId, issueTime, expiration, getDefaultMaxLifetimeDuration());
}
@Override
public void addToken(String tokenId, long issueTime, long expiration, long maxLifetimeDuration) {
issueTimes.put(tokenId, issueTime);
expirationData.put(tokenId, expiration);
maxLifetimes.put(tokenId, issueTime + maxLifetimeDuration);
}
@Override
public boolean isExpired(JWTToken token) {
return false;
}
@Override
public void revokeToken(JWTToken token) {
revokeToken(TokenUtils.getTokenId(token));
}
@Override
public void revokeToken(String tokenId) {
issueTimes.remove(tokenId);
expirationData.remove(tokenId);
maxLifetimes.remove(tokenId);
tokenMetadata.remove(tokenId);
}
@Override
public long renewToken(JWTToken token) {
return renewToken(TokenUtils.getTokenId(token));
}
@Override
public long renewToken(String tokenId) {
return renewToken(tokenId, 0L);
}
@Override
public long renewToken(JWTToken token, long renewInterval) {
return renewToken(TokenUtils.getTokenId(token), renewInterval);
}
@Override
public long renewToken(String tokenId, long renewInterval) {
return 0;
}
@Override
public long getTokenExpiration(JWT token) throws UnknownTokenException {
return 0;
}
@Override
public long getTokenExpiration(String tokenId) {
return expirationData.getOrDefault(tokenId, 0L);
}
@Override
public long getTokenExpiration(String tokenId, boolean validate) throws UnknownTokenException {
return 0;
}
@Override
public void addMetadata(String tokenId, TokenMetadata metadata) {
tokenMetadata.put(tokenId, metadata);
}
@Override
public TokenMetadata getTokenMetadata(String tokenId) throws UnknownTokenException {
return tokenMetadata.get(tokenId);
}
@Override
public Collection<KnoxToken> getTokens(String userName) {
return fetchTokens(userName, false);
}
@Override
public Collection<KnoxToken> getDoAsTokens(String createdBy) {
return fetchTokens(createdBy, true);
}
private Collection<KnoxToken> fetchTokens(String userName, boolean createdBy) {
final Collection<KnoxToken> tokens = new TreeSet<>();
final Predicate<Map.Entry<String, TokenMetadata>> filterPredicate;
if (createdBy) {
filterPredicate = entry -> userName.equals(entry.getValue().getCreatedBy());
} else {
filterPredicate = entry -> userName.equals(entry.getValue().getUserName());
}
tokenMetadata.entrySet().stream().filter(filterPredicate).forEach(metadata -> {
String tokenId = metadata.getKey();
try {
tokens.add(new KnoxToken(tokenId, getTokenIssueTime(tokenId), getTokenExpiration(tokenId), getMaxLifetime(tokenId), metadata.getValue()));
} catch (UnknownTokenException e) {
// NOP
}
});
return tokens;
}
@Override
public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
}
@Override
public void start() throws ServiceLifecycleException {
}
@Override
public void stop() throws ServiceLifecycleException {
}
}
private static class PersistentTestTokenStateService extends TestTokenStateService implements PersistentTokenStateService {
}
private static class TestJWTokenAuthority implements JWTokenAuthority {
private RSAPublicKey publicKey;
private RSAPrivateKey privateKey;
TestJWTokenAuthority(RSAPublicKey publicKey, RSAPrivateKey privateKey) {
this.publicKey = publicKey;
this.privateKey = privateKey;
}
@Override
public boolean verifyToken(JWT token) {
JWSVerifier verifier = new RSASSAVerifier(publicKey);
return token.verify(verifier);
}
@Override
public JWT issueToken(JWTokenAttributes jwtAttributes) {
String[] claimArray = new String[6];
claimArray[0] = "KNOXSSO";
claimArray[1] = jwtAttributes.getUserName();
claimArray[2] = null;
if (jwtAttributes.getExpires() == -1) {
claimArray[3] = null;
} else {
claimArray[3] = String.valueOf(jwtAttributes.getExpires());
}
claimArray[4] = "E0LDZulQ0XE_otJ5aoQtQu-RnXv8hU-M9U4dD7vDioA";
claimArray[5] = jwtAttributes.getJku();
jwtAttributes.setKid("E0LDZulQ0XE_otJ5aoQtQu-RnXv8hU-M9U4dD7vDioA");
JWT token = new JWTToken(jwtAttributes);
JWSSigner signer = new RSASSASigner(privateKey);
token.sign(signer);
return token;
}
@Override
public boolean verifyToken(JWT token, RSAPublicKey publicKey) {
JWSVerifier verifier = new RSASSAVerifier(publicKey);
return token.verify(verifier);
}
@Override
public boolean verifyToken(JWT token, String jwksurl, String algorithm, Set<JOSEObjectType> allowedJwsTypes) {
return false;
}
}
}