content: /*
   * 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.kafka.common.security.oauthbearer.internals.unsecured;
  
  import org.junit.jupiter.api.Test;
  
  import java.nio.charset.StandardCharsets;
  import java.util.Arrays;
  import java.util.Base64;
  import java.util.Base64.Encoder;
  import java.util.HashSet;
  import java.util.List;
  
  import static org.junit.jupiter.api.Assertions.assertEquals;
  import static org.junit.jupiter.api.Assertions.assertThrows;
  import static org.junit.jupiter.api.Assertions.assertTrue;
  
  public class OAuthBearerUnsecuredJwsTest {
      private static final String QUOTE = "\"";
      private static final String HEADER_COMPACT_SERIALIZATION = Base64.getUrlEncoder().withoutPadding()
              .encodeToString("{\"alg\":\"none\"}".getBytes(StandardCharsets.UTF_8)) + ".";
  
      @Test
      public void validClaims() throws OAuthBearerIllegalTokenException {
          double issuedAtSeconds = 100.1;
          double expirationTimeSeconds = 300.3;
          StringBuilder sb = new StringBuilder("{");
          appendJsonText(sb, "sub", "SUBJECT");
          appendCommaJsonText(sb, "iat", issuedAtSeconds);
          appendCommaJsonText(sb, "exp", expirationTimeSeconds);
          sb.append("}");
          String compactSerialization = HEADER_COMPACT_SERIALIZATION
                  + Base64.getUrlEncoder().withoutPadding().encodeToString(sb.toString().getBytes(StandardCharsets.UTF_8))
                  + ".";
          OAuthBearerUnsecuredJws testJwt = new OAuthBearerUnsecuredJws(compactSerialization, "sub", "scope");
          assertEquals(compactSerialization, testJwt.value());
          assertEquals("sub", testJwt.principalClaimName());
          assertEquals(1, testJwt.header().size());
          assertEquals("none", testJwt.header().get("alg"));
          assertEquals("scope", testJwt.scopeClaimName());
          assertEquals(expirationTimeSeconds, testJwt.expirationTime());
          assertTrue(testJwt.isClaimType("exp", Number.class));
          assertEquals(issuedAtSeconds, testJwt.issuedAt());
          assertEquals("SUBJECT", testJwt.subject());
      }
  
      @Test
      public void validCompactSerialization() {
          String subject = "foo";
          long issuedAt = 100;
          long expirationTime = issuedAt + 60 * 60;
          List<String> scope = Arrays.asList("scopeValue1", "scopeValue2");
          String validCompactSerialization = compactSerialization(subject, issuedAt, expirationTime, scope);
          OAuthBearerUnsecuredJws jws = new OAuthBearerUnsecuredJws(validCompactSerialization, "sub", "scope");
          assertEquals(1, jws.header().size());
          assertEquals("none", jws.header().get("alg"));
          assertEquals(4, jws.claims().size());
          assertEquals(subject, jws.claims().get("sub"));
          assertEquals(subject, jws.principalName());
          assertEquals(issuedAt, ((Number) jws.claims().get("iat")).longValue());
          assertEquals(expirationTime, ((Number) jws.claims().get("exp")).longValue());
          assertEquals(expirationTime * 1000, jws.lifetimeMs());
          assertEquals(scope, jws.claims().get("scope"));
          assertEquals(new HashSet<>(scope), jws.scope());
          assertEquals(3, jws.splits().size());
          assertEquals(validCompactSerialization.split("\\.")[0], jws.splits().get(0));
          assertEquals(validCompactSerialization.split("\\.")[1], jws.splits().get(1));
          assertEquals("", jws.splits().get(2));
      }
  
      @Test
      public void missingPrincipal() {
          String subject = null;
          long issuedAt = 100;
          Long expirationTime = null;
          List<String> scope = Arrays.asList("scopeValue1", "scopeValue2");
          String validCompactSerialization = compactSerialization(subject, issuedAt, expirationTime, scope);
          assertThrows(OAuthBearerIllegalTokenException.class,
              () -> new OAuthBearerUnsecuredJws(validCompactSerialization, "sub", "scope"));
      }
  
      @Test
      public void blankPrincipalName() {
          String subject = "   ";
          long issuedAt = 100;
          long expirationTime = issuedAt + 60 * 60;
          List<String> scope = Arrays.asList("scopeValue1", "scopeValue2");
          String validCompactSerialization = compactSerialization(subject, issuedAt, expirationTime, scope);
          assertThrows(OAuthBearerIllegalTokenException.class,
              () -> new OAuthBearerUnsecuredJws(validCompactSerialization, "sub", "scope"));
      }
  
      private static String compactSerialization(String subject, Long issuedAt, Long expirationTime, List<String> scope) {
          Encoder encoder = Base64.getUrlEncoder().withoutPadding();
          String algorithm = "none";
          String headerJson = "{\"alg\":\"" + algorithm + "\"}";
          String encodedHeader = encoder.encodeToString(headerJson.getBytes(StandardCharsets.UTF_8));
          String subjectJson = subject != null ? "\"sub\":\"" + subject + "\"" : null;
          String issuedAtJson = issuedAt != null ? "\"iat\":" + issuedAt : null;
          String expirationTimeJson = expirationTime != null ? "\"exp\":" + expirationTime : null;
          String scopeJson = scope != null ? scopeJson(scope) : null;
          String claimsJson = claimsJson(subjectJson, issuedAtJson, expirationTimeJson, scopeJson);
          String encodedClaims = encoder.encodeToString(claimsJson.getBytes(StandardCharsets.UTF_8));
          return encodedHeader + "." + encodedClaims + ".";
      }
  
      private static String claimsJson(String... jsonValues) {
          StringBuilder claimsJsonBuilder = new StringBuilder("{");
          int initialLength = claimsJsonBuilder.length();
          for (String jsonValue : jsonValues) {
              if (jsonValue != null) {
                  if (claimsJsonBuilder.length() > initialLength)
                      claimsJsonBuilder.append(',');
                  claimsJsonBuilder.append(jsonValue);
              }
          }
          claimsJsonBuilder.append('}');
          return claimsJsonBuilder.toString();
      }
  
      private static String scopeJson(List<String> scope) {
          StringBuilder scopeJsonBuilder = new StringBuilder("\"scope\":[");
          int initialLength = scopeJsonBuilder.length();
          for (String scopeValue : scope) {
              if (scopeJsonBuilder.length() > initialLength)
                  scopeJsonBuilder.append(',');
              scopeJsonBuilder.append('"').append(scopeValue).append('"');
          }
          scopeJsonBuilder.append(']');
          return scopeJsonBuilder.toString();
      }
  
      private static void appendCommaJsonText(StringBuilder sb, String claimName, Number claimValue) {
          sb.append(',').append(QUOTE).append(escape(claimName)).append(QUOTE).append(":").append(claimValue);
      }
  
      private static void appendJsonText(StringBuilder sb, String claimName, String claimValue) {
          sb.append(QUOTE).append(escape(claimName)).append(QUOTE).append(":").append(QUOTE).append(escape(claimValue))
                  .append(QUOTE);
      }
  
      private static String escape(String jsonStringValue) {
          return jsonStringValue.replace("\"", "\\\"").replace("\\", "\\\\");
      }
  }
  