|
|
|
@ -18,6 +18,7 @@ |
|
|
|
package org.keycloak.social.discord; |
|
|
|
|
|
|
|
import com.fasterxml.jackson.databind.JsonNode; |
|
|
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
|
|
|
import jakarta.ws.rs.core.Response; |
|
|
|
import org.jboss.logging.Logger; |
|
|
|
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider; |
|
|
|
@ -32,6 +33,7 @@ import org.keycloak.services.ErrorPageException; |
|
|
|
import org.keycloak.services.messages.Messages; |
|
|
|
|
|
|
|
import java.util.Set; |
|
|
|
import java.util.regex.Pattern; |
|
|
|
|
|
|
|
/** |
|
|
|
* @author <a href="mailto:wadahiro@gmail.com">Hiroyuki Wada</a> |
|
|
|
@ -45,9 +47,13 @@ public class DiscordIdentityProvider extends AbstractOAuth2IdentityProvider<Disc |
|
|
|
public static final String TOKEN_URL = "https://discord.com/api/oauth2/token"; |
|
|
|
public static final String PROFILE_URL = "https://discord.com/api/users/@me"; |
|
|
|
public static final String GROUP_URL = "https://discord.com/api/users/@me/guilds"; |
|
|
|
public static final String USER_PICTURE_URL = "https://cdn.discordapp.com/avatars/%s/%s.%s"; |
|
|
|
public static final String DEFAULT_SCOPE = "identify email"; |
|
|
|
public static final String GUILDS_SCOPE = "guilds"; |
|
|
|
|
|
|
|
private static final Pattern AVATAR_HASH_PATTERN = Pattern.compile("^(a_)?[0-9a-f]{32}$"); |
|
|
|
private static final Pattern DISCORD_ID_PATTERN = Pattern.compile("^\\d+$"); |
|
|
|
|
|
|
|
public DiscordIdentityProvider(KeycloakSession session, DiscordIdentityProviderConfig config) { |
|
|
|
super(session, config); |
|
|
|
config.setAuthorizationUrl(AUTH_URL); |
|
|
|
@ -78,6 +84,8 @@ public class DiscordIdentityProvider extends AbstractOAuth2IdentityProvider<Disc |
|
|
|
|
|
|
|
user.setUsername(username); |
|
|
|
user.setEmail(getJsonProperty(profile, "email")); |
|
|
|
|
|
|
|
setUserPicture(user, profile); |
|
|
|
user.setIdp(this); |
|
|
|
|
|
|
|
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias()); |
|
|
|
@ -85,6 +93,34 @@ public class DiscordIdentityProvider extends AbstractOAuth2IdentityProvider<Disc |
|
|
|
return user; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Constructs the OIDC 'picture' URL based on the Discord avatar hash and sets it |
|
|
|
* in the user context to ensure OIDC compliance. |
|
|
|
* <p>Discord returns an avatar hash (or null), but OIDC expects a direct URL |
|
|
|
* to the image in the 'picture' claim.</p> |
|
|
|
* @param user The context where the 'picture' attribute will be set |
|
|
|
* @param profile The raw Discord user profile JSON containing the 'avatar' hash |
|
|
|
*/ |
|
|
|
private void setUserPicture(BrokeredIdentityContext user, JsonNode profile) { |
|
|
|
if (user.getId() == null || !DISCORD_ID_PATTERN.matcher(user.getId()).matches()) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
String avatarHash = getJsonProperty(profile, "avatar"); |
|
|
|
if (avatarHash == null || avatarHash.isEmpty() || !AVATAR_HASH_PATTERN.matcher(avatarHash).matches()) { |
|
|
|
return; |
|
|
|
} |
|
|
|
String extension = "png"; |
|
|
|
if (avatarHash.startsWith("a_")) { |
|
|
|
extension = "gif"; |
|
|
|
} |
|
|
|
String finalURL = String.format(USER_PICTURE_URL, user.getId(), avatarHash, extension); |
|
|
|
user.setUserAttribute("picture", finalURL); |
|
|
|
if (profile instanceof ObjectNode objectNodeProfile) { |
|
|
|
objectNodeProfile.put("picture", finalURL); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { |
|
|
|
log.debug("doGetFederatedIdentity()"); |
|
|
|
|