asemelianov opened a new issue, #56331:
URL: https://github.com/apache/airflow/issues/56331
### Official Helm Chart version
1.15.0
### Apache Airflow version
2.10.3
### Kubernetes Version
1.30
### Helm Chart configuration
```
config:
core:
dags_folder: '{{ include "airflow_dags" . }}'
load_examples: 'False'
executor: '{{ .Values.executor }}'
colored_console_log: 'True'
remote_logging: '{{- ternary "True" "False"
.Values.elasticsearch.enabled }}'
```
### Docker Image customizations
_No response_
### What happened
When trying to make a request, we get an error:
```
Traceback (most recent call last):
File "/Users/.../.../airflow_kc/request.py", line 34, in <module>
api_response.raise_for_status()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
File "/usr/local/lib/python3.13/site-packages/requests/models.py", line
1024, in raise_for_status
raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 401 Client Error: UNAUTHORIZED for url:
https://test.fart.com/api/v1/dags
```
Our request.py:
```
import requests
keycloak_url = "https://.../auth/realms/.../protocol/openid-connect/token"
client_id = "airflow"
client_secret = "" #
username = "eas"
password = ''
data = {
'grant_type': 'password',
'client_id': client_id,
'username': username,
'password': password,
'client_secret': client_secret,
}
if client_secret:
data['client_secret'] = client_secret
response = requests.post(keycloak_url, data=data)
response.raise_for_status()
token = response.json().get('access_token')
airflow_api = 'https://.../api/v1/dags'
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': f'Bearer {token}'
}
print(token)
api_response = requests.get(airflow_api, headers=headers)
api_response.raise_for_status()
print(api_response.json())
```
Our configmap:
## airflow-cm.yaml
```
apiVersion: v1
kind: ConfigMap
metadata:
name: airflow-prod-api-server-config
namespace: airflow # Change this to your target namespace
data:
webserver_config.py: |
from airflow.providers.fab.auth_manager.security_manager.override
import FabAirflowSecurityManagerOverride
from base64 import b64decode
from cryptography.hazmat.primitives import serialization
from flask_appbuilder.security.manager import AUTH_DB, AUTH_OAUTH
from airflow import configuration as conf
from airflow.www.security import AirflowSecurityManager
import jwt
import logging
import os
import requests
log = logging.getLogger(__name__)
AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION = True
AUTH_ROLES_SYNC_AT_LOGIN = True
AUTH_USER_REGISTRATION_ROLE = "Viewer"
# Make sure you create these roles on Keycloak
AUTH_ROLES_MAPPING = {
"airflow_admin": ["Admin"],
"airflow_op": ["Op"],
"airflow_public": ["Public"],
"airflow_user": ["User"],
"airflow_viewer": ["Viewer"],
}
PROVIDER_NAME = 'keycloak'
CLIENT_ID = 'airflow'
CLIENT_SECRET = ''
OIDC_ISSUER = "https://.../auth/realms/..."
AIRFLOW__API__BASE_URL = "https://..."
OIDC_BASE_URL = f"{OIDC_ISSUER}/protocol/openid-connect"
OIDC_TOKEN_URL = f"{OIDC_BASE_URL}/token"
OIDC_AUTH_URL = f"{OIDC_BASE_URL}/auth"
OIDC_METADATA_URL = f"{OIDC_ISSUER}/.well-known/openid-configuration"
OAUTH_PROVIDERS = [
{
"name": PROVIDER_NAME,
"token_key": "access_token",
"icon": "fa-key",
"remote_app": {
"api_base_url": OIDC_BASE_URL,
"access_token_url": OIDC_TOKEN_URL,
"authorize_url": OIDC_AUTH_URL,
"server_metadata_url": OIDC_METADATA_URL,
"request_token_url": None,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"client_kwargs": {"scope": "email profile"},
},
}
]
# Fetch public key
req = requests.get(OIDC_ISSUER)
key_der_base64 = req.json()["public_key"]
key_der = b64decode(key_der_base64.encode())
public_key = serialization.load_der_public_key(key_der)
class CustomSecurityManager(AirflowSecurityManager):
def get_oauth_user_info(self, provider, response):
if provider == "keycloak":
token = response["access_token"]
me = jwt.decode(token, public_key, algorithms=["HS256",
"RS256"])
# Extract roles from resource access
realm_access = me.get("realm_access", {})
groups = realm_access.get("roles", [])
log.info(f"groups: {groups}")
if not groups:
groups = ["Viewer"]
userinfo = {
"username": me.get("preferred_username"),
"email": me.get("email"),
"first_name": me.get("given_name"),
"last_name": me.get("family_name"),
"role_keys": groups,
}
log.info("user info: {0}".format(userinfo))
return userinfo
else:
return {}
# Make sure to replace this with your own implementation of
AirflowSecurityManager class
SECURITY_MANAGER_CLASS = CustomSecurityManager
```
We also have a client "airflow" defined in Keyclock, and the user "eas" has
the **airflow_admin** role assigned. I'm attaching the generated JWT:
```
{
"alg": "RS256",
"typ": "JWT",
"kid": "1yFHYurR0PlyH0GbrJz3ejA9OlHE0hp83R625Uv335"
}
```
```
{
"exp": 1759411534,
"iat": 1759411234,
"jti": "2833b7af-ead2-4a31-9167-4b3cef387d5",
"iss": "https://.../auth/realms/...",
"aud": "airflow",
"sub": "eaa6b5e5-5093-4caf-b682-48effe66515",
"typ": "Bearer",
"azp": "airflow",
"sid": "4e221e78-9e36-40ca-9fa0-6875ffd429e",
"acr": "1",
"resource_access": {
"airflow": {
"roles": [
"airflow",
"airflow_admin"
]
}
},
"scope": "email profile",
"email_verified": false,
"roles": [
"airflow",
"airflow_admin"
],
"name": "Alex",
"preferred_username": "eas",
"given_name": "Alex,
"family_name": "E",
"email": "[email protected]"
}
```
### What you think should happen instead
_No response_
### How to reproduce
Using a custom script or curl request
### Anything else
_No response_
### Are you willing to submit PR?
- [ ] Yes I am willing to submit a PR!
### Code of Conduct
- [x] I agree to follow this project's [Code of
Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]