mirror of
https://github.com/GravitLauncher/Launcher
synced 2024-11-15 11:39:11 +03:00
[ANY] Add OpenID auth module (#709)
Co-authored-by: d3coder <admin@xakeps.dk>
This commit is contained in:
parent
748612783c
commit
8908710ad6
16 changed files with 819 additions and 0 deletions
|
@ -84,6 +84,7 @@ pack project(':LauncherAPI')
|
||||||
bundle group: 'org.slf4j', name: 'slf4j-api', version: rootProject['verSlf4j']
|
bundle group: 'org.slf4j', name: 'slf4j-api', version: rootProject['verSlf4j']
|
||||||
bundle group: 'com.mysql', name: 'mysql-connector-j', version: rootProject['verMySQLConn']
|
bundle group: 'com.mysql', name: 'mysql-connector-j', version: rootProject['verMySQLConn']
|
||||||
bundle group: 'org.postgresql', name: 'postgresql', version: rootProject['verPostgreSQLConn']
|
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: '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-core', version: rootProject['verLog4j']
|
||||||
bundle group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: rootProject['verLog4j']
|
bundle group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: rootProject['verLog4j']
|
||||||
|
|
|
@ -16,6 +16,10 @@ public AuthException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AuthException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
public static AuthException need2FA() {
|
public static AuthException need2FA() {
|
||||||
return new AuthException(AuthRequestEvent.TWO_FACTOR_NEED_ERROR_MESSAGE);
|
return new AuthException(AuthRequestEvent.TWO_FACTOR_NEED_ERROR_MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 <T> void consumeIfNotNull(Consumer<T> consumer, T val) {
|
||||||
|
if (val != null) {
|
||||||
|
consumer.accept(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@
|
||||||
import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportHardware;
|
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.AuthSupportRegistration;
|
||||||
import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportSudo;
|
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.manangers.AuthManager;
|
||||||
import pro.gravit.launchserver.socket.Client;
|
import pro.gravit.launchserver.socket.Client;
|
||||||
import pro.gravit.launchserver.socket.response.auth.AuthResponse;
|
import pro.gravit.launchserver.socket.response.auth.AuthResponse;
|
||||||
|
@ -53,6 +54,7 @@ public static void registerProviders() {
|
||||||
providers.register("postgresql", PostgresSQLCoreProvider.class);
|
providers.register("postgresql", PostgresSQLCoreProvider.class);
|
||||||
providers.register("memory", MemoryAuthCoreProvider.class);
|
providers.register("memory", MemoryAuthCoreProvider.class);
|
||||||
providers.register("merge", MergeAuthCoreProvider.class);
|
providers.register("merge", MergeAuthCoreProvider.class);
|
||||||
|
providers.register("openid", OpenIDAuthCoreProvider.class);
|
||||||
registredProviders = true;
|
registredProviders = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
}
|
|
@ -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<GetAvailabilityAuthRequestEvent.AuthAvailabilityDetails> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<GetAvailabilityAuthRequestEvent.AuthAvailabilityDetails> 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<Claims> 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<Claims> 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<Claims> 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<String> 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<String> 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<Key> {
|
||||||
|
private final Map<String, Key> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.openid;
|
||||||
|
|
||||||
|
public record TokenResponse(String accessToken, long accessTokenExpiresIn,
|
||||||
|
String refreshToken, long refreshTokenExpiresIn) {
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -14,6 +14,7 @@
|
||||||
verLog4j = '2.20.0'
|
verLog4j = '2.20.0'
|
||||||
verMySQLConn = '8.3.0'
|
verMySQLConn = '8.3.0'
|
||||||
verPostgreSQLConn = '42.7.1'
|
verPostgreSQLConn = '42.7.1'
|
||||||
|
verH2Conn = '2.2.224'
|
||||||
verProguard = '7.4.1'
|
verProguard = '7.4.1'
|
||||||
verLaunch4j = '3.50'
|
verLaunch4j = '3.50'
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue