package yushijinhun.authlibagent.service; import java.io.UnsupportedEncodingException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import yushijinhun.authlibagent.dao.ServerIdRepository; import yushijinhun.authlibagent.dao.TokenRepository; import yushijinhun.authlibagent.model.AccessPolicy; import yushijinhun.authlibagent.model.AccessRule; import yushijinhun.authlibagent.model.Account; import yushijinhun.authlibagent.model.GameProfile; import yushijinhun.authlibagent.model.Token; import yushijinhun.authlibagent.util.TokenAuthResult; import yushijinhun.authlibagent.web.yggdrasil.AuthenticateResponse; import static org.hibernate.criterion.Restrictions.eq; import static yushijinhun.authlibagent.util.UUIDUtils.unsign; @Component public class YggdrasilServiceImpl implements YggdrasilService { private static final String MSG_INVALID_PROFILE = "Invalid profile."; private static final String MSG_PROFILE_BANNED = "Game profile has been banned."; private static final String MSG_SELECTING_PROFILE_NOT_SUPPORTED = "Access token already has a profile assigned."; @Autowired private SessionFactory sessionFactory; @Autowired private LoginService loginService; @Autowired private ServerIdRepository serveridRepo; @Autowired private TokenRepository tokenRepo; @Value("#{config['feature.allowSelectingProfile']}") private boolean allowSelectingProfile; @Value("#{config['feature.autoSelectedUniqueProfile']}") private boolean autoSelectedUniqueProfile; @Value("#{config['access.policy.default']}") private String defaultPolicy; @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) @Override public AuthenticateResponse authenticate(String username, String password, String clientToken) throws ForbiddenOperationException { Account account = loginService.loginWithPassword(username, password, false); UUID selectedProfileUUID = null; GameProfile selectedProfile = null; if (autoSelectedUniqueProfile && account.getProfiles().size() == 1) { // select the unique profile selectedProfile = account.getProfiles().iterator().next(); if (selectedProfile.isBanned()) { throw new ForbiddenOperationException(MSG_PROFILE_BANNED); } selectedProfileUUID = UUID.fromString(selectedProfile.getUuid()); } Token token = loginService.createToken(username, selectedProfileUUID, clientToken); return createAuthenticateResponse(account, token, selectedProfile); } @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) @Override public AuthenticateResponse refresh(String accessToken, String clientToken) throws ForbiddenOperationException { return selectProfile(accessToken, clientToken, null); } @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) @Override public AuthenticateResponse selectProfile(String accessToken, String clientToken, UUID profileUUID) throws ForbiddenOperationException { TokenAuthResult result = loginService.loginWithToken(accessToken, clientToken); Account account = result.getAccount(); UUID selectedProfileUUID = result.getToken().getSelectedProfile(); if (profileUUID != null) { // select profile if (!allowSelectingProfile) { throw new IllegalArgumentException(MSG_SELECTING_PROFILE_NOT_SUPPORTED); } selectedProfileUUID = profileUUID; } GameProfile selectedProfile = null; if (selectedProfileUUID != null) { // check if the selected profile is banned Session session = sessionFactory.getCurrentSession(); selectedProfile = session.get(GameProfile.class, selectedProfileUUID.toString()); if (selectedProfile == null || !selectedProfile.getOwner().equals(account)) { throw new ForbiddenOperationException(MSG_INVALID_PROFILE); } if (selectedProfile.isBanned()) { throw new ForbiddenOperationException(MSG_PROFILE_BANNED); } } tokenRepo.delete(accessToken); Token token = loginService.createToken(account.getId(), selectedProfileUUID, clientToken, result.getToken().getCreateTime()); return createAuthenticateResponse(account, token, selectedProfile); } @Override public boolean validate(String accessToken, String clientToken) { return loginService.isTokenAvailable(accessToken, clientToken); } @Override public boolean validate(String accessToken) { return loginService.isTokenAvailable(accessToken); } @Override public void invalidate(String accessToken, String clientToken) throws ForbiddenOperationException { loginService.revokeToken(accessToken, clientToken); } @Override public void signout(String username, String password) throws ForbiddenOperationException { loginService.loginWithPassword(username, password, false); loginService.revokeAllTokens(username); } @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) @Override public void joinServer(String accessToken, UUID profileUUID, String serverid) throws ForbiddenOperationException { Session session = sessionFactory.getCurrentSession(); Account account = loginService.loginWithToken(accessToken).getAccount(); GameProfile profile = session.get(GameProfile.class, profileUUID.toString()); if (profile == null || !account.equals(profile.getOwner())) { throw new ForbiddenOperationException(MSG_INVALID_PROFILE); } if (profile.isBanned()) { throw new ForbiddenOperationException(MSG_PROFILE_BANNED); } serveridRepo.createServerId(serverid, profileUUID); } @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) @Override public GameProfile hasJoinServer(String playername, String serverid) { Session session = sessionFactory.getCurrentSession(); UUID profileUUID = serveridRepo.getOwner(serverid); if (profileUUID != null) { GameProfile profile = session.get(GameProfile.class, profileUUID.toString()); if (profile != null && playername.equals(profile.getName())) { serveridRepo.deleteServerId(serverid); return profile; } } return null; } @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) @Override public GameProfile lookupProfile(UUID profileUUID) { Session session = sessionFactory.getCurrentSession(); return session.get(GameProfile.class, profileUUID.toString()); } @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) @Override public GameProfile lookupProfile(String name) { Session session = sessionFactory.getCurrentSession(); @SuppressWarnings("unchecked") List<GameProfile> profiles = session.createCriteria(GameProfile.class).add(eq("name", name)).list(); if (profiles.isEmpty()) { return null; } else { return profiles.get(0); } } @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) @Override public AccessPolicy getServerAccessPolicy(String host) { Session session = sessionFactory.getCurrentSession(); AccessRule rule = session.get(AccessRule.class, host); if (rule == null) { rule = session.get(AccessRule.class, AccessRule.DEFAULT_RULE_KEY); } if (rule == null) { return AccessPolicy.valueOf(defaultPolicy); } else { return rule.getPolicy(); } } private String getUserid(Account account) { try { return unsign(UUID.nameUUIDFromBytes(account.getId().getBytes("UTF-8"))); } catch (UnsupportedEncodingException e) { throw new IllegalStateException("utf-8 not supported", e); } } private Map<String, String> getUserProperties(Account account) { Map<String, String> properties = new HashMap<>(); // twitch if (account.getTwitchToken() != null) { properties.put("twitch_access_token", account.getTwitchToken()); } return properties; } private AuthenticateResponse createAuthenticateResponse(Account account, Token token, GameProfile selectedProfileObj) { String userid = getUserid(account); Map<String, String> properties = getUserProperties(account); return new AuthenticateResponse(token.getClientToken(), token.getAccessToken(), selectedProfileObj, account.getProfiles(), userid, properties); } }