Can not log out from keycloak IDP from inside of Django app code. All stackoverflow answers did not work fo me (most are for older version of the components involved), the same goes for the keycloak documentation.
Recently we have implemented keycloak-based athentication for our Django-based website. Works fine for auth. The app is build by docker, three containers: the website on port 8000, keycloak db (postgres image), keycloak (jboss/keycloak image) on port 8080.
Now I have to add "Logout" functionality to it, meaning singing out of keycloak from my Django code, and redirect the user back to the keycloak login screen.
My setup:
Django 2.2
Python 3.5
keycloak 7
social-auth-app-django 3.1.0
social-auth-core 3.2.0
The keycloak documentation suggests this:
POST /{realm}/users/{id}/logout
with user id and realm name.
That produces nothing but 404 so I switched to this pattern (fond on stackoverflow): POST /auth/realms/acc/protocol/openid-connect/logout
This returns 403 and the error : "Forbidden (CSRF cookie not set.): /{realm name}/users/{user id}/logout/"
which I managed to solve (see update below)
urls.py
urlpaterns = [
....
url(r'logout/$', logout_view, name='logout'),
...
]
views.py
from django.http import HttpResponseRedirect
import requests
def logout_view(request):
url = '%s<realm name>/users/<user id>/logout/' % settings.LOGOUT_REDIRECT_URL
r = requests.post(url, data = {})
#r.status_code at this point is 403
#also tried another sugestion:
url = '%sauth/realms/%s/protocol/openid-connect/logout/' % (settings.LOGOUT_REDIRECT_URL, '<some realm name>')
r = requests.post(url, data = {})
#r.status_code at this point is 403
#also tried with refresh_token and client_id:
url = "http://<website ip>:8000/<realm name>/users/<user id>/logout/"
refresh_token = '<some refresh token pulled out from stout>'
r = requests.post(url, data = {'refresh_token': refresh_token, 'client_id': '<user id>'})
#for 'client_id' above tried both user id and user name
#r.status_code at this point is also 403
return HttpResponseRedirect("http://<website ip>:8000")
That sends me back to the frontpage of my website, I am still logged in and can browse the site normally. Hitting keycloak login screen at this point sends me back to my website front page already logged in, so I am still authenticated. Killing session before the POST in Django code does not change anything. Basically, there's no logout from either websste or keycloak when I am authenticated with keycloak. Keycloak expiration with time does happen, so realm.json settings work.
I did try to set this in my settings.py:
CSRF_COOKIE_SECURE = False
SESSION_COOKIE_SECURE = False
..but no effect.
So, where can I find an example of working Django code that logs out a user from Keycloak? Thanks
UPDATE The first part of the puzzle is solved (reading Django CSRF Cookie Not Set , the answer by Евгений Смирнов and similar posts): to get rid of "CSRF cookie not set" error I had to do one simple thing:
from django.views.decorators.csrf import csrf_exempt
and add the decorator for my logout view:
@csrf_exempt
def logout_view(request):
..........
After that the logout request is executed and comes back with this:
[org.keycloak.events] (default task-12) type=LOGOUT_ERROR, realmId=acclims, clientId=63cb82ff-17f4-4f2d-95e1-a8b3c742e28b, userId=null, ipAddress=172.22.0.1, error=invalid_client_credentials
status_code: 400
I am guessing I am not sending back the correct refresh_token, and I don't know how to test this suspicion.
-------------------------------------------------------
Other pieces (probably irrelevant):
settings.py
.....
LOGIN_URL = SCRIPT_NAME + '/login/keycloak'
#yes, I tried this:
CSRF_COOKIE_SECURE = False
SESSION_COOKIE_SECURE = False
SOCIAL_AUTH_KEYCLOAK_KEY = '<some keycloak key>'
SOCIAL_AUTH_KEYCLOAK_SECRET = 'some keycloak secret'
SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY = 'some public key'
SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL = 'http://<some ip>:8080/auth/realms/<some realm>/protocol/openid-connect/auth'
SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL = 'http://<some ip>:8080/auth/realms/<some realm>/protocol/openid-connect/token'
SOCIAL_AUTH_STRATEGY = 'social_django.strategy.DjangoStrategy'
SOCIAL_AUTH_STORAGE = 'social_django.models.DjangoStorage'
SOCIAL_AUTH_KEYCLOAK_ID_KEY = 'email'
SOCIAL_AUTH_POSTGRES_JSONFIELD = True
SOCIAL_AUTH_URL_NAMESPACE = 'social'
LOGIN_REDIRECT_URL = 'http://<website ip>:8000/'
LOGOUT_REDIRECT_URL = 'http://<website ip>:8000/'
SOCIAL_AUTH_POSTGRES_JSONFIELD = True
SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.auth_allowed',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'social_core.pipeline.mail.mail_validation',
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'social_core.pipeline.debug.debug',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
'social_core.pipeline.debug.debug',
)
....
docker-compose.yml
version: '3'
services:
web:
restart: unless-stopped
container_name: web-container
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "8000:8000"
environment:
PRODUCTION: 'false'
DEBUG: 'True'
DJANGO_SETTINGS_MODULE: '<something>.settings.postgres'
DB_NAME: '<some name>'
DB_USER: '<some user>'
DB_PASS: '<some password>'
DB_HOST: '<some host>'
depends_on:
- db
volumes:
- ./:/usr/src/app/
db:
image: postgres:9.6.2
# volumes:
# - ../docker-postgresql-multiple-databases:/docker-entrypoint-initdb.d
volumes:
- data:/var/lib/postgresql/data
environment:
- POSTGRES_MULTIPLE_DATABASES=<something>
- POSTGRES_USER=<some user>
- POSTGRES_PASSWORD=<some password>
keycloakdb:
image: postgres
volumes:
- keycloak_postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
keycloak:
image: jboss/keycloak
command: -b 0.0.0.0 -Dkeycloak.migration.action=import -Dkeycloak.migration.provider=dir -Dkeycloak.migration.dir=/tmp/
environment:
DB_VENDOR: POSTGRES
DB_ADDR: keycloakdb
DB_DATABASE: keycloak
DB_USER: keycloak
DB_SCHEMA: public
DB_PASSWORD: <some password>
KEYCLOAK_USER: <some user>
KEYCLOAK_PASSWORD: <some password>
#KEYCLOAK_IMPORT: /tmp/
ports:
- 8080:8080
volumes:
- ./keycloak/:/tmp/
depends_on:
- keycloakdb
volumes:
data:
# external: true
keycloak_postgres_data:
driver: local
keycloak/realm.json
{
"id" : "<some realm id>",
"realm" : "<some realm name>",
"notBefore" : 0,
"revokeRefreshToken" : false,
"refreshTokenMaxReuse" : 0,
"accessTokenLifespan" : 300,
"accessTokenLifespanForImplicitFlow" : 900,
"ssoSessionIdleTimeout" : 1800,
"ssoSessionMaxLifespan" : 36000,
"ssoSessionIdleTimeoutRememberMe" : 0,
"ssoSessionMaxLifespanRememberMe" : 0,
"offlineSessionIdleTimeout" : 2592000,
"offlineSessionMaxLifespanEnabled" : false,
"offlineSessionMaxLifespan" : 5184000,
"accessCodeLifespan" : 60,
"accessCodeLifespanUserAction" : 300,
"accessCodeLifespanLogin" : 1800,
"actionTokenGeneratedByAdminLifespan" : 43200,
"actionTokenGeneratedByUserLifespan" : 300,
"enabled" : true,
"sslRequired" : "external",
"registrationAllowed" : false,
"registrationEmailAsUsername" : false,
"rememberMe" : false,
"verifyEmail" : false,
"loginWithEmailAllowed" : true,
"duplicateEmailsAllowed" : false,
"resetPasswordAllowed" : false,
"editUsernameAllowed" : false,
"bruteForceProtected" : false,
"permanentLockout" : false,
"maxFailureWaitSeconds" : 900,
"minimumQuickLoginWaitSeconds" : 60,
"waitIncrementSeconds" : 60,
"quickLoginCheckMilliSeconds" : 1000,
"maxDeltaTimeSeconds" : 43200,
"failureFactor" : 30,
"roles" : {
....bunch of roles
}