From 5248069a7847931fc5441c51fbfea61c6dceac77 Mon Sep 17 00:00:00 2001 From: SebClem Date: Thu, 19 May 2022 01:08:47 +0200 Subject: [PATCH] Implement discord login --- build.gradle | 4 + .../java/net/Broken/Api/AuthController.java | 25 --- .../Api/Controllers/AuthController.java | 28 +++ src/main/java/net/Broken/Api/Data/Login.java | 5 + .../Security/Data/AccessTokenResponse.java | 9 + .../Security/Data/DiscordOauthUserInfo.java | 6 + .../DiscordAuthenticationProvider.java | 35 +++- .../Security/Exception/OAuthLoginFail.java | 4 + .../Api/Security/OAuth2UserAgentUtils.java | 16 -- .../Broken/Api/Security/SecurityConfig.java | 27 ++- .../Services/DiscordOauthService.java | 140 ++++++++++++++ .../DiscordUserDetailsService.java | 15 +- .../Services/UnauthorizedHandler.java | 38 ++++ .../Api/Services/DiscordOauthService.java | 46 ----- .../Services/DiscordUserDetailService.java | 12 -- .../java/net/Broken/DB/Entity/UserEntity.java | 38 +--- .../Broken/DB/Repository/UserRepository.java | 5 +- .../Broken/RestApi/PlaylistAPIController.java | 52 +++--- .../RestApi/UserManagerAPIController.java | 171 ------------------ .../net/Broken/Tools/UserManager/Oauth.java | 75 -------- .../Tools/UserManager/PasswordResetUtils.java | 64 ------- .../UserManager/Stats/UserStatsUtils.java | 22 +-- .../Broken/Tools/UserManager/UserUtils.java | 62 +------ src/main/resources/application.yml | 4 +- 24 files changed, 337 insertions(+), 566 deletions(-) delete mode 100644 src/main/java/net/Broken/Api/AuthController.java create mode 100644 src/main/java/net/Broken/Api/Controllers/AuthController.java create mode 100644 src/main/java/net/Broken/Api/Data/Login.java create mode 100644 src/main/java/net/Broken/Api/Security/Data/AccessTokenResponse.java create mode 100644 src/main/java/net/Broken/Api/Security/Data/DiscordOauthUserInfo.java create mode 100644 src/main/java/net/Broken/Api/Security/Exception/OAuthLoginFail.java delete mode 100644 src/main/java/net/Broken/Api/Security/OAuth2UserAgentUtils.java create mode 100644 src/main/java/net/Broken/Api/Security/Services/DiscordOauthService.java rename src/main/java/net/Broken/Api/Security/{ => Services}/DiscordUserDetailsService.java (68%) create mode 100644 src/main/java/net/Broken/Api/Security/Services/UnauthorizedHandler.java delete mode 100644 src/main/java/net/Broken/Api/Services/DiscordOauthService.java delete mode 100644 src/main/java/net/Broken/Api/Services/DiscordUserDetailService.java delete mode 100644 src/main/java/net/Broken/RestApi/UserManagerAPIController.java delete mode 100644 src/main/java/net/Broken/Tools/UserManager/Oauth.java delete mode 100644 src/main/java/net/Broken/Tools/UserManager/PasswordResetUtils.java diff --git a/build.gradle b/build.gradle index 1e29127..e8f0792 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,10 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-log4j2") implementation("org.springframework.boot:spring-boot-starter-oauth2-client") + implementation('io.jsonwebtoken:jjwt-api:0.11.5') + implementation('io.jsonwebtoken:jjwt-impl:0.11.5') + implementation('io.jsonwebtoken:jjwt-gson:0.11.5') + implementation 'org.codehaus.groovy:groovy-all:3.0.8' implementation 'com.sedmelluq:lavaplayer:1.3.77' diff --git a/src/main/java/net/Broken/Api/AuthController.java b/src/main/java/net/Broken/Api/AuthController.java deleted file mode 100644 index ae1e24a..0000000 --- a/src/main/java/net/Broken/Api/AuthController.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.Broken.Api; - -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; -import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/v2") -public class AuthController { - @GetMapping("user") - public String helloUser() { - return "Hello User"; - } - - @GetMapping("admin") - public String helloAdmin() { - return "Hello Admin"; - } - - -} diff --git a/src/main/java/net/Broken/Api/Controllers/AuthController.java b/src/main/java/net/Broken/Api/Controllers/AuthController.java new file mode 100644 index 0000000..5ccb57e --- /dev/null +++ b/src/main/java/net/Broken/Api/Controllers/AuthController.java @@ -0,0 +1,28 @@ +package net.Broken.Api.Controllers; + +import net.Broken.Api.Data.Login; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v2") +public class AuthController { + private final AuthenticationManager authenticationManager; + + public AuthController(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @PostMapping("login/discord") + public String helloUser(@RequestBody Login login) { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(login.redirectUri(), login.code()) + ); + + return "Hello User"; + } + + +} diff --git a/src/main/java/net/Broken/Api/Data/Login.java b/src/main/java/net/Broken/Api/Data/Login.java new file mode 100644 index 0000000..dc8b443 --- /dev/null +++ b/src/main/java/net/Broken/Api/Data/Login.java @@ -0,0 +1,5 @@ +package net.Broken.Api.Data; + +public record Login (String code, String redirectUri){ + +} diff --git a/src/main/java/net/Broken/Api/Security/Data/AccessTokenResponse.java b/src/main/java/net/Broken/Api/Security/Data/AccessTokenResponse.java new file mode 100644 index 0000000..e8447df --- /dev/null +++ b/src/main/java/net/Broken/Api/Security/Data/AccessTokenResponse.java @@ -0,0 +1,9 @@ +package net.Broken.Api.Security.Data; + +public class AccessTokenResponse { + public String access_token; + public String token_type; + public String expires_in; + public String refresh_token; + public String scope; +} diff --git a/src/main/java/net/Broken/Api/Security/Data/DiscordOauthUserInfo.java b/src/main/java/net/Broken/Api/Security/Data/DiscordOauthUserInfo.java new file mode 100644 index 0000000..08398e3 --- /dev/null +++ b/src/main/java/net/Broken/Api/Security/Data/DiscordOauthUserInfo.java @@ -0,0 +1,6 @@ +package net.Broken.Api.Security.Data; + +public class DiscordOauthUserInfo { + public String id; + public String username; +} diff --git a/src/main/java/net/Broken/Api/Security/DiscordAuthenticationProvider.java b/src/main/java/net/Broken/Api/Security/DiscordAuthenticationProvider.java index 1a998f9..de1fca7 100644 --- a/src/main/java/net/Broken/Api/Security/DiscordAuthenticationProvider.java +++ b/src/main/java/net/Broken/Api/Security/DiscordAuthenticationProvider.java @@ -1,18 +1,47 @@ package net.Broken.Api.Security; +import net.Broken.Api.Security.Data.DiscordOauthUserInfo; +import net.Broken.Api.Security.Exception.OAuthLoginFail; +import net.Broken.Api.Security.Services.DiscordOauthService; +import net.Broken.DB.Entity.UserEntity; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; +import java.util.ArrayList; + +@Component public class DiscordAuthenticationProvider implements AuthenticationProvider { + private final Logger logger = LogManager.getLogger(); + private final DiscordOauthService discordOauthService; + + public DiscordAuthenticationProvider(DiscordOauthService discordOauthService) { + this.discordOauthService = discordOauthService; + } + @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { - authentication.getCredentials() - return null; + String redirectUri = authentication.getPrincipal().toString(); + String code = authentication.getCredentials().toString(); + try { + String token = discordOauthService.getAccessToken(code, redirectUri); + DiscordOauthUserInfo discordOauthUserInfo = discordOauthService.getUserInfo(token); + discordOauthService.revokeToken(token); + UserEntity userEntity = discordOauthService.loginOrRegisterDiscordUser(discordOauthUserInfo); + return new UsernamePasswordAuthenticationToken(userEntity, null, new ArrayList<>()); + + } catch (OAuthLoginFail e) { + throw new BadCredentialsException("Bad response form Discord Oauth server ! Code expired ?"); + } } @Override public boolean supports(Class authentication) { - return false; + return authentication.equals(UsernamePasswordAuthenticationToken.class); } } diff --git a/src/main/java/net/Broken/Api/Security/Exception/OAuthLoginFail.java b/src/main/java/net/Broken/Api/Security/Exception/OAuthLoginFail.java new file mode 100644 index 0000000..5e0d387 --- /dev/null +++ b/src/main/java/net/Broken/Api/Security/Exception/OAuthLoginFail.java @@ -0,0 +1,4 @@ +package net.Broken.Api.Security.Exception; + +public class OAuthLoginFail extends Exception{ +} diff --git a/src/main/java/net/Broken/Api/Security/OAuth2UserAgentUtils.java b/src/main/java/net/Broken/Api/Security/OAuth2UserAgentUtils.java deleted file mode 100644 index ca37878..0000000 --- a/src/main/java/net/Broken/Api/Security/OAuth2UserAgentUtils.java +++ /dev/null @@ -1,16 +0,0 @@ -package net.Broken.Api.Security; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.RequestEntity; - -class OAuth2UserAgentUtils { - static RequestEntity withUserAgent(RequestEntity request) { - HttpHeaders headers = new HttpHeaders(); - headers.putAll(request.getHeaders()); - headers.add(HttpHeaders.USER_AGENT, DISCORD_BOT_USER_AGENT); - - return new RequestEntity<>(request.getBody(), headers, request.getMethod(), request.getUrl()); - } - - private static final String DISCORD_BOT_USER_AGENT = "DiscordBot (https://github.com/fourscouts/blog/tree/master/oauth2-discord)"; -} \ No newline at end of file diff --git a/src/main/java/net/Broken/Api/Security/SecurityConfig.java b/src/main/java/net/Broken/Api/Security/SecurityConfig.java index 8fdcf6f..6c7abef 100644 --- a/src/main/java/net/Broken/Api/Security/SecurityConfig.java +++ b/src/main/java/net/Broken/Api/Security/SecurityConfig.java @@ -1,31 +1,42 @@ package net.Broken.Api.Security; +import net.Broken.Api.Security.Services.UnauthorizedHandler; import net.Broken.DB.Repository.UserRepository; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; @EnableWebSecurity @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { - - private final UserRepository userRepository; - - public SecurityConfig(UserRepository userRepository) { - this.userRepository = userRepository; + private final UnauthorizedHandler unauthorizedHandler; + public SecurityConfig(UnauthorizedHandler unauthorizedHandler) { + this.unauthorizedHandler = unauthorizedHandler; } @Override protected void configure(HttpSecurity http) throws Exception { - - http.authorizeRequests() + http.cors().and().csrf().disable() + .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + .authorizeRequests() // Our private endpoints - .anyRequest().authenticated(); + .antMatchers("/api/v2/**").permitAll() + .anyRequest().permitAll(); +// http.authenticationProvider(discordAuthenticationProvider); // http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> { // response.sendError(HttpServletResponse.SC_UNAUTHORIZED); // }); + } + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); } } diff --git a/src/main/java/net/Broken/Api/Security/Services/DiscordOauthService.java b/src/main/java/net/Broken/Api/Security/Services/DiscordOauthService.java new file mode 100644 index 0000000..1eee8f3 --- /dev/null +++ b/src/main/java/net/Broken/Api/Security/Services/DiscordOauthService.java @@ -0,0 +1,140 @@ +package net.Broken.Api.Security.Services; + +import com.google.gson.Gson; +import net.Broken.Api.Security.Data.AccessTokenResponse; +import net.Broken.Api.Security.Data.DiscordOauthUserInfo; +import net.Broken.Api.Security.Exception.OAuthLoginFail; +import net.Broken.DB.Entity.UserEntity; +import net.Broken.DB.Repository.UserRepository; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.HashMap; +import java.util.Map; + +@Service +public class DiscordOauthService { + + private final Logger logger = LogManager.getLogger(); + @Value("${discord.client-id}") + private String clientId; + + @Value("${discord.client-secret}") + private String clientSecret; + + @Value("${discord.token-endpoint}") + private String tokenEndpoint; + + @Value("${discord.tokenRevokeEndpoint}") + private String tokenRevokeEndpoint; + + @Value("${discord.userInfoEnpoint}") + private String userInfoEnpoint; + + private final UserRepository userRepository; + + public DiscordOauthService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public String getAccessToken(String code, String redirectUrl) throws OAuthLoginFail { + logger.debug("[OAUTH] Getting access token"); + HashMap data = new HashMap<>(); + data.put("client_id", this.clientId); + data.put("client_secret", this.clientSecret); + data.put("grant_type", "authorization_code"); + data.put("code", code); + data.put("redirect_uri", redirectUrl); + try { + HttpResponse response = makeFormPost(this.tokenEndpoint, data); + if (response.statusCode() != 200) { + logger.warn("[OAUTH] Invalid response while getting AccessToken: Status Code: " + response.statusCode() + " Body:" + response.body()); + throw new OAuthLoginFail(); + } + Gson gson = new Gson(); + AccessTokenResponse accessTokenResponse = gson.fromJson(response.body(), AccessTokenResponse.class); + return accessTokenResponse.access_token; + } catch (IOException | InterruptedException e) { + logger.catching(e); + throw new OAuthLoginFail(); + } + } + + public DiscordOauthUserInfo getUserInfo(String token) throws OAuthLoginFail { + logger.debug("[OAUTH] Getting user info"); + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(this.userInfoEnpoint)) + .header("Authorization", "Bearer " + token) + .GET() + .build(); + HttpClient client = HttpClient.newHttpClient(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + logger.warn("[OAUTH] Invalid response while getting UserInfo: Status Code: " + response.statusCode() + " Body:" + response.body()); + throw new OAuthLoginFail(); + } + Gson gson = new Gson(); + return gson.fromJson(response.body(), DiscordOauthUserInfo.class); + } catch (IOException | InterruptedException e) { + logger.catching(e); + throw new OAuthLoginFail(); + } + } + + public void revokeToken(String token) { + logger.debug("[OAUTH] Revoking access token"); + HashMap data = new HashMap<>(); + data.put("token", token); + try { + HttpResponse response = makeFormPost(this.tokenRevokeEndpoint, data); + if (response.statusCode() != 200) { + logger.warn("OAUTH] Invalid response while token revocation: Status Code: " + response.statusCode() + " Body:" + response.body()); + } + } catch (IOException | InterruptedException e) { + logger.catching(e); + } + } + + + public UserEntity loginOrRegisterDiscordUser(DiscordOauthUserInfo discordOauthUserInfo) { + return userRepository + .findByJdaId(discordOauthUserInfo.id) + .orElseGet(() -> userRepository.save(new UserEntity(discordOauthUserInfo.username, discordOauthUserInfo.id))); + } + + private String getFormString(HashMap params) throws UnsupportedEncodingException { + StringBuilder result = new StringBuilder(); + boolean first = true; + for (Map.Entry entry : params.entrySet()) { + if (first) + first = false; + else + result.append("&"); + result.append(URLEncoder.encode(entry.getKey(), "UTF-8")); + result.append("="); + result.append(URLEncoder.encode(entry.getValue(), "UTF-8")); + } + return result.toString(); + } + + private HttpResponse makeFormPost(String endpoint, HashMap data) throws IOException, InterruptedException { + HttpRequest.BodyPublisher body = HttpRequest.BodyPublishers.ofString(getFormString(data)); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(tokenEndpoint)) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(body) + .build(); + HttpClient client = HttpClient.newHttpClient(); + return client.send(request, HttpResponse.BodyHandlers.ofString()); + } +} diff --git a/src/main/java/net/Broken/Api/Security/DiscordUserDetailsService.java b/src/main/java/net/Broken/Api/Security/Services/DiscordUserDetailsService.java similarity index 68% rename from src/main/java/net/Broken/Api/Security/DiscordUserDetailsService.java rename to src/main/java/net/Broken/Api/Security/Services/DiscordUserDetailsService.java index bd7d044..12007a4 100644 --- a/src/main/java/net/Broken/Api/Security/DiscordUserDetailsService.java +++ b/src/main/java/net/Broken/Api/Security/Services/DiscordUserDetailsService.java @@ -1,14 +1,12 @@ -package net.Broken.Api.Security; +package net.Broken.Api.Security.Services; -import net.Broken.DB.Entity.UserEntity; +import net.Broken.Api.Security.DiscordUserPrincipal; import net.Broken.DB.Repository.UserRepository; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; -import java.util.List; - @Service public class DiscordUserDetailsService implements UserDetailsService { private final UserRepository userRepository; @@ -20,10 +18,9 @@ public class DiscordUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - List user = userRepository.findByName(username); - if(user.isEmpty()){ - throw new UsernameNotFoundException(username); - } - return new DiscordUserPrincipal(user.get(0)); + return new DiscordUserPrincipal( + userRepository.findByJdaId(username) + .orElseThrow(() -> new UsernameNotFoundException(username)) + ); } } diff --git a/src/main/java/net/Broken/Api/Security/Services/UnauthorizedHandler.java b/src/main/java/net/Broken/Api/Security/Services/UnauthorizedHandler.java new file mode 100644 index 0000000..7debb4e --- /dev/null +++ b/src/main/java/net/Broken/Api/Security/Services/UnauthorizedHandler.java @@ -0,0 +1,38 @@ +package net.Broken.Api.Security.Services; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Component +public class UnauthorizedHandler implements AuthenticationEntryPoint { + private final Logger logger = LogManager.getLogger(); + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + logger.error("[API] Unauthorized error: {}", authException.getMessage()); + + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + final Map body = new HashMap<>(); + body.put("status", HttpServletResponse.SC_UNAUTHORIZED); + body.put("error", "Unauthorized"); + body.put("message", authException.getMessage()); + body.put("path", request.getServletPath()); + + final ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(response.getOutputStream(), body); + } +} diff --git a/src/main/java/net/Broken/Api/Services/DiscordOauthService.java b/src/main/java/net/Broken/Api/Services/DiscordOauthService.java deleted file mode 100644 index 39e9466..0000000 --- a/src/main/java/net/Broken/Api/Services/DiscordOauthService.java +++ /dev/null @@ -1,46 +0,0 @@ -package net.Broken.Api.Services; - -import com.google.gson.Gson; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.HashMap; - -@Service -public class DiscordOauthService { - - @Value("${discord.client-id}") - private String clientId; - - @Value("${discord.client-secret}") - private String clientSecret; - - @Value("${discord.token-endpoint}") - private String tokenEndpoint; - - public String getAccessToken(String code, String redirectUrl){ - HashMap data = new HashMap<>(); - data.put("client_id", this.clientId); - data.put("client_secret", this.clientSecret); - data.put("grant_type", "authorization_code"); - data.put("code", code); - data.put("redirect_uri", redirectUrl); - - Gson gson = new Gson(); - HttpRequest.BodyPublisher body = HttpRequest.BodyPublishers.ofString(gson.toJson(data)); - - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(tokenEndpoint)) - .header("Content-Type", "application/json") - .POST(body) - .build(); - HttpClient client = HttpClient.newHttpClient(); - client.send(request, HttpResponse.BodyHandlers.ofString()); - - } -} diff --git a/src/main/java/net/Broken/Api/Services/DiscordUserDetailService.java b/src/main/java/net/Broken/Api/Services/DiscordUserDetailService.java deleted file mode 100644 index b637054..0000000 --- a/src/main/java/net/Broken/Api/Services/DiscordUserDetailService.java +++ /dev/null @@ -1,12 +0,0 @@ -package net.Broken.Api.Services; - -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -public class DiscordUserDetailService implements UserDetailsService { - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - return null; - } -} diff --git a/src/main/java/net/Broken/DB/Entity/UserEntity.java b/src/main/java/net/Broken/DB/Entity/UserEntity.java index 7101522..d6110ba 100644 --- a/src/main/java/net/Broken/DB/Entity/UserEntity.java +++ b/src/main/java/net/Broken/DB/Entity/UserEntity.java @@ -21,20 +21,15 @@ public class UserEntity { private String name; + @Column(unique=true) private String jdaId; - private String apiToken; - private boolean isBotAdmin = false; @JsonIgnore @OneToMany(fetch = FetchType.EAGER, mappedBy = "user") private List userStats; - @JsonIgnore - private String password; - - @OneToMany(mappedBy = "user") private List playlists; @@ -42,36 +37,17 @@ public class UserEntity { public UserEntity() { } - public UserEntity(PendingUserEntity pendingUserEntity, String apiToken) { - this.name = pendingUserEntity.getName(); - this.jdaId = pendingUserEntity.getJdaId(); - this.password = pendingUserEntity.getPassword(); - this.apiToken = apiToken; - } - - public UserEntity(User user, PasswordEncoder passwordEncoder) { + public UserEntity(User user) { this.name = user.getName(); this.jdaId = user.getId(); - this.apiToken = UserUtils.getInstance().generateApiToken(); - this.password = passwordEncoder.encode(UserUtils.getInstance().generateCheckToken()); } - public UserEntity(String name, String id, PasswordEncoder passwordEncoder) { + public UserEntity(String name, String id) { this.name = name; this.jdaId = id; - this.apiToken = UserUtils.getInstance().generateApiToken(); - this.password = passwordEncoder.encode(UserUtils.getInstance().generateCheckToken()); } - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - public Integer getId() { return id; } @@ -96,14 +72,6 @@ public class UserEntity { this.jdaId = jdaId; } - public String getApiToken() { - return apiToken; - } - - public void setApiToken(String apiToken) { - this.apiToken = apiToken; - } - public List getPlaylists() { return playlists; } diff --git a/src/main/java/net/Broken/DB/Repository/UserRepository.java b/src/main/java/net/Broken/DB/Repository/UserRepository.java index 1b9410f..b60d126 100644 --- a/src/main/java/net/Broken/DB/Repository/UserRepository.java +++ b/src/main/java/net/Broken/DB/Repository/UserRepository.java @@ -4,6 +4,7 @@ import net.Broken.DB.Entity.UserEntity; import org.springframework.data.repository.CrudRepository; import java.util.List; +import java.util.Optional; /** * Repository for UserEntity @@ -12,7 +13,5 @@ import java.util.List; public interface UserRepository extends CrudRepository { List findByName(String name); - List findByJdaId(String jdaId); - - List findByApiToken(String apiToken); + Optional findByJdaId(String jdaId); } diff --git a/src/main/java/net/Broken/RestApi/PlaylistAPIController.java b/src/main/java/net/Broken/RestApi/PlaylistAPIController.java index 9e67669..403c857 100644 --- a/src/main/java/net/Broken/RestApi/PlaylistAPIController.java +++ b/src/main/java/net/Broken/RestApi/PlaylistAPIController.java @@ -43,33 +43,33 @@ public class PlaylistAPIController { } - @RequestMapping("/myPlaylist") - public List myPlaylist(@CookieValue(value = "token", defaultValue = "") String token) { - if (token.isEmpty()) - return null; - else { - UserEntity user = userRepository.findByApiToken(token).get(0); - return user.getPlaylists(); - } +// @RequestMapping("/myPlaylist") +// public List myPlaylist(@CookieValue(value = "token", defaultValue = "") String token) { +// if (token.isEmpty()) +// return null; +// else { +// UserEntity user = userRepository.findByApiToken(token).get(0); +// return user.getPlaylists(); +// } +// +// } - } - - @RequestMapping("/createPlaylist") - public ResponseEntity createPlaylist(@CookieValue(value = "token", defaultValue = "") String token, @RequestBody CreatePlaylistData data) { - - if (token.isEmpty()) - return new ResponseEntity<>(new PlaylistResponseData("Unknown Token!\nPlease Re-connect.", "token"), HttpStatus.UNAUTHORIZED); - else { - UserEntity user = userRepository.findByApiToken(token).get(0); - PlaylistEntity playlistEntity = new PlaylistEntity(data.name, user); - playlistEntity = playlistRepository.save(playlistEntity); - user.addPlaylist(playlistEntity); - userRepository.save(user); - return new ResponseEntity<>(new PlaylistResponseData("Ok", playlistEntity), HttpStatus.OK); - } - - - } +// @RequestMapping("/createPlaylist") +// public ResponseEntity createPlaylist(@CookieValue(value = "token", defaultValue = "") String token, @RequestBody CreatePlaylistData data) { +// +// if (token.isEmpty()) +// return new ResponseEntity<>(new PlaylistResponseData("Unknown Token!\nPlease Re-connect.", "token"), HttpStatus.UNAUTHORIZED); +// else { +// UserEntity user = userRepository.findByApiToken(token).get(0); +// PlaylistEntity playlistEntity = new PlaylistEntity(data.name, user); +// playlistEntity = playlistRepository.save(playlistEntity); +// user.addPlaylist(playlistEntity); +// userRepository.save(user); +// return new ResponseEntity<>(new PlaylistResponseData("Ok", playlistEntity), HttpStatus.OK); +// } +// +// +// } @RequestMapping("/addToPlaylist") public ResponseEntity addToPlaylist(@CookieValue(value = "token", defaultValue = "") String token, @RequestBody AddToPlaylistData data) { diff --git a/src/main/java/net/Broken/RestApi/UserManagerAPIController.java b/src/main/java/net/Broken/RestApi/UserManagerAPIController.java deleted file mode 100644 index ebd2c47..0000000 --- a/src/main/java/net/Broken/RestApi/UserManagerAPIController.java +++ /dev/null @@ -1,171 +0,0 @@ -package net.Broken.RestApi; - -import net.Broken.DB.Entity.UserEntity; -import net.Broken.DB.Repository.PendingUserRepository; -import net.Broken.DB.Repository.UserRepository; -import net.Broken.MainBot; -import net.Broken.RestApi.Data.UserManager.GuildInfo; -import net.Broken.RestApi.Data.UserManager.UserConnectionData; -import net.Broken.RestApi.Data.UserManager.UserInfoData; -import net.Broken.Tools.UserManager.Exceptions.PasswordNotMatchException; -import net.Broken.Tools.UserManager.Exceptions.UnknownTokenException; -import net.Broken.Tools.UserManager.Exceptions.UserNotFoundException; -import net.Broken.Tools.UserManager.Oauth; -import net.Broken.Tools.UserManager.Stats.GuildStatsPack; -import net.Broken.Tools.UserManager.Stats.UserStatsUtils; -import net.Broken.Tools.UserManager.UserUtils; -import net.dv8tion.jda.api.Permission; -import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.User; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.web.bind.annotation.*; - -import java.util.ArrayList; -import java.util.List; - - -/** - * Rest Api controller for /api/userManagement - */ -@RestController -@RequestMapping("/api/userManagement") -public class UserManagerAPIController { - final - PendingUserRepository pendingUserRepository; - final - UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - Logger logger = LogManager.getLogger(); - UserUtils userUtils = UserUtils.getInstance(); - - @Autowired - public UserManagerAPIController(PendingUserRepository pendingUserRepository, UserRepository userRepository, PasswordEncoder passwordEncoder) { - this.pendingUserRepository = pendingUserRepository; - this.userRepository = userRepository; - this.passwordEncoder = passwordEncoder; - } - - -// @RequestMapping(value = "/preRegister", method = RequestMethod.POST) -// public ResponseEntity command(@RequestBody UserInfoData data) { -// if (data != null && data.name != null) { -// try { -// String id = userUtils.sendCheckToken(pendingUserRepository, userRepository, passwordEncoder, data); -// return new ResponseEntity<>(new CheckResposeData(true, data.name, "Message sent", id), HttpStatus.OK); -// } catch (UserNotFoundException e) { -// logger.warn("User \"" + data.name + "\" not found!"); -// return new ResponseEntity<>(new CheckResposeData(false, data.name, "User not found on server!", ""), HttpStatus.NOT_FOUND); -// } catch (PasswordNotMatchException userAlreadyRegistered) { -// return new ResponseEntity<>(new CheckResposeData(false, data.name, "User already registered in pending database and password not match!", ""), HttpStatus.NOT_ACCEPTABLE); -// -// } catch (UserAlreadyRegistered userAlreadyRegistered) { -// return new ResponseEntity<>(new CheckResposeData(false, data.name, "User already registered in database!", ""), HttpStatus.NOT_ACCEPTABLE); -// } -// } else { -// return new ResponseEntity<>(new CheckResposeData(false, "", "Missing parameter(s)", ""), HttpStatus.BAD_REQUEST); -// } -// } - -// @RequestMapping(value = "/confirmAccount", method = RequestMethod.POST) -// public ResponseEntity confirAccount(@RequestBody ConfirmData data) { -// try { -// PendingUserEntity pUser = userUtils.confirmCheckToken(pendingUserRepository, Integer.parseInt(data.id), data.checkToken); -// UserEntity user = new UserEntity(pUser, userUtils.generateApiToken()); -// userRepository.save(user); -// pendingUserRepository.delete(pUser); -// -// return new ResponseEntity<>(new UserConnectionData(true, user.getName(), user.getApiToken(), ""), HttpStatus.OK); -// } catch (TokenNotMatch tokenNotMatch) { -// logger.warn("Pre token not match for " + data.id + "!"); -// return new ResponseEntity<>(new UserConnectionData(false, "Token not match!", "token"), HttpStatus.NOT_ACCEPTABLE); -// } catch (UserNotFoundException e) { -// logger.warn("Id not found in DB (" + data.id + ")"); -// return new ResponseEntity<>(new UserConnectionData(false, "User not found on DB!", "user"), HttpStatus.NOT_ACCEPTABLE); -// } -// } - - @RequestMapping(value = "/requestToken", method = RequestMethod.POST) - public ResponseEntity requestToken(@RequestBody UserInfoData data) { - try { - UserEntity user = userUtils.getUser(userRepository, passwordEncoder, data); - return new ResponseEntity<>(new UserConnectionData(true, user.getName(), user.getApiToken(), ""), HttpStatus.OK); - - } catch (UserNotFoundException e) { - return new ResponseEntity<>(new UserConnectionData(false, "User not registered!", "user"), HttpStatus.NOT_ACCEPTABLE); - } catch (PasswordNotMatchException e) { - return new ResponseEntity<>(new UserConnectionData(false, "Wrong user name or password!", "password"), HttpStatus.NOT_ACCEPTABLE); - } - } - - @RequestMapping(value = "/getGuilds", method = RequestMethod.GET) - public ResponseEntity> getGuilds(@CookieValue("token") String token) { - try { - UserEntity userE = userUtils.getUserWithApiToken(userRepository, token); - User user = MainBot.jda.getUserById(userE.getJdaId()); - List temp = new ArrayList<>(); - if (user != null) { - for (Guild guild : user.getMutualGuilds()) { - - temp.add(new GuildInfo(guild.getName(), guild.getId(), guild.getMember(user).hasPermission(Permission.ADMINISTRATOR), guild.getIconUrl())); - } - } - - return new ResponseEntity<>(temp, HttpStatus.OK); - - - } catch (UnknownTokenException e) { - logger.catching(e); - return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); - } - - } - - - @RequestMapping(value = "/oauthLogin", method = RequestMethod.POST) - public ResponseEntity oauthLogin(@RequestParam(value = "token") String discordToken) { - logger.debug(discordToken); - UserEntity user = Oauth.getInstance().getUserEntity(discordToken, userRepository, passwordEncoder); - logger.info("OAuth login for " + user.getName()); - return new ResponseEntity<>(new UserConnectionData(true, user.getName(), user.getApiToken(), ""), HttpStatus.OK); - - - } - - - @RequestMapping(value = "/checkToken", method = RequestMethod.GET) - public ResponseEntity checkToken(@CookieValue(value = "token") String token) { - try { - userUtils.getUserWithApiToken(userRepository, token); - return new ResponseEntity(HttpStatus.OK); - } catch (UnknownTokenException e) { - logger.info("Token check fail"); - return new ResponseEntity(HttpStatus.UNAUTHORIZED); - } - } - - @RequestMapping(value = "/getStatsPack", method = RequestMethod.GET) - public ResponseEntity getStatsPack(@CookieValue(value = "token") String token, @RequestParam(value = "guild") String guildID) { - try { - UserEntity user = userUtils.getUserWithApiToken(userRepository, token); - Guild guild = MainBot.jda.getGuildById(guildID); - if (guild == null) { - logger.warn("Request whit no guild!"); - return new ResponseEntity(HttpStatus.BAD_REQUEST); - } - - return new ResponseEntity<>(UserStatsUtils.getINSTANCE().getStatPack(user, guildID), HttpStatus.OK); - - - } catch (UnknownTokenException e) { - logger.info("Token check fail"); - return new ResponseEntity(HttpStatus.UNAUTHORIZED); - } - } - - -} diff --git a/src/main/java/net/Broken/Tools/UserManager/Oauth.java b/src/main/java/net/Broken/Tools/UserManager/Oauth.java deleted file mode 100644 index 7c388d0..0000000 --- a/src/main/java/net/Broken/Tools/UserManager/Oauth.java +++ /dev/null @@ -1,75 +0,0 @@ -package net.Broken.Tools.UserManager; - -import net.Broken.DB.Entity.UserEntity; -import net.Broken.DB.Repository.UserRepository; -import net.Broken.MainBot; -import net.dv8tion.jda.api.entities.User; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.json.JSONObject; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.List; - -public class Oauth { - private static Oauth INSTANCE = new Oauth(); - Logger logger = LogManager.getLogger(); - private String baseUrl = "https://discordapp.com/api"; - private String mePath = "/users/@me"; - - public static Oauth getInstance() { - return INSTANCE; - } - - private JSONObject getUserId(String token) { - StringBuffer content = new StringBuffer(); - try { - String httpsURL = baseUrl + mePath; - URL myUrl = new URL(httpsURL); - HttpURLConnection con = (HttpURLConnection) myUrl.openConnection(); - con.setRequestProperty("Authorization", "Bearer " + token); - con.setRequestProperty("User-Agent", "DiscordBot (claptrapbot.com, 0.1)"); - con.setRequestMethod("GET"); - logger.debug("Response code: " + con.getResponseCode()); - BufferedReader in = new BufferedReader( - new InputStreamReader(con.getInputStream())); - String inputLine; - - while ((inputLine = in.readLine()) != null) { - content.append(inputLine); - } - in.close(); - - } catch (IOException e) { - e.printStackTrace(); - } - JSONObject json = new JSONObject(content.toString()); - logger.debug(json); - - - return json; - } - - - public UserEntity getUserEntity(String token, UserRepository userRepository, PasswordEncoder passwordEncoder) { - JSONObject discorResponse = getUserId(token); - List userEntitys = userRepository.findByJdaId(discorResponse.getString("id")); - if (userEntitys.size() != 0) { - return userEntitys.get(0); - } else { - User jdaUser = MainBot.jda.getUserById(discorResponse.getString("id")); - UserEntity user; - if (jdaUser == null) - user = new UserEntity(discorResponse.getString("username"), discorResponse.getString("id"), passwordEncoder); - else - user = new UserEntity(MainBot.jda.getUserById(discorResponse.getString("id")), passwordEncoder); - user = userRepository.save(user); - return user; - } - } -} diff --git a/src/main/java/net/Broken/Tools/UserManager/PasswordResetUtils.java b/src/main/java/net/Broken/Tools/UserManager/PasswordResetUtils.java deleted file mode 100644 index 8700330..0000000 --- a/src/main/java/net/Broken/Tools/UserManager/PasswordResetUtils.java +++ /dev/null @@ -1,64 +0,0 @@ -package net.Broken.Tools.UserManager; - -import net.Broken.DB.Entity.PendingPwdResetEntity; -import net.Broken.DB.Entity.UserEntity; -import net.Broken.DB.Repository.PendingPwdResetRepository; -import net.Broken.DB.Repository.UserRepository; -import net.Broken.SpringContext; -import net.Broken.Tools.UserManager.Exceptions.TokenNotMatch; -import net.Broken.Tools.UserManager.Exceptions.UserNotFoundException; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.springframework.context.ApplicationContext; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.List; - -public class PasswordResetUtils { - private static PasswordResetUtils INSTANCE = new PasswordResetUtils(); - private Logger logger = LogManager.getLogger(); - private PasswordEncoder passwordEncoder; - private PendingPwdResetRepository pendingPwdResetRepository; - private UserRepository userRepository; - - /** - * Private default constructor - */ - private PasswordResetUtils() { - ApplicationContext context = SpringContext.getAppContext(); - passwordEncoder = (PasswordEncoder) context.getBean("passwordEncoder"); - pendingPwdResetRepository = (PendingPwdResetRepository) context.getBean("pendingPwdResetRepository"); - userRepository = (UserRepository) context.getBean("userRepository"); - } - - - /** - * Singleton - * - * @return Unique PasswordResetUtils instance - */ - public static PasswordResetUtils getInstance() { - return INSTANCE; - } - - public String resetRequest(UserEntity userEntity) { - String token = UserUtils.getInstance().generateCheckToken(); - String encodedToken = passwordEncoder.encode(token); - PendingPwdResetEntity entity = new PendingPwdResetEntity(userEntity, encodedToken); - pendingPwdResetRepository.save(entity); - return encodedToken; - } - - public void changePass(UserEntity userEntity, String token, String newPassword) throws UserNotFoundException, TokenNotMatch { - List dbResults = pendingPwdResetRepository.findByUserEntity(userEntity); - if (dbResults.size() == 0) - throw new UserNotFoundException(); - PendingPwdResetEntity pendingPwdReset = dbResults.get(0); - if (!passwordEncoder.matches(token, pendingPwdReset.getSecurityToken())) - throw new TokenNotMatch(); - - userEntity.setPassword(passwordEncoder.encode(newPassword)); - userRepository.save(userEntity); - } - -} diff --git a/src/main/java/net/Broken/Tools/UserManager/Stats/UserStatsUtils.java b/src/main/java/net/Broken/Tools/UserManager/Stats/UserStatsUtils.java index 1b9ca6b..3fafa50 100644 --- a/src/main/java/net/Broken/Tools/UserManager/Stats/UserStatsUtils.java +++ b/src/main/java/net/Broken/Tools/UserManager/Stats/UserStatsUtils.java @@ -24,6 +24,7 @@ import java.awt.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Optional; public class UserStatsUtils { @@ -105,27 +106,16 @@ public class UserStatsUtils { } public List getUserStats(User user) { - UserEntity userEntity; - List userList = userRepository.findByJdaId(user.getId()); - if (userList.size() == 0) { - logger.debug("User not registered, generate it. User: " + user.getName() + " " + user.getDiscriminator()); - userEntity = genUserEntity(user); - } else - userEntity = userList.get(0); - + UserEntity userEntity = userRepository.findByJdaId(user.getId()) + .orElseGet(() -> genUserEntity(user)); return getUserStats(userEntity); } public UserStats getGuildUserStats(Member member) { - List userEntityList = userRepository.findByJdaId(member.getUser().getId()); - UserEntity userEntity; - if (userEntityList.size() == 0) { - logger.debug("UserEntity not found for user " + member.getNickname()); - userEntity = genUserEntity(member.getUser()); - } else - userEntity = userEntityList.get(0); + UserEntity userEntity = userRepository.findByJdaId(member.getUser().getId()) + .orElseGet(() -> genUserEntity(member.getUser())); List userStatsList = userStatsRepository.findByUserAndGuildId(userEntity, member.getGuild().getId()); if (userStatsList.size() == 0) { @@ -177,7 +167,7 @@ public class UserStatsUtils { private UserEntity genUserEntity(User user) { - UserEntity userEntity = new UserEntity(user, passwordEncoder); + UserEntity userEntity = new UserEntity(user); return userRepository.save(userEntity); } diff --git a/src/main/java/net/Broken/Tools/UserManager/UserUtils.java b/src/main/java/net/Broken/Tools/UserManager/UserUtils.java index c3b3503..301a1d8 100644 --- a/src/main/java/net/Broken/Tools/UserManager/UserUtils.java +++ b/src/main/java/net/Broken/Tools/UserManager/UserUtils.java @@ -39,32 +39,6 @@ public class UserUtils { public static UserUtils getInstance() { return INSTANCE; } - /** - * Get user Entity - * - * @param userRepository User DB interface - * @param passwordEncoder Password encoder - * @param userInfoData Received data - * @return User Entity - * @throws UserNotFoundException User not found in User DB - * @throws PasswordNotMatchException Given password not match - */ - public UserEntity getUser(UserRepository userRepository, PasswordEncoder passwordEncoder, UserInfoData userInfoData) throws UserNotFoundException, PasswordNotMatchException { - List users = userRepository.findByName(userInfoData.name); - if (users.size() < 1) { - logger.warn("Login with unknown username: " + userInfoData.name); - throw new UserNotFoundException(); - } else { - UserEntity user = users.get(0); - if (passwordEncoder.matches(userInfoData.password, user.getPassword())) { - logger.info("Login successful for " + user.getName()); - return user; - } else { - logger.warn("Login fail for " + user.getName() + ", wrong password!"); - throw new PasswordNotMatchException(); - } - } - } /** * return token's UserEntity @@ -75,36 +49,12 @@ public class UserUtils { * @throws UnknownTokenException Can't find token on User DB */ public UserEntity getUserWithApiToken(UserRepository userRepository, String token) throws UnknownTokenException { - List users = userRepository.findByApiToken(token); - if (users.size() > 0) { - return users.get(0); - } else - throw new UnknownTokenException(); +// List users = userRepository.findByApiToken(token); +// if (users.size() > 0) { +// return users.get(0); +// } else +// throw new UnknownTokenException(); + return null; } - - /** - * Generate API Token - * - * @return UUID String TODO Find something more secure - */ - public String generateApiToken() { - return UUID.randomUUID().toString(); - } - - /** - * Generate short check token - * - * @return check token as string - */ - public String generateCheckToken() { - SecureRandom random = new SecureRandom(); - long longToken = Math.abs(random.nextLong()); - String randomStr = Long.toString(longToken, 16); - randomStr = randomStr.substring(0, 4); - randomStr = randomStr.toUpperCase(); - return randomStr; - } - - } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4f17324..6f7ebe4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -38,4 +38,6 @@ server: discord: client-id: ${CLIENT_ID} client-secret: ${CLIENT_SECRET} - token-endpoint: https://discord.com/api/oauth2/token \ No newline at end of file + token-endpoint: https://discord.com/api/oauth2/token + tokenRevokeEndpoint: https://discord.com/api/oauth2/token/revoke + userInfoEnpoint: https://discord.com/api/users/@me \ No newline at end of file