Flask-OIDC集成Keycloak:登出后自动登录无需凭证问题
Flask-OIDC集成Keycloak:登出后自动登录问题解决
问题概述
使用Flask-OIDC将Keycloak与Flask应用集成实现认证功能,目标是用户执行登出操作后,Flask和Keycloak端的会话均被完全清除,访问受保护页面时需重新输入账号密码登录。但目前登出后用户无需输入凭证即可自动登录进入受保护页面。
预期行为
- 用户通过Keycloak输入账号密码成功登录
- 用户执行登出操作后,所有会话被清除
- 访问受保护页面
/profile时,被重定向到Keycloak登录页面,需重新输入凭证
当前行为
- 用户成功登录后执行登出操作,Flask会话已清除(
oidc.user_loggedin变为False) - 首次访问
/profile被重定向到主页 - 再次访问
/profile时无需跳转登录页面,直接自动完成登录进入页面
问题代码
app.config.update({ 'SECRET_KEY': '20221e754l7a45909ef4Ij66ac984bea', # random secret key 'OIDC_CLIENT_SECRETS': 'keycloak.json', # information about issuer, client_secret, token_uri etc 'OIDC_SCOPES': ['openid', 'email'], 'OIDC_REDIRECT_URI': 'http://localhost:5000/oidc/callback', # redirect link, conventionally named 'OIDC_INTROSPECTION_AUTH_METHOD': 'client_secret_post', 'OIDC_INTROSPECTION_AUTH': True }) oidc = OpenIDConnect(app) @app.route('/') def home(): return 'Welcome to the Flask App! <a href="/login">Log in</a>' @app.route('/login') def login(): print("login, oidc.user_loggedin ", oidc.user_loggedin) if oidc.user_loggedin: return redirect(url_for('profile')) # redirect the user to the Keycloak login page redirect_uri = url_for('oidc_callback', _external=True) return oidc.authorize_redirect(redirect_uri, prompt='login') @app.route('/oidc/callback') def oidc_callback(): print("oidc_callback, oidc.user_loggedin ", oidc.user_loggedin) token = oidc.authorize_access_token() if token: user_info = oidc.user_getinfo(['sub', 'email']) session['user'] = user_info return redirect(url_for('profile')) return 'Login failed', 401 @app.route('/profile') def profile(): print("profile, oidc.user_loggedin ", oidc.user_loggedin) if oidc.user_loggedin: user_email = oidc.user_getfield('email') return f"Hello, {user_email}!" return redirect(url_for('login')) @app.route('/logout') def logout(): print("logout, oidc.user_loggedin ", oidc.user_loggedin) if oidc.user_loggedin: session.clear() oidc.logout() id_token = oidc.get_id_token() logout_url = (oidc.client_secrets['issuer'] + '/protocol/openid-connect/logout?id_token_hint=' + id_token + '&post_logout_redirect_uri=' + url_for('home', _external=True)) return redirect(logout_url) return redirect(url_for('home')) if __name__ == '__main__': app.run(debug=True)
日志信息
profile, oidc.user_loggedin False 127.0.0.1 - - [30/Sep/2024 09:28:27] "GET /profile HTTP/1.1" 302 - profile, oidc.user_loggedin False 127.0.0.1 - - [30/Sep/2024 09:28:27] "GET /profile HTTP/1.1" 302 - 127.0.0.1 - - [30/Sep/2024 09:28:27] "GET /login HTTP/1.1" 302 - 127.0.0.1 - - [30/Sep/2024 09:28:32] "GET /authorize?state=UYE5nA1KHfXwYI5Dn6Id0JJzhsZOa4&session_state=5551c9d7-4f6e-4ad8-8c9c-4e23b94fd13c&iss=http://localhost:8080/realms/my-realm&code=c4976ae2-b0f8-48ae-8de0-37445b00b732.5551c9d7-4f6e-4ad8-8c9c-4e23b94fd13c.5f8c05b4-8d59-4210-a147-1a4d29ea0026 HTTP/1.1" 302 - 127.0.0.1 - - [30/Sep/2024 09:28:33] "GET / HTTP/1.1" 200 - profile, oidc.user_loggedin True 127.0.0.1 - - [30/Sep/2024 09:28:36] "GET /profile HTTP/1.1" 200 - profile, oidc.user_loggedin True 127.0.0.1 - - [30/Sep/2024 09:28:37] "GET /profile HTTP/1.1" 200 - 127.0.0.1 - - [30/Sep/2024 09:28:40] "GET /logout HTTP/1.1" 302 - 127.0.0.1 - - [30/Sep/2024 09:28:41] "GET / HTTP/1.1" 200 - profile, oidc.user_loggedin False 127.0.0.1 - - [30/Sep/2024 09:28:42] "GET /profile HTTP/1.1" 302 - profile, oidc.user_loggedin False 127.0.0.1 - - [30/Sep/2024 09:28:42] "GET /profile HTTP/1.1" 302 - 127.0.0.1 - - [30/Sep/2024 09:28:42] "GET /login HTTP/1.1" 302 - 127.0.0.1 - - [30/Sep/2024 09:28:42] "GET /authorize?state=Hhoq1PAusTxuX7BI9zh0xRxTpukG3V&session_state=5551c9d7-4f6e-4ad8-8c9c-4e23b94fd13c&iss=http://localhost:8080/realms/my-realm&code=0e10de2f-5f8b-4032-8741-639499f1cf5c.5551c9d7-4f6e-4ad8-8c9c-4e23b94fd13c.5f8c05b4-8d59-4210-a147-1a4d29ea0026 HTTP/1.1" 302 - 127.0.0.1 - - [30/Sep/2024 09:28:42] "GET / HTTP/1.1" 200 - profile, oidc.user_loggedin True 127.0.0.1 - - [30/Sep/2024 09:28:43] "GET /profile HTTP/1.1" 200 - profile, oidc.user_loggedin True 127.0.0.1 - - [30/Sep/2024 09:28:44] "GET /profile HTTP/1.1" 200 -
已尝试方法
- 禁用客户端设置中的Direct Access Grants
- 登出后清除Flask会话,
oidc.user_loggedin变为False - 启用Front-Channel Logout
- 在不同浏览器及隐身模式下测试
- 在授权请求中添加
prompt='login'参数 - 使用
@oidc.require_login装饰器
解决方案
1. 修复登出逻辑,确保Keycloak会话彻底销毁
当前登出代码的执行顺序和参数传递存在问题,导致Keycloak端会话未被完全销毁。修改logout路由:
@app.route('/logout') def logout(): print("logout, oidc.user_loggedin ", oidc.user_loggedin) if oidc.user_loggedin: # 获取id_token用于Keycloak登出验证 id_token = oidc.get_id_token() # 先清除Flask-OIDC内部会话 oidc.logout() # 清除Flask全局session session.clear() # 清除Flask-OIDC本地令牌存储 oidc.store.clear() # 构造完整的Keycloak登出URL,添加client_id确保会话匹配 logout_url = ( oidc.client_secrets['issuer'] + '/protocol/openid-connect/logout' f'?id_token_hint={id_token}' f'&post_logout_redirect_uri={url_for("home", _external=True)}' f'&client_id={你的Keycloak客户端ID}' # 替换为实际客户端ID ) return redirect(logout_url) return redirect(url_for('home'))
2. 强化授权请求的强制认证参数
在login路由中添加max_age=0参数,强制Keycloak忽略现有会话,要求用户重新输入凭证:
@app.route('/login') def login(): print("login, oidc.user_loggedin ", oidc.user_loggedin) if oidc.user_loggedin: return redirect(url_for('profile')) redirect_uri = url_for('oidc_callback', _external=True) # 同时设置prompt='login'和max_age=0,强制重新认证 return oidc.authorize_redirect( redirect_uri, prompt='login', max_age=0 )
3. 调整Keycloak客户端配置
- 将客户端Access Type设置为
confidential(匹配代码中client_secret_post的认证方式) - 在Logout Settings中,确保Post Logout Redirect URIs包含Flask应用主页URL(如
http://localhost:5000/) - 启用Backchannel Logout(相比Front-Channel Logout,Backchannel能更可靠地销毁服务器端会话)
问题原因分析
从日志可见,登出后访问/login时Keycloak仍返回了授权码,说明Keycloak端的用户会话未被彻底销毁。核心原因包括:
- 登出URL未携带
client_id,Keycloak无法精准匹配并销毁对应客户端的会话 - 授权请求的强制认证参数未完全生效,Keycloak仍复用了保留的用户会话
- Flask-OIDC本地存储的令牌未被完全清除,导致客户端仍能获取有效认证信息
内容的提问来源于stack exchange,提问作者greenblack48
相关产品推荐
相关产品推荐

