#Discord oauth authentication retrieve guild ids and store them

1 messages ยท Page 1 of 1 (latest)

somber surge
#

I'm trying to retrieve and store the guilds a Discord user is in so that at in application specific policies there can be checked per application if a user is in a specific guild or not.

The method to actually store more data alongside a user or have it retain in the current authentication session is not clear to me (the policy below doesn't actually store the guild ids).
Is there a way to "store" this data in the authentication session or the user model somehow?
Somewhere in a (semi) persistent location so it can be access in policies that define access to specific applications?

See the authentication flow and the retrieval policy for what I've gotten up with so far.
Thanks in advance ๐Ÿ˜„

# Get the user-source connection object from the context, and get the access token
connection = context["goauthentik.io/sources/connection"]
access_token = connection.access_token

guilds_response = requests.get(
    "https://discord.com/api/users/@me/guilds",
    headers={
        "Authorization": "Bearer " + access_token
    }
)

if guilds_response.status_code is not 200:
    return False

guilds_response.raise_for_status()
guilds = guilds_response.json()
guilds = list(map(lambda x: x['id'], guilds))

request.context["flow_plan"].context["pending_user"].attributes["discord"] = {}
request.context["flow_plan"].context["pending_user"].attributes["discord"]["guilds"] = guilds
return True
somber surge
#

I was hoping that the write user stage made it possible to modify the pending_user and store data in the user attributes. I can't get this to work though sadly.

formal oxide
#

@somber surge Why not just have the guild IDs themselves mapped to authentik groups?

Then have the authentication and the enrollment flows assign the groups based on which guilds the user is in?

#

Just have to ensure that if users leave guilds the flows also remove them from those groups - probably by just rewriting all their groups.

formal oxide
#

I think dynamic groups to generate the groups from the guilds + editing the groups in the "prompt_data" "flow_plan" followed by a user write would do this?

formal oxide
#

Something like this works on enrollment, when bound to a user write stage :


from authentik.core.models import Group

# 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['goauthentik.io/sources/connection']
access_token = connection.access_token

guilds = requests.get(
    "https://discord.com/api/users/@me/guilds",
    headers= {
        "Authorization": "Bearer " + access_token,
    }
).json()

discordGuilds = []

for g in guilds:
    groupname = "discordGuild_" + g["id"]
    group, _ = Group.objects.get_or_create(name=groupname)
    discordGuilds.append(group)

request.context["flow_plan"].context["groups"] = discordGuilds

return True

aka - get then user's full guild ID list, create a authentik group per guild id, and add the user's authentik account to each of those authentik groups.

formal oxide
#

You may need context['pending_user'] during authentication however?

somber surge
#

Thanks @formal oxide
Although that does indeed work with enrollment the issue I'm still stuck with is what you also mentioned removing the groups when a user leaves guilds. And all subsequent logins are handled by the authentication flow not the enrollment flow (I think).

formal oxide
#

Been chatting and working on it in #development fyi.

#

Should be possible.

somber surge
#

Cool. I'll have read. What I have up and running in enrollment is this.

from authentik.core.models import Group

# 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['goauthentik.io/sources/connection']

guilds = requests.get(
    "https://discord.com/api/users/@me/guilds",
    headers= {
        "Authorization": "Bearer " + connection.access_token,
    }
).json()

groups = []

# Get or create the parent group "Discord Guilds"
parent_group, _ = Group.objects.get_or_create(name="Discord Guilds")

for guild in guilds:
    groupname = "discordGuild_" + guild["id"]
    group, _ = Group.objects.get_or_create(
      name=groupname,
      defaults={"parent": parent_group}
    )
    groups.append(group)

request.context["flow_plan"].context["groups"] = groups

return True
#

I have an extra parent group for reasons ๐Ÿคทโ€โ™€๏ธ . Maybe better organisation in the UI sometime in the future ๐Ÿ˜‚

formal oxide
#

Yeah I added a parent "DiscordGuilds" group to help organise it.

#

Might be some milage in that ๐Ÿคทโ€โ™‚๏ธ

somber surge
#

Yep. Just trying to figure out the updating/deleting now. Last time I looked at this I didn't even realise a lot of the methods on the objects were actually from the Django ORM.

#

Knowing that now makes looking for documentation slightly easier ๐Ÿ˜‚

formal oxide
#

If only I already remembered Python or ever looked at Django before ๐Ÿ˜›

somber surge
#

Yea same. I know more about C and C++ then about Python

formal oxide
#

Atm, my policy expression is looking like:

from authentik.core.models import Group

# 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

guilds = requests.get(
    "https://discord.com/api/users/@me/guilds",
    headers= {
        "Authorization": "Bearer " + access_token,
    }
).json()

# Define empty array to be populated by group objects.
discordGuilds = []

# Define the parent group for the created Discord guild groups.
authentikParentGroup = "123456789-1234-1234-1234-123456789"

for g in guilds:
    # update_or_create is used here to ensure that guild name and chosen attributes
    # are updated when they change.
    
    group, _ = Group.objects.update_or_create(
        parent_id=authentikParentGroup, 
        attributes={
            "discord_guild_id": g['id']
        },
        defaults={
            "name": f"DiscordGuild_{g['name']}",
            #broken -"guildIconURL": f"https://cdn.discordapp.com/icons/{g['id']}/{g['icon']}.png",
        }
    )
    discordGuilds.append(group)

request.context["flow_plan"].context["groups"] = discordGuilds

# Return true for the policy to 'pass'.
return True

Still need to tinker with getting some attributes mirrored like said iconURL etc...

somber surge
# formal oxide Atm, my policy expression is looking like: ```python from authentik.core.models...

Hmm. That icon url does seem correct.

https://cdn.discordapp.com/icons/GUILDID/HASH.png

For this server the (shortend) guild json output is:

  "id": "809154715984199690"
  "icon" : "5d969c5bae80576777699fd11b2243dd"
https://cdn.discordapp.com/icons/809154715984199690/5d969c5bae80576777699fd11b2243dd.png

So the url template you have should be fine? Or is it something else that isn't working right?