package io.kaif.model.account; import java.time.Instant; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; import io.kaif.token.Bytes; /** * since token are transfer to client side, so to make it shorter, we only use hashCode() of * passwordHash and authorities. the current size is around 115 bytes * <p> * this also prevent leak detail passwordHash to client side */ public class AccountAccessToken implements Authorization { public static final String HEADER_KEY = "X-KAIF-ACCESS-TOKEN"; public static Optional<AccountAccessToken> tryDecode(String rawToken, AccountSecret secret) { List<byte[]> fields = secret.getCodec().tryDecode(rawToken); if (fields == null || fields.size() != 3) { return Optional.empty(); } final UUID accountId; final long authoritiesBits; try { accountId = Bytes.uuidFromBytes(fields.get(0)); authoritiesBits = Bytes.longFromBytes(fields.get(2)); } catch (RuntimeException e) { //malformed UUID or bits, this should not be possible, unless we change protocol return Optional.empty(); } return Optional.of(new AccountAccessToken(accountId, fields.get(1), authoritiesBits)); } private static byte[] passwordHashToBytes(String passwordHash) { return Bytes.intToBytes(passwordHash.hashCode()); } private final UUID accountId; private final long authoritiesBits; private final byte[] passwordHashDigest; public AccountAccessToken(UUID accountId, String passwordHash, Set<Authority> authorities) { this(accountId, passwordHashToBytes(passwordHash), Authority.toBits(authorities)); } private AccountAccessToken(UUID accountId, byte[] passwordHashDigest, long authoritiesBits) { this.accountId = accountId; this.passwordHashDigest = passwordHashDigest; this.authoritiesBits = authoritiesBits; } public String encode(Instant expireTime, AccountSecret secret) { List<byte[]> fields = Arrays.asList(Bytes.uuidToBytes(accountId), passwordHashDigest, Bytes.longToBytes(authoritiesBits)); return secret.getCodec().encode(expireTime.toEpochMilli(), fields); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } AccountAccessToken that = (AccountAccessToken) o; if (authoritiesBits != that.authoritiesBits) { return false; } if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) { return false; } if (!Arrays.equals(passwordHashDigest, that.passwordHashDigest)) { return false; } return true; } @Override public int hashCode() { int result = accountId != null ? accountId.hashCode() : 0; result = 31 * result + (int) (authoritiesBits ^ (authoritiesBits >>> 32)); result = 31 * result + (passwordHashDigest != null ? Arrays.hashCode(passwordHashDigest) : 0); return result; } @Override public String toString() { return "AccountAccessToken{" + "accountId='" + accountId + '\'' + '}'; } @Override public UUID authenticatedId() { return accountId; } @Override public boolean containsAuthority(Authority authority) { return Authority.bitsContains(authoritiesBits, authority); } @Override public boolean matches(Account account) { return authenticatedId().equals(account.getAccountId()) && Arrays.equals(this.passwordHashDigest, passwordHashToBytes(account.getPasswordHash())) && this.authoritiesBits == Authority.toBits(account.getAuthorities()); } }