diff --git a/LaunchServer/build.gradle b/LaunchServer/build.gradle index 6743d5ec..44b75deb 100644 --- a/LaunchServer/build.gradle +++ b/LaunchServer/build.gradle @@ -84,6 +84,7 @@ pack project(':LauncherAPI') bundle group: 'org.slf4j', name: 'slf4j-api', version: rootProject['verSlf4j'] bundle group: 'com.mysql', name: 'mysql-connector-j', version: rootProject['verMySQLConn'] bundle group: 'org.postgresql', name: 'postgresql', version: rootProject['verPostgreSQLConn'] + bundle group: 'com.h2database', name: 'h2', version: rootProject['verH2Conn'] bundle group: 'com.guardsquare', name: 'proguard-base', version: rootProject['verProguard'] bundle group: 'org.apache.logging.log4j', name: 'log4j-core', version: rootProject['verLog4j'] bundle group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: rootProject['verLog4j'] diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/AuthException.java b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/AuthException.java index e30f9842..ecf6ab0c 100644 --- a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/AuthException.java +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/AuthException.java @@ -16,6 +16,10 @@ public AuthException(String message) { super(message); } + public AuthException(String message, Throwable cause) { + super(message, cause); + } + public static AuthException need2FA() { return new AuthException(AuthRequestEvent.TWO_FACTOR_NEED_ERROR_MESSAGE); } diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/HikariSQLSourceConfig.java b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/HikariSQLSourceConfig.java new file mode 100644 index 00000000..974ce17e --- /dev/null +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/HikariSQLSourceConfig.java @@ -0,0 +1,50 @@ +package pro.gravit.launchserver.auth; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; +import java.util.function.Consumer; + +public class HikariSQLSourceConfig implements SQLSourceConfig { + private transient HikariDataSource dataSource; + private String dsClass; + private Properties dsProps; + private String driverClass; + private String jdbcUrl; + private String username; + private String password; + + public void init() { + if (dataSource != null) { + return; + } + HikariConfig config = new HikariConfig(); + consumeIfNotNull(config::setDataSourceClassName, dsClass); + consumeIfNotNull(config::setDataSourceProperties, dsProps); + consumeIfNotNull(config::setDriverClassName, driverClass); + consumeIfNotNull(config::setJdbcUrl, jdbcUrl); + consumeIfNotNull(config::setUsername, username); + consumeIfNotNull(config::setPassword, password); + + this.dataSource = new HikariDataSource(config); + } + + @Override + public Connection getConnection() throws SQLException { + return dataSource.getConnection(); + } + + @Override + public void close() { + dataSource.close(); + } + + private static void consumeIfNotNull(Consumer consumer, T val) { + if (val != null) { + consumer.accept(val); + } + } +} diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/AuthCoreProvider.java b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/AuthCoreProvider.java index b6e14d8d..806b657d 100644 --- a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/AuthCoreProvider.java +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/AuthCoreProvider.java @@ -21,6 +21,7 @@ import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportHardware; import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportRegistration; import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportSudo; +import pro.gravit.launchserver.auth.core.openid.OpenIDAuthCoreProvider; import pro.gravit.launchserver.manangers.AuthManager; import pro.gravit.launchserver.socket.Client; import pro.gravit.launchserver.socket.response.auth.AuthResponse; @@ -53,6 +54,7 @@ public static void registerProviders() { providers.register("postgresql", PostgresSQLCoreProvider.class); providers.register("memory", MemoryAuthCoreProvider.class); providers.register("merge", MergeAuthCoreProvider.class); + providers.register("openid", OpenIDAuthCoreProvider.class); registredProviders = true; } } diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/AccessTokenResponse.java b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/AccessTokenResponse.java new file mode 100644 index 00000000..8a1d3f97 --- /dev/null +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/AccessTokenResponse.java @@ -0,0 +1,14 @@ +package pro.gravit.launchserver.auth.core.openid; + +import com.google.gson.annotations.SerializedName; + +public record AccessTokenResponse(@SerializedName("access_token") String accessToken, + @SerializedName("expires_in") Long expiresIn, + @SerializedName("refresh_expires_in") Long refreshExpiresIn, + @SerializedName("refresh_token") String refreshToken, + @SerializedName("token_type") String tokenType, + @SerializedName("id_token") String idToken, + @SerializedName("not-before-policy") Integer notBeforePolicy, + @SerializedName("session_state") String sessionState, + @SerializedName("scope") String scope) { +} diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/OpenIDAuthCoreProvider.java b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/OpenIDAuthCoreProvider.java new file mode 100644 index 00000000..77233f26 --- /dev/null +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/OpenIDAuthCoreProvider.java @@ -0,0 +1,178 @@ +package pro.gravit.launchserver.auth.core.openid; + +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import pro.gravit.launcher.base.ClientPermissions; +import pro.gravit.launcher.base.events.request.GetAvailabilityAuthRequestEvent; +import pro.gravit.launcher.base.request.auth.AuthRequest; +import pro.gravit.launcher.base.request.auth.password.AuthCodePassword; +import pro.gravit.launchserver.LaunchServer; +import pro.gravit.launchserver.auth.AuthException; +import pro.gravit.launchserver.auth.AuthProviderPair; +import pro.gravit.launchserver.auth.HikariSQLSourceConfig; +import pro.gravit.launchserver.auth.core.AuthCoreProvider; +import pro.gravit.launchserver.auth.core.User; +import pro.gravit.launchserver.auth.core.UserSession; +import pro.gravit.launchserver.manangers.AuthManager; +import pro.gravit.launchserver.socket.Client; +import pro.gravit.launchserver.socket.response.auth.AuthResponse; +import pro.gravit.utils.helper.LogHelper; + +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class OpenIDAuthCoreProvider extends AuthCoreProvider { + private transient SQLUserStore sqlUserStore; + private transient SQLServerSessionStore sqlSessionStore; + private transient OpenIDAuthenticator openIDAuthenticator; + + private OpenIDConfig openIDConfig; + private HikariSQLSourceConfig sqlSourceConfig; + + @Override + public List getDetails(Client client) { + return openIDAuthenticator.getDetails(); + } + + @Override + public User getUserByUsername(String username) { + return sqlUserStore.getByUsername(username); + } + + @Override + public User getUserByUUID(UUID uuid) { + return sqlUserStore.getUserByUUID(uuid); + } + + @Override + public UserSession getUserSessionByOAuthAccessToken(String accessToken) throws OAuthAccessTokenExpired { + return openIDAuthenticator.getUserSessionByOAuthAccessToken(accessToken); + } + + @Override + public AuthManager.AuthReport refreshAccessToken(String oldRefreshToken, AuthResponse.AuthContext context) { + var tokens = openIDAuthenticator.refreshAccessToken(oldRefreshToken); + var accessToken = tokens.accessToken(); + var refreshToken = tokens.refreshToken(); + long expiresIn = TimeUnit.SECONDS.toMillis(tokens.accessTokenExpiresIn()); + + UserSession session; + try { + session = openIDAuthenticator.getUserSessionByOAuthAccessToken(accessToken); + } catch (OAuthAccessTokenExpired e) { + throw new RuntimeException("invalid token", e); + } + + + return AuthManager.AuthReport.ofOAuth(accessToken, refreshToken, + expiresIn, session); + } + + @Override + public AuthManager.AuthReport authorize(String login, AuthResponse.AuthContext context, AuthRequest.AuthPasswordInterface password, boolean minecraftAccess) throws IOException { + if (password == null) { + throw AuthException.wrongPassword(); + } + var authCodePassword = (AuthCodePassword) password; + + var tokens = openIDAuthenticator.authorize(authCodePassword); + + var accessToken = tokens.accessToken(); + var refreshToken = tokens.refreshToken(); + var user = openIDAuthenticator.createUserFromToken(accessToken); + long expiresIn = TimeUnit.SECONDS.toMillis(tokens.accessTokenExpiresIn()); + + sqlUserStore.createOrUpdateUser(user); + + UserSession session; + try { + session = openIDAuthenticator.getUserSessionByOAuthAccessToken(accessToken); + } catch (OAuthAccessTokenExpired e) { + throw new AuthException("invalid token", e); + } + + if (minecraftAccess) { + var minecraftToken = generateMinecraftToken(user); + return AuthManager.AuthReport.ofOAuthWithMinecraft(minecraftToken, accessToken, refreshToken, + expiresIn, session); + } else { + return AuthManager.AuthReport.ofOAuth(accessToken, refreshToken, + expiresIn, session); + } + } + + private String generateMinecraftToken(User user) { + return Jwts.builder() + .issuer("LaunchServer") + .subject(user.getUUID().toString()) + .claim("preferred_username", user.getUsername()) + .expiration(Date.from(Instant.now().plus(24, ChronoUnit.HOURS))) + .signWith(server.keyAgreementManager.ecdsaPrivateKey) + .compact(); + } + + private User createUserFromMinecraftToken(String accessToken) throws AuthException { + try { + var parser = Jwts.parser() + .requireIssuer("LaunchServer") + .verifyWith(server.keyAgreementManager.ecdsaPublicKey) + .build(); + var claims = parser.parseSignedClaims(accessToken); + var username = claims.getPayload().get("preferred_username", String.class); + var uuid = UUID.fromString(claims.getPayload().getSubject()); + return new UserEntity(username, uuid, new ClientPermissions()); + } catch (JwtException e) { + throw new AuthException("Bad minecraft token", e); + } + } + + @Override + public void init(LaunchServer server, AuthProviderPair pair) { + super.init(server, pair); + this.sqlSourceConfig.init(); + this.sqlUserStore = new SQLUserStore(sqlSourceConfig); + this.sqlUserStore.init(); + this.sqlSessionStore = new SQLServerSessionStore(sqlSourceConfig); + this.sqlSessionStore.init(); + this.openIDAuthenticator = new OpenIDAuthenticator(openIDConfig); + } + + @Override + public User checkServer(Client client, String username, String serverID) throws IOException { + var savedServerId = sqlSessionStore.getServerIdByUsername(username); + if (!serverID.equals(savedServerId)) { + return null; + } + + return sqlUserStore.getByUsername(username); + } + + @Override + public boolean joinServer(Client client, String username, UUID uuid, String accessToken, String serverID) throws IOException { + User user; + try { + user = createUserFromMinecraftToken(accessToken); + } catch (AuthException e) { + LogHelper.error(e); + return false; + } + if (!user.getUUID().equals(uuid)) { + return false; + } + + sqlUserStore.createOrUpdateUser(user); + + return sqlSessionStore.joinServer(user.getUUID(), user.getUsername(), serverID); + } + + @Override + public void close() { + sqlSourceConfig.close(); + } + +} diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/OpenIDAuthenticator.java b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/OpenIDAuthenticator.java new file mode 100644 index 00000000..5e0f9e0b --- /dev/null +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/OpenIDAuthenticator.java @@ -0,0 +1,229 @@ +package pro.gravit.launchserver.auth.core.openid; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Jwk; +import io.jsonwebtoken.security.JwkSet; +import io.jsonwebtoken.security.Jwks; +import pro.gravit.launcher.base.ClientPermissions; +import pro.gravit.launcher.base.Launcher; +import pro.gravit.launcher.base.events.request.GetAvailabilityAuthRequestEvent; +import pro.gravit.launcher.base.request.auth.details.AuthWebViewDetails; +import pro.gravit.launcher.base.request.auth.password.AuthCodePassword; +import pro.gravit.launchserver.auth.AuthException; +import pro.gravit.launchserver.auth.core.AuthCoreProvider; +import pro.gravit.launchserver.auth.core.User; +import pro.gravit.launchserver.auth.core.UserSession; +import pro.gravit.utils.helper.CommonHelper; +import pro.gravit.utils.helper.QueryHelper; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.security.Key; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +public class OpenIDAuthenticator { + private static final HttpClient CLIENT = HttpClient.newBuilder().build(); + private final OpenIDConfig openIDConfig; + private final JwtParser jwtParser; + + public OpenIDAuthenticator(OpenIDConfig openIDConfig) { + this.openIDConfig = openIDConfig; + var keyLocator = loadKeyLocator(openIDConfig); + this.jwtParser = Jwts.parser().keyLocator(keyLocator) + .build(); + } + + public List getDetails() { + var state = UUID.randomUUID().toString(); + var uri = QueryBuilder.get(openIDConfig.authorizationEndpoint()) + .addQuery("response_type", "code") + .addQuery("client_id", openIDConfig.clientId()) + .addQuery("redirect_uri", openIDConfig.redirectUri()) + .addQuery("scope", openIDConfig.scopes()) + .addQuery("state", state) + .toUriString(); + + return List.of(new AuthWebViewDetails(uri, openIDConfig.redirectUri())); + } + + public TokenResponse refreshAccessToken(String oldRefreshToken) { + var postBody = QueryBuilder.post() + .addQuery("grant_type", "refresh_token") + .addQuery("refresh_token", oldRefreshToken) + .addQuery("client_id", openIDConfig.clientId()) + .addQuery("client_secret", openIDConfig.clientSecret()) + .toString(); + + var accessTokenResponse = requestToken(postBody); + var accessToken = accessTokenResponse.accessToken(); + var refreshToken = accessTokenResponse.refreshToken(); + + try { + readAndVerifyToken(accessToken); + } catch (AuthException e) { + throw new RuntimeException(e); + } + + var accessTokenExpiresIn = Objects.requireNonNullElse(accessTokenResponse.expiresIn(), 0L); + var refreshTokenExpiresIn = Objects.requireNonNullElse(accessTokenResponse.refreshExpiresIn(), 0L); + + return new TokenResponse(accessToken, accessTokenExpiresIn, + refreshToken, refreshTokenExpiresIn); + } + + public UserSession getUserSessionByOAuthAccessToken(String accessToken) throws AuthCoreProvider.OAuthAccessTokenExpired { + Jws token; + try { + token = readAndVerifyToken(accessToken); + } catch (AuthException e) { + throw new AuthCoreProvider.OAuthAccessTokenExpired("Can't read token", e); + } + var user = createUserFromToken(token); + long expiresIn = 0; + var expDate = token.getPayload().getExpiration(); + if (expDate != null) { + expiresIn = expDate.toInstant().toEpochMilli(); + } + + return new OpenIDUserSession(user, accessToken, expiresIn); + } + + public TokenResponse authorize(AuthCodePassword authCode) throws IOException { + var uri = URI.create(authCode.uri); + var queries = QueryHelper.splitUriQuery(uri); + + String code = CommonHelper.multimapFirstOrNullValue("code", queries); + String error = CommonHelper.multimapFirstOrNullValue("error", queries); + String errorDescription = CommonHelper.multimapFirstOrNullValue("error_description", queries); + + if (error != null && !error.isBlank()) { + throw new AuthException("Auth error. Error: %s, description: %s".formatted(error, errorDescription)); + } + + + var postBody = QueryBuilder.post() + .addQuery("grant_type", "authorization_code") + .addQuery("code", code) + .addQuery("redirect_uri", openIDConfig.redirectUri()) + .addQuery("client_id", openIDConfig.clientId()) + .addQuery("client_secret", openIDConfig.clientSecret()) + .toString(); + + var accessTokenResponse = requestToken(postBody); + var accessToken = accessTokenResponse.accessToken(); + var refreshToken = accessTokenResponse.refreshToken(); + + readAndVerifyToken(accessToken); + + var accessTokenExpiresIn = Objects.requireNonNullElse(accessTokenResponse.expiresIn(), 0L); + var refreshTokenExpiresIn = Objects.requireNonNullElse(accessTokenResponse.refreshExpiresIn(), 0L); + + return new TokenResponse(accessToken, accessTokenExpiresIn, + refreshToken, refreshTokenExpiresIn); + } + + public User createUserFromToken(String accessToken) throws AuthException { + return createUserFromToken(readAndVerifyToken(accessToken)); + } + + private Jws readAndVerifyToken(String accessToken) throws AuthException { + if (accessToken == null) { + throw new AuthException("Token is null"); + } + + try { + return jwtParser.parseSignedClaims(accessToken); + } catch (JwtException e) { + throw new AuthException("Bad token", e); + } + } + + private User createUserFromToken(Jws token) { + var username = token.getPayload().get(openIDConfig.extractorConfig().usernameClaim(), String.class); + var uuidStr = token.getPayload().get(openIDConfig.extractorConfig().uuidClaim(), String.class); + var uuid = UUID.fromString(uuidStr); + return new UserEntity(username, uuid, new ClientPermissions()); + } + + private AccessTokenResponse requestToken(String postBody) { + var request = HttpRequest.newBuilder() + .uri(openIDConfig.tokenUri()) + .header("Content-Type", "application/x-www-form-urlencoded") + .header("Accept", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(postBody)) + .build(); + + HttpResponse resp; + try { + resp = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + return Launcher.gsonManager.gson.fromJson(resp.body(), AccessTokenResponse.class); + } + + private static KeyLocator loadKeyLocator(OpenIDConfig openIDConfig) { + var request = HttpRequest.newBuilder(openIDConfig.jwksUri()).GET().build(); + HttpResponse response; + try { + response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + var jwks = Jwks.setParser().build().parse(response.body()); + return new KeyLocator(jwks); + } + + private static class KeyLocator extends LocatorAdapter { + private final Map keys; + + public KeyLocator(JwkSet jwks) { + this.keys = jwks.getKeys().stream().collect( + Collectors.toMap(jwk -> String.valueOf(jwk.get("kid")), Jwk::toKey)); + } + + @Override + protected Key locate(JweHeader header) { + return super.locate(header); + } + + @Override + protected Key locate(JwsHeader header) { + return keys.get(header.getKeyId()); + } + + @Override + protected Key doLocate(Header header) { + return super.doLocate(header); + } + } + + record OpenIDUserSession(User user, String token, long expiresIn) implements UserSession { + @Override + public String getID() { + return user.getUsername(); + } + + @Override + public User getUser() { + return user; + } + + @Override + public String getMinecraftAccessToken() { + return token; + } + + @Override + public long getExpireIn() { + return expiresIn; + } + } +} diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/OpenIDConfig.java b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/OpenIDConfig.java new file mode 100644 index 00000000..395f2046 --- /dev/null +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/OpenIDConfig.java @@ -0,0 +1,9 @@ +package pro.gravit.launchserver.auth.core.openid; + +import java.net.URI; + +public record OpenIDConfig(URI tokenUri, String authorizationEndpoint, String clientId, String clientSecret, + String redirectUri, URI jwksUri, String scopes, ClaimExtractorConfig extractorConfig) { + + public record ClaimExtractorConfig(String usernameClaim, String uuidClaim) {} +} diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/QueryBuilder.java b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/QueryBuilder.java new file mode 100644 index 00000000..c3175ffc --- /dev/null +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/QueryBuilder.java @@ -0,0 +1,59 @@ +package pro.gravit.launchserver.auth.core.openid; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +/** + * @author Xakep_SDK + */ +public class QueryBuilder { + private final String uri; + private final StringBuilder query = new StringBuilder(); + + public QueryBuilder(String uri) { + this.uri = uri; + } + + public static QueryBuilder get(String uri) { + Objects.requireNonNull(uri, "uri"); + if (uri.endsWith("/")) { + uri = uri.substring(0, uri.length() - 1); + } + return new QueryBuilder(uri); + } + + public static QueryBuilder post() { + return new QueryBuilder(null); + } + + public QueryBuilder addQuery(String key, String value) { + if (!query.isEmpty()) { + query.append('&'); + } + query.append(URLEncoder.encode(key, StandardCharsets.UTF_8)) + .append('=') + .append(URLEncoder.encode(value, StandardCharsets.UTF_8)); + return this; + } + + public String toUriString() { + if (uri != null) { + if (query. isEmpty()) { + return uri; + } + return uri + '?' + query; + } + return toQueryString(); + } + + public String toQueryString() { + return query.toString(); + } + + @Override + public String toString() { + return toUriString(); + } +} + diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/SQLServerSessionStore.java b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/SQLServerSessionStore.java new file mode 100644 index 00000000..6acebc5d --- /dev/null +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/SQLServerSessionStore.java @@ -0,0 +1,99 @@ +package pro.gravit.launchserver.auth.core.openid; + +import pro.gravit.launchserver.auth.SQLSourceConfig; +import pro.gravit.utils.helper.LogHelper; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.sql.SQLException; +import java.util.UUID; + +public class SQLServerSessionStore implements ServerSessionStore { + private static final String CREATE_TABLE = """ + create table if not exists `gravit_server_session` ( + id int auto_increment, + uuid varchar(36), + username varchar(255), + server_id varchar(41), + primary key (id), + unique (uuid), + unique (username) + ); + """; + private static final String DELETE_SERVER_ID = """ + delete from `gravit_server_session` where uuid = ? + """; + private static final String INSERT_SERVER_ID = """ + insert into `gravit_server_session` (uuid, username, server_id) values (?, ?, ?) + """; + private static final String SELECT_SERVER_ID_BY_USERNAME = """ + select server_id from `gravit_server_session` where username = ? + """; + + private final SQLSourceConfig sqlSourceConfig; + + public SQLServerSessionStore(SQLSourceConfig sqlSourceConfig) { + this.sqlSourceConfig = sqlSourceConfig; + } + + @Override + public boolean joinServer(UUID uuid, String username, String serverId) { + try (var connection = sqlSourceConfig.getConnection()) { + connection.setAutoCommit(false); + var savepoint = connection.setSavepoint(); + try (var deleteServerIdStmt = connection.prepareStatement(DELETE_SERVER_ID); + var insertServerIdStmt = connection.prepareStatement(INSERT_SERVER_ID)) { + deleteServerIdStmt.setString(1, uuid.toString()); + deleteServerIdStmt.execute(); + insertServerIdStmt.setString(1, uuid.toString()); + insertServerIdStmt.setString(2, username); + insertServerIdStmt.setString(3, serverId); + insertServerIdStmt.execute(); + connection.commit(); + return true; + } catch (Exception e) { + connection.rollback(savepoint); + throw e; + } + } catch (SQLException e) { + LogHelper.debug("Can't join server. Username: %s".formatted(username)); + LogHelper.error(e); + } + + return false; + } + + @Override + public String getServerIdByUsername(String username) { + try (var connection = sqlSourceConfig.getConnection(); + var selectServerId = connection.prepareStatement(SELECT_SERVER_ID_BY_USERNAME)) { + selectServerId.setString(1, username); + try (var rs = selectServerId.executeQuery()) { + if (!rs.next()) { + return null; + } + return rs.getString("server_id"); + } + } catch (SQLException e) { + LogHelper.debug("Can't find server id by username. Username: %s".formatted(username)); + LogHelper.error(e); + } + return null; + } + + public void init() { + try (var connection = sqlSourceConfig.getConnection()) { + connection.setAutoCommit(false); + var savepoint = connection.setSavepoint(); + try (var createTableStmt = connection.prepareStatement(CREATE_TABLE)) { + createTableStmt.execute(); + connection.commit(); + } catch (Exception e) { + connection.rollback(savepoint); + throw e; + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/SQLUserStore.java b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/SQLUserStore.java new file mode 100644 index 00000000..3ac59b2d --- /dev/null +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/SQLUserStore.java @@ -0,0 +1,124 @@ +package pro.gravit.launchserver.auth.core.openid; + +import pro.gravit.launcher.base.ClientPermissions; +import pro.gravit.launchserver.auth.HikariSQLSourceConfig; +import pro.gravit.launchserver.auth.core.User; +import pro.gravit.utils.helper.LogHelper; + +import java.sql.SQLException; +import java.util.UUID; + +public class SQLUserStore implements UserStore { + private static final String CREATE_USER_TABLE = """ + create table if not exists `gravit_user` ( + id int auto_increment, + uuid varchar(36), + username varchar(255), + primary key (id), + unique (uuid), + unique (username) + ) + """; + private static final String INSERT_USER = """ + insert into `gravit_user` (uuid, username) values (?, ?) + """; + private static final String DELETE_USER_BY_NAME = """ + delete `gravit_user` where username = ? + """; + private static final String SELECT_USER_BY_NAME = """ + select uuid, username from `gravit_user` where username = ? + """; + private static final String SELECT_USER_BY_UUID = """ + select uuid, username from `gravit_user` where uuid = ? + """; + + private final HikariSQLSourceConfig sqlSourceConfig; + + public SQLUserStore(HikariSQLSourceConfig sqlSourceConfig) { + this.sqlSourceConfig = sqlSourceConfig; + } + + @Override + public User getByUsername(String username) { + try (var connection = sqlSourceConfig.getConnection(); + var selectUserStmt = connection.prepareStatement(SELECT_USER_BY_NAME)) { + selectUserStmt.setString(1, username); + try (var rs = selectUserStmt.executeQuery()) { + if (!rs.next()) { + LogHelper.debug("User not found, username: %s".formatted(username)); + return null; + } + return new UserEntity(rs.getString("username"), + UUID.fromString(rs.getString("uuid")), + new ClientPermissions()); + } + } catch (SQLException e) { + LogHelper.error(e); + } + + return null; + } + + @Override + public User getUserByUUID(UUID uuid) { + try (var connection = sqlSourceConfig.getConnection(); + var selectUserStmt = connection.prepareStatement(SELECT_USER_BY_UUID)) { + selectUserStmt.setString(1, uuid.toString()); + try (var rs = selectUserStmt.executeQuery()) { + if (!rs.next()) { + LogHelper.debug("User not found, UUID: %s".formatted(uuid)); + return null; + } + return new UserEntity(rs.getString("username"), + UUID.fromString(rs.getString("uuid")), + new ClientPermissions()); + } + } catch (SQLException e) { + LogHelper.error(e); + } + + return null; + } + + @Override + public void createOrUpdateUser(User user) { + try (var connection = sqlSourceConfig.getConnection()) { + connection.setAutoCommit(false); + var savepoint = connection.setSavepoint(); + try (var deleteUserStmt = connection.prepareStatement(DELETE_USER_BY_NAME); + var insertUserStmt = connection.prepareStatement(INSERT_USER)) { + deleteUserStmt.setString(1, user.getUsername()); + deleteUserStmt.execute(); + insertUserStmt.setString(1, user.getUUID().toString()); + insertUserStmt.setString(2, user.getUsername()); + insertUserStmt.execute(); + connection.commit(); + LogHelper.debug("User saved. UUID: %s, username: %s".formatted(user.getUUID(), user.getUsername())); + } catch (Exception e) { + connection.rollback(savepoint); + throw e; + } + } catch (SQLException e) { + LogHelper.debug("Failed to save user"); + LogHelper.error(e); + throw new RuntimeException("Failed to save user", e); + } + } + + public void init() { + try (var connection = sqlSourceConfig.getConnection()) { + connection.setAutoCommit(false); + var savepoint = connection.setSavepoint(); + try (var createUserTableStmt = connection.prepareStatement(CREATE_USER_TABLE)) { + createUserTableStmt.execute(); + connection.commit(); + } catch (Exception e) { + connection.rollback(savepoint); + throw e; + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/ServerSessionStore.java b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/ServerSessionStore.java new file mode 100644 index 00000000..cf049951 --- /dev/null +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/ServerSessionStore.java @@ -0,0 +1,8 @@ +package pro.gravit.launchserver.auth.core.openid; + +import java.util.UUID; + +public interface ServerSessionStore { + boolean joinServer(UUID uuid, String username, String serverId); + String getServerIdByUsername(String username); +} diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/TokenResponse.java b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/TokenResponse.java new file mode 100644 index 00000000..4775213c --- /dev/null +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/TokenResponse.java @@ -0,0 +1,5 @@ +package pro.gravit.launchserver.auth.core.openid; + +public record TokenResponse(String accessToken, long accessTokenExpiresIn, + String refreshToken, long refreshTokenExpiresIn) { +} diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/UserEntity.java b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/UserEntity.java new file mode 100644 index 00000000..f383a16c --- /dev/null +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/UserEntity.java @@ -0,0 +1,23 @@ +package pro.gravit.launchserver.auth.core.openid; + +import pro.gravit.launcher.base.ClientPermissions; +import pro.gravit.launchserver.auth.core.User; + +import java.util.UUID; + +record UserEntity(String username, UUID uuid, ClientPermissions permissions) implements User { + @Override + public String getUsername() { + return username; + } + + @Override + public UUID getUUID() { + return uuid; + } + + @Override + public ClientPermissions getPermissions() { + return permissions; + } +} diff --git a/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/UserStore.java b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/UserStore.java new file mode 100644 index 00000000..55c26e71 --- /dev/null +++ b/LaunchServer/src/main/java/pro/gravit/launchserver/auth/core/openid/UserStore.java @@ -0,0 +1,13 @@ +package pro.gravit.launchserver.auth.core.openid; + +import pro.gravit.launchserver.auth.core.User; + +import java.util.UUID; + +public interface UserStore { + User getByUsername(String username); + + User getUserByUUID(UUID uuid); + + void createOrUpdateUser(User user); +} diff --git a/props.gradle b/props.gradle index 6ac97aee..1694aeb1 100644 --- a/props.gradle +++ b/props.gradle @@ -14,6 +14,7 @@ verLog4j = '2.20.0' verMySQLConn = '8.3.0' verPostgreSQLConn = '42.7.1' + verH2Conn = '2.2.224' verProguard = '7.4.1' verLaunch4j = '3.50' }