*: add support for bearer authentication on API

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-04-13 19:57:33 +02:00
parent 513d3c1c31
commit 6821679fbc
6 changed files with 31 additions and 28 deletions

View File

@ -10,29 +10,29 @@ from structlog.stdlib import get_logger
from authentik.core.models import Token, TokenIntents, User from authentik.core.models import Token, TokenIntents, User
LOGGER = get_logger() LOGGER = get_logger()
X_AUTHENTIK_PREVENT_BASIC_HEADER = "HTTP_X_AUTHENTIK_PREVENT_BASIC"
def token_from_header(raw_header: bytes) -> Optional[Token]: def token_from_header(raw_header: bytes) -> Optional[Token]:
"""raw_header in the Format of `Basic dGVzdDp0ZXN0`""" """raw_header in the Format of `Basic dGVzdDp0ZXN0`"""
auth_credentials = raw_header.decode() auth_credentials = raw_header.decode()
# Accept headers with Type format and without # Accept headers with Type format and without
if " " in auth_credentials: if " " not in auth_credentials:
auth_type, auth_credentials = auth_credentials.split()
if auth_type.lower() != "basic":
LOGGER.debug(
"Unsupported authentication type, denying", type=auth_type.lower()
)
return None
try:
auth_credentials = b64decode(auth_credentials.encode()).decode()
except (UnicodeDecodeError, Error):
return None return None
# Accept credentials with username and without auth_type, auth_credentials = auth_credentials.split()
if ":" in auth_credentials: if auth_type.lower() not in ["basic", "bearer"]:
_, password = auth_credentials.split(":") LOGGER.debug("Unsupported authentication type, denying", type=auth_type.lower())
else: return None
password = auth_credentials password = auth_credentials
if auth_type.lower() == "basic":
try:
auth_credentials = b64decode(auth_credentials.encode()).decode()
except (UnicodeDecodeError, Error):
return None
# Accept credentials with username and without
if ":" in auth_credentials:
_, password = auth_credentials.split(":")
else:
password = auth_credentials
if password == "": # nosec if password == "": # nosec
return None return None
tokens = Token.filter_not_expired(key=password, intent=TokenIntents.INTENT_API) tokens = Token.filter_not_expired(key=password, intent=TokenIntents.INTENT_API)
@ -43,10 +43,10 @@ def token_from_header(raw_header: bytes) -> Optional[Token]:
class AuthentikTokenAuthentication(BaseAuthentication): class AuthentikTokenAuthentication(BaseAuthentication):
"""Token-based authentication using HTTP Basic authentication""" """Token-based authentication using HTTP Bearer authentication"""
def authenticate(self, request: Request) -> Union[tuple[User, Any], None]: def authenticate(self, request: Request) -> Union[tuple[User, Any], None]:
"""Token-based authentication using HTTP Basic authentication""" """Token-based authentication using HTTP Bearer authentication"""
auth = get_authorization_header(request) auth = get_authorization_header(request)
token = token_from_header(auth) token = token_from_header(auth)
@ -56,6 +56,4 @@ class AuthentikTokenAuthentication(BaseAuthentication):
return (token.user, None) return (token.user, None)
def authenticate_header(self, request: Request) -> str: def authenticate_header(self, request: Request) -> str:
if X_AUTHENTIK_PREVENT_BASIC_HEADER in request._request.META: return "Bearer"
return ""
return 'Basic realm="authentik"'

View File

@ -11,7 +11,7 @@ from authentik.core.models import Token, TokenIntents
class TestAPIAuth(TestCase): class TestAPIAuth(TestCase):
"""Test API Authentication""" """Test API Authentication"""
def test_valid(self): def test_valid_basic(self):
"""Test valid token""" """Test valid token"""
token = Token.objects.create( token = Token.objects.create(
intent=TokenIntents.INTENT_API, user=get_anonymous_user() intent=TokenIntents.INTENT_API, user=get_anonymous_user()
@ -19,6 +19,13 @@ class TestAPIAuth(TestCase):
auth = b64encode(f":{token.key}".encode()).decode() auth = b64encode(f":{token.key}".encode()).decode()
self.assertEqual(token_from_header(f"Basic {auth}".encode()), token) self.assertEqual(token_from_header(f"Basic {auth}".encode()), token)
def test_valid_bearer(self):
"""Test valid token"""
token = Token.objects.create(
intent=TokenIntents.INTENT_API, user=get_anonymous_user()
)
self.assertEqual(token_from_header(f"Bearer {token.key}".encode()), token)
def test_invalid_type(self): def test_invalid_type(self):
"""Test invalid type""" """Test invalid type"""
self.assertIsNone(token_from_header("foo bar".encode())) self.assertIsNone(token_from_header("foo bar".encode()))

View File

@ -143,7 +143,7 @@ SWAGGER_SETTINGS = {
"authentik.api.pagination_schema.PaginationInspector", "authentik.api.pagination_schema.PaginationInspector",
], ],
"SECURITY_DEFINITIONS": { "SECURITY_DEFINITIONS": {
"token": {"type": "apiKey", "name": "Authorization", "in": "header"} "Bearer": {"type": "apiKey", "name": "Authorization", "in": "header"}
}, },
} }

View File

@ -2,7 +2,6 @@ package ak
import ( import (
"crypto/tls" "crypto/tls"
"encoding/base64"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
@ -20,7 +19,7 @@ func (ac *APIController) initWS(pbURL url.URL, outpostUUID strfmt.UUID) {
pathTemplate := "%s://%s/ws/outpost/%s/" pathTemplate := "%s://%s/ws/outpost/%s/"
scheme := strings.ReplaceAll(pbURL.Scheme, "http", "ws") scheme := strings.ReplaceAll(pbURL.Scheme, "http", "ws")
authHeader := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("Basic :%s", ac.token))) authHeader := fmt.Sprintf("Bearer %s", ac.token)
header := http.Header{ header := http.Header{
"Authorization": []string{authHeader}, "Authorization": []string{authHeader},

View File

@ -13,12 +13,12 @@ consumes:
produces: produces:
- application/json - application/json
securityDefinitions: securityDefinitions:
token: Bearer:
type: apiKey type: apiKey
name: Authorization name: Authorization
in: header in: header
security: security:
- token: [] - Bearer: []
paths: paths:
/admin/apps/: /admin/apps/:
get: get:

View File

@ -16,7 +16,6 @@ export const DEFAULT_CONFIG = new Configuration({
basePath: "/api/v2beta", basePath: "/api/v2beta",
headers: { headers: {
"X-CSRFToken": getCookie("authentik_csrf"), "X-CSRFToken": getCookie("authentik_csrf"),
"X-Authentik-Prevent-Basic": "true"
}, },
middleware: [ middleware: [
API_DRAWER_MIDDLEWARE, API_DRAWER_MIDDLEWARE,