Skip to content

Commit 345ef4d

Browse files
authored
Refactors how authentication providers are managed. (Netflix#139)
1 parent 49a1426 commit 345ef4d

9 files changed

Lines changed: 203 additions & 163 deletions

File tree

‎setup.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ def run(self):
405405
"dispatch.plugins": [
406406
"dispatch_participants = dispatch.plugins.dispatch_core.plugin:DispatchParticipantPlugin",
407407
"dispatch_document_resolver = dispatch.plugins.dispatch_core.plugin:DispatchDocumentResolverPlugin",
408+
"dispatch_pkce_auth = dispatch.plugins.dispatch_core.plugin:PKCEAuthProviderPlugin",
408409
"google_docs_document = dispatch.plugins.dispatch_google.docs.plugin:GoogleDocsDocumentPlugin",
409410
"google_drive_storage = dispatch.plugins.dispatch_google.drive.plugin:GoogleDriveStoragePlugin",
410411
"google_drive_task = dispatch.plugins.dispatch_google.drive.plugin:GoogleDriveTaskPlugin",

‎src/dispatch/auth/service.py‎

Lines changed: 23 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,28 @@
1-
import requests
2-
from cachetools import TTLCache
3-
from fastapi import HTTPException
4-
from fastapi.security.utils import get_authorization_scheme_param
5-
from jose import JWTError, jwt
6-
from starlette.requests import Request
7-
from starlette.status import HTTP_401_UNAUTHORIZED
8-
9-
from dispatch.config import JWKS_URL, DISPATCH_AUTH_HEADER_KEY
10-
11-
jwk_key_cache = TTLCache(maxsize=1, ttl=60 * 60)
12-
13-
14-
def from_bearer_token(request: Request):
15-
"""Fetches user from provided bearer token."""
16-
authorization: str = request.headers.get("Authorization")
17-
scheme, param = get_authorization_scheme_param(authorization)
18-
if not authorization or scheme.lower() != "bearer":
19-
return
20-
21-
token = authorization.split()[1]
22-
23-
# TODO should we warm this cache up on application start?
24-
try:
25-
key = jwk_key_cache[JWKS_URL]
26-
except Exception:
27-
key = requests.get(JWKS_URL).json()["keys"][0]
28-
jwk_key_cache[JWKS_URL] = key
29-
30-
try:
31-
data = jwt.decode(token, key)
32-
except JWTError:
33-
return
34-
35-
return data["email"]
1+
"""
2+
.. module: dispatch.auth.service
3+
:platform: Unix
4+
:copyright: (c) 2019 by Netflix Inc., see AUTHORS for more
5+
:license: Apache, see LICENSE for more details.
6+
"""
7+
import logging
368

9+
from starlette.requests import Request
10+
from dispatch.plugins.base import plugins
11+
from dispatch.config import (
12+
DISPATCH_AUTHENTICATION_PROVIDER_SLUG,
13+
DISPATCH_AUTHENTICATION_DEFAULT_USER,
14+
)
3715

38-
def from_headers(request: Request):
39-
"""Fetches user from provider headers. (Email header specified by DISPATCH_AUTH_HEADER)"""
40-
return request.headers.get(DISPATCH_AUTH_HEADER_KEY)
16+
log = logging.getLogger(__name__)
4117

4218

4319
def get_current_user(*, request: Request):
44-
"""Gets the current user based on the token."""
45-
credentials_exception = HTTPException(
46-
status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials"
47-
)
48-
49-
user_email = from_bearer_token(request)
50-
51-
if not user_email:
52-
raise credentials_exception
53-
authorization: str = request.headers.get("Authorization")
54-
scheme, param = get_authorization_scheme_param(authorization)
55-
if not authorization or scheme.lower() != "bearer":
56-
raise credentials_exception
57-
58-
token = authorization.split()[1]
59-
60-
# TODO should we warm this cache up on application start?
61-
try:
62-
key = jwk_key_cache[JWKS_URL]
63-
except Exception:
64-
key = requests.get(JWKS_URL).json()["keys"][0]
65-
jwk_key_cache[JWKS_URL] = key
66-
67-
try:
68-
data = jwt.decode(token, key)
69-
except JWTError:
70-
raise credentials_exception
71-
72-
return data["email"]
20+
"""Attempts to get the current user depending on the configured authentication provider."""
21+
if DISPATCH_AUTHENTICATION_PROVIDER_SLUG:
22+
auth_plugin = plugins.get(DISPATCH_AUTHENTICATION_PROVIDER_SLUG)
23+
return auth_plugin.get_current_user(request)
24+
else:
25+
log.warning(
26+
"No authentication provider has been provided. There is currently no user authentication."
27+
)
28+
return DISPATCH_AUTHENTICATION_DEFAULT_USER

‎src/dispatch/cli.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,12 @@ def restore_database():
546546
def dump_database():
547547
"""Dumps the database via pg_dump."""
548548
from sh import pg_dump
549-
from dispatch.config import DATABASE_HOSTNAME, DATABASE_NAME, DATABASE_PORT, DATABASE_CREDENTIALS
549+
from dispatch.config import (
550+
DATABASE_HOSTNAME,
551+
DATABASE_NAME,
552+
DATABASE_PORT,
553+
DATABASE_CREDENTIALS,
554+
)
550555

551556
username, password = str(DATABASE_CREDENTIALS).split(":")
552557

@@ -797,7 +802,11 @@ def run_server(log_level):
797802
import atexit
798803
from subprocess import Popen
799804

800-
p = Popen(["npm", "run", "serve"], cwd="src/dispatch/static/dispatch")
805+
# take our frontend vars and export them for the frontend to consume
806+
envvars = os.environ.copy()
807+
envvars.update({x: getattr(config, x) for x in dir(config) if x.startswith("VUE_APP_")})
808+
809+
p = Popen(["npm", "run", "serve"], cwd="src/dispatch/static/dispatch", env=envvars)
801810
atexit.register(p.terminate)
802811
uvicorn.run("dispatch.main:app", debug=True, log_level=log_level)
803812

‎src/dispatch/config.py‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,24 @@ def __str__(self) -> str:
4444
DISPATCH_HELP_SLACK_CHANNEL = config("DISPATCH_HELP_SLACK_CHANNEL")
4545

4646
# authentication
47-
DISPATCH_PKCE_AUTH_ENABLED = config("DISPATCH_PKCE_AUTH", cast=bool, default=True)
48-
DISPATCH_HEADER_AUTH_ENABLED = config("DISPATCH_HEADER_AUTH", cast=bool, default=False)
49-
50-
DISPATCH_AUTH_HEADER_KEY = config("DISPATCH_AUTH_HEADER", default=None)
51-
JWKS_URL = config("JWKS_URL", default=None)
47+
DISPATCH_AUTHENTICATION_PROVIDER_SLUG = config(
48+
"DISPATCH_AUTHENTICATION_PROVIDER_SLUG", default="dispatch-auth-provider-pkce"
49+
)
50+
VUE_APP_DISPATCH_AUTHENTICATION_PROVIDER_SLUG = DISPATCH_AUTHENTICATION_PROVIDER_SLUG
5251

53-
if DISPATCH_PKCE_AUTH_ENABLED:
54-
if not JWKS_URL:
55-
raise Exception("JWKS_URL must be set when DISPATCH_PKCE_AUTH_ENABLED")
52+
DISPATCH_AUTHENTICATION_DEFAULT_USER = config(
53+
"DISPATCH_AUTHENTICATION_DEFAULT_USER", default="dispatch@example.com"
54+
)
5655

57-
if DISPATCH_HEADER_AUTH_ENABLED:
58-
if not DISPATCH_AUTH_HEADER_KEY:
59-
raise Exception("DISPATCH_AUTH_HEADER_KEY must be when when DISPATCH_HEADER_AUTH_ENABLED")
56+
DISPATCH_AUTHENTICATION_PROVIDER_PKCE_JWKS = config(
57+
"DISPATCH_AUTHENTICATION_PROVIDER_PKCE_JWKS", default=None
58+
)
59+
VUE_APP_DISPATCH_AUTHENTICATION_PROVIDER_PKCE_OPEN_ID_CONNECT_URL = config(
60+
"VUE_APP_DISPATCH_AUTHENTICATION_PROVIDER_PKCE_OPEN_ID_CONNECT_URL", default=None
61+
)
62+
VUE_APP_DISPATCH_AUTHENTICATION_PROVIDER_PKCE_CLIENT_ID = config(
63+
"VUE_APP_DISPATCH_AUTHENTICATION_PROVIDER_PKCE_CLIENT_ID", default=None
64+
)
6065

6166
# static files
6267
DEFAULT_STATIC_DIR = os.path.join(

‎src/dispatch/plugins/bases/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@
1212
from .ticket import TicketPlugin # noqa
1313
from .document_resolver import DocumentResolverPlugin # noqa
1414
from .application import ApplicationPlugin # noqa
15+
from .auth_provider import AuthenticationProviderPlugin # noqa
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""
2+
.. module: dispatch.plugins.bases.application
3+
:platform: Unix
4+
:copyright: (c) 2019 by Netflix Inc., see AUTHORS for more
5+
:license: Apache, see LICENSE for more details.
6+
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
7+
"""
8+
from dispatch.models import PluginOptionModel
9+
from dispatch.plugins.base import Plugin
10+
from starlette.requests import Request
11+
12+
13+
class AuthenticationProviderPlugin(Plugin):
14+
type = "auth-provider"
15+
_schema = PluginOptionModel
16+
17+
def get_current_user(self, request: Request, **kwargs):
18+
raise NotImplementedError

‎src/dispatch/plugins/dispatch_core/plugin.py‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,61 @@
66
"""
77
import logging
88

9+
import requests
10+
from fastapi import HTTPException
11+
from fastapi.security.utils import get_authorization_scheme_param
12+
13+
from jose import JWTError, jwt
14+
from starlette.status import HTTP_401_UNAUTHORIZED
15+
from starlette.requests import Request
16+
917
from dispatch.individual import service as individual_service
1018
from dispatch.plugins import dispatch_core as dispatch_plugin
1119
from dispatch.plugins.base import plugins
12-
from dispatch.plugins.bases import ParticipantPlugin, DocumentResolverPlugin
20+
from dispatch.plugins.bases import (
21+
ParticipantPlugin,
22+
DocumentResolverPlugin,
23+
AuthenticationProviderPlugin,
24+
)
25+
1326
from dispatch.route import service as route_service
1427
from dispatch.route.models import RouteRequest
1528

29+
from dispatch.config import DISPATCH_AUTHENTICATION_PROVIDER_PKCE_JWKS
30+
1631
log = logging.getLogger(__name__)
1732

1833

34+
class PKCEAuthProviderPlugin(AuthenticationProviderPlugin):
35+
title = "Dispatch - PKCE Authentication Provider"
36+
slug = "dispatch-auth-provider-pkce"
37+
description = "Generic PCKE authentication provider."
38+
version = dispatch_plugin.__version__
39+
40+
author = "Kevin Glisson"
41+
author_url = "https://github.com/netflix/dispatch.git"
42+
43+
def get_current_user(self, request: Request, **kwargs):
44+
credentials_exception = HTTPException(
45+
status_code=HTTP_401_UNAUTHORIZED, detail="Could not validate credentials"
46+
)
47+
48+
authorization: str = request.headers.get("Authorization")
49+
scheme, param = get_authorization_scheme_param(authorization)
50+
if not authorization or scheme.lower() != "bearer":
51+
raise credentials_exception
52+
53+
token = authorization.split()[1]
54+
key = requests.get(DISPATCH_AUTHENTICATION_PROVIDER_PKCE_JWKS).json()["keys"][0]
55+
56+
try:
57+
data = jwt.decode(token, key)
58+
except JWTError:
59+
raise credentials_exception
60+
61+
return data["email"]
62+
63+
1964
class DispatchDocumentResolverPlugin(DocumentResolverPlugin):
2065
title = "Dispatch - Document Resolver"
2166
slug = "dispatch-document-resolver"

‎src/dispatch/static/dispatch/src/auth/store.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { differenceInMilliseconds, fromUnixTime, subMinutes } from "date-fns"
44

55
const state = {
66
status: { loggedIn: false },
7-
userInfo: null,
7+
userInfo: { email: "" },
88
accessToken: null
99
}
1010

0 commit comments

Comments
 (0)