move api auth from authorization header to X-API-Key header
This commit is contained in:
parent
f64e65f297
commit
4491f68dc7
8 changed files with 43 additions and 41 deletions
|
@ -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}"}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue