Implement access control

This commit is contained in:
xtaodada 2022-05-13 16:49:46 +08:00
parent 4f4c722b8c
commit b06d249224
Signed by: xtaodada
GPG Key ID: 4CBB3F4FA8C85659
10 changed files with 92 additions and 59 deletions

View File

@ -1,22 +1,25 @@
# GCAuth # GCAuth
Grasscutter Authentication System Grasscutter Authentication System
### Usage : ### Usage :
- Place jar inside plugins folder of Grasscutter. - Place jar inside plugins folder of Grasscutter.
- To change hash algorithm change `Hash` in config.json inside plugins/GCAuth (Only Bcrypt and Scrypt is supported) - To change hash algorithm change `Hash` in config.json inside plugins/GCAuth (Only Bcrypt and Scrypt is supported)
- To use access control, you need set the `ACCESS_KEY` in config.json inside plugins/GCAuth. (Optional)
- All payload must be send with `application/json` and Compact JSON format ( without unnecessary spaces ) - All payload must be send with `application/json` and Compact JSON format ( without unnecessary spaces )
- Auth endpoint is: - Auth endpoint is:
- Authentication Checking : `/authentication/type` (GET) , it'll return `me.exzork.gcauth.handler.GCAuthAuthenticationHandler` if GCAuth is loaded and enabled. - Authentication Checking : `/authentication/type` (GET) , it'll return `me.exzork.gcauth.handler.GCAuthAuthenticationHandler` if GCAuth is loaded and enabled.
- Register: `/authentication/register` (POST) - Register: `/authentication/register` (POST)
``` ```
{"username":"username","password":"password","password_confirmation":"password_confirmation"} {"username":"username","password":"password","password_confirmation":"password_confirmation","access_key":"access_key"}
``` ```
- Login: `/authentication/login` (POST) - Login: `/authentication/login` (POST)
``` ```
{"username":"username","password":"password"} {"username":"username","password":"password","access_key":"access_key"}
``` ```
- Change password: `/authentication/change_password` (POST) - Change password: `/authentication/change_password` (POST)
``` ```
{"username":"username","new_password":"new_password","new_password_confirmation":"new_password_confirmation","old_password":"old_password"} {"username":"username","new_password":"new_password","new_password_confirmation":"new_password_confirmation","old_password":"old_password","access_key":"access_key"}
``` ```
- Response is `JSON` with following keys: - Response is `JSON` with following keys:
- `status` : `success` or `error` - `status` : `success` or `error`
@ -29,6 +32,7 @@ Grasscutter Authentication System
- UNKNOWN : Unknown error - UNKNOWN : Unknown error
- INVALID_ACCOUNT : Username or password is invalid - INVALID_ACCOUNT : Username or password is invalid
- NO_PASSWORD : Password is not set, please set password first by resetting it (change password) - NO_PASSWORD : Password is not set, please set password first by resetting it (change password)
- ERROR_ACCESS_KEY : Access key is invalid (if access control is enabled)
- `jwt` : JWT token if success with body : - `jwt` : JWT token if success with body :
- `token` : Token used for authentication, paste it in username field of client. - `token` : Token used for authentication, paste it in username field of client.
- `username` : Username of the user. - `username` : Username of the user.

View File

@ -5,4 +5,5 @@ import me.exzork.gcauth.utils.Authentication;
public final class Config { public final class Config {
public String Hash = "BCRYPT"; public String Hash = "BCRYPT";
public String jwtSecret = Authentication.generateRandomString(32); public String jwtSecret = Authentication.generateRandomString(32);
public String ACCESS_KEY = "";
} }

View File

@ -6,6 +6,7 @@ import emu.grasscutter.game.Account;
import express.http.HttpContextHandler; import express.http.HttpContextHandler;
import express.http.Request; import express.http.Request;
import express.http.Response; import express.http.Response;
import me.exzork.gcauth.GCAuth;
import me.exzork.gcauth.json.AuthResponseJson; import me.exzork.gcauth.json.AuthResponseJson;
import me.exzork.gcauth.json.ChangePasswordAccount; import me.exzork.gcauth.json.ChangePasswordAccount;
import me.exzork.gcauth.utils.Authentication; import me.exzork.gcauth.utils.Authentication;
@ -25,30 +26,36 @@ public class ChangePasswordHandler implements HttpContextHandler {
authResponse.jwt = ""; authResponse.jwt = "";
} else { } else {
ChangePasswordAccount changePasswordAccount = new Gson().fromJson(requestBody, ChangePasswordAccount.class); ChangePasswordAccount changePasswordAccount = new Gson().fromJson(requestBody, ChangePasswordAccount.class);
if (changePasswordAccount.new_password.equals(changePasswordAccount.new_password_confirmation)) { if (!GCAuth.getConfigStatic().ACCESS_KEY.isEmpty() && !GCAuth.getConfigStatic().ACCESS_KEY.equals(changePasswordAccount.access_key)){
Account account = Authentication.getAccountByUsernameAndPassword(changePasswordAccount.username, changePasswordAccount.old_password); authResponse.success = false;
if (account == null) { authResponse.message = "ERROR_ACCESS_KEY"; // ENG = "Error access key was sent with the request"
authResponse.success = false; authResponse.jwt = "";
authResponse.message = "INVALID_ACCOUNT"; // ENG = "Invalid username or password" } else {
authResponse.jwt = ""; if (changePasswordAccount.new_password.equals(changePasswordAccount.new_password_confirmation)) {
} else { Account account = Authentication.getAccountByUsernameAndPassword(changePasswordAccount.username, changePasswordAccount.old_password);
if (changePasswordAccount.new_password.length() >= 8) { if (account == null) {
String newPassword = Authentication.generateHash(changePasswordAccount.new_password); authResponse.success = false;
account.setPassword(newPassword); authResponse.message = "INVALID_ACCOUNT"; // ENG = "Invalid username or password"
account.save();
authResponse.success = true;
authResponse.message = "";
authResponse.jwt = ""; authResponse.jwt = "";
} else { } else {
authResponse.success = false; if (changePasswordAccount.new_password.length() >= 8) {
authResponse.message = "PASSWORD_INVALID"; // ENG = "Password must be at least 8 characters long" String newPassword = Authentication.generateHash(changePasswordAccount.new_password);
authResponse.jwt = ""; account.setPassword(newPassword);
account.save();
authResponse.success = true;
authResponse.message = "";
authResponse.jwt = "";
} else {
authResponse.success = false;
authResponse.message = "PASSWORD_INVALID"; // ENG = "Password must be at least 8 characters long"
authResponse.jwt = "";
}
} }
} else {
authResponse.success = false;
authResponse.message = "PASSWORD_MISMATCH"; // ENG = "Passwords do not match."
authResponse.jwt = "";
} }
} else {
authResponse.success = false;
authResponse.message = "PASSWORD_MISMATCH"; // ENG = "Passwords do not match."
authResponse.jwt = "";
} }
} }
} catch (Exception e) { } catch (Exception e) {

View File

@ -44,6 +44,11 @@ public class GCAuthAuthenticationHandler implements AuthenticationHandler {
} }
} }
@Override
public boolean verifyUser(String details) {
return false;
}
@Override @Override
public LoginResultJson handleGameLogin(Request request, LoginAccountRequestJson requestData) { public LoginResultJson handleGameLogin(Request request, LoginAccountRequestJson requestData) {
LoginResultJson responseData = new LoginResultJson(); LoginResultJson responseData = new LoginResultJson();

View File

@ -6,6 +6,7 @@ import emu.grasscutter.game.Account;
import express.http.HttpContextHandler; import express.http.HttpContextHandler;
import express.http.Request; import express.http.Request;
import express.http.Response; import express.http.Response;
import me.exzork.gcauth.GCAuth;
import me.exzork.gcauth.json.AuthResponseJson; import me.exzork.gcauth.json.AuthResponseJson;
import me.exzork.gcauth.json.LoginGenerateToken; import me.exzork.gcauth.json.LoginGenerateToken;
import me.exzork.gcauth.utils.Authentication; import me.exzork.gcauth.utils.Authentication;
@ -26,20 +27,26 @@ public class LoginHandler implements HttpContextHandler {
authResponse.jwt = ""; authResponse.jwt = "";
} else { } else {
LoginGenerateToken loginGenerateToken = new Gson().fromJson(requestBody, LoginGenerateToken.class); LoginGenerateToken loginGenerateToken = new Gson().fromJson(requestBody, LoginGenerateToken.class);
Account account = Authentication.getAccountByUsernameAndPassword(loginGenerateToken.username, loginGenerateToken.password); if (!GCAuth.getConfigStatic().ACCESS_KEY.isEmpty() && !GCAuth.getConfigStatic().ACCESS_KEY.equals(loginGenerateToken.access_key)){
if (account == null) {
authResponse.success = false; authResponse.success = false;
authResponse.message = "INVALID_ACCOUNT"; // ENG = "Invalid username or password" authResponse.message = "ERROR_ACCESS_KEY"; // ENG = "Error access key was sent with the request"
authResponse.jwt = ""; authResponse.jwt = "";
} else { } else {
if (account.getPassword() != null && !account.getPassword().isEmpty()) { Account account = Authentication.getAccountByUsernameAndPassword(loginGenerateToken.username, loginGenerateToken.password);
authResponse.success = true; if (account == null) {
authResponse.message = "";
authResponse.jwt = Authentication.generateJwt(account);
} else {
authResponse.success = false; authResponse.success = false;
authResponse.message = "NO_PASSWORD"; // ENG = "There is no account password set. Please create a password by resetting it." authResponse.message = "INVALID_ACCOUNT"; // ENG = "Invalid username or password"
authResponse.jwt = ""; authResponse.jwt = "";
} else {
if (account.getPassword() != null && !account.getPassword().isEmpty()) {
authResponse.success = true;
authResponse.message = "";
authResponse.jwt = Authentication.generateJwt(account);
} else {
authResponse.success = false;
authResponse.message = "NO_PASSWORD"; // ENG = "There is no account password set. Please create a password by resetting it."
authResponse.jwt = "";
}
} }
} }
} }

View File

@ -7,6 +7,7 @@ import emu.grasscutter.game.Account;
import express.http.HttpContextHandler; import express.http.HttpContextHandler;
import express.http.Request; import express.http.Request;
import express.http.Response; import express.http.Response;
import me.exzork.gcauth.GCAuth;
import me.exzork.gcauth.json.AuthResponseJson; import me.exzork.gcauth.json.AuthResponseJson;
import me.exzork.gcauth.json.RegisterAccount; import me.exzork.gcauth.json.RegisterAccount;
import me.exzork.gcauth.utils.Authentication; import me.exzork.gcauth.utils.Authentication;
@ -26,43 +27,49 @@ public class RegisterHandler implements HttpContextHandler {
authResponse.jwt = ""; authResponse.jwt = "";
} else { } else {
RegisterAccount registerAccount = new Gson().fromJson(requestBody, RegisterAccount.class); RegisterAccount registerAccount = new Gson().fromJson(requestBody, RegisterAccount.class);
if (registerAccount.password.equals(registerAccount.password_confirmation)) { if (!GCAuth.getConfigStatic().ACCESS_KEY.isEmpty() && !GCAuth.getConfigStatic().ACCESS_KEY.equals(registerAccount.access_key)){
if (registerAccount.password.length() >= 8) { authResponse.success = false;
String password = Authentication.generateHash(registerAccount.password); authResponse.message = "ERROR_ACCESS_KEY"; // ENG = "Error access key was sent with the request"
try{ authResponse.jwt = "";
Account account = Authentication.getAccountByUsernameAndPassword(registerAccount.username, ""); } else {
if (account != null) { if (registerAccount.password.equals(registerAccount.password_confirmation)) {
account.setPassword(password); if (registerAccount.password.length() >= 8) {
account.save(); String password = Authentication.generateHash(registerAccount.password);
authResponse.success = true; try{
authResponse.message = ""; Account account = Authentication.getAccountByUsernameAndPassword(registerAccount.username, "");
authResponse.jwt = ""; if (account != null) {
} else { account.setPassword(password);
account = DatabaseHelper.createAccountWithPassword(registerAccount.username, password); account.save();
if (account == null) {
authResponse.success = false;
authResponse.message = "USERNAME_TAKEN"; // ENG = "Username has already been taken by another user."
authResponse.jwt = "";
} else {
authResponse.success = true; authResponse.success = true;
authResponse.message = ""; authResponse.message = "";
authResponse.jwt = ""; authResponse.jwt = "";
} else {
account = DatabaseHelper.createAccountWithPassword(registerAccount.username, password);
if (account == null) {
authResponse.success = false;
authResponse.message = "USERNAME_TAKEN"; // ENG = "Username has already been taken by another user."
authResponse.jwt = "";
} else {
authResponse.success = true;
authResponse.message = "";
authResponse.jwt = "";
}
} }
}catch (Exception ignored){
authResponse.success = false;
authResponse.message = "UNKNOWN"; // ENG = "Username has already been taken by another user."
authResponse.jwt = "";
} }
}catch (Exception ignored){ } else {
authResponse.success = false; authResponse.success = false;
authResponse.message = "UNKNOWN"; // ENG = "Username has already been taken by another user." authResponse.message = "PASSWORD_INVALID"; // ENG = "Password must be at least 8 characters long"
authResponse.jwt = ""; authResponse.jwt = "";
} }
} else { } else {
authResponse.success = false; authResponse.success = false;
authResponse.message = "PASSWORD_INVALID"; // ENG = "Password must be at least 8 characters long" authResponse.message = "PASSWORD_MISMATCH"; // ENG = "Passwords do not match."
authResponse.jwt = ""; authResponse.jwt = "";
} }
} else {
authResponse.success = false;
authResponse.message = "PASSWORD_MISMATCH"; // ENG = "Passwords do not match."
authResponse.jwt = "";
} }
} }
} catch (Exception e) { } catch (Exception e) {

View File

@ -5,4 +5,5 @@ public class ChangePasswordAccount {
public String new_password; public String new_password;
public String new_password_confirmation; public String new_password_confirmation;
public String old_password; public String old_password;
public String access_key;
} }

View File

@ -3,4 +3,5 @@ package me.exzork.gcauth.json;
public class LoginGenerateToken { public class LoginGenerateToken {
public String username; public String username;
public String password; public String password;
public String access_key;
} }

View File

@ -4,4 +4,5 @@ public class RegisterAccount {
public String username; public String username;
public String password; public String password;
public String password_confirmation; public String password_confirmation;
public String access_key;
} }

View File

@ -49,12 +49,11 @@ public final class Authentication {
} }
public static String generateJwt(Account account) { public static String generateJwt(Account account) {
String jws = JWT.create() return JWT.create()
.withClaim("token",generateOneTimeToken(account)) .withClaim("token",generateOneTimeToken(account))
.withClaim("username",account.getUsername()) .withClaim("username",account.getUsername())
.withClaim("uid",account.getPlayerUid()) .withClaim("uid",account.getPlayerUid())
.sign(key); .sign(key);
return jws;
} }
public static String generateHash(String password) { public static String generateHash(String password) {