This is an automated email from the ASF dual-hosted git repository.
gopidesu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 4ffb0a6fd38 Set JWT token to localStorage from cookies (#47432)
4ffb0a6fd38 is described below
commit 4ffb0a6fd38ae97bd02e1eb4e40d3781318ef9ef
Author: GPK <[email protected]>
AuthorDate: Fri Mar 14 14:59:49 2025 +0000
Set JWT token to localStorage from cookies (#47432)
* Set JWT token to localStorage onsuccess login
* Set jwt to localstorage in fab
* Set jwt token to cookies from redirect
* Set jwt token to cookies from redirect
* Fix tests for tokenHandler
* Update auth manager documentation
* Update cookie to use secure property
* Use react-cookie
* Use react-cookie
* Rename cookie constant to COOKIE_NAME_JWT_TOKEN and fix k8s and docker
tests
* Remove samesite param and update docs
* Fix auth manager docs error
* Remove samesite param in auth manager
* Update docs/apache-airflow/core-concepts/auth-manager/index.rst
Co-authored-by: Pierre Jeambrun <[email protected]>
* Fix static checks
---------
Co-authored-by: Pierre Jeambrun <[email protected]>
---
.../api_fastapi/auth/managers/base_auth_manager.py | 3 +
.../auth/managers/simple/routes/login.py | 13 +++--
.../auth/managers/simple/ui/package.json | 3 +-
.../auth/managers/simple/ui/pnpm-lock.yaml | 65 ++++++++++++++++++++--
.../auth/managers/simple/ui/src/login/Login.tsx | 7 ++-
airflow/ui/src/utils/tokenHandler.test.ts | 36 ++++++------
airflow/ui/src/utils/tokenHandler.ts | 32 ++++++-----
docker_tests/test_docker_compose_quick_start.py | 29 ++--------
.../core-concepts/auth-manager/index.rst | 19 ++++++-
kubernetes_tests/test_base.py | 35 +++---------
.../amazon/aws/auth_manager/router/login.py | 9 ++-
.../amazon/aws/auth_manager/router/test_login.py | 3 +-
.../fab/src/airflow/providers/fab/www/views.py | 7 ++-
.../auth/managers/simple/routes/test_login.py | 2 +-
14 files changed, 161 insertions(+), 102 deletions(-)
diff --git a/airflow/api_fastapi/auth/managers/base_auth_manager.py
b/airflow/api_fastapi/auth/managers/base_auth_manager.py
index e1ac86e090b..c8714e41034 100644
--- a/airflow/api_fastapi/auth/managers/base_auth_manager.py
+++ b/airflow/api_fastapi/auth/managers/base_auth_manager.py
@@ -72,6 +72,9 @@ log = logging.getLogger(__name__)
T = TypeVar("T", bound=BaseUser)
+COOKIE_NAME_JWT_TOKEN = "_token"
+
+
class BaseAuthManager(Generic[T], LoggingMixin, metaclass=ABCMeta):
"""
Class to derive in order to implement concrete auth managers.
diff --git a/airflow/api_fastapi/auth/managers/simple/routes/login.py
b/airflow/api_fastapi/auth/managers/simple/routes/login.py
index da53e1f82b8..b637a12373b 100644
--- a/airflow/api_fastapi/auth/managers/simple/routes/login.py
+++ b/airflow/api_fastapi/auth/managers/simple/routes/login.py
@@ -17,12 +17,11 @@
from __future__ import annotations
-from urllib.parse import urljoin
-
from fastapi import HTTPException, status
from starlette.responses import RedirectResponse
from airflow.api_fastapi.app import get_auth_manager
+from airflow.api_fastapi.auth.managers.base_auth_manager import
COOKIE_NAME_JWT_TOKEN
from airflow.api_fastapi.auth.managers.simple.datamodels.login import
LoginBody, LoginResponse
from airflow.api_fastapi.auth.managers.simple.services.login import
SimpleAuthManagerLogin
from airflow.api_fastapi.auth.managers.simple.user import SimpleAuthManagerUser
@@ -62,8 +61,14 @@ def create_token_all_admins() -> RedirectResponse:
username="Anonymous",
role="ADMIN",
)
- url = urljoin(conf.get("api", "base_url"),
f"?token={get_auth_manager().generate_jwt(user)}")
- return RedirectResponse(url=url)
+
+ response = RedirectResponse(url=conf.get("api", "base_url"))
+ response.set_cookie(
+ COOKIE_NAME_JWT_TOKEN,
+ get_auth_manager().generate_jwt(user),
+ secure=True,
+ )
+ return response
@login_router.post(
diff --git a/airflow/api_fastapi/auth/managers/simple/ui/package.json
b/airflow/api_fastapi/auth/managers/simple/ui/package.json
index f4d766d9f08..a26b9aad6b2 100644
--- a/airflow/api_fastapi/auth/managers/simple/ui/package.json
+++ b/airflow/api_fastapi/auth/managers/simple/ui/package.json
@@ -18,7 +18,8 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.20.0",
- "react-router-dom": "^6.26.2"
+ "react-router-dom": "^6.26.2",
+ "react-cookie": "^7.0.0"
},
"devDependencies": {
"@7nohe/openapi-react-query-codegen": "^1.6.0",
diff --git a/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml
b/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml
index bb2dd4fd45f..d0c2f468571 100644
--- a/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml
+++ b/airflow/api_fastapi/auth/managers/simple/ui/pnpm-lock.yaml
@@ -10,7 +10,7 @@ importers:
dependencies:
'@chakra-ui/react':
specifier: ^3.1.1
- version:
3.3.3(@emotion/[email protected]([email protected]))([email protected]([email protected]))([email protected])
+ version:
3.3.3(@emotion/[email protected](@types/[email protected])([email protected]))([email protected]([email protected]))([email protected])
'@tanstack/react-query':
specifier: ^5.52.1
version: 5.64.1([email protected])
@@ -20,6 +20,9 @@ importers:
react:
specifier: ^18.3.1
version: 18.3.1
+ react-cookie:
+ specifier: ^7.0.0
+ version: 7.2.2([email protected])
react-dom:
specifier: ^18.3.1
version: 18.3.1([email protected])
@@ -38,7 +41,7 @@ importers:
version: 6.6.3
'@testing-library/react':
specifier: ^16.0.0
- version:
16.2.0(@testing-library/[email protected])([email protected]([email protected]))([email protected])
+ version:
16.2.0(@testing-library/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
'@vitejs/plugin-react-swc':
specifier: ^3.7.0
version: 3.7.2(@swc/[email protected])([email protected]([email protected]))
@@ -663,15 +666,24 @@ packages:
'@types/[email protected]':
resolution: {integrity:
sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+ '@types/[email protected]':
+ resolution: {integrity:
sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
+
'@types/[email protected]':
resolution: {integrity:
sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
+ '@types/[email protected]':
+ resolution: {integrity:
sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==}
+
'@types/[email protected]':
resolution: {integrity:
sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/[email protected]':
resolution: {integrity:
sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
+ '@types/[email protected]':
+ resolution: {integrity:
sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==}
+
'@vitejs/[email protected]':
resolution: {integrity:
sha512-y0byko2b2tSVVf5Gpng1eEhX1OvPC7x8yns1Fx8jDzlJp4LS6CMkCPfLw47cjyoMrshQDoQw4qcgjsU9VvlCew==}
peerDependencies:
@@ -1070,6 +1082,10 @@ packages:
[email protected]:
resolution: {integrity:
sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
+ [email protected]:
+ resolution: {integrity:
sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
+ engines: {node: '>= 0.6'}
+
[email protected]:
resolution: {integrity:
sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
engines: {node: '>=10'}
@@ -1702,6 +1718,11 @@ packages:
[email protected]:
resolution: {integrity:
sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==}
+ [email protected]:
+ resolution: {integrity:
sha512-e+hi6axHcw9VODoeVu8WyMWyoosa1pzpyjfvrLdF7CexfU+WSGZdDuRfHa4RJgTpfv3ZjdIpHE14HpYBieHFhg==}
+ peerDependencies:
+ react: '>= 16.3.0'
+
[email protected]:
resolution: {integrity:
sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
@@ -1892,6 +1913,9 @@ packages:
engines: {node: '>=0.8.0'}
hasBin: true
+ [email protected]:
+ resolution: {integrity:
sha512-fMiOcS3TmzP2x5QV26pIH3mvhexLIT0HmPa3V7Q7knRfT9HG6kTwq02HZGLPw0sAOXrAmotElGRvTLCMbJsvxQ==}
+
[email protected]:
resolution: {integrity:
sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==}
@@ -2157,11 +2181,11 @@ snapshots:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
-
'@chakra-ui/[email protected](@emotion/[email protected]([email protected]))([email protected]([email protected]))([email protected])':
+
'@chakra-ui/[email protected](@emotion/[email protected](@types/[email protected])([email protected]))([email protected]([email protected]))([email protected])':
dependencies:
'@ark-ui/react': 4.8.0([email protected]([email protected]))([email protected])
'@emotion/is-prop-valid': 1.3.1
- '@emotion/react': 11.14.0([email protected])
+ '@emotion/react': 11.14.0(@types/[email protected])([email protected])
'@emotion/serialize': 1.3.3
'@emotion/use-insertion-effect-with-fallbacks': 1.2.0([email protected])
'@emotion/utils': 1.4.2
@@ -2202,7 +2226,7 @@ snapshots:
'@emotion/[email protected]': {}
- '@emotion/[email protected]([email protected])':
+ '@emotion/[email protected](@types/[email protected])([email protected])':
dependencies:
'@babel/runtime': 7.26.9
'@emotion/babel-plugin': 11.13.5
@@ -2213,6 +2237,8 @@ snapshots:
'@emotion/weak-memoize': 0.4.0
hoist-non-react-statics: 3.3.2
react: 18.3.1
+ optionalDependencies:
+ '@types/react': 19.0.10
transitivePeerDependencies:
- supports-color
@@ -2585,12 +2611,14 @@ snapshots:
lodash: 4.17.21
redent: 3.0.0
-
'@testing-library/[email protected](@testing-library/[email protected])([email protected]([email protected]))([email protected])':
+
'@testing-library/[email protected](@testing-library/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])':
dependencies:
'@babel/runtime': 7.26.0
'@testing-library/dom': 10.4.0
react: 18.3.1
react-dom: 18.3.1([email protected])
+ optionalDependencies:
+ '@types/react': 19.0.10
'@ts-morph/[email protected]':
dependencies:
@@ -2601,12 +2629,23 @@ snapshots:
'@types/[email protected]': {}
+ '@types/[email protected]': {}
+
'@types/[email protected]': {}
+ '@types/[email protected]':
+ dependencies:
+ '@types/react': 19.0.10
+ hoist-non-react-statics: 3.3.2
+
'@types/[email protected]': {}
'@types/[email protected]': {}
+ '@types/[email protected]':
+ dependencies:
+ csstype: 3.1.3
+
'@vitejs/[email protected](@swc/[email protected])([email protected]([email protected]))':
dependencies:
'@swc/core': 1.10.7(@swc/[email protected])
@@ -3286,6 +3325,8 @@ snapshots:
[email protected]: {}
+ [email protected]: {}
+
[email protected]:
dependencies:
'@types/parse-json': 4.0.2
@@ -3915,6 +3956,13 @@ snapshots:
defu: 6.1.4
destr: 2.0.3
+ [email protected]([email protected]):
+ dependencies:
+ '@types/hoist-non-react-statics': 3.3.6
+ hoist-non-react-statics: 3.3.2
+ react: 18.3.1
+ universal-cookie: 7.2.2
+
[email protected]([email protected]):
dependencies:
loose-envify: 1.4.0
@@ -4096,6 +4144,11 @@ snapshots:
[email protected]:
optional: true
+ [email protected]:
+ dependencies:
+ '@types/cookie': 0.6.0
+ cookie: 0.7.2
+
[email protected]: {}
[email protected]:
diff --git a/airflow/api_fastapi/auth/managers/simple/ui/src/login/Login.tsx
b/airflow/api_fastapi/auth/managers/simple/ui/src/login/Login.tsx
index 915e70b55c5..e0321ad08e9 100644
--- a/airflow/api_fastapi/auth/managers/simple/ui/src/login/Login.tsx
+++ b/airflow/api_fastapi/auth/managers/simple/ui/src/login/Login.tsx
@@ -25,6 +25,7 @@ import {LoginForm} from "src/login/LoginForm";
import type {ApiError} from "openapi-gen/requests/core/ApiError";
import type {LoginResponse, HTTPExceptionResponse, HTTPValidationError} from
"openapi-gen/requests/types.gen";
import { useSearchParams } from "react-router-dom";
+import { useCookies } from 'react-cookie';
export type LoginBody = {
username: string; password: string;
@@ -36,11 +37,15 @@ type ExpandedApiError = {
export const Login = () => {
const [searchParams, setSearchParams] = useSearchParams();
+ const [cookies, setCookie] = useCookies(['_token']);
const onSuccess = (data: LoginResponse) => {
// Redirect to appropriate page with the token
const next = searchParams.get("next")
- globalThis.location.replace(`${next ?? ""}?token=${data.jwt_token}`);
+
+ setCookie('_token', data.jwt_token, {path: "/", secure: true});
+
+ globalThis.location.replace(`${next ?? ""}`);
}
const {createToken, error: err, isPending, setError} =
useCreateToken({onSuccess});
const error = err as ExpandedApiError;
diff --git a/airflow/ui/src/utils/tokenHandler.test.ts
b/airflow/ui/src/utils/tokenHandler.test.ts
index b0073f9560d..8359bc193a1 100644
--- a/airflow/ui/src/utils/tokenHandler.test.ts
+++ b/airflow/ui/src/utils/tokenHandler.test.ts
@@ -17,21 +17,23 @@
* under the License.
*/
import type { InternalAxiosRequestConfig } from "axios";
-import { afterEach, describe, it, vi, expect } from "vitest";
+import { afterEach, describe, it, vi, expect, beforeAll } from "vitest";
-import { TOKEN_QUERY_PARAM_NAME, TOKEN_STORAGE_KEY, tokenHandler } from
"./tokenHandler";
+import { TOKEN_STORAGE_KEY, tokenHandler } from "./tokenHandler";
-describe.each([
- { searchParams: new URLSearchParams({ token: "something" }) },
- { searchParams: new URLSearchParams({ param2: "someParam2", token: "else" })
},
- { searchParams: new URLSearchParams({}) },
-])("TokenFlow Interceptor", ({ searchParams }) => {
- it("Should read from the SearchParams, persist to the localStorage and
remove from the SearchParams", () => {
- const token = searchParams.get(TOKEN_QUERY_PARAM_NAME);
+describe("TokenFlow Interceptor", () => {
+ beforeAll(() => {
+ Object.defineProperty(document, "cookie", {
+ writable: true,
+ });
+ });
- const setItemMock = vi.spyOn(localStorage, "setItem");
+ it("Should read from the cookie, persist to the localStorage and remove from
the cookie", () => {
+ const token = "test-token";
- vi.stubGlobal("location", { search: searchParams.toString() });
+ document.cookie = `_token=${token};`;
+
+ const setItemMock = vi.spyOn(localStorage, "setItem");
const headers = {};
@@ -39,14 +41,10 @@ describe.each([
const { headers: updatedHeaders } = tokenHandler(config as
InternalAxiosRequestConfig);
- if (token === null) {
- expect(setItemMock).toHaveBeenCalledTimes(0);
- } else {
- expect(setItemMock).toHaveBeenCalledOnce();
- expect(setItemMock).toHaveBeenCalledWith(TOKEN_STORAGE_KEY, token);
- expect(searchParams).not.to.contains.keys(TOKEN_QUERY_PARAM_NAME);
- expect(updatedHeaders).toEqual({ Authorization: `Bearer ${token}` });
- }
+ expect(setItemMock).toHaveBeenCalledOnce();
+ expect(setItemMock).toHaveBeenCalledWith(TOKEN_STORAGE_KEY, token);
+ expect(updatedHeaders).toEqual({ Authorization: `Bearer ${token}` });
+ expect(document.cookie).toContain("_token=; expires=");
});
});
diff --git a/airflow/ui/src/utils/tokenHandler.ts
b/airflow/ui/src/utils/tokenHandler.ts
index ab267cf96ec..b257dd06c08 100644
--- a/airflow/ui/src/utils/tokenHandler.ts
+++ b/airflow/ui/src/utils/tokenHandler.ts
@@ -19,25 +19,29 @@
import type { InternalAxiosRequestConfig } from "axios";
export const TOKEN_STORAGE_KEY = "token";
-export const TOKEN_QUERY_PARAM_NAME = "token";
+const getTokenFromCookies = (): string | undefined => {
+ const cookies = document.cookie.split(";");
-export const tokenHandler = (config: InternalAxiosRequestConfig) => {
- const searchParams = new URLSearchParams(globalThis.location.search);
-
- const tokenUrl = searchParams.get(TOKEN_QUERY_PARAM_NAME);
+ for (const cookie of cookies) {
+ if (cookie.startsWith("_token=")) {
+ const [, token] = cookie.split("=");
- let token: string | null;
+ if (token !== undefined) {
+ localStorage.setItem(TOKEN_STORAGE_KEY, token);
+ document.cookie = "_token=; expires=Thu, 01 Jan 2000 00:00:00 UTC;
path=/;";
- if (tokenUrl === null) {
- token = localStorage.getItem(TOKEN_STORAGE_KEY);
- } else {
- localStorage.setItem(TOKEN_QUERY_PARAM_NAME, tokenUrl);
- searchParams.delete(TOKEN_QUERY_PARAM_NAME);
- globalThis.location.search = searchParams.toString();
- token = tokenUrl;
+ return token;
+ }
+ }
}
- if (token !== null) {
+ return undefined;
+};
+
+export const tokenHandler = (config: InternalAxiosRequestConfig) => {
+ const token = localStorage.getItem(TOKEN_STORAGE_KEY) ??
getTokenFromCookies();
+
+ if (token !== undefined) {
config.headers.Authorization = `Bearer ${token}`;
}
diff --git a/docker_tests/test_docker_compose_quick_start.py
b/docker_tests/test_docker_compose_quick_start.py
index ccf4ef18fcd..452ad41aa8e 100644
--- a/docker_tests/test_docker_compose_quick_start.py
+++ b/docker_tests/test_docker_compose_quick_start.py
@@ -18,12 +18,10 @@ from __future__ import annotations
import json
import os
-import re
import shlex
from pprint import pprint
from shutil import copyfile
from time import sleep
-from urllib.parse import parse_qs, urlparse
import pytest
import requests
@@ -61,32 +59,17 @@ def get_jwt_token() -> str:
"""
# get csrf token from login page
session = requests.Session()
- get_login_form_response =
session.get(f"http://{DOCKER_COMPOSE_HOST_PORT}/auth/login")
- csrf_token = re.search(
- r'<input id="csrf_token" name="csrf_token" type="hidden"
value="(.+?)">',
- get_login_form_response.text,
- )
- assert csrf_token, "Failed to get csrf token from login page"
- csrf_token_str = csrf_token.group(1)
- assert csrf_token_str, "Failed to get csrf token from login page"
- # login with form data
+ url = f"http://{DOCKER_COMPOSE_HOST_PORT}/auth/token"
login_response = session.post(
- f"http://{DOCKER_COMPOSE_HOST_PORT}/auth/login",
- data={
+ url,
+ json={
"username": AIRFLOW_WWW_USER_USERNAME,
"password": AIRFLOW_WWW_USER_PASSWORD,
- "csrf_token": csrf_token_str,
},
)
- redirect_url = login_response.url
- # ensure redirect_url is a string
- redirect_url_str = str(redirect_url) if redirect_url is not None else ""
- assert "/?token" in redirect_url_str, f"Login failed with redirect url
{redirect_url_str}"
- parsed_url = urlparse(redirect_url_str)
- query_params = parse_qs(str(parsed_url.query))
- jwt_token_list = query_params.get("token")
- jwt_token = jwt_token_list[0] if jwt_token_list else None
- assert jwt_token, f"Failed to get JWT token from redirect url
{redirect_url_str}"
+ jwt_token = login_response.json().get("jwt_token")
+
+ assert jwt_token, f"Failed to get JWT token from redirect url {url} with
status code {login_response}"
return jwt_token
diff --git a/docs/apache-airflow/core-concepts/auth-manager/index.rst
b/docs/apache-airflow/core-concepts/auth-manager/index.rst
index dc973f1d5e7..170a819985f 100644
--- a/docs/apache-airflow/core-concepts/auth-manager/index.rst
+++ b/docs/apache-airflow/core-concepts/auth-manager/index.rst
@@ -92,13 +92,30 @@ Some reasons you may want to write a custom auth manager
include:
* You'd like to use an auth manager that leverages an identity provider from
your preferred cloud provider.
* You have a private user management tool that is only available to you or
your organization.
-
Authentication related BaseAuthManager methods
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* ``get_user``: Return the signed-in user.
* ``get_url_login``: Return the URL the user is redirected to for signing in.
+JWT token management by auth managers
+-------------------------------------
+The auth manager is responsible of creating the JWT token and pass it to
Airflow UI. The protocol to exchange the JWT
+token between the auth manager and Airflow UI is using cookies. The auth
manager needs to save the JWT token in a
+cookie named ``_token`` before redirecting to the Airflow UI. The Airflow UI
will then read the cookie, save it and
+delete the cookie.
+
+.. code-block:: python
+
+ from airflow.api_fastapi.auth.managers.base_auth_manager import
COOKIE_NAME_JWT_TOKEN
+
+ response = RedirectResponse(url="/")
+ response.set_cookie(COOKIE_NAME_JWT_TOKEN, token, secure=True)
+ return response
+
+.. note::
+ Do not set the cookie parameter ``httponly`` to ``True``. Airflow UI needs
to access the JWT token from the cookie.
+
Authorization related BaseAuthManager methods
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/kubernetes_tests/test_base.py b/kubernetes_tests/test_base.py
index 8cad42de296..03e361f0eaa 100644
--- a/kubernetes_tests/test_base.py
+++ b/kubernetes_tests/test_base.py
@@ -25,7 +25,6 @@ import time
from datetime import datetime, timezone
from pathlib import Path
from subprocess import check_call, check_output
-from urllib.parse import parse_qs, urlparse
import pytest
import requests
@@ -145,13 +144,9 @@ class BaseK8STest:
Note: API server is still using FAB Auth Manager.
Steps:
- 1. Get the login page to get the csrf token
- - The csrf token is in the hidden input field with id "csrf_token"
- 2. Login with the username and password
+ 1. Login with the username and password
- Must use the same session to keep the csrf token session
- 3. Extract the JWT token from the redirect url
- - Expected to have a connection error
- - The redirect url should have the JWT token as a query parameter
+ 2. Extract the JWT token from the auth/token url
:param session: The session to use for the request
:param username: The username to use for the login
@@ -163,28 +158,14 @@ class BaseK8STest:
session = requests.Session()
session.mount("http://", HTTPAdapter(max_retries=retry))
session.mount("https://", HTTPAdapter(max_retries=retry))
- get_login_form_response =
session.get(f"http://{KUBERNETES_HOST_PORT}/auth/login")
- csrf_token = re.search(
- r'<input id="csrf_token" name="csrf_token" type="hidden"
value="(.+?)">',
- get_login_form_response.text,
- )
- assert csrf_token, "Failed to get csrf token from login page"
- csrf_token_str = csrf_token.group(1)
- assert csrf_token_str, "Failed to get csrf token from login page"
- # login with form data
+ url = f"http://{KUBERNETES_HOST_PORT}/auth/token"
login_response = session.post(
- f"http://{KUBERNETES_HOST_PORT}/auth/login",
- data={"username": username, "password": password, "csrf_token":
csrf_token_str},
+ url,
+ json={"username": username, "password": password},
)
- redirect_url = login_response.url
- # ensure redirect_url is a string
- redirect_url_str = str(redirect_url) if redirect_url is not None else
""
- assert "/?token" in redirect_url_str, f"Login failed with redirect url
{redirect_url_str}"
- parsed_url = urlparse(redirect_url_str)
- query_params = parse_qs(str(parsed_url.query))
- jwt_token_list = query_params.get("token")
- jwt_token = jwt_token_list[0] if jwt_token_list else None
- assert jwt_token, f"Failed to get JWT token from redirect url
{redirect_url_str}"
+ jwt_token = login_response.json().get("jwt_token")
+
+ assert jwt_token, f"Failed to get JWT token from redirect url {url}
with status code {login_response}"
return jwt_token
def _get_session_with_retries(self):
diff --git
a/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/router/login.py
b/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/router/login.py
index 293ed79b4a0..d00ef2e7feb 100644
---
a/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/router/login.py
+++
b/providers/amazon/src/airflow/providers/amazon/aws/auth_manager/router/login.py
@@ -19,7 +19,6 @@ from __future__ import annotations
import logging
from typing import Any
-from urllib.parse import urljoin
import anyio
from fastapi import HTTPException, Request
@@ -27,6 +26,7 @@ from starlette import status
from starlette.responses import RedirectResponse
from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX,
get_auth_manager
+from airflow.api_fastapi.auth.managers.base_auth_manager import
COOKIE_NAME_JWT_TOKEN
from airflow.api_fastapi.common.router import AirflowRouter
from airflow.configuration import conf
from airflow.providers.amazon.aws.auth_manager.constants import
CONF_SAML_METADATA_URL_KEY, CONF_SECTION_NAME
@@ -80,8 +80,11 @@ def login_callback(request: Request):
username=saml_auth.get_nameid(),
email=attributes["email"][0] if "email" in attributes else None,
)
- url = urljoin(conf.get("api", "base_url"),
f"?token={get_auth_manager().generate_jwt(user)}")
- return RedirectResponse(url=url, status_code=303)
+ url = conf.get("api", "base_url")
+ token = get_auth_manager().generate_jwt(user)
+ response = RedirectResponse(url=url, status_code=303)
+ response.set_cookie(COOKIE_NAME_JWT_TOKEN, token, secure=True)
+ return response
def _init_saml_auth(request: Request) -> OneLogin_Saml2_Auth:
diff --git
a/providers/amazon/tests/unit/amazon/aws/auth_manager/router/test_login.py
b/providers/amazon/tests/unit/amazon/aws/auth_manager/router/test_login.py
index af763444cae..07c61e32f67 100644
--- a/providers/amazon/tests/unit/amazon/aws/auth_manager/router/test_login.py
+++ b/providers/amazon/tests/unit/amazon/aws/auth_manager/router/test_login.py
@@ -119,7 +119,8 @@ class TestLoginRouter:
)
assert response.status_code == 303
assert "location" in response.headers
- assert
response.headers["location"].startswith("http://localhost:8080/?token=")
+ assert "_token" in response.cookies
+ assert
response.headers["location"].startswith("http://localhost:8080/")
def test_login_callback_unsuccessful(self):
with conf_vars(
diff --git a/providers/fab/src/airflow/providers/fab/www/views.py
b/providers/fab/src/airflow/providers/fab/www/views.py
index e2e63c0eec2..ccc9488ed52 100644
--- a/providers/fab/src/airflow/providers/fab/www/views.py
+++ b/providers/fab/src/airflow/providers/fab/www/views.py
@@ -21,6 +21,7 @@ from urllib.parse import unquote, urljoin, urlsplit
from flask import (
g,
+ make_response,
redirect,
render_template,
request,
@@ -29,6 +30,7 @@ from flask import (
from flask_appbuilder import IndexView, expose
from airflow.api_fastapi.app import get_auth_manager
+from airflow.api_fastapi.auth.managers.base_auth_manager import
COOKIE_NAME_JWT_TOKEN
from airflow.configuration import conf
# Following the release of https://github.com/python/cpython/issues/102153 in
Python 3.9.17 on
@@ -66,7 +68,10 @@ class FabIndexView(IndexView):
def index(self):
if g.user is not None and g.user.is_authenticated:
token = get_auth_manager().generate_jwt(g.user)
- return redirect(urljoin(conf.get("api", "base_url"),
f"?token={token}"), code=302)
+ response = make_response(redirect(f"{conf.get('api',
'base_url')}", code=302))
+ response.set_cookie(COOKIE_NAME_JWT_TOKEN, token, secure=True)
+
+ return response
else:
return redirect(conf.get("api", "base_url"), code=302)
diff --git a/tests/api_fastapi/auth/managers/simple/routes/test_login.py
b/tests/api_fastapi/auth/managers/simple/routes/test_login.py
index fdde46df07e..70a39814f2d 100644
--- a/tests/api_fastapi/auth/managers/simple/routes/test_login.py
+++ b/tests/api_fastapi/auth/managers/simple/routes/test_login.py
@@ -62,7 +62,7 @@ class TestLogin:
response = test_client.get("/auth/token", follow_redirects=False)
assert response.status_code == 307
assert "location" in response.headers
- assert
response.headers["location"].startswith("http://localhost:8080/?token=")
+ assert response.cookies.get("_token") is not None
def test_create_token_all_admins_config_disabled(self, test_client):
response = test_client.get("/auth/token")