OIDC group sync to Openfga with authentik

Authentik has “expression policies”, which are essentially are arbitrary python code. You are supposed to have them evaluate some stuff, and then return true/false depending on whether or not you want to allow the action/login.

But instead of doing that, I wrote an Authentik expression policy that makes requests to the Openfga server, and removes/adds users depending on which Authentik groups they are in.

import json
from authentik.core.models import Group

system_groups = ["authentik Read-only", "authentik Admins"]

all_groups = [group.name for group in Group.objects.all()]

all_groups = list(set(all_groups) - set(system_groups))

user_groups = [group.name for group in request.user.all_groups()]

# I have my openfga server gated behind a token, but it's not by default
openfga_api_key = "secretkey"

writes = {
    "writes": {
        "tuple_keys": [
            {
                "user": f"user:{request.user.username}",
                "relation": "member",
                "object": "group"
            }
        ]
    },
    "authorization_model_id": "01K70P5HAQ8K2J3AN978F7Y1EC"
}

deletes = {
    "deletes": {
        "tuple_keys": [
            {
                "user": f"user:{request.user.username}",
                "relation": "member",
                "object": "group"
            }
        ]
    },
    "authorization_model_id": "01K70P5HAQ8K2J3AN978F7Y1EC"
}


headers = {
    "Authorization": f"Bearer {openfga_api_key}",
    "content-type": "application/json"
}


for group in list(set(all_groups) - set(user_groups)):
  deletes["deletes"]["tuple_keys"][0]["object"] = f"group:{group}"
  requests.post('https://openfga.moonpiedumpl.ing/stores/01K70P3ZX9DJEHF85XDJS4PXN2/write', data=json.dumps(deletes), headers=headers)


for group in user_groups:
    writes["writes"]["tuple_keys"][0]["object"] = f"group:{group}"
    requests.post('https://openfga.moonpiedumpl.ing/stores/01K70P3ZX9DJEHF85XDJS4PXN2/write', data=json.dumps(writes), headers=headers)

# It's supposed to return true/false depending on whether or not you want to pass/fail the action but I just always return true here    
return True

Then I bound this expression policy to the Incus application in authentik, so when users log into Incus, their groups are synced.

The “proper” way to do this is probably to have webhooks that make calls to a small custom app you have written which does the role sync. As far as I can tell, this feature of arbitrary code is unique to Authentik.

Anyway, this seems to work great. My boot SSD died and since I had the chance to rebuild, I decided to do this, instead of what I was previously doing, which was a one time openfga user add that only happened when they signed up via an invite link on Authentik. Now the groups sync constantly.

I’ve been using Incus for my college’s cybersecurity club and I am thinking that this will make it easy to do some kind of PvP CTF, where I can just spin up projects for each team and then add/remove users from Authentik groups where I have synced the group name with view only permissions to the relevant projects.

1 Like

That’s pretty neat!

Yeah, normally you’d effectively have a “bridge” script which looks in OpenFGA for what groups are defined in there and then syncs the membership of those groups from whatever source of truth you have for authorization (Entra/Active Directory is common).

But having a way for the IDP to effectively do that directly is pretty neat and great for those using Authentik.