From 39f23a0c47e97eb11c7a431d337178b0ee713949 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Fri, 13 May 2022 11:38:17 -0400 Subject: [PATCH] Add new authentication system --- .../auth/AuthenticationSystem.java | 101 +++++++++++ .../emu/grasscutter/auth/Authenticator.java | 17 ++ .../auth/DefaultAuthentication.java | 40 +++++ .../auth/DefaultAuthenticators.java | 161 ++++++++++++++++++ 4 files changed, 319 insertions(+) create mode 100644 src/main/java/emu/grasscutter/auth/AuthenticationSystem.java create mode 100644 src/main/java/emu/grasscutter/auth/Authenticator.java create mode 100644 src/main/java/emu/grasscutter/auth/DefaultAuthentication.java create mode 100644 src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java diff --git a/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java new file mode 100644 index 000000000..dae3402f2 --- /dev/null +++ b/src/main/java/emu/grasscutter/auth/AuthenticationSystem.java @@ -0,0 +1,101 @@ +package emu.grasscutter.auth; + +import emu.grasscutter.server.http.objects.*; +import express.http.Request; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import javax.annotation.Nullable; + +/** + * Defines an authenticator for the server. + * Can be changed by plugins. + */ +public interface AuthenticationSystem { + + /** + * Called when a user requests to make an account. + * @param username The provided username. + * @param password The provided password. (SHA-256'ed) + */ + void createAccount(String username, String password); + + /** + * Called when a user requests to reset their password. + * @param username The username of the account to reset. + */ + void resetPassword(String username); + + /** + * This is the authenticator used for password authentication. + * @return An authenticator. + */ + Authenticator getPasswordAuthenticator(); + + /** + * This is the authenticator used for token authentication. + * @return An authenticator. + */ + Authenticator getTokenAuthenticator(); + + /** + * This is the authenticator used for session authentication. + * @return An authenticator. + */ + Authenticator getSessionKeyAuthenticator(); + + /** + * A data container that holds relevant data for authenticating a client. + * Call {@link AuthenticationRequest#builder()} to create a builder. + */ + @Builder @AllArgsConstructor @Getter + class AuthenticationRequest { + private final Request request; + @Nullable private final LoginAccountRequestJson passwordRequest; + @Nullable private final LoginTokenRequestJson tokenRequest; + @Nullable private final ComboTokenReqJson sessionKeyRequest; + @Nullable private final ComboTokenReqJson.LoginTokenData sessionKeyData; + } + + /** + * Generates an authentication request from a {@link LoginAccountRequestJson} object. + * @param request The Express request. + * @param jsonData The JSON data. + * @return An authentication request. + */ + static AuthenticationRequest fromPasswordRequest(Request request, LoginAccountRequestJson jsonData) { + return AuthenticationRequest.builder() + .request(request) + .passwordRequest(jsonData) + .build(); + } + + /** + * Generates an authentication request from a {@link LoginTokenRequestJson} object. + * @param request The Express request. + * @param jsonData The JSON data. + * @return An authentication request. + */ + static AuthenticationRequest fromTokenRequest(Request request, LoginTokenRequestJson jsonData) { + return AuthenticationRequest.builder() + .request(request) + .tokenRequest(jsonData) + .build(); + } + + /** + * Generates an authentication request from a {@link ComboTokenReqJson} object. + * @param request The Express request. + * @param jsonData The JSON data. + * @return An authentication request. + */ + static AuthenticationRequest fromComboTokenRequest(Request request, ComboTokenReqJson jsonData, + ComboTokenReqJson.LoginTokenData tokenData) { + return AuthenticationRequest.builder() + .request(request) + .sessionKeyRequest(jsonData) + .sessionKeyData(tokenData) + .build(); + } +} diff --git a/src/main/java/emu/grasscutter/auth/Authenticator.java b/src/main/java/emu/grasscutter/auth/Authenticator.java new file mode 100644 index 000000000..a5d756d8c --- /dev/null +++ b/src/main/java/emu/grasscutter/auth/Authenticator.java @@ -0,0 +1,17 @@ +package emu.grasscutter.auth; + +import emu.grasscutter.server.http.objects.*; + +/** + * Handles username/password authentication from the client. + * @param The response object type. Should be {@link LoginResultJson} or {@link ComboTokenResJson} + */ +public interface Authenticator { + + /** + * Attempt to authenticate the client with the provided credentials. + * @param request The authentication request wrapped in a {@link AuthenticationSystem.AuthenticationRequest} object. + * @return The result of the login in an object. + */ + T authenticate(AuthenticationSystem.AuthenticationRequest request); +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java new file mode 100644 index 000000000..2864b80b5 --- /dev/null +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthentication.java @@ -0,0 +1,40 @@ +package emu.grasscutter.auth; + +import emu.grasscutter.auth.DefaultAuthenticators.*; +import emu.grasscutter.server.http.objects.ComboTokenResJson; +import emu.grasscutter.server.http.objects.LoginResultJson; + +/** + * The default Grasscutter authentication implementation. + * Allows all users to access any account. + */ +public final class DefaultAuthentication implements AuthenticationSystem { + private final Authenticator passwordAuthenticator = new PasswordAuthenticator(); + private final Authenticator tokenAuthenticator = new TokenAuthenticator(); + private final Authenticator sessionKeyAuthenticator = new SessionKeyAuthenticator(); + + @Override + public void createAccount(String username, String password) { + // Unhandled. The default authenticator doesn't store passwords. + } + + @Override + public void resetPassword(String username) { + // Unhandled. The default authenticator doesn't store passwords. + } + + @Override + public Authenticator getPasswordAuthenticator() { + return this.passwordAuthenticator; + } + + @Override + public Authenticator getTokenAuthenticator() { + return this.tokenAuthenticator; + } + + @Override + public Authenticator getSessionKeyAuthenticator() { + return this.sessionKeyAuthenticator; + } +} diff --git a/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java new file mode 100644 index 000000000..298d24493 --- /dev/null +++ b/src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java @@ -0,0 +1,161 @@ +package emu.grasscutter.auth; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.Account; +import emu.grasscutter.server.http.objects.*; + +import static emu.grasscutter.Configuration.*; +import static emu.grasscutter.utils.Language.translate; + +/** + * A class containing default authenticators. + */ +public final class DefaultAuthenticators { + + /** + * Handles the authentication request from the username & password form. + */ + public static class PasswordAuthenticator implements Authenticator { + @Override public LoginResultJson authenticate(AuthenticationRequest request) { + var response = new LoginResultJson(); + + var requestData = request.getPasswordRequest(); + assert requestData != null; // This should never be null. + + boolean successfulLogin = false; + String address = request.getRequest().ip(); + String responseMessage = translate("messages.dispatch.account.username_error"); + + // Get account from database. + Account account = DatabaseHelper.getAccountByName(requestData.account); + + // Check if account exists. + if(account == null && ACCOUNT.autoCreate) { + // This account has been created AUTOMATICALLY. There will be no permissions added. + account = DatabaseHelper.createAccountWithId(requestData.account, 0); + + // Check if the account was created successfully. + if(account == null) { + responseMessage = translate("messages.dispatch.account.username_create_error"); + Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_error", address)); + } else { + // Add default permissions. + for (var permission : ACCOUNT.defaultPermissions) + account.addPermission(permission); + + // Continue with login. + successfulLogin = true; + + // Log the creation. + Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_success", address, response.data.account.uid)); + } + } else if(account != null) + successfulLogin = true; + + // Set response data. + if(successfulLogin) { + response.message = "OK"; + response.data.account.uid = account.getId(); + response.data.account.token = account.generateSessionKey(); + response.data.account.email = account.getEmail(); + + // Log the login. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_success", address, account.getId())); + } else { + response.retcode = -201; + response.message = responseMessage; + + // Log the failure. + Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_exist_error", address)); + } + + return response; + } + } + + /** + * Handles the authentication request from the game when using a registry token. + */ + public static class TokenAuthenticator implements Authenticator { + @Override public LoginResultJson authenticate(AuthenticationRequest request) { + var response = new LoginResultJson(); + + var requestData = request.getTokenRequest(); + assert requestData != null; + + boolean successfulLogin; + String address = request.getRequest().ip(); + + // Log the attempt. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_attempt", address)); + + // Get account from database. + Account account = DatabaseHelper.getAccountById(requestData.uid); + + // Check if account exists/token is valid. + successfulLogin = account != null && account.getSessionKey().equals(requestData.token); + + // Set response data. + if(successfulLogin) { + response.message = "OK"; + response.data.account.uid = account.getId(); + response.data.account.token = account.getSessionKey(); + response.data.account.email = account.getEmail(); + + // Log the login. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_success", address, requestData.uid)); + } else { + response.retcode = -201; + response.message = translate("messages.dispatch.account.account_cache_error"); + + // Log the failure. + Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_error", address)); + } + + return response; + } + } + + /** + * Handles the authentication request from the game when using a combo token/session key. + */ + public static class SessionKeyAuthenticator implements Authenticator { + @Override public ComboTokenResJson authenticate(AuthenticationRequest request) { + var response = new ComboTokenResJson(); + + var requestData = request.getSessionKeyRequest(); + var loginData = request.getSessionKeyData(); + assert requestData != null; assert loginData != null; + + boolean successfulLogin; + String address = request.getRequest().ip(); + + // Get account from database. + Account account = DatabaseHelper.getAccountById(loginData.uid); + + // Check if account exists/token is valid. + successfulLogin = account != null && account.getSessionKey().equals(loginData.token); + + // Set response data. + if(successfulLogin) { + response.message = "OK"; + response.data.open_id = account.getId(); + response.data.combo_id = "157795300"; + response.data.combo_token = account.generateLoginToken(); + + // Log the login. + Grasscutter.getLogger().info(translate("messages.dispatch.account.combo_token_success", address)); + } else { + response.retcode = -201; + response.message = translate("messages.dispatch.account.session_key_error"); + + // Log the failure. + Grasscutter.getLogger().info(translate("messages.dispatch.account.combo_token_error", address)); + } + + return response; + } + } +}