diff --git a/website/docs/sources/discord/index.md b/website/docs/sources/discord/index.md index bcdd7adb2c..3f16d7fe3a 100644 --- a/website/docs/sources/discord/index.md +++ b/website/docs/sources/discord/index.md @@ -154,3 +154,209 @@ return user_matched ``` Now bind this policy to the chosen enrollment and authentication flows for the Discord OAuth source. + +### Syncing Discord roles to authentik groups + +:::info +Ensure that the Discord OAuth source in 'Federation & Social login' has the additional `guilds.members.read` scopes added under the 'Protocol settings'. +::: + +Create a new 'Expression Policy' with the content below, adjusting the variables where required. + +#### Sync on enrollment + +```python +# To get the role and guild ID numbers for the parameters, open Discord, go to Settings > Advanced and +# enable developer mode. +# Right-click on the server/guild title and select "Copy ID" to get the guild ID. +# Right-click on the server/guild title and select server settings > roles, right click on the role and click +# "Copy ID" to get the role ID. + +from authentik.core.models import Group + +GUILD_ID = "" +MAPPED_ROLES = { + "": Group.objects.get_or_create(name="")[0], + "": Group.objects.get_or_create(name="")[0], + # You can add mapped roles by copying the above line and adjusting to your needs +} + +# Only change below here if you know what you are doing. +GUILD_API_URL = "https://discord.com/api/users/@me/guilds/{guild_id}/member" + +# Ensure flow is only run during OAuth logins via Discord +if context["source"].provider_type != "discord": + return True + +# Get the user-source connection object from the context, and get the access token +connection = context.get("goauthentik.io/sources/connection") +if not connection: + return False +access_token = connection.access_token + +guild_member_info = requests.get( + GUILD_API_URL.format(guild_id=GUILD_ID), + headers={"Authorization": "Bearer " + access_token}, +).json() + +# Ensure user is a member of the guild +if "code" in guild_member_info: + if guild_member_info["code"] == 10004: + ak_message("User is not a member of the guild") + else: + ak_create_event( + "discord_error", source=context["source"], code=guild_member_info["code"] + ) + ak_message("Discord API error, try again later.") + return False + +# Add all mapped roles the user has in the guild +groups_to_add = [] +for role_id in MAPPED_ROLES: + if role_id in guild_member_info["roles"]: + groups_to_add.append(MAPPED_ROLES[role_id]) + +request.context["flow_plan"].context["groups"] = groups_to_add +return True + +``` + +Now bind this policy to the chosen enrollment flows for the Discord OAuth source. + +#### Sync on authentication + +```python +# To get the role and guild ID numbers for the parameters, open Discord, go to Settings > Advanced and +# enable developer mode. +# Right-click on the server/guild title and select "Copy ID" to get the guild ID. +# Right-click on the server/guild title and select server settings > roles, right click on the role and click +# "Copy ID" to get the role ID. + +from authentik.core.models import Group + +GUILD_ID = "" +MAPPED_ROLES = { + "": Group.objects.get_or_create(name="")[0], + "": Group.objects.get_or_create(name="")[0], + # You can add mapped roles by copying the above line and adjusting to your needs +} + +# Only change below here if you know what you are doing. +GUILD_API_URL = "https://discord.com/api/users/@me/guilds/{guild_id}/member" + +# Ensure flow is only run during OAuth logins via Discord +if context["source"].provider_type != "discord": + return True + +# Get the user-source connection object from the context, and get the access token +connection = context.get("goauthentik.io/sources/connection") +if not connection: + return False +access_token = connection.access_token + +guild_member_info = requests.get( + GUILD_API_URL.format(guild_id=GUILD_ID), + headers={"Authorization": "Bearer " + access_token}, +).json() + +# Ensure user is a member of the guild +if "code" in guild_member_info: + if guild_member_info["code"] == 10004: + ak_message("User is not a member of the guild") + else: + ak_create_event( + "discord_error", source=context["source"], code=guild_member_info["code"] + ) + ak_message("Discord API error, try again later.") + return False + +# Get the user's current roles and remove all roles we want to remap +new_groups = [ + role for role in request.user.ak_groups.all() if role not in MAPPED_ROLES.values() +] + +# Add back mapped roles which the user has in the guild +for role_id in MAPPED_ROLES: + if role_id in guild_member_info["roles"]: + new_groups.append(MAPPED_ROLES[role_id]) + +# Update user's groups +request.user.ak_groups.set(new_groups) +request.user.save() + +return True + +``` + +Now bind this policy to the chosen authentication flows for the Discord OAuth source. + +### Store OAuth info in attribute and create avatar attribute from Discord avatar + +:::info +Ensure that the Discord OAuth source in 'Federation & Social login' has the additional `guilds.members.read` scopes added under the 'Protocol settings'. +::: + +:::info +In order to use the created attribute in authentik you will have to set authentik configuration arguments found at: https://docs.goauthentik.io/docs/core/settings#avatars +::: + +Create a new 'Expression Policy' with the content below, adjusting the variables where required: + +```python +import base64 +import requests + +AVATAR_SIZE = "64" # Valid values: 16,32,64,128,256,512,1024 + +# Only change below here if you know what you are doing. +AVATAR_URL = "https://cdn.discordapp.com/avatars/{id}/{avatar}.png?site={avatar_size}" +AVATAR_STREAM_CONTENT = "data:image/png;base64,{base64_string}" # Converts base64 image into html syntax useable with authentik's avatar attributes feature + + +def get_as_base64(url): + """Returns the base64 content of the url""" + return base64.b64encode(requests.get(url).content) + + +def get_avatar_from_avatar_url(url): + """Returns an authentik-avatar-attributes-compatible string from an image url""" + cut_url = f"{url}?size=64" + return AVATAR_STREAM_CONTENT.format( + base64_string=(get_as_base64(cut_url).decode("utf-8")) + ) + + +# Ensure flow is only run during OAuth logins via Discord +if context["source"].provider_type != "discord": + return True + +user = request.user +userinfo = request.context["oauth_userinfo"] + +# Assigns the discord attributes to the user +user.attributes["discord"] = { + "id": userinfo["id"], + "username": userinfo["username"], + "discriminator": userinfo["discriminator"], + "email": userinfo["email"], + "avatar": userinfo["avatar"], + "avatar_url": ( + AVATAR_URL.format( + id=userinfo["id"], avatar=userinfo["avatar"], avatar_size=AVATAR_SIZE + ) + if userinfo["avatar"] + else None + ), +} + +# If the user has an avatar, assign it to the user +avatar_url = user.attributes["discord"].get("avatar_url", None) +if avatar_url is not None: + user.attributes["avatar"] = get_avatar_from_avatar_url(avatar_url) + +user.save() +return True + +``` + +Now bind this policy to the chosen enrollment and authentication flows for the Discord OAuth source.