如何在 Flask REST API 服务中使用 Keycloak

How to use Keycloak with Flask REST API Service

我正在尝试为我的 Flask Rest 服务实现 Keycloak,但它总是给出以下错误。

{"error": "invalid_token", "error_description": "Token required but invalid"}

client_secrets.json

    {
    "web": {
        "issuer": "http://localhost:18080/auth/realms/Dev-Auth",
        "auth_uri": "http://localhost:18080/auth/realms/Dev-Auth/protocol/openid-connect/auth",
        "client_id": "flask_api",
        "client_secret": "0bff8456-9be2-4f82-884e-c7f9bea65bd1",
        "redirect_uris": [
            "http://localhost:5001/*"
        ],
        "userinfo_uri": "http://localhost:18080/auth/realms/Dev-Auth/protocol/openid-connect/userinfo",
        "token_uri": "http://localhost:18080/auth/realms/Dev-Auth/protocol/openid-connect/token",
        "token_introspection_uri": "http://localhost:18080/auth/realms/Dev-Auth/protocol/openid-connect/token/introspect",
        "bearer_only": "true"
    } 
}

run.py

    import json
    import logging

    from flask import Flask, g, jsonify
    from flask_oidc import OpenIDConnect
    import requests

    app = Flask(__name__)

    app.config.update({
        'SECRET_KEY': 'TESTING-ANURAG',
        'TESTING': True,
        'DEBUG': True,
        'OIDC_CLIENT_SECRETS': 'client_secrets.json',
        'OIDC_OPENID_REALM': 'Dev-Auth',
        'OIDC_INTROSPECTION_AUTH_METHOD': 'bearer',
        'OIDC-SCOPES': ['openid']
    })


    oidc = OpenIDConnect(app)

@app.route('/api', methods=['GET'])
@oidc.accept_token(require_token=True, scopes_required=['openid'])
def hello_api():
    """OAuth 2.0 protected API endpoint accessible via AccessToken"""

    return json.dumps({'hello': 'Welcome %s' % g.oidc_token_info['sub']})


if __name__ == '__main__':

任何人都有想法,如果这里有任何问题。

我遇到了同样的问题,我(终于\o/)解决了这个问题。尝试以下操作:

'OIDC_INTROSPECTION_AUTH_METHOD': 'client_secret_post'
'OIDC_TOKEN_TYPE_HINT': 'access_token'

同时删除所需范围的列表以避免出现任何可能的错误:

@oidc.accept_token(require_token=True)

如果您正尝试访问您的 Rest 服务,例如:

http://127.0.0.1:5001/api

那就不行了,因为没有access token。

您可以做的是访问 http://127.0.0.1:5001/private 并从内部调用 /api 在 header =)

中传递令牌
  @app.route('/private')
  @oidc.require_login
  def hello_me():
    info = oidc.user_getinfo(['email', 'openid_id'])

    if user_id in oidc.credentials_store:

        try:
            from oauth2client.client import OAuth2Credentials
            access_token = OAuth2Credentials.from_json(oidc.credentials_store[user_id]).access_token

            headers = {'Authorization': f'Bearer {access_token}'}

            access_like_this = requests.get('http://localhost:5001/api', headers=headers).text
        except:

            access_like_this = "we failed"

        return f'Hello, api: {access_like_this} <a href="/">Return</a>'

    else:

        return f'Ops, <a href="/">Return</a>'

documentation 说了以下关于 accept_token 装饰器

Tokens are accepted as part of the query URL (access_token value) or a POST form value (access_token).

所以当你有类似

的东西时
@app.route('/api', methods=['GET'])
@oidc.accept_token(require_token=True, scopes_required=['openid'])
def hello_api():
...

您的 GET 请求应该是

https://yourhost/api?access_token=blahblahsomethingquiteopaque

其中 blahblahsomethingquiteopaque 是您的 OAUTH2 访问令牌。

对于 POST api 调用,您必须在数据中提供 access_token key/value。

这是我项目中的一个示例:

@app.route('/api/new_employee', methods=['POST'])
@oidc.accept_token(require_token=True)
def new_employee():
...

API 路由用于提交由该视图呈现的表单

from flask import Flask, g, redirect, request, Response, render_template
from flask_oidc import OpenIDConnect
from oauth2client.client import OAuth2Credentials

def auth(user):
user_id = user.get('sub')
username = user.get('preferred_username')
if user_id in oidc.credentials_store:
    access_token = OAuth2Credentials.from_json(oidc.credentials_store[user_id]).access_token
else:
    access_token = None
return username, access_token

app = Flask(__name__)
app.config.update({
    'SECRET_KEY': 'SomethingNotEntirelySecret',
    'TESTING': True,
    'DEBUG': True,
    'OIDC_CLIENT_SECRETS': 'client_secrets.json',
    'OIDC_ID_TOKEN_COOKIE_SECURE': False,
    'OIDC_REQUIRE_VERIFIED_EMAIL': False,
    'OIDC_USER_INFO_ENABLED': True,
    'OIDC_OPENID_REALM': 'flask-demo',
    'OIDC_SCOPES': ['openid', 'email', 'profile'],
    'OIDC_INTROSPECTION_AUTH_METHOD': 'client_secret_post',
})
oidc = OpenIDConnect(app)

@app.route('/grants/employees')
@oidc.require_login
def grants_employees():
    user = oidc.user_getinfo(['preferred_username', 'email', 'sub', 'groups'])
    vars['username'], vars['access_token'] = auth(user)
    if not vars['access_token']:
        oidc.logout()
        return redirect(request.url)
    return render_template('grants_employees.html', vars=vars)

然后在表单内的 grants_employees.html 模板中,我有一个隐藏字段,其中访问令牌在 vars[[=​​41=]]

中传递
<input type="hidden" class="form-control" id="inputAccessToken" name="access_token" value="{{ vars.access_token }}" required>

还值得一提的是文档说

Note that this only works if a token introspection url is configured, as that URL will be queried for the validity and scopes of a token.

因此您必须确保您的 client_secrets.json 包含 token_introspection_uri

的正确值
{
    "web": {
        "issuer": "https://yourkeycloak/auth/realms/master",
        "auth_uri": "https://yourkeycloak/auth/realms/master/protocol/openid-connect/auth",
        "client_id": "your_client_id",
        "client_secret": "your_client_secret",
        "redirect_uris": [
            "http://127.0.0.1/*"
        ],
        "userinfo_uri": "https://yourkeycloak/auth/realms/master/protocol/openid-connect/userinfo", 
        "token_uri": "https://yourkeycloak/auth/realms/master/protocol/openid-connect/token",
        "token_introspection_uri": "https://yourkeycloak/auth/realms/master/protocol/openid-connect/token/introspect"
    }
}