使用requests_oauthlib在Flask中集成Authentik SSO遇InvalidClientError
OpenID Connect SSO 实现报错:InvalidClientError
我正在尝试在Web应用中用OpenID Connect实现单点登录(SSO),技术栈包括Python 3.12、身份提供商Authentik、Flask框架和requests_oauthlib库。参照requests_oauthlib的Web应用示例开发步骤如下:
- 在Authentik中创建OAuth2/OpenID Provider类型的应用;
- 基于Flask搭建包含
/login和/callback两个端点的Web服务:/login负责重定向到Authentik并获取authorization_url和state,/callback尝试通过client_id、client_secret、state和authorization_response获取access token。
但在获取access token时,收到错误:
oauthlib.oauth2.rfc6749.errors.InvalidClientError: (invalid_client) Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)
我的代码
import json import os.path from uuid import uuid4 from requests_oauthlib import OAuth2Session from waitress import serve from flask import Flask, jsonify, request, url_for, redirect, session from pprint import pprint with open(os.path.join("Config", "client_secrets.json"), "r") as f: idp = json.load(f) os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" def main(): app = Flask(__name__) # 允许使用HTTP回调 os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = "1" app.config['SECRET_KEY'] = str(uuid4()) @app.route('/') def index(): return """ <a href="/login">Login</a> """ @app.route("/login") def login(): oauth = OAuth2Session(client_id=idp["client_id"], scope=idp["scope"], redirect_uri=idp["callback"] ) authorization_url, state = oauth.authorization_url(idp["authorize"]) session['oauth_state'] = state return redirect(authorization_url) @app.route("/callback") def callback(): pprint(request.__dict__) oauth = OAuth2Session(client_id=idp["client_id"], state=session['oauth_state'] ) # 获取token时失败 token = oauth.fetch_token( idp["token"], client_secret=idp["client_secret"], authorization_response=request.url ) # 永远到不了这一行 session['oauth_token'] = token return "I cannot see this :(", print("Starting webserver") serve(app, host='0.0.0.0', port=5000) print("Webserver running") if __name__ == "__main__": main()
回调请求详情
{'cookies': ImmutableMultiDict([('session', 'REDACTED_SESSION')]), 'environ': {'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'HTTP_ACCEPT_LANGUAGE': 'en-US,en;q=0.5', 'HTTP_CONNECTION': 'keep-alive', 'HTTP_COOKIE': 'session=REDACTED_SESSION', 'HTTP_DNT': '1', 'HTTP_HOST': 'localhost:5000', 'HTTP_SEC_FETCH_DEST': 'document', 'HTTP_SEC_FETCH_MODE': 'navigate', 'HTTP_SEC_FETCH_SITE': 'cross-site', 'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 'HTTP_USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; ' 'rv:123.0) Gecko/20100101 Firefox/123.0', 'PATH_INFO': '/callback', 'QUERY_STRING': 'code=REDACTED_CODE&state=REDACTED_STATE', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_HOST': '127.0.0.1', 'REMOTE_PORT': '64951', 'REQUEST_METHOD': 'GET', 'REQUEST_URI': '/callback?code=REDACTED_CODE&state=REDACTED_STATE', 'SCRIPT_NAME': '', 'SERVER_NAME': 'waitress.invalid', 'SERVER_PORT': '5000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': 'waitress', 'waitress.client_disconnected': <bound method HTTPChannel.check_client_disconnected of <waitress.channel.HTTPChannel connected 127.0.0.1:64951 at 0x285f5356ba0>>, 'werkzeug.request': <Request 'http://localhost:5000/callback?code=REDACTED_CODE&state=REDACTED_STATE' [GET]>, 'wsgi.errors': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>, 'wsgi.file_wrapper': <class 'waitress.buffers.ReadOnlyFileBasedBuffer'>, 'wsgi.input': <_io.BytesIO object at 0x00000285F5391E90>, 'wsgi.input_terminated': True, 'wsgi.multiprocess': False, 'wsgi.multithread': True, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.version': (1, 0)}, 'headers': EnvironHeaders([('Host', 'localhost:5000'), ('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0'), ('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8'), ('Accept-Language', 'en-US,en;q=0.5'), ('Accept-Encoding', 'gzip, deflate, br'), ('Dnt', '1'), ('Connection', 'keep-alive'), ('Cookie', 'session=REDACTED_SESSION'), ('Upgrade-Insecure-Requests', '1'), ('Sec-Fetch-Dest', 'document'), ('Sec-Fetch-Mode', 'navigate'), ('Sec-Fetch-Site', 'cross-site')]), 'host': 'localhost:5000', 'json_module': <flask.json.provider.DefaultJSONProvider object at 0x00000285F5354530>, 'method': 'GET', 'path': '/callback', 'query_string': b'code=REDACTED_CODE&state=REDACTED_' b'_STATE', 'remote_addr': '127.0.0.1', 'root_path': '', 'scheme': 'http', 'server': ('waitress.invalid', 5000), 'shallow': False, 'url': 'http://localhost:5000/callback?code=REDACTED_CODE&state=REDACTED_STATE', 'url_rule': <Rule '/callback' (GET, OPTIONS, HEAD) -> callback>, 'view_args': {}}
解决InvalidClientError的方案
这个错误通常是客户端认证方式不匹配、凭据错误或请求格式不符合Authentik要求导致的,以下是具体排查和修复方向:
1. 调整Authentik应用的客户端认证设置
进入Authentik的应用配置页面,修改Client Authentication选项:
- 优先选择
Client Secret Post,这是兼容性最好的认证方式,确保fetch_token请求会将client_id和client_secret放在请求体中发送; - 如果使用
Client Secret Basic,需确认client_secret无特殊字符导致Base64编码异常,且requests_oauthlib能正确生成Basic Auth头。
2. 在callback中补充redirect_uri参数
初始化OAuth2Session时必须传入和login端点一致的redirect_uri,否则会触发认证失败:
@app.route("/callback") def callback(): pprint(request.__dict__) # 添加redirect_uri参数,和login中的配置保持一致 oauth = OAuth2Session(client_id=idp["client_id"], state=session['oauth_state'], redirect_uri=idp["callback"] ) token = oauth.fetch_token( idp["token"], client_secret=idp["client_secret"], authorization_response=request.url ) session['oauth_token'] = token return "登录成功!"
3. 验证client_secrets.json配置准确性
确保配置文件中的参数和Authentik应用完全匹配:
client_id和client_secret必须和Authentik应用页面显示的内容完全一致;authorize和token的URL格式正确:- 授权URL:
https://你的Authentik域名/application/o/authorize/ - Token URL:
https://你的Authentik域名/application/o/token/
- 授权URL:
redirect_uri必须和Authentik应用中Redirect URIs配置完全一致(包含协议、域名、端口和路径)。
4. 统一协议传输方式
测试环境中确保本地Flask服务和Authentik的协议一致(均为HTTP或HTTPS),如果Authentik强制HTTPS,需关闭本地的OAUTHLIB_INSECURE_TRANSPORT设置,改用HTTPS服务。
内容的提问来源于stack exchange,提问作者Timmy
相关产品推荐
相关产品推荐

