Discord OAuth in Java: Complete Integration Guide

Discord OAuth allows applications to authenticate users and access Discord API resources. This implementation provides a complete Java solution for Discord OAuth 2.0 integration.

Discord OAuth Overview

Discord OAuth provides:

  • User authentication via Discord accounts
  • Access to user data (profile, guilds, connections)
  • Bot integration for server management
  • Webhook and message capabilities

Dependencies and Setup

Maven Configuration:

<dependencies>
<!-- Spring Boot OAuth2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Discord API Client -->
<dependency>
<groupId>com.github.napstr</groupId>
<artifactId>logback-discord-appender</artifactId>
<version>1.0.0</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- JWT for token validation -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
</dependencies>

Configuration Properties

DiscordOAuthProperties.java:

@Configuration
@ConfigurationProperties(prefix = "discord.oauth")
@Data
public class DiscordOAuthProperties {
// OAuth2 configuration
private String clientId;
private String clientSecret;
private String redirectUri = "http://localhost:8080/login/oauth2/code/discord";
private List<String> scope = Arrays.asList("identify", "email", "guilds", "guilds.join");
// Discord API endpoints
private String authUrl = "https://discord.com/api/oauth2/authorize";
private String tokenUrl = "https://discord.com/api/oauth2/token";
private String userUrl = "https://discord.com/api/users/@me";
private String guildsUrl = "https://discord.com/api/users/@me/guilds";
private String connectionsUrl = "https://discord.com/api/users/@me/connections";
// Application settings
private String botToken; // For bot operations
private long defaultGuildId; // Default guild for operations
private boolean autoJoinGuild = false;
private String joinGuildId;
private String joinGuildRoleId;
// Security settings
private int tokenRefreshMargin = 300; // 5 minutes
private boolean validateState = true;
private String stateSecret;
public String getAuthorizationUrl(String state) {
return String.format("%s?client_id=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s",
authUrl, clientId, URLEncoder.encode(redirectUri, StandardCharsets.UTF_8),
String.join("%20", scope), state);
}
public String getScopesAsString() {
return String.join(" ", scope);
}
}

OAuth Token Management

DiscordToken.java:

@Data
@Builder
public class DiscordToken {
private String accessToken;
private String tokenType;
private String refreshToken;
private long expiresIn;
private Instant issuedAt;
private String scope;
public boolean isExpired() {
return Instant.now().isAfter(issuedAt.plusSeconds(expiresIn));
}
public boolean needsRefresh() {
return Instant.now().isAfter(issuedAt.plusSeconds(expiresIn - 300)); // 5 min margin
}
public static DiscordToken fromMap(Map<String, Object> tokenResponse) {
return DiscordToken.builder()
.accessToken((String) tokenResponse.get("access_token"))
.tokenType((String) tokenResponse.get("token_type"))
.refreshToken((String) tokenResponse.get("refresh_token"))
.expiresIn(Long.parseLong(tokenResponse.get("expires_in").toString()))
.issuedAt(Instant.now())
.scope((String) tokenResponse.get("scope"))
.build();
}
}

TokenService.java:

@Service
@Slf4j
public class TokenService {
private final DiscordOAuthProperties properties;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
public TokenService(DiscordOAuthProperties properties) {
this.properties = properties;
this.restTemplate = createRestTemplate();
this.objectMapper = new ObjectMapper();
}
public DiscordToken exchangeCodeForToken(String authorizationCode) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setBasicAuth(properties.getClientId(), properties.getClientSecret());
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("client_id", properties.getClientId());
body.add("client_secret", properties.getClientSecret());
body.add("grant_type", "authorization_code");
body.add("code", authorizationCode);
body.add("redirect_uri", properties.getRedirectUri());
body.add("scope", properties.getScopesAsString());
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(
properties.getTokenUrl(), request, Map.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
return DiscordToken.fromMap(response.getBody());
} else {
throw new DiscordOAuthException("Failed to exchange code for token: " + response.getStatusCode());
}
} catch (Exception e) {
throw new DiscordOAuthException("Token exchange failed", e);
}
}
public DiscordToken refreshToken(String refreshToken) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setBasicAuth(properties.getClientId(), properties.getClientSecret());
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("client_id", properties.getClientId());
body.add("client_secret", properties.getClientSecret());
body.add("grant_type", "refresh_token");
body.add("refresh_token", refreshToken);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(
properties.getTokenUrl(), request, Map.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
return DiscordToken.fromMap(response.getBody());
} else {
throw new DiscordOAuthException("Failed to refresh token: " + response.getStatusCode());
}
} catch (Exception e) {
throw new DiscordOAuthException("Token refresh failed", e);
}
}
public void revokeToken(String token) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("token", token);
body.add("client_id", properties.getClientId());
body.add("client_secret", properties.getClientSecret());
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
restTemplate.postForEntity("https://discord.com/api/oauth2/token/revoke", 
request, Void.class);
log.info("Successfully revoked token");
} catch (Exception e) {
log.warn("Failed to revoke token", e);
}
}
private RestTemplate createRestTemplate() {
RestTemplate template = new RestTemplate();
// Add error handler
template.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getStatusCode().is4xxClientError()) {
log.error("Discord API client error: {}", response.getStatusCode());
}
super.handleError(response);
}
});
return template;
}
}

Discord User Data Models

DiscordUser.java:

@Data
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public class DiscordUser {
private String id;
private String username;
private String discriminator;
private String globalName;
private String avatar;
private String banner;
private String accentColor;
private boolean bot;
private boolean system;
private boolean mfaEnabled;
private String locale;
private boolean verified;
private String email;
private Integer flags;
private Integer premiumType;
private Integer publicFlags;
private String avatarDecoration;
@JsonProperty("created_at")
private Instant createdAt;
public String getFullUsername() {
return username + (discriminator != null && !"0".equals(discriminator) ? "#" + discriminator : "");
}
public String getAvatarUrl() {
if (avatar == null) {
return null;
}
return String.format("https://cdn.discordapp.com/avatars/%s/%s.png", id, avatar);
}
public String getBannerUrl() {
if (banner == null) {
return null;
}
return String.format("https://cdn.discordapp.com/banners/%s/%s.png", id, banner);
}
}

DiscordGuild.java:

@Data
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public class DiscordGuild {
private String id;
private String name;
private String icon;
private boolean owner;
private Integer permissions;
private String[] features;
private String permissionsNew;
public String getIconUrl() {
if (icon == null) {
return null;
}
return String.format("https://cdn.discordapp.com/icons/%s/%s.png", id, icon);
}
public boolean hasPermission(String permission) {
// Check if user has specific permission in guild
// This would parse the permissions integer
return true; // Simplified for example
}
}

DiscordConnection.java:

@Data
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public class DiscordConnection {
private String id;
private String name;
private String type;
private boolean revoked;
private Map<String, Object> integrations;
private boolean verified;
private boolean friendSync;
private boolean showActivity;
private Integer visibility;
}

Discord API Service

DiscordApiService.java:

@Service
@Slf4j
public class DiscordApiService {
private final DiscordOAuthProperties properties;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
public DiscordApiService(DiscordOAuthProperties properties) {
this.properties = properties;
this.restTemplate = createRestTemplate();
this.objectMapper = new ObjectMapper();
}
public DiscordUser getCurrentUser(String accessToken) {
try {
HttpHeaders headers = createHeaders(accessToken);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<DiscordUser> response = restTemplate.exchange(
properties.getUserUrl(), HttpMethod.GET, entity, DiscordUser.class);
return response.getBody();
} catch (Exception e) {
throw new DiscordApiException("Failed to get current user", e);
}
}
public List<DiscordGuild> getUserGuilds(String accessToken) {
try {
HttpHeaders headers = createHeaders(accessToken);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<DiscordGuild[]> response = restTemplate.exchange(
properties.getGuildsUrl(), HttpMethod.GET, entity, DiscordGuild[].class);
return Arrays.asList(response.getBody());
} catch (Exception e) {
throw new DiscordApiException("Failed to get user guilds", e);
}
}
public List<DiscordConnection> getUserConnections(String accessToken) {
try {
HttpHeaders headers = createHeaders(accessToken);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<DiscordConnection[]> response = restTemplate.exchange(
properties.getConnectionsUrl(), HttpMethod.GET, entity, DiscordConnection[].class);
return Arrays.asList(response.getBody());
} catch (Exception e) {
throw new DiscordApiException("Failed to get user connections", e);
}
}
public void addUserToGuild(String accessToken, String userId, String guildId, String roleId) {
try {
HttpHeaders headers = createHeaders(properties.getBotToken());
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> body = new HashMap<>();
body.put("access_token", accessToken);
if (roleId != null) {
body.put("roles", Collections.singletonList(roleId));
}
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
String url = String.format("https://discord.com/api/guilds/%s/members/%s", guildId, userId);
restTemplate.put(url, entity);
log.info("Added user {} to guild {}", userId, guildId);
} catch (Exception e) {
throw new DiscordApiException("Failed to add user to guild", e);
}
}
public void sendDirectMessage(String accessToken, String userId, String message) {
try {
// First, create DM channel
HttpHeaders headers = createHeaders(accessToken);
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> channelBody = new HashMap<>();
channelBody.put("recipient_id", userId);
HttpEntity<Map<String, Object>> channelEntity = new HttpEntity<>(channelBody, headers);
ResponseEntity<Map> channelResponse = restTemplate.postForEntity(
"https://discord.com/api/users/@me/channels", channelEntity, Map.class);
if (channelResponse.getStatusCode().is2xxSuccessful() && channelResponse.getBody() != null) {
String channelId = (String) channelResponse.getBody().get("id");
// Send message to DM channel
Map<String, Object> messageBody = new HashMap<>();
messageBody.put("content", message);
HttpEntity<Map<String, Object>> messageEntity = new HttpEntity<>(messageBody, headers);
String messageUrl = String.format("https://discord.com/api/channels/%s/messages", channelId);
restTemplate.postForEntity(messageUrl, messageEntity, Map.class);
log.info("Sent DM to user {}", userId);
}
} catch (Exception e) {
throw new DiscordApiException("Failed to send direct message", e);
}
}
public DiscordGuild getGuild(String guildId) {
try {
HttpHeaders headers = createHeaders(properties.getBotToken());
HttpEntity<String> entity = new HttpEntity<>(headers);
String url = String.format("https://discord.com/api/guilds/%s", guildId);
ResponseEntity<DiscordGuild> response = restTemplate.exchange(
url, HttpMethod.GET, entity, DiscordGuild.class);
return response.getBody();
} catch (Exception e) {
throw new DiscordApiException("Failed to get guild information", e);
}
}
private HttpHeaders createHeaders(String token) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + token);
return headers;
}
private RestTemplate createRestTemplate() {
RestTemplate template = new RestTemplate();
// Configure timeout
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(5000);
factory.setReadTimeout(10000);
template.setRequestFactory(factory);
return template;
}
}

Spring Security Configuration

DiscordOAuth2SecurityConfig.java:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class DiscordOAuth2SecurityConfig {
private final DiscordOAuthProperties discordProperties;
private final DiscordUserService discordUserService;
public DiscordOAuth2SecurityConfig(DiscordOAuthProperties discordProperties,
DiscordUserService discordUserService) {
this.discordProperties = discordProperties;
this.discordUserService = discordUserService;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/", "/login", "/oauth2/**", "/webhook/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.failureUrl("/login?error=true")
.userInfoEndpoint(userInfo -> userInfo
.userService(discordUserService)
)
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.addLogoutHandler(discordLogoutHandler())
)
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
);
return http;
}
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(discordClientRegistration());
}
@Bean
public OAuth2AuthorizedClientService authorizedClientService() {
return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository());
}
@Bean
public LogoutHandler discordLogoutHandler() {
return (request, response, authentication) -> {
if (authentication != null) {
// Revoke Discord tokens on logout
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
OAuth2AuthorizedClient client = authorizedClientService().loadAuthorizedClient(
oauthToken.getAuthorizedClientRegistrationId(), oauthToken.getName());
if (client != null) {
// Token revocation would happen here
log.info("Revoking Discord tokens for user: {}", oauthToken.getName());
}
}
};
}
private ClientRegistration discordClientRegistration() {
return ClientRegistration.withRegistrationId("discord")
.clientId(discordProperties.getClientId())
.clientSecret(discordProperties.getClientSecret())
.scope(discordProperties.getScope().toArray(new String[0]))
.authorizationUri(discordProperties.getAuthUrl())
.tokenUri(discordProperties.getTokenUrl())
.userInfoUri(discordProperties.getUserUrl())
.userNameAttributeName("id")
.clientName("Discord")
.redirectUri(discordProperties.getRedirectUri())
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.build();
}
}

Custom User Service

DiscordUserService.java:

@Service
@Slf4j
public class DiscordUserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final DiscordApiService discordApiService;
private final UserRepository userRepository;
private final RoleService roleService;
private final DiscordOAuthProperties properties;
public DiscordUserService(DiscordApiService discordApiService,
UserRepository userRepository,
RoleService roleService,
DiscordOAuthProperties properties) {
this.discordApiService = discordApiService;
this.userRepository = userRepository;
this.roleService = roleService;
this.properties = properties;
}
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
try {
// Get user info from Discord
OAuth2AccessToken accessToken = userRequest.getAccessToken();
DiscordUser discordUser = discordApiService.getCurrentUser(accessToken.getTokenValue());
log.info("Loading Discord user: {} ({})", discordUser.getFullUsername(), discordUser.getId());
// Find or create local user
UserEntity userEntity = userRepository.findByDiscordId(discordUser.getId())
.orElseGet(() -> createUserFromDiscord(discordUser, accessToken));
// Update user information
updateUserFromDiscord(userEntity, discordUser);
// Additional Discord data
List<DiscordGuild> userGuilds = discordApiService.getUserGuilds(accessToken.getTokenValue());
List<DiscordConnection> connections = discordApiService.getUserConnections(accessToken.getTokenValue());
// Auto-join guild if configured
if (properties.isAutoJoinGuild() && properties.getJoinGuildId() != null) {
autoJoinGuild(accessToken.getTokenValue(), discordUser.getId(), userGuilds);
}
// Convert to Spring Security user with Discord attributes
return createOAuth2User(userEntity, discordUser, userGuilds, connections);
} catch (Exception e) {
throw new OAuth2AuthenticationException("Failed to load user from Discord", e);
}
}
private UserEntity createUserFromDiscord(DiscordUser discordUser, OAuth2AccessToken accessToken) {
UserEntity user = new UserEntity();
user.setDiscordId(discordUser.getId());
user.setUsername(discordUser.getUsername());
user.setEmail(discordUser.getEmail());
user.setAvatarUrl(discordUser.getAvatarUrl());
user.setEnabled(true);
user.setCreatedAt(Instant.now());
user.setLastLogin(Instant.now());
// Assign default role
Role defaultRole = roleService.getDefaultRole();
user.setRoles(Set.of(defaultRole));
// Store Discord-specific data
user.setDiscordData(Map.of(
"global_name", discordUser.getGlobalName(),
"discriminator", discordUser.getDiscriminator(),
"locale", discordUser.getLocale(),
"verified", String.valueOf(discordUser.isVerified()),
"mfa_enabled", String.valueOf(discordUser.isMfaEnabled())
));
UserEntity savedUser = userRepository.save(user);
log.info("Created new user from Discord: {}", discordUser.getId());
return savedUser;
}
private void updateUserFromDiscord(UserEntity user, DiscordUser discordUser) {
user.setUsername(discordUser.getUsername());
user.setEmail(discordUser.getEmail());
user.setAvatarUrl(discordUser.getAvatarUrl());
user.setLastLogin(Instant.now());
// Update Discord data
Map<String, String> discordData = user.getDiscordData();
discordData.put("global_name", discordUser.getGlobalName());
discordData.put("discriminator", discordUser.getDiscriminator());
discordData.put("banner", discordUser.getBannerUrl());
discordData.put("accent_color", String.valueOf(discordUser.getAccentColor()));
userRepository.save(user);
}
private void autoJoinGuild(String accessToken, String userId, List<DiscordGuild> userGuilds) {
String targetGuildId = properties.getJoinGuildId();
// Check if user is already in the guild
boolean alreadyInGuild = userGuilds.stream()
.anyMatch(guild -> guild.getId().equals(targetGuildId));
if (!alreadyInGuild) {
try {
discordApiService.addUserToGuild(accessToken, userId, targetGuildId, properties.getJoinGuildRoleId());
log.info("Auto-joined user {} to guild {}", userId, targetGuildId);
} catch (Exception e) {
log.warn("Failed to auto-join user to guild", e);
}
}
}
private OAuth2User createOAuth2User(UserEntity user, DiscordUser discordUser, 
List<DiscordGuild> guilds, List<DiscordConnection> connections) {
Set<GrantedAuthority> authorities = user.getRoles().stream()
.flatMap(role -> role.getPermissions().stream())
.map(permission -> new SimpleGrantedAuthority(permission.name()))
.collect(Collectors.toSet());
// Add role authorities
user.getRoles().forEach(role -> 
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName())));
// Create attributes map with Discord data
Map<String, Object> attributes = new HashMap<>();
attributes.put("id", discordUser.getId());
attributes.put("username", discordUser.getUsername());
attributes.put("global_name", discordUser.getGlobalName());
attributes.put("avatar", discordUser.getAvatar());
attributes.put("discriminator", discordUser.getDiscriminator());
attributes.put("email", discordUser.getEmail());
attributes.put("verified", discordUser.isVerified());
attributes.put("guilds", guilds);
attributes.put("connections", connections);
attributes.put("local_user_id", user.getId());
return new DefaultOAuth2User(authorities, attributes, "id");
}
}

OAuth Controller

DiscordOAuthController.java:

@RestController
@RequestMapping("/api/oauth/discord")
@Slf4j
public class DiscordOAuthController {
private final DiscordOAuthProperties properties;
private final TokenService tokenService;
private final DiscordApiService discordApiService;
private final UserRepository userRepository;
public DiscordOAuthController(DiscordOAuthProperties properties,
TokenService tokenService,
DiscordApiService discordApiService,
UserRepository userRepository) {
this.properties = properties;
this.tokenService = tokenService;
this.discordApiService = discordApiService;
this.userRepository = userRepository;
}
@GetMapping("/authorize")
public ResponseEntity<AuthorizationResponse> getAuthorizationUrl(@RequestParam(required = false) String state) {
try {
String actualState = state != null ? state : generateState();
String authUrl = properties.getAuthorizationUrl(actualState);
AuthorizationResponse response = AuthorizationResponse.builder()
.authorizationUrl(authUrl)
.state(actualState)
.build();
return ResponseEntity.ok(response);
} catch (Exception e) {
throw new DiscordOAuthException("Failed to generate authorization URL", e);
}
}
@PostMapping("/token")
public ResponseEntity<TokenResponse> exchangeCode(@RequestBody CodeExchangeRequest request) {
try {
// Validate state if provided
if (properties.isValidateState() && request.getState() != null) {
validateState(request.getState());
}
DiscordToken token = tokenService.exchangeCodeForToken(request.getCode());
DiscordUser user = discordApiService.getCurrentUser(token.getAccessToken());
TokenResponse response = TokenResponse.builder()
.accessToken(token.getAccessToken())
.refreshToken(token.getRefreshToken())
.expiresIn(token.getExpiresIn())
.tokenType(token.getTokenType())
.user(user)
.build();
return ResponseEntity.ok(response);
} catch (Exception e) {
throw new DiscordOAuthException("Code exchange failed", e);
}
}
@PostMapping("/refresh")
public ResponseEntity<TokenResponse> refreshToken(@RequestBody RefreshTokenRequest request) {
try {
DiscordToken newToken = tokenService.refreshToken(request.getRefreshToken());
DiscordUser user = discordApiService.getCurrentUser(newToken.getAccessToken());
TokenResponse response = TokenResponse.builder()
.accessToken(newToken.getAccessToken())
.refreshToken(newToken.getRefreshToken())
.expiresIn(newToken.getExpiresIn())
.tokenType(newToken.getTokenType())
.user(user)
.build();
return ResponseEntity.ok(response);
} catch (Exception e) {
throw new DiscordOAuthException("Token refresh failed", e);
}
}
@PostMapping("/revoke")
public ResponseEntity<Void> revokeToken(@RequestBody RevokeTokenRequest request) {
try {
tokenService.revokeToken(request.getToken());
return ResponseEntity.ok().build();
} catch (Exception e) {
throw new DiscordOAuthException("Token revocation failed", e);
}
}
@GetMapping("/user/guilds")
public ResponseEntity<List<DiscordGuild>> getUserGuilds(@RequestHeader("Authorization") String authHeader) {
try {
String accessToken = extractAccessToken(authHeader);
List<DiscordGuild> guilds = discordApiService.getUserGuilds(accessToken);
return ResponseEntity.ok(guilds);
} catch (Exception e) {
throw new DiscordApiException("Failed to get user guilds", e);
}
}
@PostMapping("/guild/join")
public ResponseEntity<Void> joinGuild(@RequestHeader("Authorization") String authHeader,
@RequestBody JoinGuildRequest request) {
try {
String accessToken = extractAccessToken(authHeader);
DiscordUser user = discordApiService.getCurrentUser(accessToken);
discordApiService.addUserToGuild(accessToken, user.getId(), 
request.getGuildId(), request.getRoleId());
return ResponseEntity.ok().build();
} catch (Exception e) {
throw new DiscordApiException("Failed to join guild", e);
}
}
private String generateState() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[32];
random.nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
private void validateState(String state) {
// Implement state validation logic
// This would typically involve checking against a stored state
}
private String extractAccessToken(String authHeader) {
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
throw new InvalidTokenException("Invalid authorization header");
}
// DTO classes
@Data
public static class CodeExchangeRequest {
private String code;
private String state;
}
@Data
public static class RefreshTokenRequest {
private String refreshToken;
}
@Data
public static class RevokeTokenRequest {
private String token;
}
@Data
public static class JoinGuildRequest {
private String guildId;
private String roleId;
}
@Data
@Builder
public static class AuthorizationResponse {
private String authorizationUrl;
private String state;
}
@Data
@Builder
public static class TokenResponse {
private String accessToken;
private String refreshToken;
private long expiresIn;
private String tokenType;
private String scope;
private DiscordUser user;
}
}

Webhook Support

DiscordWebhookService.java:

@Service
@Slf4j
public class DiscordWebhookService {
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
public DiscordWebhookService() {
this.restTemplate = new RestTemplate();
this.objectMapper = new ObjectMapper();
}
public void sendWebhookMessage(String webhookUrl, WebhookMessage message) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String jsonPayload = objectMapper.writeValueAsString(message);
HttpEntity<String> entity = new HttpEntity<>(jsonPayload, headers);
restTemplate.postForEntity(webhookUrl, entity, String.class);
log.info("Sent webhook message to: {}", webhookUrl);
} catch (Exception e) {
throw new DiscordWebhookException("Failed to send webhook message", e);
}
}
public void sendEmbedMessage(String webhookUrl, String title, String description, 
String color, List<WebhookField> fields) {
WebhookEmbed embed = WebhookEmbed.builder()
.title(title)
.description(description)
.color(color)
.fields(fields)
.build();
WebhookMessage message = WebhookMessage.builder()
.embeds(Collections.singletonList(embed))
.build();
sendWebhookMessage(webhookUrl, message);
}
@Data
@Builder
public static class WebhookMessage {
private String content;
private String username;
private String avatarUrl;
private List<WebhookEmbed> embeds;
private boolean tts;
}
@Data
@Builder
public static class WebhookEmbed {
private String title;
private String description;
private String url;
private String color;
private WebhookFooter footer;
private WebhookImage thumbnail;
private WebhookImage image;
private WebhookAuthor author;
private List<WebhookField> fields;
private String timestamp;
}
@Data
@Builder
public static class WebhookField {
private String name;
private String value;
private boolean inline;
}
@Data
@Builder
public static class WebhookFooter {
private String text;
private String iconUrl;
}
@Data
@Builder
public static class WebhookImage {
private String url;
}
@Data
@Builder
public static class WebhookAuthor {
private String name;
private String url;
private String iconUrl;
}
}

Application Configuration

application.yml:

discord:
oauth:
client-id: ${DISCORD_CLIENT_ID:}
client-secret: ${DISCORD_CLIENT_SECRET:}
redirect-uri: "${app.base-url}/login/oauth2/code/discord"
scope:
- identify
- email
- guilds
- guilds.join
bot-token: ${DISCORD_BOT_TOKEN:}
auto-join-guild: false
join-guild-id: ${DISCORD_GUILD_ID:}
join-guild-role-id: ${DISCORD_ROLE_ID:}
validate-state: true
app:
base-url: http://localhost:8080
spring:
security:
oauth2:
client:
registration:
discord:
client-id: ${discord.oauth.client-id}
client-secret: ${discord.oauth.client-secret}
scope: ${discord.oauth.scope}
redirect-uri: ${discord.oauth.redirect-uri}
authorization-grant-type: authorization_code
provider:
discord:
authorization-uri: https://discord.com/api/oauth2/authorize
token-uri: https://discord.com/api/oauth2/token
user-info-uri: https://discord.com/api/users/@me
user-name-attribute: id
logging:
level:
com.example.discord: DEBUG

Best Practices

  1. Secure Token Storage - Store tokens encrypted and use secure sessions
  2. State Validation - Always validate state parameter to prevent CSRF
  3. Error Handling - Implement proper error handling for OAuth flows
  4. Rate Limiting - Respect Discord API rate limits
  5. Token Refresh - Implement automatic token refresh before expiration
  6. User Consent - Always request appropriate scopes and explain data usage
  7. Security Headers - Use appropriate security headers for OAuth endpoints

Conclusion

Discord OAuth in Java provides:

  • Seamless authentication using Discord accounts
  • Access to rich user data including profile, guilds, and connections
  • Bot integration for enhanced functionality
  • Webhook support for notifications and messaging
  • Spring Security integration for robust authentication flows

By implementing this comprehensive Discord OAuth solution, applications can leverage Discord's extensive user base and rich API ecosystem while maintaining security and usability standards.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper