You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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/
  • redirect_uri必须和Authentik应用中Redirect URIs配置完全一致(包含协议、域名、端口和路径)。

4. 统一协议传输方式

测试环境中确保本地Flask服务和Authentik的协议一致(均为HTTP或HTTPS),如果Authentik强制HTTPS,需关闭本地的OAUTHLIB_INSECURE_TRANSPORT设置,改用HTTPS服务。


内容的提问来源于stack exchange,提问作者Timmy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 05:35:54