2020-08-19 08:32:44 +00:00
|
|
|
"""passbook OAuth2 Token Introspection Views"""
|
2020-09-28 09:42:27 +00:00
|
|
|
from dataclasses import dataclass, field
|
2020-08-19 08:32:44 +00:00
|
|
|
|
|
|
|
from django.http import HttpRequest, HttpResponse
|
|
|
|
from django.views import View
|
|
|
|
from structlog import get_logger
|
|
|
|
|
|
|
|
from passbook.providers.oauth2.errors import TokenIntrospectionError
|
|
|
|
from passbook.providers.oauth2.models import IDToken, OAuth2Provider, RefreshToken
|
2020-09-28 09:42:27 +00:00
|
|
|
from passbook.providers.oauth2.utils import (
|
|
|
|
TokenResponse,
|
|
|
|
extract_access_token,
|
|
|
|
extract_client_auth,
|
|
|
|
)
|
2020-08-19 08:32:44 +00:00
|
|
|
|
|
|
|
LOGGER = get_logger()
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class TokenIntrospectionParams:
|
|
|
|
"""Parameters for Token Introspection"""
|
|
|
|
|
2020-09-28 09:42:27 +00:00
|
|
|
token: RefreshToken
|
2020-08-19 08:32:44 +00:00
|
|
|
|
2020-09-28 09:42:27 +00:00
|
|
|
provider: OAuth2Provider = field(init=False)
|
|
|
|
id_token: IDToken = field(init=False)
|
2020-08-19 08:32:44 +00:00
|
|
|
|
2020-09-28 09:42:27 +00:00
|
|
|
def __post_init__(self):
|
2020-09-28 07:04:31 +00:00
|
|
|
if self.token.is_expired:
|
2020-09-28 09:42:27 +00:00
|
|
|
LOGGER.debug("Token is not valid")
|
2020-08-19 08:32:44 +00:00
|
|
|
raise TokenIntrospectionError()
|
|
|
|
|
2020-09-28 09:42:27 +00:00
|
|
|
self.provider = self.token.provider
|
2020-08-19 08:32:44 +00:00
|
|
|
self.id_token = self.token.id_token
|
|
|
|
|
|
|
|
if not self.token.id_token:
|
|
|
|
LOGGER.debug(
|
2020-09-30 17:34:22 +00:00
|
|
|
"token not an authentication token",
|
|
|
|
token=self.token,
|
2020-08-19 08:32:44 +00:00
|
|
|
)
|
|
|
|
raise TokenIntrospectionError()
|
|
|
|
|
2020-09-28 09:42:27 +00:00
|
|
|
def authenticate_basic(self, request: HttpRequest) -> bool:
|
|
|
|
"""Attempt to authenticate via Basic auth of client_id:client_secret"""
|
|
|
|
client_id, client_secret = extract_client_auth(request)
|
|
|
|
if client_id == client_secret == "":
|
|
|
|
return False
|
|
|
|
if (
|
|
|
|
client_id != self.provider.client_id
|
|
|
|
or client_secret != self.provider.client_secret
|
|
|
|
):
|
|
|
|
LOGGER.debug("(basic) Provider for basic auth does not exist")
|
2020-08-19 08:32:44 +00:00
|
|
|
raise TokenIntrospectionError()
|
2020-09-28 09:42:27 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
def authenticate_bearer(self, request: HttpRequest) -> bool:
|
|
|
|
"""Attempt to authenticate via token sent as bearer header"""
|
|
|
|
body_token = extract_access_token(request)
|
|
|
|
if not body_token:
|
|
|
|
return False
|
|
|
|
tokens = RefreshToken.objects.filter(access_token=body_token).select_related(
|
|
|
|
"provider"
|
|
|
|
)
|
|
|
|
if not tokens.exists():
|
|
|
|
LOGGER.debug("(bearer) Token does not exist")
|
|
|
|
raise TokenIntrospectionError()
|
|
|
|
if tokens.first().provider != self.provider:
|
|
|
|
LOGGER.debug("(bearer) Token providers don't match")
|
2020-08-19 08:32:44 +00:00
|
|
|
raise TokenIntrospectionError()
|
2020-09-28 09:42:27 +00:00
|
|
|
return True
|
2020-08-19 08:32:44 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def from_request(request: HttpRequest) -> "TokenIntrospectionParams":
|
|
|
|
"""Extract required Parameters from HTTP Request"""
|
2020-09-28 09:42:27 +00:00
|
|
|
raw_token = request.POST.get("token")
|
|
|
|
token_type_hint = request.POST.get("token_type_hint", "access_token")
|
|
|
|
token_filter = {token_type_hint: raw_token}
|
|
|
|
|
|
|
|
if token_type_hint not in ["access_token", "refresh_token"]:
|
|
|
|
LOGGER.debug("token_type_hint has invalid value", value=token_type_hint)
|
|
|
|
raise TokenIntrospectionError()
|
|
|
|
|
|
|
|
try:
|
|
|
|
token: RefreshToken = RefreshToken.objects.select_related("provider").get(
|
|
|
|
**token_filter
|
|
|
|
)
|
|
|
|
except RefreshToken.DoesNotExist:
|
|
|
|
LOGGER.debug("Token does not exist", token=raw_token)
|
|
|
|
raise TokenIntrospectionError()
|
|
|
|
|
|
|
|
params = TokenIntrospectionParams(token=token)
|
|
|
|
if not any(
|
|
|
|
[params.authenticate_basic(request), params.authenticate_bearer(request)]
|
|
|
|
):
|
|
|
|
LOGGER.debug("Not authenticated")
|
|
|
|
raise TokenIntrospectionError()
|
|
|
|
return params
|
2020-08-19 08:32:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
class TokenIntrospectionView(View):
|
|
|
|
"""Token Introspection
|
|
|
|
https://tools.ietf.org/html/rfc7662"""
|
|
|
|
|
|
|
|
token: RefreshToken
|
|
|
|
params: TokenIntrospectionParams
|
|
|
|
provider: OAuth2Provider
|
|
|
|
id_token: IDToken
|
|
|
|
|
|
|
|
def post(self, request: HttpRequest) -> HttpResponse:
|
|
|
|
"""Introspection handler"""
|
|
|
|
try:
|
2020-09-28 07:04:31 +00:00
|
|
|
self.params = TokenIntrospectionParams.from_request(request)
|
|
|
|
|
2020-08-19 08:32:44 +00:00
|
|
|
response_dic = {}
|
2020-09-28 09:42:27 +00:00
|
|
|
if self.params.id_token:
|
|
|
|
token_dict = self.params.id_token.to_dict()
|
2020-08-19 08:32:44 +00:00
|
|
|
for k in ("aud", "sub", "exp", "iat", "iss"):
|
|
|
|
response_dic[k] = token_dict[k]
|
|
|
|
response_dic["active"] = True
|
2020-09-28 09:42:27 +00:00
|
|
|
response_dic["client_id"] = self.params.token.provider.client_id
|
2020-08-19 08:32:44 +00:00
|
|
|
|
|
|
|
return TokenResponse(response_dic)
|
|
|
|
except TokenIntrospectionError:
|
|
|
|
return TokenResponse({"active": False})
|