-
Notifications
You must be signed in to change notification settings - Fork 17.4k
Add REST API auth flow for the new UI #15042
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
Closed
Closed
Changes from all commits
Commits
Show all changes
53 commits
Select commit
Hold shift + click to select a range
b243f16
Initialize api auth flow
ephraimbuddy f46b1e0
add oauth endpoint
ephraimbuddy c96319d
Add more auth flow and token refreshing
ephraimbuddy ed06c4a
Add tests for dblogin and ldaplogin
ephraimbuddy bf4f01f
test oauth login flow
ephraimbuddy 0d09ec8
Fix test & minor fix in OpenApi yaml
kaxil 746de47
add test for oauth authorize
ephraimbuddy 165c618
fix ldap sign in
ephraimbuddy 0151714
improve test for ldap
ephraimbuddy 936e83f
More tests for authorize oauth
ephraimbuddy 1fe3aea
add tests for remote user
ephraimbuddy c7d3435
Fix test and remove openid provision for now
ephraimbuddy b8caa9a
Add logout and fix incorrect user loading
ephraimbuddy 73f5e36
add tests for logout
ephraimbuddy 4871a38
remove jwt schema
ephraimbuddy 0f69cd7
Apply suggestions from code review
ephraimbuddy 5c74749
add token to response body
ephraimbuddy 209ccfd
add token to response body
ephraimbuddy 00e28e4
Return 401 for every auth failure
ephraimbuddy 69a2c3e
Add blocklist model
ephraimbuddy 041805c
add tokens table
ephraimbuddy 0463660
Apply suggestion from review
ephraimbuddy 4ab7cb0
add tests for token model
ephraimbuddy 7a94db5
improve auth flow and tests
ephraimbuddy 22a2d30
add token revoke endpoint and improve tests
ephraimbuddy 21595c3
import from api-connnection utils
ephraimbuddy 47509af
add object type to response object
ephraimbuddy b4eb910
Add bearer auth to security scheme, we can change to using cookie any…
ephraimbuddy 9c85de1
Fix model doc and pylint
ephraimbuddy cb8a616
add words to spelling
ephraimbuddy 0af79b3
fixup! add words to spelling
ephraimbuddy aa11314
Rename token table
ephraimbuddy 88c30dd
fixup! Rename token table
ephraimbuddy f0f63c0
fix jwttoken model test
ephraimbuddy f4488f2
Fix migration
ephraimbuddy b5fe8aa
Do not save oauth_token in session
ephraimbuddy 8009eea
Use token blocklist and add necessary endpoints
ephraimbuddy b6c2b26
Move some common code to security manager
ephraimbuddy 54fc35b
change token table migration and fix table
ephraimbuddy 924bea3
Apply suggestions from code review
ephraimbuddy a01cabd
Move auth_schema out of security manager and fix tests
ephraimbuddy 92247fe
Removing check for if user already logged in, since we no longer set …
ephraimbuddy 8ca6889
Allow storing token in header or cookie.
ephraimbuddy e71a685
fixup! Allow storing token in header or cookie.
ephraimbuddy fad8be2
fixup! fixup! Allow storing token in header or cookie.
ephraimbuddy 09658c6
Make Oauth redirect happen inside the server
ephraimbuddy 22223e4
fixup! Make Oauth redirect happen inside the server
ephraimbuddy 9866f12
move some api related stuff from security manager
ephraimbuddy 47e46cd
Drop support for token in cookie
ephraimbuddy da25fa1
fix test
ephraimbuddy 7dcd433
fixup! fix test
ephraimbuddy a507911
add method to delete expired tokens
ephraimbuddy 60988fb
Delete expired tokens at startup
ephraimbuddy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,242 @@ | ||
| # 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 | ||
| import re | ||
|
|
||
| import jwt | ||
| from flask import current_app, jsonify, request | ||
| from flask_appbuilder.const import AUTH_DB, AUTH_LDAP, AUTH_OAUTH, AUTH_OID, AUTH_REMOTE_USER | ||
| from flask_jwt_extended import ( | ||
| create_access_token, | ||
| decode_token, | ||
| get_jwt_identity, | ||
| get_raw_jwt, | ||
| jwt_refresh_token_required, | ||
| jwt_required, | ||
| ) | ||
| from flask_login import login_user | ||
| from marshmallow import ValidationError | ||
| from sqlalchemy.exc import IntegrityError | ||
|
|
||
| from airflow.api_connexion.exceptions import BadRequest, Unauthenticated | ||
| from airflow.api_connexion.schemas.auth_schema import ( | ||
| auth_schema, | ||
| info_schema, | ||
| login_form_schema, | ||
| token_schema, | ||
| ) | ||
| from airflow.configuration import conf | ||
| from airflow.models.auth import TokenBlockList | ||
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def get_auth_info(): | ||
| """Get site authentication info""" | ||
| security_manager = current_app.appbuilder.sm | ||
| config = current_app.config | ||
| auth_type = security_manager.auth_type | ||
| type_mapping = { | ||
| AUTH_DB: "auth_db", | ||
| AUTH_LDAP: "auth_ldap", | ||
| AUTH_OID: "auth_oid", | ||
| AUTH_OAUTH: "auth_oauth", | ||
| AUTH_REMOTE_USER: "auth_remote_user", | ||
| } | ||
| oauth_providers = config.get("OAUTH_PROVIDERS", None) | ||
| openid_providers = config.get("OPENID_PROVIDERS", None) | ||
| return info_schema.dump( | ||
| { | ||
| "auth_type": type_mapping[auth_type], | ||
| "oauth_providers": oauth_providers, | ||
| "openid_providers": openid_providers, | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| def auth_login(): | ||
| """Handle DB login""" | ||
| appbuilder = current_app.appbuilder | ||
| body = request.json | ||
| try: | ||
| data = login_form_schema.load(body) | ||
| except ValidationError as err: | ||
| raise BadRequest(detail=str(err.messages)) | ||
| security_manager = appbuilder.sm | ||
| if security_manager.auth_type not in [AUTH_DB, AUTH_LDAP]: | ||
| raise Unauthenticated(detail="Authentication type does not match") | ||
| user = security_manager.login_with_user_pass(data['username'], data['password']) | ||
| if not user: | ||
| raise Unauthenticated(detail="Invalid login") | ||
| login_user(user, remember=False) | ||
| return security_manager.create_tokens_and_dump(user, auth_schema) | ||
|
|
||
|
|
||
| def auth_oauthlogin(provider, register=None): | ||
| """Authenticates user and redirect to callback endpoint""" | ||
| appbuilder = current_app.appbuilder | ||
| security_manager = appbuilder.sm | ||
| if security_manager.auth_type != AUTH_OAUTH: | ||
| raise Unauthenticated(detail="Authentication type does not match") | ||
| api_base_path = "/api/v1/" | ||
| base_url = conf.get("webserver", "base_url") + api_base_path | ||
| redirect_uri = base_url + "oauth-authorized?provider=" + provider | ||
|
|
||
| arg = {'register': [str(register)]} if register else {} | ||
| state = jwt.encode( | ||
| arg, | ||
| key=appbuilder.app.config['SECRET_KEY'], | ||
| algorithm='HS256', | ||
| ) | ||
| auth_provider = security_manager.oauth_remotes[provider] | ||
| try: | ||
| if provider == "twitter": | ||
| redirect_uri = redirect_uri + f"&state={state}" | ||
| return auth_provider.authorize_redirect(redirect_uri=redirect_uri) | ||
| else: | ||
| state = state.decode("ascii") if isinstance(state, bytes) else state | ||
| return auth_provider.authorize_redirect( | ||
| redirect_uri=redirect_uri, | ||
| state=state, | ||
| ) | ||
| except Exception as err: # pylint: disable=broad-except | ||
| log.error("Error on OAuth authorize: %s", err) | ||
| raise Unauthenticated(detail=str(err)) | ||
|
|
||
|
|
||
| def authorize_oauth(): | ||
| """Callback to authorize Oauth.""" | ||
| args = request.args | ||
| provider = args.get('provider', None) | ||
| state = args.get('state', None) | ||
| if not (provider and state): | ||
| raise BadRequest(detail="Missing required parameters: provider and state") | ||
| appbuilder = current_app.appbuilder | ||
| security_manager = appbuilder.sm | ||
| log.debug("Authorized init") | ||
| resp = security_manager.oauth_remotes[provider].authorize_access_token() | ||
| if resp is None: | ||
| raise Unauthenticated(detail="You denied the request to sign in") | ||
| # Verify state | ||
| try: | ||
| jwt.decode( | ||
| state, | ||
| key=appbuilder.app.config['SECRET_KEY'], | ||
| algorithms='HS256', | ||
| ) | ||
| except jwt.InvalidTokenError: | ||
| raise BadRequest(detail="State signature is not valid!") | ||
| log.debug("OAUTH Authorized resp: %s", resp) | ||
| # Retrieves specific user info from the provider | ||
| try: # pylint: disable=too-many-nested-blocks | ||
| userinfo = security_manager.oauth_user_info(provider, resp) | ||
| except Exception as err: # pylint: disable=broad-except | ||
| log.debug("Error retrieving user info. Error =>%s", err) | ||
| user = None | ||
| else: | ||
| log.debug("User info retrieved from %s: %s", provider, userinfo) | ||
| # User email is not whitelisted | ||
| if provider in security_manager.oauth_whitelists: | ||
| whitelist = security_manager.oauth_whitelists[provider] | ||
| allow = False | ||
| for e in whitelist: | ||
| if re.search(e, userinfo["email"]): | ||
| allow = True | ||
| break | ||
| if not allow: | ||
| raise Unauthenticated(detail="You are not authorized.") | ||
| else: | ||
| log.debug("No allow list for OAuth provider") | ||
| user = security_manager.auth_user_oauth(userinfo) | ||
| log.debug("User is %s", user) | ||
| if user is None: | ||
| raise Unauthenticated(detail="Invalid login") | ||
| login_user(user, remember=False) | ||
| return security_manager.create_tokens_and_dump(user, auth_schema) | ||
|
|
||
|
|
||
| def auth_remoteuser(): | ||
| """Handle remote user auth""" | ||
| appbuilder = current_app.appbuilder | ||
| security_manager = appbuilder.sm | ||
| if security_manager.auth_type != AUTH_REMOTE_USER: | ||
| raise Unauthenticated(detail="Authentication type does not match") | ||
| username = request.environ.get("REMOTE_USER") | ||
| if not username: | ||
| raise Unauthenticated(detail="Invalid login") | ||
| user = security_manager.auth_user_remote_user(username) | ||
| if user is None: | ||
| raise Unauthenticated(detail="Invalid login") | ||
| login_user(user, remember=False) | ||
| return security_manager.create_tokens_and_dump(user, auth_schema) | ||
|
|
||
|
|
||
| @jwt_refresh_token_required | ||
| def refresh_token(): | ||
| """Refresh token""" | ||
| user = get_jwt_identity() | ||
| access_token = create_access_token(identity=user) | ||
| return {'access_token': access_token} | ||
|
|
||
|
|
||
| @jwt_required | ||
| def revoke_token(): | ||
| """ | ||
| An endpoint for revoking both access and refresh token. | ||
|
|
||
| This is intended for a case where a logged in user want to revoke | ||
| another user's tokens | ||
| """ | ||
| body = request.json | ||
| try: | ||
| data = token_schema.load(body) | ||
| except ValidationError as err: | ||
| raise BadRequest(detail=str(err.messages)) | ||
| token = decode_token(data['token']) | ||
|
Contributor
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. Couldn't understand the logic here or maybe I misinterpreted it. How is a user going to provide the token of another user? |
||
| try: | ||
| TokenBlockList.add_token(jti=token['jti'], expiry_delta=token['exp']) | ||
| except IntegrityError: | ||
| pass | ||
| return jsonify({"revoked": True}) | ||
|
|
||
|
|
||
| @jwt_required | ||
| def revoke_current_user_token(): | ||
| """ | ||
| An endpoint for revoking current user access token | ||
| This should be called during current user logout | ||
| """ | ||
| raw_jwt = get_raw_jwt() | ||
| try: | ||
| TokenBlockList.add_token(jti=raw_jwt['jti'], expiry_delta=raw_jwt['exp']) | ||
| except IntegrityError: | ||
| pass | ||
| return jsonify({"revoked": True}) | ||
|
|
||
|
|
||
| @jwt_refresh_token_required | ||
| def revoke_current_user_refresh_token(): | ||
| """ | ||
| An endpoint for revoking current user refresh token | ||
| This should be called during current user logout | ||
| """ | ||
| raw_jwt = get_raw_jwt() | ||
| try: | ||
| TokenBlockList.add_token(jti=raw_jwt['jti'], expiry_delta=raw_jwt['exp']) | ||
| except IntegrityError: | ||
| pass | ||
| return jsonify({"revoked": True}) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
I think
type_mappingcan be a constant & be out of this function.