JWTValidator class

Пакет: com.hypixel.hytale.server.core.auth

Файл: com/hypixel/hytale/server/core/auth/JWTValidator.java

Поля (73)

МодификаторыТипИмя
final HytaleLogger LOGGER
public String audience
break
public String certificateFingerprint
public String[] entitlements
public Long expiresAt
public Long expiresAt
public Long expiresAt
public Long issuedAt
public Long issuedAt
public Long issuedAt
public String issuer
public String issuer
public String issuer
public Long notBefore
public Long notBefore
public Long notBefore
public String scope
public String scope
public String skin
public String subject
public String subject
public String subject
public String username
public String username
SessionServiceClient.JwksResponse var1
Instant var1
JWKSet var1
Path var1
boolean var10
StringBuilder var10
String var11
long var12
String[] var2
CompletableFuture var2
ArrayList var2
JWKSet var2
String var2
JWKSet var2
String var2
String var2
JWTValidator.IdentityTokenClaims var2
String var2
String var2
int var3
String var3
String var3
JWKSet var3
JWKSet var3
SignedJWT var3
SignedJWT var3
long var3
SignedJWT var4
OctetKeyPair var4
var4
JWSAlgorithm var4
JWSAlgorithm var4
JWSAlgorithm var5
JWTClaimsSet var5
JWTClaimsSet var5
String var5
JWTClaimsSet var6
JWTValidator.IdentityTokenClaims var6
JWTValidator.SessionTokenClaims var6
JWTValidator.JWTClaims var7
JWK var7
Map var7
long var7
Map var8
long var8
long var9
Ed25519Verifier var9
JWKSet var9

Методы (54)

МодификаторыВозвратСигнатура
public IdentityTokenClaimspublic IdentityTokenClaims()
public JWTClaimspublic JWTClaims()
public SessionTokenClaimspublic SessionTokenClaims()
private TokenClaimsLogFormatterprivate TokenClaimsLogFormatter()
private boolean canForceRefreshJwksboolean canForceRefreshJwks()
for for(SessionServiceClient.JwkKey var6 : var1.keys)
static String formatstatic String format(@Nullable String var0, @Nullable String var1, @Nullable String var2, @Nullable String var3, @Nullable Long var4, @Nullable Long var5, @Nullable Long var6, @Nullable String var7)
private static String formatInstantprivate static String formatInstant(@Nullable Long var0)
private static String formatInstantWithDeltaprivate static String formatInstantWithDelta(@Nullable Long var0, long var1)
public String[] getScopespublic String[] getScopes()
public String[] getScopespublic String[] getScopes()
public UUID getSubjectAsUUIDpublic UUID getSubjectAsUUID()
public UUID getSubjectAsUUIDpublic UUID getSubjectAsUUID()
public UUID getSubjectAsUUIDpublic UUID getSubjectAsUUID()
public boolean hasScopepublic boolean hasScope(@Nonnull String var1)
public boolean hasScopepublic boolean hasScope(@Nonnull String var1)
if if(var2.length != 3)
if if(var3 != null)
if if(var8 != null)
if if(var7.expiresAt == null)
if if(var4 == null)
if if(var10)
if if(!var1 && this.cachedJwkSet != null)
if if(!var1 && this.cachedJwkSet != null)
if if(var1)
if if(var1 != null && var1.keys != null && var1.keys.length != 0)
if if(var7 != null)
if if(var2 == null)
if if(var1 != null)
if if(var2 != null)
if if(var1 == null)
if if(var2 == null)
if if(var2 != null)
if if(var7 != null)
if if(var6.expiresAt == null)
if if(var2 != null)
if if(var6.expiresAt == null)
if if(this.subject == null)
if if(this.subject == null)
if if(this.subject == null)
if if(var1 != null)
if if(var3 != null)
if if(var6 != null)
if if(var4 != null && var5 != null)
if if(var7 != null)
if if(var0 == null)
public void invalidateJwksCachevoid invalidateJwksCache()
private void preSeedJwksCachevoid preSeedJwksCache()
private void saveJwksCacheToDiskvoid saveJwksCacheToDisk(@Nonnull JWKSet var1)
public String toLogSummarypublic String toLogSummary()
public String toLogSummarypublic String toLogSummary()
public String toLogSummarypublic String toLogSummary()
private boolean verifySignatureboolean verifySignature(SignedJWT var1, JWKSet var2)
private boolean verifySignatureWithRetryboolean verifySignatureWithRetry(SignedJWT var1)

Исходный код

Показать/скрыть
class="kw">package com.hypixel.hytale.server.core.auth;

class="kw">import com.hypixel.hytale.logger.HytaleLogger;
class="kw">import com.nimbusds.jose.JWSAlgorithm;
class="kw">import com.nimbusds.jose.crypto.Ed25519Verifier;
class="kw">import com.nimbusds.jose.jwk.JWK;
class="kw">import com.nimbusds.jose.jwk.JWKSet;
class="kw">import com.nimbusds.jose.jwk.OctetKeyPair;
class="kw">import com.nimbusds.jwt.JWTClaimsSet;
class="kw">import com.nimbusds.jwt.SignedJWT;
class="kw">import java.io.IOException;
class="kw">import java.io.InputStream;
class="kw">import java.nio.charset.StandardCharsets;
class="kw">import java.nio.file.Files;
class="kw">import java.nio.file.Path;
class="kw">import java.security.cert.X509Certificate;
class="kw">import java.text.ParseException;
class="kw">import java.time.Duration;
class="kw">import java.time.Instant;
class="kw">import java.util.ArrayList;
class="kw">import java.util.List;
class="kw">import java.util.Map;
class="kw">import java.util.UUID;
class="kw">import java.util.concurrent.CompletableFuture;
class="kw">import java.util.concurrent.locks.ReentrantLock;
class="kw">import java.util.logging.Level;
class="kw">import javax.annotation.Nonnull;
class="kw">import javax.annotation.Nullable;

class="kw">public class JWTValidator {
   class="kw">private class="kw">static class="kw">final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
   class="kw">private class="kw">static class="kw">final long CLOCK_SKEW_SECONDS = 300L;
   class="kw">private class="kw">static class="kw">final JWSAlgorithm SUPPORTED_ALGORITHM = JWSAlgorithm.EdDSA;
   class="kw">private class="kw">static class="kw">final int MIN_SIGNATURE_LENGTH = 80;
   class="kw">private class="kw">static class="kw">final int MAX_SIGNATURE_LENGTH = 90;
   class="kw">private class="kw">static class="kw">final Duration JWKS_REFRESH_MIN_INTERVAL = Duration.ofMinutes(5L);
   class="kw">private class="kw">static class="kw">final String JWKS_BUNDLED_RESOURCE = "/jwks_bundled.json";
   class="kw">private class="kw">static class="kw">final String JWKS_CACHE_FILE = ".jwks.json";
   class="kw">private class="kw">final SessionServiceClient sessionServiceClient;
   class="kw">private class="kw">final String expectedIssuer;
   class="kw">private class="kw">final String expectedAudience;
   class="kw">private class="kw">volatile JWKSet cachedJwkSet;
   class="kw">private class="kw">final ReentrantLock jwksFetchLock = new ReentrantLock();
   class="kw">private class="kw">volatile CompletableFuture<JWKSet> pendingFetch = null;
   class="kw">private class="kw">volatile Instant lastJwksRefresh;

   class="kw">public JWTValidator(@Nonnull SessionServiceClient var1, @Nonnull String var2, @Nonnull String var3) {
      this.sessionServiceClient = var1;
      this.expectedIssuer = var2;
      this.expectedAudience = var3;
      this.preSeedJwksCache();
   }

   @Nullable
   class="kw">private class="kw">static String validateJwtStructure(@Nonnull String var0, @Nonnull String var1) {
      if (var0.isEmpty()) {
         class="kw">return var1 + " is empty";
      }

      String[] var2 = var0.split("\\.", -1);
      if (var2.length != 3) {
         class="kw">return String.format("%s has invalid format (expected 3 parts, got %d)", var1, var2.length);
      }

      if (var2[2].isEmpty()) {
         class="kw">return var1 + " has empty signature - possible signature stripping attack";
      }

      int var3 = var2[2].length();
      class="kw">return var3 >= 80 && var3 <= 90 ? null : String.format("%s has invalid signature length: %d (expected %d-%d)", var1, var3, 80, 90);
   }

   @Nullable
   class="kw">public JWTValidator.JWTClaims validateToken(@Nonnull String var1, @Nullable X509Certificate var2) {
      String var3 = validateJwtStructure(var1, "Access token");
      if (var3 != null) {
         LOGGER.at(Level.WARNING).log("%s", var3);
         class="kw">return null;
      }

      try {
         SignedJWT var4 = SignedJWT.parse(var1);
         JWSAlgorithm var5 = var4.getHeader().getAlgorithm();
         if (!SUPPORTED_ALGORITHM.equals(var5)) {
            LOGGER.at(Level.WARNING).log("Unsupported JWT algorithm: %s (expected EdDSA)", var5);
            class="kw">return null;
         }

         if (!this.verifySignatureWithRetry(var4)) {
            LOGGER.at(Level.WARNING).log("JWT signature verification failed");
            class="kw">return null;
         }

         JWTClaimsSet var6 = var4.getJWTClaimsSet();
         JWTValidator.JWTClaims var7 = new JWTValidator.JWTClaims();
         var7.issuer = var6.getIssuer();
         var7.audience = var6.getAudience() != null && !var6.getAudience().isEmpty() ? (String)var6.getAudience().get(0) : null;
         var7.subject = var6.getSubject();
         var7.username = var6.getStringClaim("username");
         var7.issuedAt = var6.getIssueTime() != null ? var6.getIssueTime().toInstant().getEpochSecond() : null;
         var7.expiresAt = var6.getExpirationTime() != null ? var6.getExpirationTime().toInstant().getEpochSecond() : null;
         var7.notBefore = var6.getNotBeforeTime() != null ? var6.getNotBeforeTime().toInstant().getEpochSecond() : null;
         Map var8 = var6.getJSONObjectClaim("cnf");
         if (var8 != null) {
            var7.certificateFingerprint = (String)var8.get("x5t#S256");
         }

         if (!this.expectedIssuer.equals(var7.issuer)) {
            LOGGER.at(Level.WARNING).log("Invalid issuer: expected %s, got %s. Token: %s", this.expectedIssuer, var7.issuer, var7.toLogSummary());
            class="kw">return null;
         } else if (!this.expectedAudience.equals(var7.audience)) {
            LOGGER.at(Level.WARNING).log("Invalid audience: expected %s, got %s. Token: %s", this.expectedAudience, var7.audience, var7.toLogSummary());
            class="kw">return null;
         } else {
            long var9 = Instant.now().getEpochSecond();
            if (var7.expiresAt == null) {
               LOGGER.at(Level.WARNING).log("Access token missing expiration claim. Token: %s", var7.toLogSummary());
               class="kw">return null;
            } else if (var9 >= var7.expiresAt + 300L) {
               LOGGER.at(Level.WARNING).log("Token expired (exp: %d, now: %d). Token: %s", var7.expiresAt, var9, var7.toLogSummary());
               class="kw">return null;
            } else if (var7.notBefore != null && var9 < var7.notBefore - 300L) {
               LOGGER.at(Level.WARNING).log("Token not yet valid (nbf: %d, now: %d). Token: %s", var7.notBefore, var9, var7.toLogSummary());
               class="kw">return null;
            } else if (var7.issuedAt != null && var7.issuedAt > var9 + 300L) {
               LOGGER.at(Level.WARNING).log("Token issued in the future (iat: %d, now: %d). Token: %s", var7.issuedAt, var9, var7.toLogSummary());
               class="kw">return null;
            } else if (!CertificateUtil.validateCertificateBinding(var7.certificateFingerprint, var2)) {
               LOGGER.at(Level.WARNING).log("Certificate binding validation failed. Token: %s", var7.toLogSummary());
               class="kw">return null;
            } else if (var7.getSubjectAsUUID() == null) {
               LOGGER.at(Level.WARNING).log("Access token has invalid or missing subject UUID. Token: %s", var7.toLogSummary());
               class="kw">return null;
            } else {
               LOGGER.at(Level.INFO).log("JWT validated successfully for user %s. Token: %s", var7.username, var7.toLogSummary());
               class="kw">return var7;
            }
         }
      } catch (ParseException var11) {
         ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var11)).log("Failed to parse JWT");
         class="kw">return null;
      } catch (Exception var12) {
         ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var12)).log("JWT validation error");
         class="kw">return null;
      }
   }

   class="kw">private boolean verifySignature(SignedJWT var1, JWKSet var2) {
      try {
         String var3 = var1.getHeader().getKeyID();
         OctetKeyPair var4 = null;

         for (JWK var6 : var2.getKeys()) {
            if (var6 class="kw">instanceof OctetKeyPair var7 && (var3 == null || var3.equals(var6.getKeyID()))) {
               var4 = var7;
               break;
            }
         }

         if (var4 == null) {
            LOGGER.at(Level.WARNING).log("No Ed25519 key found for kid=%s", var3);
            class="kw">return false;
         }

         Ed25519Verifier var9 = new Ed25519Verifier(var4);
         boolean var10 = var1.verify(var9);
         if (var10) {
            LOGGER.at(Level.FINE).log("JWT signature verified with key kid=%s", var3);
         }

         class="kw">return var10;
      } catch (Exception var8) {
         ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var8)).log("JWT signature verification failed");
         class="kw">return false;
      }
   }

   @Nullable
   class="kw">private JWKSet getJwkSet() {
      class="kw">return this.getJwkSet(false);
   }

   @Nullable
   class="kw">private JWKSet getJwkSet(boolean var1) {
      if (!var1 && this.cachedJwkSet != null) {
         class="kw">return this.cachedJwkSet;
      }

      this.jwksFetchLock.lock();

      try {
         if (!var1 && this.cachedJwkSet != null) {
            class="kw">return this.cachedJwkSet;
         }

         CompletableFuture var2 = this.pendingFetch;
         if (var2 != null && !var2.isDone()) {
            this.jwksFetchLock.unlock();

            try {
               class="kw">return var2.join();
            } class="kw">finally {
               this.jwksFetchLock.lock();
            }
         }

         if (var1) {
            LOGGER.at(Level.INFO).log("Force refreshing JWKS cache (key rotation or verification failure)");
         }

         this.pendingFetch = CompletableFuture.supplyAsync(this::fetchJwksFromService);
      } class="kw">finally {
         this.jwksFetchLock.unlock();
      }

      class="kw">return this.pendingFetch.join();
   }

   @Nullable
   class="kw">private JWKSet fetchJwksFromService() {
      SessionServiceClient.JwksResponse var1 = this.sessionServiceClient.getJwks();
      if (var1 != null && var1.keys != null && var1.keys.length != 0) {
         try {
            ArrayList var2 = new ArrayList<>();

            for (SessionServiceClient.JwkKey var6 : var1.keys) {
               JWK var7 = this.convertToJWK(var6);
               if (var7 != null) {
                  var2.add(var7);
               }
            }

            if (var2.isEmpty()) {
               LOGGER.at(Level.WARNING).log("No valid JWKs found in JWKS response");
               class="kw">return this.cachedJwkSet;
            } else {
               JWKSet var9 = new JWKSet(var2);
               this.cachedJwkSet = var9;
               this.lastJwksRefresh = Instant.now();
               this.saveJwksCacheToDisk(var9);
               LOGGER.at(Level.INFO).log("JWKS loaded with %d keys (cached permanently)", var2.size());
               class="kw">return var9;
            }
         } catch (Exception var8) {
            ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var8)).log("Failed to parse JWKS");
            class="kw">return this.cachedJwkSet;
         }
      } else {
         LOGGER.at(Level.WARNING).log("Failed to fetch JWKS or no keys available");
         class="kw">return this.cachedJwkSet;
      }
   }

   class="kw">private boolean verifySignatureWithRetry(SignedJWT var1) {
      JWKSet var2 = this.getJwkSet();
      if (var2 == null) {
         class="kw">return false;
      } else if (this.verifySignature(var1, var2)) {
         class="kw">return true;
      } else if (!this.canForceRefreshJwks()) {
         LOGGER.at(Level.FINE).log("Signature verification failed but JWKS was refreshed recently; skipping refresh");
         class="kw">return false;
      } else {
         LOGGER.at(Level.INFO).log("Signature verification failed with cached JWKS, retrying with fresh keys");
         JWKSet var3 = this.getJwkSet(true);
         class="kw">return var3 != null && var3 != var2 ? this.verifySignature(var1, var3) : false;
      }
   }

   class="kw">private boolean canForceRefreshJwks() {
      Instant var1 = this.lastJwksRefresh;
      class="kw">return var1 == null ? true : Duration.between(var1, Instant.now()).compareTo(JWKS_REFRESH_MIN_INTERVAL) >= 0;
   }

   @Nullable
   class="kw">private JWK convertToJWK(SessionServiceClient.JwkKey var1) {
      if (!"OKP".equals(var1.kty)) {
         LOGGER.at(Level.WARNING).log("Unsupported key type: %s (expected OKP)", var1.kty);
         class="kw">return null;
      }

      try {
         String var2 = String.format("{\"kty\":\"OKP\",\"crv\":\"%s\",\"x\":\"%s\",\"kid\":\"%s\",\"alg\":\"EdDSA\"}", var1.crv, var1.x, var1.kid);
         class="kw">return JWK.parse(var2);
      } catch (Exception var3) {
         ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var3)).log("Failed to parse Ed25519 key");
         class="kw">return null;
      }
   }

   class="kw">private void preSeedJwksCache() {
      JWKSet var1 = this.loadJwksCacheFromDisk();
      if (var1 != null) {
         this.cachedJwkSet = var1;
         LOGGER.at(Level.INFO).log("JWKS pre-seeded from disk cache (%d keys)", var1.getKeys().size());
      } else {
         JWKSet var2 = this.loadEmbeddedJwks();
         if (var2 != null) {
            this.cachedJwkSet = var2;
            LOGGER.at(Level.INFO).log("JWKS pre-seeded from embedded resource (%d keys)", var2.getKeys().size());
         } else {
            LOGGER.at(Level.INFO).log("No pre-seeded JWKS available, will fetch from network on first validation");
         }
      }
   }

   @Nullable
   class="kw">private JWKSet loadEmbeddedJwks() {
      try (InputStream var1 = this.getClass().getResourceAsStream("/jwks_bundled.json")) {
         if (var1 == null) {
            LOGGER.at(Level.FINE).log("No embedded JWKS resource found");
            class="kw">return null;
         } else {
            String var2 = new String(var1.readAllBytes(), StandardCharsets.UTF_8);
            class="kw">return JWKSet.parse(var2);
         }
      } catch (IOException | ParseException var6) {
         ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var6)).log("Failed to load embedded JWKS");
         class="kw">return null;
      }
   }

   @Nullable
   class="kw">private JWKSet loadJwksCacheFromDisk() {
      Path var1 = Path.of(".jwks.json");
      if (!Files.exists(var1)) {
         class="kw">return null;
      }

      try {
         String var2 = Files.readString(var1, StandardCharsets.UTF_8);
         JWKSet var3 = JWKSet.parse(var2);
         LOGGER.at(Level.FINE).log("Loaded JWKS cache from disk: %s", var1);
         class="kw">return var3;
      } catch (IOException | ParseException var4) {
         ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var4)).log("Failed to load JWKS cache from disk");
         class="kw">return null;
      }
   }

   class="kw">private void saveJwksCacheToDisk(@Nonnull JWKSet var1) {
      try {
         Files.writeString(Path.of(".jwks.json"), var1.toString(), StandardCharsets.UTF_8);
         LOGGER.at(Level.FINE).log("Saved JWKS cache to disk");
      } catch (IOException var3) {
         ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var3)).log("Failed to save JWKS cache to disk");
      }
   }

   @Nullable
   class="kw">public JWTValidator.IdentityTokenClaims validateOfflineToken(@Nonnull String var1) {
      JWTValidator.IdentityTokenClaims var2 = this.validateIdentityToken(var1);
      if (var2 == null) {
         class="kw">return null;
      } else if (!var2.hasScope("hytale:offline")) {
         LOGGER.at(Level.WARNING).log("Offline token missing required scope: expected %s, got %s", "hytale:offline", var2.scope);
         class="kw">return null;
      } else {
         class="kw">return var2;
      }
   }

   class="kw">public void invalidateJwksCache() {
      this.jwksFetchLock.lock();

      try {
         this.cachedJwkSet = null;
         this.pendingFetch = null;
      } class="kw">finally {
         this.jwksFetchLock.unlock();
      }
   }

   @Nullable
   class="kw">public JWTValidator.IdentityTokenClaims validateIdentityToken(@Nonnull String var1) {
      String var2 = validateJwtStructure(var1, "Identity token");
      if (var2 != null) {
         LOGGER.at(Level.WARNING).log("%s", var2);
         class="kw">return null;
      }

      try {
         SignedJWT var3 = SignedJWT.parse(var1);
         JWSAlgorithm var4 = var3.getHeader().getAlgorithm();
         if (!SUPPORTED_ALGORITHM.equals(var4)) {
            LOGGER.at(Level.WARNING).log("Unsupported identity token algorithm: %s (expected EdDSA)", var4);
            class="kw">return null;
         }

         if (!this.verifySignatureWithRetry(var3)) {
            LOGGER.at(Level.WARNING).log("Identity token signature verification failed");
            class="kw">return null;
         }

         JWTClaimsSet var5 = var3.getJWTClaimsSet();
         JWTValidator.IdentityTokenClaims var6 = new JWTValidator.IdentityTokenClaims();
         var6.issuer = var5.getIssuer();
         var6.subject = var5.getSubject();
         var6.issuedAt = var5.getIssueTime() != null ? var5.getIssueTime().toInstant().getEpochSecond() : null;
         var6.expiresAt = var5.getExpirationTime() != null ? var5.getExpirationTime().toInstant().getEpochSecond() : null;
         var6.notBefore = var5.getNotBeforeTime() != null ? var5.getNotBeforeTime().toInstant().getEpochSecond() : null;
         var6.scope = var5.getStringClaim("scope");
         Map var7 = var5.getJSONObjectClaim("profile");
         if (var7 != null) {
            var6.username = (String)var7.get("username");
            var6.skin = (String)var7.get("skin");
            if (var7.get("entitlements") class="kw">instanceof List var9) {
               var6.entitlements = var9.stream().filter(String.class::isInstance).map(String.class::cast).toArray(String[]::new);
            }
         }

         if (!this.expectedIssuer.equals(var6.issuer)) {
            LOGGER.at(Level.WARNING)
               .log("Invalid identity token issuer: expected %s, got %s. Token: %s", this.expectedIssuer, var6.issuer, var6.toLogSummary());
            class="kw">return null;
         } else {
            long var12 = Instant.now().getEpochSecond();
            if (var6.expiresAt == null) {
               LOGGER.at(Level.WARNING).log("Identity token missing expiration claim. Token: %s", var6.toLogSummary());
               class="kw">return null;
            } else if (var12 >= var6.expiresAt + 300L) {
               LOGGER.at(Level.WARNING).log("Identity token expired (exp: %d, now: %d). Token: %s", var6.expiresAt, var12, var6.toLogSummary());
               class="kw">return null;
            } else if (var6.notBefore != null && var12 < var6.notBefore - 300L) {
               LOGGER.at(Level.WARNING).log("Identity token not yet valid (nbf: %d, now: %d). Token: %s", var6.notBefore, var12, var6.toLogSummary());
               class="kw">return null;
            } else if (var6.issuedAt != null && var6.issuedAt > var12 + 300L) {
               LOGGER.at(Level.WARNING).log("Identity token issued in the future (iat: %d, now: %d). Token: %s", var6.issuedAt, var12, var6.toLogSummary());
               class="kw">return null;
            } else if (var6.getSubjectAsUUID() == null) {
               LOGGER.at(Level.WARNING).log("Identity token has invalid or missing subject UUID. Token: %s", var6.toLogSummary());
               class="kw">return null;
            } else {
               LOGGER.at(Level.INFO).log("Identity token validated successfully for user %s. Token: %s", var6.username, var6.toLogSummary());
               class="kw">return var6;
            }
         }
      } catch (ParseException var10) {
         ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var10)).log("Failed to parse identity token");
         class="kw">return null;
      } catch (Exception var11) {
         ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var11)).log("Identity token validation error");
         class="kw">return null;
      }
   }

   @Nullable
   class="kw">public JWTValidator.SessionTokenClaims validateSessionToken(@Nonnull String var1) {
      String var2 = validateJwtStructure(var1, "Session token");
      if (var2 != null) {
         LOGGER.at(Level.WARNING).log("%s", var2);
         class="kw">return null;
      }

      try {
         SignedJWT var3 = SignedJWT.parse(var1);
         JWSAlgorithm var4 = var3.getHeader().getAlgorithm();
         if (!SUPPORTED_ALGORITHM.equals(var4)) {
            LOGGER.at(Level.WARNING).log("Unsupported session token algorithm: %s (expected EdDSA)", var4);
            class="kw">return null;
         } else if (!this.verifySignatureWithRetry(var3)) {
            LOGGER.at(Level.WARNING).log("Session token signature verification failed");
            class="kw">return null;
         } else {
            JWTClaimsSet var5 = var3.getJWTClaimsSet();
            JWTValidator.SessionTokenClaims var6 = new JWTValidator.SessionTokenClaims();
            var6.issuer = var5.getIssuer();
            var6.subject = var5.getSubject();
            var6.issuedAt = var5.getIssueTime() != null ? var5.getIssueTime().toInstant().getEpochSecond() : null;
            var6.expiresAt = var5.getExpirationTime() != null ? var5.getExpirationTime().toInstant().getEpochSecond() : null;
            var6.notBefore = var5.getNotBeforeTime() != null ? var5.getNotBeforeTime().toInstant().getEpochSecond() : null;
            var6.scope = var5.getStringClaim("scope");
            if (!this.expectedIssuer.equals(var6.issuer)) {
               LOGGER.at(Level.WARNING)
                  .log("Invalid session token issuer: expected %s, got %s. Token: %s", this.expectedIssuer, var6.issuer, var6.toLogSummary());
               class="kw">return null;
            } else {
               long var7 = Instant.now().getEpochSecond();
               if (var6.expiresAt == null) {
                  LOGGER.at(Level.WARNING).log("Session token missing expiration claim. Token: %s", var6.toLogSummary());
                  class="kw">return null;
               } else if (var7 >= var6.expiresAt + 300L) {
                  LOGGER.at(Level.WARNING).log("Session token expired (exp: %d, now: %d). Token: %s", var6.expiresAt, var7, var6.toLogSummary());
                  class="kw">return null;
               } else if (var6.notBefore != null && var7 < var6.notBefore - 300L) {
                  LOGGER.at(Level.WARNING).log("Session token not yet valid (nbf: %d, now: %d). Token: %s", var6.notBefore, var7, var6.toLogSummary());
                  class="kw">return null;
               } else if (var6.issuedAt != null && var6.issuedAt > var7 + 300L) {
                  LOGGER.at(Level.WARNING).log("Session token issued in the future (iat: %d, now: %d). Token: %s", var6.issuedAt, var7, var6.toLogSummary());
                  class="kw">return null;
               } else if (var6.getSubjectAsUUID() == null) {
                  LOGGER.at(Level.WARNING).log("Session token has invalid or missing subject UUID. Token: %s", var6.toLogSummary());
                  class="kw">return null;
               } else {
                  LOGGER.at(Level.INFO).log("Session token validated successfully. Token: %s", var6.toLogSummary());
                  class="kw">return var6;
               }
            }
         }
      } catch (ParseException var9) {
         ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var9)).log("Failed to parse session token");
         class="kw">return null;
      } catch (Exception var10) {
         ((HytaleLogger.Api)LOGGER.at(Level.WARNING).withCause(var10)).log("Session token validation error");
         class="kw">return null;
      }
   }

   class="kw">public class="kw">static class IdentityTokenClaims {
      class="kw">public String issuer;
      class="kw">public String subject;
      class="kw">public String username;
      class="kw">public String[] entitlements;
      class="kw">public String skin;
      class="kw">public Long issuedAt;
      class="kw">public Long expiresAt;
      class="kw">public Long notBefore;
      class="kw">public String scope;

      class="kw">public IdentityTokenClaims() {
      }

      @Nullable
      class="kw">public UUID getSubjectAsUUID() {
         if (this.subject == null) {
            class="kw">return null;
         }

         try {
            class="kw">return UUID.fromString(this.subject);
         } catch (IllegalArgumentException var2) {
            class="kw">return null;
         }
      }

      @Nonnull
      class="kw">public String[] getScopes() {
         class="kw">return this.scope != null && !this.scope.isEmpty() ? this.scope.split(" ", 0) : new String[0];
      }

      class="kw">public boolean hasScope(@Nonnull String var1) {
         for (String var5 : this.getScopes()) {
            if (var5.equals(var1)) {
               class="kw">return true;
            }
         }

         class="kw">return false;
      }

      @Nonnull
      class="kw">public String toLogSummary() {
         class="kw">return JWTValidator.TokenClaimsLogFormatter.format(this.issuer, null, this.subject, this.scope, this.issuedAt, this.expiresAt, this.notBefore, null);
      }
   }

   class="kw">public class="kw">static class JWTClaims {
      class="kw">public String issuer;
      class="kw">public String audience;
      class="kw">public String subject;
      class="kw">public String username;
      class="kw">public Long issuedAt;
      class="kw">public Long expiresAt;
      class="kw">public Long notBefore;
      class="kw">public String certificateFingerprint;

      class="kw">public JWTClaims() {
      }

      @Nullable
      class="kw">public UUID getSubjectAsUUID() {
         if (this.subject == null) {
            class="kw">return null;
         }

         try {
            class="kw">return UUID.fromString(this.subject);
         } catch (IllegalArgumentException var2) {
            class="kw">return null;
         }
      }

      @Nonnull
      class="kw">public String toLogSummary() {
         class="kw">return JWTValidator.TokenClaimsLogFormatter.format(
            this.issuer, this.audience, this.subject, null, this.issuedAt, this.expiresAt, this.notBefore, this.certificateFingerprint
         );
      }
   }

   class="kw">public class="kw">static class SessionTokenClaims {
      class="kw">public String issuer;
      class="kw">public String subject;
      class="kw">public Long issuedAt;
      class="kw">public Long expiresAt;
      class="kw">public Long notBefore;
      class="kw">public String scope;

      class="kw">public SessionTokenClaims() {
      }

      @Nullable
      class="kw">public UUID getSubjectAsUUID() {
         if (this.subject == null) {
            class="kw">return null;
         }

         try {
            class="kw">return UUID.fromString(this.subject);
         } catch (IllegalArgumentException var2) {
            class="kw">return null;
         }
      }

      @Nonnull
      class="kw">public String[] getScopes() {
         class="kw">return this.scope != null && !this.scope.isEmpty() ? this.scope.split(" ", 0) : new String[0];
      }

      class="kw">public boolean hasScope(@Nonnull String var1) {
         for (String var5 : this.getScopes()) {
            if (var5.equals(var1)) {
               class="kw">return true;
            }
         }

         class="kw">return false;
      }

      @Nonnull
      class="kw">public String toLogSummary() {
         class="kw">return JWTValidator.TokenClaimsLogFormatter.format(this.issuer, null, this.subject, this.scope, this.issuedAt, this.expiresAt, this.notBefore, null);
      }
   }

   class="kw">static class="kw">final class TokenClaimsLogFormatter {
      class="kw">private TokenClaimsLogFormatter() {
      }

      @Nonnull
      class="kw">static String format(
         @Nullable String var0,
         @Nullable String var1,
         @Nullable String var2,
         @Nullable String var3,
         @Nullable Long var4,
         @Nullable Long var5,
         @Nullable Long var6,
         @Nullable String var7
      ) {
         long var8 = Instant.now().getEpochSecond();
         StringBuilder var10 = new StringBuilder();
         var10.append("iss=").append(var0 == null ? "null" : var0);
         if (var1 != null) {
            var10.append(", aud=").append(var1);
         }

         var10.append(", sub=").append(var2 == null ? "null" : var2);
         if (var3 != null) {
            var10.append(", scope=").append(var3);
         }

         var10.append(", iat=").append(formatInstant(var4));
         var10.append(", exp=").append(formatInstantWithDelta(var5, var8));
         if (var6 != null) {
            var10.append(", nbf=").append(formatInstant(var6));
         }

         if (var4 != null && var5 != null) {
            var10.append(", lifetime=").append(var5 - var4).append("s");
         }

         if (var7 != null) {
            String var11 = var7.length() > 12 ? var7.substring(0, 12) + "..." : var7;
            var10.append(", cnf=").append(var11);
         }

         class="kw">return var10.toString();
      }

      @Nonnull
      class="kw">private class="kw">static String formatInstant(@Nullable Long var0) {
         class="kw">return var0 == null ? "null" : Instant.ofEpochSecond(var0).toString();
      }

      @Nonnull
      class="kw">private class="kw">static String formatInstantWithDelta(@Nullable Long var0, long var1) {
         if (var0 == null) {
            class="kw">return "null";
         }

         long var3 = var0 - var1;
         String var5 = var3 >= 0L ? "+" : "";
         class="kw">return Instant.ofEpochSecond(var0) + " (" + var5 + var3 + "s)";
      }
   }
}