django-allauth icon indicating copy to clipboard operation
django-allauth copied to clipboard

Backchannel Logout

Open jharmon96 opened this issue 2 years ago • 3 comments

Are there any plans to add backchannel logouts to this library?

I have a collection of applications that use one oidc provider. When a user logs out of one app I'd like for them to be logged out of the others.

https://openid.net/specs/openid-connect-backchannel-1_0.html#BCRequest

For reference, this is what I've done in the time being to get this to work with Keycloak:

## Keycloak Backchannel Logout
from allauth.socialaccount.models import SocialAccount
from django.http import HttpResponse, HttpRequest
from django.views.decorators.csrf import csrf_exempt
from django.contrib.sessions.models import Session
from importlib import import_module
import jwt

def init_session(session_key):
    """
    Initialize same session as done for ``SessionMiddleware``.
    """
    engine = import_module(settings.SESSION_ENGINE)
    return engine.SessionStore(session_key)

@csrf_exempt
def backchannel_logout(request):
    ''' Keycloak is set to send a POST request to this route if a user
        logs out via another application, such as Superset. 
        This function will ensure the user is logged out of this app
        as well.
    '''
    # First run a bunch of checks to make sure that the request is valid
    if request.method != 'POST': return HttpResponse(status=405)
    if settings.KEYCLOAK_PUBLIC_KEY == None: return HttpResponse(status=501)
    t = request.POST['logout_token']
    try:
        decoded = jwt.decode(t, key=settings.KEYCLOAK_PUBLIC_KEY, algorithms=['RS256', ], audience=settings.KEYCLOAK_APPS)
        uid = decoded['sub']
        aud = decoded['aud']
    except Exception as e:
        logger.error(f"Backchannel logout failed: {e}")
        return HttpResponse(406)
    if not SocialAccount.objects.filter(uid=uid).exists(): 
        logger.warning(f"User {uid} does not exist in this application.")
        return HttpResponse(406)

    # If we made it this far, then the request checks out. Continue to log user out.
    from django.contrib.auth import logout
    user = SocialAccount.objects.get(uid=uid).user
    sessions = [s for s in Session.objects.all() if s.get_decoded().get('_auth_user_id') == str(user.id)]
    request = HttpRequest()

    for session in sessions:
        request.session = init_session(session.session_key)
        logout(request)
    
    if len(sessions) != 0: logger.debug("{user.username} has been logged out by {aud}")

    return HttpResponse(status=200)

jharmon96 avatar Jun 07 '22 22:06 jharmon96

@jharmon96 ,

Side question, do you have any guide / article / blog about best practice of using django / allauth with keycloak?

I just created a keycloak vanilla client for django app, without client scopes / groups. Quite sure I should, but 0 clue 😁

shawnngtq avatar Sep 17 '23 07:09 shawnngtq

  SOCIALACCOUNT_PROVIDERS = {
      "openid_connect": {
          "APPS": [
              {
                  "provider_id": "keycloak",
                  "name": "Keycloak",
                  "client_id": "<insert-id>",
                  "secret": "<insert-secret>",
                  "settings": {
                      "server_url": "http://keycloak:8080/realms/master/.well-known/openid-configuration",
                  },
              }
          ]
      }
  }

pennersr avatar Sep 17 '23 07:09 pennersr

@pennersr , I managed to get it working, allauth is flawless here 😁

What I'm unsure of is the keycloak side

image

There is a concept of client scopes and groups that you can attach additional attributes. It's complex and depends on user experience rather than documentation. Was hoping to hear how others django users are doing these.

Just created a post on Django forum too:

https://forum.djangoproject.com/t/django-keycloak-best-practice/23924

shawnngtq avatar Sep 17 '23 12:09 shawnngtq