move api auth from authorization header to X-API-Key header

This commit is contained in:
Laura Klünder 2023-12-04 23:07:45 +01:00
parent f64e65f297
commit 4491f68dc7
8 changed files with 43 additions and 41 deletions

View file

@ -44,20 +44,20 @@ def get_status(request):
)
class APITokenSchema(Schema):
token: NonEmptyStr = APIField(
title="API token",
description="API token to be directly used with `Authorization: Bearer <token>` HTTP header."
class APIKeySchema(Schema):
key: NonEmptyStr = APIField(
title="API key",
description="API secret to be directly used with `X-API-Key` HTTP header."
)
@auth_api_router.get('/session/', response=APITokenSchema, auth=None,
summary="get session-bound token")
def session_token(request):
@auth_api_router.get('/session/', response=APIKeySchema, auth=None,
summary="get session-bound key")
def session_key(request):
"""
Get an API token that is bound to the transmitted session cookie.
Get an API key that is bound to the transmitted session cookie.
Keep in mind that this API token will be invalid if the session gets signed out or similar.
Keep in mind that this API key will be invalid if the session gets signed out or similar.
"""
session_id = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)
return {"token": "anonymous" if session_id is None else f"session:{session_id}"}
return {"key": "anonymous" if session_id is None else f"session:{session_id}"}

View file

@ -6,10 +6,10 @@ from importlib import import_module
from django.contrib.auth import get_user as auth_get_user
from django.contrib.auth.models import AnonymousUser
from django.utils.functional import SimpleLazyObject, lazy
from ninja.security import HttpBearer
from ninja.security import APIKeyHeader
from c3nav import settings
from c3nav.api.exceptions import APIPermissionDenied, APITokenInvalid
from c3nav.api.exceptions import APIPermissionDenied, APIKeyInvalid
from c3nav.api.models import Secret
from c3nav.api.schema import APIErrorSchema
from c3nav.control.middleware import UserPermissionsMiddleware
@ -31,16 +31,18 @@ class APIAuthDetails:
description = """
An API token can be acquired in 4 ways:
An API key can be acquired in 4 ways:
* Use `anonymous` for guest access.
* Generate a session-bound temporary token using the auth session endpoint.
* Generate a session-bound temporary key using the auth session endpoint.
* Create an API secret in your user account settings.
""".strip()
class APITokenAuth(HttpBearer):
openapi_name = "api token authentication"
class APIKeyAuth(APIKeyHeader):
param_name = "X-API-Key"
openapi_name = "api key authentication"
openapi_description = description
def __init__(self, logged_in=False, superuser=False, permissions: set[str] = None, is_readonly=False):
@ -52,32 +54,32 @@ class APITokenAuth(HttpBearer):
engine = import_module(settings.SESSION_ENGINE)
self.SessionStore = engine.SessionStore
def _authenticate(self, request, token) -> APIAuthDetails:
def _authenticate(self, request, key) -> APIAuthDetails:
request.user = AnonymousUser()
request.user_permissions = SimpleLazyObject(lambda: UserPermissionsMiddleware.get_user_permissions(request))
request.user_space_accesses = lazy(UserPermissionsMiddleware.get_user_space_accesses, dict)(request)
if token == "anonymous":
if key == "anonymous":
return APIAuthDetails(
key_type=APIKeyType.ANONYMOUS,
readonly=True,
)
elif token.startswith("session:"):
session = self.SessionStore(token.removeprefix("session:"))
elif key.startswith("session:"):
session = self.SessionStore(key.removeprefix("session:"))
print('session is empty:', request.session.is_empty())
user = auth_get_user(FakeRequest(session=session))
if not user.is_authenticated:
raise APITokenInvalid
raise APIKeyInvalid
request.user = user
return APIAuthDetails(
key_type=APIKeyType.SESSION,
readonly=False,
)
elif token.startswith("secret:"):
elif key.startswith("secret:"):
try:
secret = Secret.objects.get_by_secret(token.removeprefix("secret:")).get()
secret = Secret.objects.get_by_secret(key.removeprefix("secret:")).get()
except Secret.DoesNotExist:
raise APITokenInvalid
raise APIKeyInvalid
# get user permissions and restrict them based on scopes
user_permissions: UserPermissions = UserPermissions.get_for_user(secret.user)
@ -95,10 +97,10 @@ class APITokenAuth(HttpBearer):
key_type=APIKeyType.SESSION,
readonly=secret.readonly
)
raise APITokenInvalid
raise APIKeyInvalid
def authenticate(self, request, token):
auth_result = self._authenticate(request, token)
def authenticate(self, request, key):
auth_result = self._authenticate(request, key)
if self.logged_in and not request.user.is_authenticated:
raise APIPermissionDenied('You need to be signed in for this request.')
if self.superuser and not request.user.is_superuser:

View file

@ -22,9 +22,9 @@ class APIUnauthorized(CustomAPIException):
detail = "Authorization is required for this endpoint."
class APITokenInvalid(CustomAPIException):
class APIKeyInvalid(CustomAPIException):
status_code = 401
detail = "Invalid API token."
detail = "Invalid API key."
class APIPermissionDenied(CustomAPIException):

View file

@ -3,7 +3,7 @@ from ninja.openapi.docs import DocsBase
from ninja.operation import Operation
from ninja.schema import NinjaGenerateJsonSchema
from c3nav.api.auth import APITokenAuth
from c3nav.api.auth import APIKeyAuth
from c3nav.api.exceptions import CustomAPIException
@ -60,7 +60,7 @@ ninja_api = c3navAPI(
docs_url="/",
docs=SwaggerAndRedoc(),
auth=APITokenAuth(),
auth=APIKeyAuth(),
openapi_extra={
"tags": [

View file

@ -2,7 +2,7 @@ from django.urls import Resolver404, resolve
from django.utils.translation import gettext_lazy as _
from ninja import Router as APIRouter
from c3nav.api.auth import APITokenAuth, auth_permission_responses
from c3nav.api.auth import APIKeyAuth, auth_permission_responses
from c3nav.api.exceptions import API404
from c3nav.editor.api.base import api_etag_with_update_cache_key
from c3nav.editor.api.geometries import get_level_geometries_result, get_space_geometries_result
@ -12,7 +12,7 @@ from c3nav.mapdata.api.base import api_etag
from c3nav.mapdata.models import Source
from c3nav.mapdata.schemas.responses import WithBoundsSchema
editor_api_router = APIRouter(tags=["editor"], auth=APITokenAuth(permissions={"editor_access"}))
editor_api_router = APIRouter(tags=["editor"], auth=APIKeyAuth(permissions={"editor_access"}))
@editor_api_router.get('/bounds/', summary="boundaries",

View file

@ -9,13 +9,13 @@ from ninja import Schema, UploadedFile
from ninja.pagination import paginate
from pydantic import PositiveInt, field_validator
from c3nav.api.auth import APITokenAuth, auth_permission_responses, auth_responses, validate_responses
from c3nav.api.auth import APIKeyAuth, auth_permission_responses, auth_responses, validate_responses
from c3nav.api.exceptions import API404, APIConflict, APIRequestValidationFailed
from c3nav.mesh.dataformats import BoardType, ChipType, FirmwareImage
from c3nav.mesh.messages import MeshMessageType
from c3nav.mesh.models import FirmwareBuild, FirmwareVersion, NodeMessage
mesh_api_router = APIRouter(tags=["mesh"], auth=APITokenAuth(permissions={"mesh_control"}))
mesh_api_router = APIRouter(tags=["mesh"], auth=APIKeyAuth(permissions={"mesh_control"}))
class FirmwareBuildSchema(Schema):

View file

@ -9,7 +9,7 @@ from ninja import Router as APIRouter
from ninja import Schema
from pydantic import PositiveInt
from c3nav.api.auth import APITokenAuth, auth_responses, validate_responses
from c3nav.api.auth import APIKeyAuth, auth_responses, validate_responses
from c3nav.api.exceptions import APIRequestValidationFailed
from c3nav.api.utils import NonEmptyStr
from c3nav.mapdata.api.base import api_stats_clean_location_value
@ -171,7 +171,7 @@ def get_request_pk(location):
return location.slug if isinstance(location, Position) else location.pk
@routing_api_router.post('/route/', summary="query route", auth=APITokenAuth(is_readonly=True),
@routing_api_router.post('/route/', summary="query route", auth=APIKeyAuth(is_readonly=True),
description="query route between two locations",
response={200: RouteResponse | NoRouteResponse, **validate_responses, **auth_responses})
# todo: route failure responses

View file

@ -1,7 +1,7 @@
(function () {
class C3NavApi {
token = 'anonymous';
key = 'anonymous';
constructor(base ) {
this.base = base;
@ -11,7 +11,7 @@
})
.then(res => res.json())
.then(data => {
this.token = data.token
this.key = data.key
})
.catch(err => {
throw err;
@ -36,7 +36,7 @@
credentials: 'omit',
method: 'GET',
headers: {
'Authorization': `Bearer ${this.token}`,
'X-API-Key': this.key,
'Accept': 'application/json'
}
})
@ -48,7 +48,7 @@
credentials: 'omit',
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Authorization': `Bearer ${this.key}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
},