-
Notifications
You must be signed in to change notification settings - Fork 17.4k
Provide login endpoint for the REST API with JWT authentication method #14219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
35e3609
9a730da
382dcd4
2652af1
a1eac3a
bd11dcc
19284fc
e37316d
71d31d0
a28dacf
89f0056
fd0d320
a0af676
7ffaa5e
4e1a9f2
e6a68b3
586b5d6
c011305
d50811f
d3f7225
0894744
b25f2aa
4ae0322
17fdee5
ef4fdb2
b3dc520
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # Licensed to the Apache Software Foundation (ASF) under one | ||
| # or more contributor license agreements. See the NOTICE file | ||
| # distributed with this work for additional information | ||
| # regarding copyright ownership. The ASF licenses this file | ||
| # to you under the Apache License, Version 2.0 (the | ||
| # "License"); you may not use this file except in compliance | ||
| # with the License. You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, | ||
| # software distributed under the License is distributed on an | ||
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
|
|
||
| from flask import current_app, jsonify | ||
| from flask_jwt_extended import set_access_cookies, unset_jwt_cookies | ||
|
|
||
| from airflow.api_connexion.schemas.user_schema import user_collection_item_schema | ||
|
|
||
|
|
||
| def login(): | ||
| """User login""" | ||
| ab_security_manager = current_app.appbuilder.sm | ||
| access_token = ab_security_manager.create_access_token() | ||
| resp = jsonify(user_collection_item_schema.dump(ab_security_manager.current_user)) | ||
| set_access_cookies(resp, access_token) | ||
| return resp | ||
|
|
||
|
|
||
| def logout(): | ||
| """Sign out""" | ||
| resp = jsonify({'logged_out': True}) | ||
| unset_jwt_cookies(resp) | ||
| return resp, 200 |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -1490,6 +1490,48 @@ paths: | |||||
| '404': | ||||||
| $ref: '#/components/responses/NotFound' | ||||||
|
|
||||||
| /login: | ||||||
| post: | ||||||
| summary: Webserver login | ||||||
| description: | | ||||||
| Verify user and set jwt token in cookies | ||||||
| x-openapi-router-controller: airflow.api_connexion.endpoints.auth_endpoint | ||||||
| operationId: login | ||||||
| tags: [WebserverAuth] | ||||||
|
|
||||||
| responses: | ||||||
| '200': | ||||||
| description: Success. | ||||||
| content: | ||||||
| application/json: | ||||||
| schema: | ||||||
| $ref: '#/components/schemas/User' | ||||||
| '400': | ||||||
| $ref: '#/components/responses/BadRequest' | ||||||
| '401': | ||||||
| $ref: '#/components/responses/Unauthenticated' | ||||||
|
|
||||||
| /logout: | ||||||
| post: | ||||||
| summary: Webserver logout | ||||||
| description: Logout user by unsetting cookies | ||||||
| x-openapi-router-controller: airflow.api_connexion.endpoints.auth_endpoint | ||||||
| operationId: logout | ||||||
| tags: [WebserverAuth] | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| responses: | ||||||
| '200': | ||||||
| description: Success | ||||||
| content: | ||||||
| application/json: | ||||||
| schema: | ||||||
| properties: | ||||||
| logged_out: | ||||||
| type: boolean | ||||||
| description: Indicate user is logged out | ||||||
| '400': | ||||||
| $ref: '#/components/responses/BadRequest' | ||||||
|
|
||||||
| components: | ||||||
| # Reusable schemas (data models) | ||||||
| schemas: | ||||||
|
|
@@ -3275,6 +3317,10 @@ components: | |||||
| Kerberos: | ||||||
| type: http | ||||||
| scheme: negotiate | ||||||
| WebserverAuth: | ||||||
| type: apiKey | ||||||
| in: cookie | ||||||
| name: access_token_cookie | ||||||
|
|
||||||
| # The API will provide support for plugins to support various authorization mechanisms. | ||||||
| # Detailed information will be available in the plugin specification. | ||||||
|
|
@@ -3296,6 +3342,7 @@ tags: | |||||
| - name: Role | ||||||
| - name: Permission | ||||||
| - name: User | ||||||
| - name: WebserverAuth | ||||||
|
|
||||||
| externalDocs: | ||||||
|
ephraimbuddy marked this conversation as resolved.
Outdated
|
||||||
| url: https://airflow.apache.org/docs/apache-airflow/stable/ | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,24 +14,27 @@ | |
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
|
|
||
| from functools import wraps | ||
| from typing import Callable, Optional, Sequence, Tuple, TypeVar, cast | ||
|
|
||
| from flask import Response, current_app | ||
|
|
||
| from airflow.api_connexion.exceptions import PermissionDenied, Unauthenticated | ||
| from airflow.api_connexion.webserver_auth import auth_current_user | ||
|
|
||
| T = TypeVar("T", bound=Callable) # pylint: disable=invalid-name | ||
|
|
||
|
|
||
| def check_authentication() -> None: | ||
| """Checks that the request has valid authorization information.""" | ||
| response = current_app.api_auth.requires_authentication(Response)() | ||
| if response.status_code != 200: | ||
| # since this handler only checks authentication, not authorization, | ||
| # we should always return 401 | ||
| raise Unauthenticated(headers=response.headers) | ||
| if response.status_code == 200: | ||
| return | ||
| if auth_current_user(): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By having this here, it means that any/all requests would accept and process |
||
| return | ||
| # since this handler only checks authentication, not authorization, | ||
| # we should always return 401 | ||
| raise Unauthenticated(headers=response.headers) | ||
|
|
||
|
|
||
| def requires_access(permissions: Optional[Sequence[Tuple[str, str]]] = None) -> Callable[[T], T]: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # Licensed to the Apache Software Foundation (ASF) under one | ||
| # or more contributor license agreements. See the NOTICE file | ||
| # distributed with this work for additional information | ||
| # regarding copyright ownership. The ASF licenses this file | ||
| # to you under the Apache License, Version 2.0 (the | ||
| # "License"); you may not use this file except in compliance | ||
| # with the License. You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, | ||
| # software distributed under the License is distributed on an | ||
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
|
|
||
| import logging | ||
| from typing import Callable, TypeVar | ||
|
|
||
| from flask import current_app, request | ||
| from flask_appbuilder.const import AUTH_LDAP | ||
| from flask_jwt_extended import verify_jwt_in_request | ||
| from flask_login import login_user | ||
|
|
||
| from airflow.configuration import conf | ||
|
|
||
| SECRET = conf.get("webserver", "secret_key") | ||
| T = TypeVar("T", bound=Callable) # pylint: disable=invalid-name | ||
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def auth_current_user(): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't strictly only tied to webserver use of the API, so lets have this function live in security.py |
||
| """Checks the authentication and return a value""" | ||
| db_login_disabled = conf.getboolean("api", "disable_db_login") | ||
| auth_header = request.headers.get("Authorization", None) | ||
| if auth_header and not db_login_disabled: | ||
| if auth_header.startswith("Basic"): | ||
| user = None | ||
| auth = request.authorization | ||
| if auth is None or not (auth.username and auth.password): | ||
| return None | ||
| ab_security_manager = current_app.appbuilder.sm | ||
| if ab_security_manager.auth_type == AUTH_LDAP: | ||
| user = ab_security_manager.auth_user_ldap(auth.username, auth.password) | ||
| if user is None: | ||
| user = ab_security_manager.auth_user_db(auth.username, auth.password) | ||
| if user is not None: | ||
| login_user(user, remember=False) | ||
| return user | ||
| try: | ||
| verify_jwt_in_request() | ||
| return True | ||
| except Exception as err: # pylint: disable=broad-except | ||
| log.debug("Can't verify jwt: %s", str(err)) | ||
| return None | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -728,6 +728,27 @@ | |
| type: string | ||
| example: ~ | ||
| default: "airflow.api.auth.backend.deny_all" | ||
| - name: jwt_access_token_expires | ||
| description: How long an access token should live before it expires(in minutes). | ||
| version_added: 2.0.2 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2.1 for new features. |
||
| type: integer | ||
| example: ~ | ||
| default: "60" | ||
| - name: jwt_cookie_secure | ||
| description: | | ||
| If the secure flag should be set on your JWT cookies. This will only | ||
| allow the cookies to be sent over https. Defaults to False, | ||
| but in production this should likely be set to True. | ||
| version_added: 2.0.2 | ||
| type: boolean | ||
| example: ~ | ||
| default: "False" | ||
| - name: disable_db_login | ||
| description: Disable use of username and password to login to the REST API | ||
| version_added: 2.0.2 | ||
| type: boolean | ||
| example: ~ | ||
| default: "False" | ||
| - name: maximum_page_limit | ||
| description: | | ||
| Used to set the maximum page limit for API requests | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -910,6 +910,7 @@ json | |
| jthomas | ||
| jupyter | ||
| jupytercmd | ||
| jwt | ||
| kaxil | ||
| kaxilnaik | ||
| keepalive | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.