Keycloak UserInfo 端点403 Forbidden 错误解决

Keycloak UserInfo 端点403 Forbidden 错误解决

Intro

实习改一个接口的bug,方法内调用其他系统(A)提供的服务。

  1. 方法内通过请求 Keycloak /auth/realms/momenta-prod/protocol/openid-connect/token获取到Access Token
  2. 将Access Token 放入构造的请求头中,调用其他系统(A)提供的服务,发送请求

返回的http状态码是 500,看response的内容定位是没handle异常,好在对接的服务日志还是挺完整的。可以分析出,在服务内部使用mozilla_django_oidc库来处理OpenID Connect的认证时抛出了HTTPError异常。可以具体到是请求 Keycloak 的 UserInfo 端点时返回了 403 Forbidden 错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
HTTPError at /api/v1/xxx
403 Client Error: Forbidden for url: https://{keycloak-server}/auth/realms/momenta-prod/protocol/openid-connect/userinfo
Request Method: GET
Request URL: https://{a-server}/api/v1/xxx?Vehiclename=L7-PMV304
Django Version: 4.2.6
Python Executable: /usr/local/bin/python3
Python Version: 3.11.4
Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/views/decorators/csrf.py", line 56, in wrapper_view
    return view_func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/rest_framework/viewsets.py", line 125, in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/rest_framework/views.py", line 515, in dispatch
    response = self.handle_exception(exc)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/rest_framework/views.py", line 475, in handle_exception
    self.raise_uncaught_exception(exc)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/rest_framework/views.py", line 486, in raise_uncaught_exception
    raise exc
    ^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/rest_framework/views.py", line 503, in dispatch
    self.initial(request, *args, **kwargs)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/rest_framework/views.py", line 420, in initial
    self.perform_authentication(request)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/rest_framework/views.py", line 330, in perform_authentication
    request.user
    ^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/rest_framework/request.py", line 232, in user
    self._authenticate()
    ^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/rest_framework/request.py", line 385, in _authenticate
    user_auth_tuple = authenticator.authenticate(self)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/mozilla_django_oidc/contrib/drf.py", line 80, in authenticate
    user = self.backend.get_or_create_user(access_token, None, None)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/mozilla_django_oidc/auth.py", line 347, in get_or_create_user
    user_info = self.get_userinfo(access_token, id_token, payload)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/mozilla_django_oidc/auth.py", line 281, in get_userinfo
    user_response.raise_for_status()
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/requests/models.py", line 1024, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Exception Type: HTTPError at /api/v1/VersionLaster
Exception Value: 403 Client Error: Forbidden for url: https://{keycloak-server}/auth/realms/momenta-prod/protocol/openid-connect/userinfo
Note

按理说是一个很容易修复的bug,一方面由于是集成其他服务,debug起来不方便,只能对着服务返回不完整日志定位问题;OIDC疑似会在响应的Header里返回一些提示信息,告诉是缺少openid scope导致的403 Forbidden错误,但响应太长还真没注意到()
另一方面就是OIDC涉及到知识盲区了,没踩坑经验

Solution

Keycloak 的 UserInfo 端点要求 Access Token 必须包含 openid scope,否则会返回 403 Forbidden 错误。这是 OpenID Connect 协议规范的要求,确保只有经过身份验证的令牌才能访问用户信息。

部分 Keycloak 配置下,Access Token 的 JWT payload 可能未直接包含 scope 字段,但只要申请 token 时带上了 openid scope,UserInfo 端点即可正常使用。可以参考如下python代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests

def get_access_token():
    client_id = 'your client id'
    client_secret = 'your client secret'
    url = 'https://{your-keycloak-server}/auth/realms/{your-realm}/protocol/openid-connect/token'
    headers = {"Content-Type": "application/x-www-form-urlencoded"}
    payload = {
        "grant_type": "client_credentials",
        "client_id": client_id,
        "client_secret": client_secret,
        "scope": "openid"  # ❗️ 确保包含 openid scope
    }
    resp = requests.request("POST", url, headers=headers, data=payload)
    content = resp.json()
    return content['access_token']

def request():
    access_token = get_access_token()
    url = 'your request url'
    headers = {'Authorization': f'Bearer {access_token}'}
    resp = requests.request(method, url, headers=headers)

在请求体中添加 scope=openid,确保 Access Token 包含 openid scope。在JSON Web Tokens - jwt.io解码 Access Token 后,可以看到新生成的 Access Token 在scope这个Claim中包含了openid

左侧/右侧分别为未包含/包含 openid scope 的 token

两种JWT对比

Ref

JWT
OIDC 与 OAuth2.0 综述 | Authing 文档

Keycloak UserInfo 端点403 Forbidden 错误解决

https://vluv.space/Dev/Backend/keycloak校验异常记录/

作者

GnixAij

发布于

2025-06-04

更新于

2025-06-05

许可协议

评论