| /* |
| * |
| * * 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.provider.federation; |
| |
| import static org.junit.Assert.fail; |
| |
| import java.nio.charset.StandardCharsets; |
| import java.text.ParseException; |
| import java.time.Instant; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.TreeSet; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.TimeUnit; |
| |
| import javax.servlet.ServletException; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.apache.commons.codec.binary.Base64; |
| import org.apache.commons.codec.digest.HmacAlgorithms; |
| import org.apache.knox.gateway.config.GatewayConfig; |
| import org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter; |
| import org.apache.knox.gateway.provider.federation.jwt.filter.JWTFederationFilter; |
| import org.apache.knox.gateway.services.ServiceLifecycleException; |
| import org.apache.knox.gateway.services.security.token.KnoxToken; |
| 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.easymock.EasyMock; |
| import org.junit.Assert; |
| import org.junit.Test; |
| |
| import com.nimbusds.jwt.SignedJWT; |
| |
| @SuppressWarnings({"PMD.JUnit4TestShouldUseBeforeAnnotation", "PMD.JUnit4TestShouldUseTestAnnotation"}) |
| public class TokenIDAsHTTPBasicCredsFederationFilterTest extends JWTAsHTTPBasicCredsFederationFilterTest { |
| |
| TestTokenStateService tss; |
| TokenMAC tokenMAC; |
| |
| @Override |
| public void setUp() throws Exception { |
| super.setUp(); |
| tss = new TestTokenStateService(); |
| ((TestJWTFederationFilter) handler).setTokenStateService(tss); |
| tokenMAC = new TokenMAC(HmacAlgorithms.HMAC_SHA_256.getName(), "sPj8FCgQhCEi6G18kBfpswxYSki33plbelGLs0hMSbk".toCharArray()); |
| ((TestJWTFederationFilter) handler).setTokenMac(tokenMAC); |
| } |
| |
| @Override |
| protected void setTokenOnRequest(final HttpServletRequest request, final SignedJWT jwt) { |
| try { |
| final long issueTime = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(5); |
| final String subject = (String) jwt.getJWTClaimsSet().getClaim(JWTToken.SUBJECT); |
| final String passcode = (String) jwt.getJWTClaimsSet().getClaims().get(PASSCODE_CLAIM); |
| addTokenState(jwt, issueTime, subject, passcode); |
| setTokenOnRequest(request, TestJWTFederationFilter.PASSCODE, generatePasscodeField(getTokenId(jwt), passcode)); |
| } catch(ParseException e) { |
| Assert.fail(e.getMessage()); |
| } |
| } |
| |
| private String generatePasscodeField(String tokenId, String passcode) { |
| final String base64TokenIdPasscode = Base64.encodeBase64String(tokenId.getBytes(StandardCharsets.UTF_8)) + "::" + Base64.encodeBase64String(passcode.getBytes(StandardCharsets.UTF_8)); |
| return Base64.encodeBase64String(base64TokenIdPasscode.getBytes(StandardCharsets.UTF_8)); |
| } |
| |
| /** |
| * Bind the specified JWT to the specified request, and apply the specified authUsername as the HTTP Basic |
| * username in the Authorization header value. |
| * |
| * @param request The request to which the JWT should be bound. |
| * @param jwt The JWT |
| * @param authUsername The HTTP Basic auth username to apply in the Authorization header value. |
| */ |
| protected void setTokenOnRequest(final HttpServletRequest request, |
| final SignedJWT jwt, |
| final String authUsername) { |
| try { |
| final long issueTime = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(5); |
| final String subject = (String) jwt.getJWTClaimsSet().getClaim(JWTToken.SUBJECT); |
| final String passcode = (String) jwt.getJWTClaimsSet().getClaims().get(PASSCODE_CLAIM); |
| addTokenState(jwt, issueTime, subject, passcode); |
| setTokenOnRequest(request, authUsername, generatePasscodeField(getTokenId(jwt), passcode)); |
| } catch(ParseException e) { |
| Assert.fail(e.getMessage()); |
| } |
| } |
| |
| @Override |
| protected void setGarbledTokenOnRequest(final HttpServletRequest request, final SignedJWT jwt) { |
| setTokenOnRequest(request, TestJWTFederationFilter.PASSCODE, "junk" + getTokenId(jwt)); |
| } |
| |
| private String getTokenId(final SignedJWT jwt) { |
| String tokenId = null; |
| try { |
| tokenId = (String) jwt.getJWTClaimsSet().getClaim(JWTToken.KNOX_ID_CLAIM); |
| } catch (ParseException e) { |
| // |
| } |
| return tokenId; |
| } |
| |
| private void addTokenState(final SignedJWT jwt, long issueTime, String subject, String passcode) { |
| try { |
| JWTToken token = new JWTToken(jwt.serialize()); |
| tss.addToken(token, issueTime); |
| |
| final TokenMetadata metadata = new TokenMetadata(subject); |
| metadata.setPasscode(tokenMAC.hash(TokenUtils.getTokenId(token), issueTime, subject, passcode)); |
| tss.addMetadata(TokenUtils.getTokenId(token), metadata); |
| } catch (ParseException e) { |
| Assert.fail(e.getMessage()); |
| } |
| } |
| |
| @Override |
| public void testAlternativeCaseUsername() throws Exception { |
| try { |
| Properties props = getProperties(); |
| handler.init(new TestFilterConfig(props)); |
| |
| SignedJWT jwt = getJWT(AbstractJWTFilter.JWT_DEFAULT_ISSUER, "bob", |
| new Date(new Date().getTime() + 5000), privateKey); |
| |
| HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); |
| setTokenOnRequest(request, jwt, JWTFederationFilter.PASSCODE.toLowerCase(Locale.ROOT)); |
| |
| EasyMock.expect(request.getRequestURL()).andReturn(new StringBuffer(SERVICE_URL)).anyTimes(); |
| EasyMock.expect(request.getPathInfo()).andReturn("resource").anyTimes(); |
| EasyMock.expect(request.getQueryString()).andReturn(null); |
| HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); |
| EasyMock.expect(response.encodeRedirectURL(SERVICE_URL)).andReturn(SERVICE_URL); |
| EasyMock.expect(response.getOutputStream()).andAnswer(DummyServletOutputStream::new).anyTimes(); |
| EasyMock.replay(request, response); |
| |
| TestFilterChain chain = new TestFilterChain(); |
| handler.doFilter(request, response, chain); |
| Assert.assertTrue("doFilterCalled should be true.", chain.doFilterCalled ); |
| } catch (ServletException se) { |
| fail("Should NOT have thrown a ServletException."); |
| } |
| } |
| |
| @Test |
| public void testInvalidUsername() throws Exception { |
| try { |
| Properties props = getProperties(); |
| handler.init(new TestFilterConfig(props)); |
| |
| SignedJWT jwt = getJWT(AbstractJWTFilter.JWT_DEFAULT_ISSUER, "bob", |
| new Date(new Date().getTime() + 5000), privateKey); |
| |
| HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); |
| setTokenOnRequest(request, jwt, "InvalidBasicAuthUsername"); |
| |
| EasyMock.expect(request.getRequestURL()).andReturn(new StringBuffer(SERVICE_URL)).anyTimes(); |
| EasyMock.expect(request.getPathInfo()).andReturn("resource").anyTimes(); |
| EasyMock.expect(request.getQueryString()).andReturn(null); |
| HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); |
| EasyMock.expect(response.encodeRedirectURL(SERVICE_URL)).andReturn(SERVICE_URL); |
| EasyMock.expect(response.getOutputStream()).andAnswer(DummyServletOutputStream::new).anyTimes(); |
| EasyMock.replay(request, response); |
| |
| TestFilterChain chain = new TestFilterChain(); |
| handler.doFilter(request, response, chain); |
| Assert.assertFalse("doFilterCalled should be false.", chain.doFilterCalled ); |
| } catch (ServletException se) { |
| fail("Should NOT have thrown a ServletException."); |
| } |
| } |
| |
| /** |
| * Negative test, specifying the Basic auth username as the one used for JWTs while specifying |
| * the passcode as the password. |
| */ |
| @Test |
| public void testInvalidJWTForPasscode() throws Exception { |
| try { |
| Properties props = getProperties(); |
| handler.init(new TestFilterConfig(props)); |
| |
| SignedJWT jwt = getJWT(AbstractJWTFilter.JWT_DEFAULT_ISSUER, "bob", |
| new Date(new Date().getTime() + 5000), privateKey); |
| |
| HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); |
| setTokenOnRequest(request, jwt, JWTFederationFilter.TOKEN); |
| |
| EasyMock.expect(request.getRequestURL()).andReturn(new StringBuffer(SERVICE_URL)).anyTimes(); |
| EasyMock.expect(request.getPathInfo()).andReturn("resource").anyTimes(); |
| EasyMock.expect(request.getQueryString()).andReturn(null); |
| HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); |
| EasyMock.expect(response.encodeRedirectURL(SERVICE_URL)).andReturn(SERVICE_URL); |
| EasyMock.expect(response.getOutputStream()).andAnswer(DummyServletOutputStream::new).anyTimes(); |
| EasyMock.replay(request, response); |
| |
| TestFilterChain chain = new TestFilterChain(); |
| handler.doFilter(request, response, chain); |
| Assert.assertFalse("doFilterCalled should be false.", chain.doFilterCalled ); |
| } catch (ServletException se) { |
| fail("Should NOT have thrown a ServletException."); |
| } |
| } |
| |
| /** |
| * Negative test, specifying the Basic auth username as the one used for passcodes while specifying the JWT |
| * as the password. |
| */ |
| @Test |
| public void testInvalidPasscodeForJWT() throws Exception { |
| try { |
| Properties props = getProperties(); |
| handler.init(new TestFilterConfig(props)); |
| |
| SignedJWT jwt = getJWT(AbstractJWTFilter.JWT_DEFAULT_ISSUER, "bob", |
| new Date(new Date().getTime() + 5000), privateKey); |
| |
| HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); |
| setTokenOnRequest(request, JWTFederationFilter.PASSCODE, jwt.serialize()); |
| |
| EasyMock.expect(request.getRequestURL()).andReturn(new StringBuffer(SERVICE_URL)).anyTimes(); |
| EasyMock.expect(request.getPathInfo()).andReturn("resource").anyTimes(); |
| EasyMock.expect(request.getQueryString()).andReturn(null); |
| HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); |
| EasyMock.expect(response.encodeRedirectURL(SERVICE_URL)).andReturn(SERVICE_URL); |
| EasyMock.expect(response.getOutputStream()).andAnswer(DummyServletOutputStream::new).anyTimes(); |
| EasyMock.replay(request, response); |
| |
| TestFilterChain chain = new TestFilterChain(); |
| handler.doFilter(request, response, chain); |
| Assert.assertFalse("doFilterCalled should be false.", chain.doFilterCalled ); |
| Assert.assertNull("Subject should be null since authentication should have failed.", chain.subject); |
| } catch (ServletException se) { |
| fail("Should NOT have thrown a ServletException."); |
| } |
| } |
| |
| @Override |
| public void testJWTWithoutKnoxUUIDClaim() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testExpiredJWTWithoutKnoxUUIDClaim() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testInvalidAudienceJWT() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testNoTokenAudience() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testNoAudienceConfigured() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testEmptyAudienceConfigured() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testValidAudienceJWT() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testValidAudienceJWTWhitespace() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testValidIssuerViaConfig() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testInvalidIssuer() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testFailedSignatureValidationJWT() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testInvalidVerificationPEM() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testRS512SignatureAlgorithm() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testInvalidSignatureAlgorithm() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testValidVerificationPEM() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testNotBeforeJWT() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testExpiredTokensEvictedFromSignatureVerificationCache() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| @Override |
| public void testVerificationOptimization_NoTokenID() throws Exception { |
| // Override to disable N/A test |
| } |
| |
| /** |
| * Very basic TokenStateService implementation for these tests only |
| */ |
| private static class TestTokenStateService implements TokenStateService { |
| |
| private final Map<String, Long> tokenIssueTimes = new ConcurrentHashMap<>(); |
| private final Map<String, Long> tokenExpirations = new ConcurrentHashMap<>(); |
| private final Map<String, TokenMetadata> tokenMetadata = new ConcurrentHashMap<>(); |
| |
| @Override |
| public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException { |
| } |
| |
| @Override |
| public void start() throws ServiceLifecycleException { |
| } |
| |
| @Override |
| public void stop() throws ServiceLifecycleException { |
| } |
| |
| @Override |
| public long getDefaultRenewInterval() { |
| return TimeUnit.DAYS.toMillis(1); |
| } |
| |
| @Override |
| public long getDefaultMaxLifetimeDuration() { |
| return TimeUnit.DAYS.toMillis(7); |
| } |
| |
| @Override |
| public void addToken(JWTToken token, long issueTime) { |
| String expiration = token.getExpires(); |
| if (expiration == null || expiration.isEmpty()) { |
| expiration = "0"; |
| } |
| addToken(TokenUtils.getTokenId(token), issueTime, Long.parseLong(expiration)); |
| } |
| |
| private void addToken(String tokenId, long expiration) { |
| tokenExpirations.put(tokenId, expiration); |
| } |
| |
| @Override |
| public void addToken(String tokenId, long issueTime, long expiration) { |
| addToken(tokenId, expiration); |
| tokenIssueTimes.put(tokenId, issueTime); |
| } |
| |
| @Override |
| public void addToken(String tokenId, long issueTime, long expiration, long maxLifetimeDuration) { |
| addToken(tokenId, expiration); |
| tokenIssueTimes.put(tokenId, issueTime); |
| } |
| |
| @Override |
| public long getTokenIssueTime(String tokenId) throws UnknownTokenException { |
| return tokenIssueTimes.getOrDefault(tokenId, 0L); |
| } |
| |
| @Override |
| public boolean isExpired(JWTToken token) throws UnknownTokenException { |
| Long expiration; |
| String tokenId = TokenUtils.getTokenId(token); |
| if (tokenId != null) { |
| expiration = tokenExpirations.get(tokenId); |
| } else { |
| expiration = Long.parseLong(token.getExpires()); |
| } |
| |
| return Instant.ofEpochMilli(expiration).isBefore(Instant.now()); |
| } |
| |
| @Override |
| public void revokeToken(JWTToken token) throws UnknownTokenException { |
| |
| } |
| |
| @Override |
| public void revokeToken(String tokenId) throws UnknownTokenException { |
| |
| } |
| |
| @Override |
| public long renewToken(JWTToken token) throws UnknownTokenException { |
| return 0; |
| } |
| |
| @Override |
| public long renewToken(JWTToken token, long renewInterval) throws UnknownTokenException { |
| return 0; |
| } |
| |
| @Override |
| public long renewToken(String tokenId) throws UnknownTokenException { |
| return 0; |
| } |
| |
| @Override |
| public long renewToken(String tokenId, long renewInterval) throws UnknownTokenException { |
| return 0; |
| } |
| |
| @Override |
| public long getTokenExpiration(JWT token) throws UnknownTokenException { |
| return getTokenExpiration(TokenUtils.getTokenId(token)); |
| } |
| |
| @Override |
| public long getTokenExpiration(String tokenId) throws UnknownTokenException { |
| if (!tokenExpirations.containsKey(tokenId)) { |
| throw new UnknownTokenException(tokenId); |
| } |
| return tokenExpirations.get(tokenId); |
| } |
| |
| @Override |
| public long getTokenExpiration(String tokenId, boolean validate) throws UnknownTokenException { |
| return getTokenExpiration(tokenId); |
| } |
| |
| @Override |
| public void addMetadata(String tokenId, TokenMetadata metadata) { |
| tokenMetadata.put(tokenId, metadata); |
| } |
| |
| @Override |
| public TokenMetadata getTokenMetadata(String tokenId) throws UnknownTokenException { |
| if (!tokenMetadata.containsKey(tokenId)) { |
| throw new UnknownTokenException(tokenId); |
| } |
| return tokenMetadata.get(tokenId); |
| } |
| |
| @Override |
| public Collection<KnoxToken> getTokens(String userName) { |
| final Collection<KnoxToken> tokens = new TreeSet<>(); |
| tokenMetadata.entrySet().stream().filter(entry -> entry.getValue().getUserName().equals(userName)).forEach(metadata -> { |
| String tokenId = metadata.getKey(); |
| try { |
| tokens.add(new KnoxToken(tokenId, getTokenIssueTime(tokenId), getTokenExpiration(tokenId), 0L, metadata.getValue())); |
| } catch (UnknownTokenException e) { |
| // NOP |
| } |
| }); |
| return tokens; |
| } |
| } |
| } |