Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3137 new token pair endpoint #3181

Merged
merged 41 commits into from
Apr 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
aac983d
Island: Add token refresh endpoint
VakarisZ Mar 30, 2023
73e24f4
Island: Remove unused imports in token.py
cakekoa Mar 30, 2023
b0b7a7f
Island: Add Token to package imports
cakekoa Mar 30, 2023
3789d82
UT: Add tests for Token resource
cakekoa Mar 30, 2023
c75b53b
UT: Test AuthenticationFacade.generate_new_token_pair
cakekoa Mar 30, 2023
7681e33
UT: Add tests for TokenValidator.get_token_payload()
shreyamalviya Mar 31, 2023
c97dbf0
UT: Add failing test for TokenValidator.validate_token()
shreyamalviya Mar 31, 2023
2f3fe59
Island: Don't expose unnecessary resources in security_service
VakarisZ Mar 31, 2023
47be4c4
Island: Register Token resource that allows token refresh
VakarisZ Mar 31, 2023
2e259f4
UT: Remove unit test that asserts old refresh token invalidity
VakarisZ Mar 31, 2023
6b9c26b
Island: Extract access token key name into a const
VakarisZ Mar 31, 2023
61063d9
Changelog: Add entry for 'POST api/token' endpoint
shreyamalviya Mar 31, 2023
0654d0c
UT: Rename test in test_authentication_service.py
shreyamalviya Mar 31, 2023
c269089
Island: Improve AuthenticationFacade.generate_new_token_pair
VakarisZ Mar 31, 2023
59d5c07
Island: Fix "needs_registration" method and decouple from mongo
VakarisZ Mar 31, 2023
f75c79c
Island: Fixup documentation of the new refresh token endpoint
VakarisZ Mar 31, 2023
976b288
UT: Add registration tests back in
mssalvatore Mar 31, 2023
323c64a
UT: Improve test for AuthenticationService.generate_new_token_pair()
shreyamalviya Apr 3, 2023
57a1cab
Island: Rename resource Token to RefreshAuthenticationToken
shreyamalviya Apr 3, 2023
f7e8d91
UT: Rename test_token.py -> test_refresh_authentication_token.py
shreyamalviya Apr 3, 2023
45c64dd
UT: Fix AuthenticationService.needs_registration() tests
shreyamalviya Apr 3, 2023
52fc4d6
Island: Add TokenParser
shreyamalviya Apr 3, 2023
74b380d
UT: Update and add tests for new TokenParser class
shreyamalviya Apr 3, 2023
7de970c
Changelog: Fix the entry for /api/refesh-authentication-token
mssalvatore Apr 3, 2023
4b47f8c
Island: Fix linter errors in token_parser.py
mssalvatore Apr 3, 2023
8eb6192
UT: Add failing tests for BadSignature and SignatureExpired in refresh
ilija-lazoroski Apr 3, 2023
b327ae1
Island: Handle token exceptions in RefreshAuthenticationToken endpoint
ilija-lazoroski Apr 3, 2023
64d4ecd
Island: Convert ParsedToken from dataclass to pydantic model
mssalvatore Apr 3, 2023
07ffb4b
Island: Rename ParsedToken.{payload,user_uniquifier}
mssalvatore Apr 3, 2023
ad1f1c6
Island: Rename ParsedToken.{token,raw_token}
mssalvatore Apr 3, 2023
9e4a894
Island: Add ParsedToken.is_expired()
mssalvatore Apr 3, 2023
9553ec1
Island: Validate token signature upon construction of ParsedToken object
mssalvatore Apr 3, 2023
f1ff245
Island: Prevent invalid ParsedToken objects from being created
mssalvatore Apr 3, 2023
8da903b
Island: Raise specific TokenValidationError from TokenParser
mssalvatore Apr 3, 2023
5724313
Island: Remove AuthenticationFacade's dependency on TokenValidator
mssalvatore Apr 3, 2023
c08d71d
Island: Remove disused TokenValidator
mssalvatore Apr 3, 2023
e1259de
Island: Decouple RefreshAuthenticationToken from itsdangerous
mssalvatore Apr 3, 2023
d306017
Island: Move token parsing responsibility within AuthenticationFacade
mssalvatore Apr 3, 2023
0150492
Island: Expose TokenValidationError from authentication_service.token
mssalvatore Apr 3, 2023
88091d5
Island: Remove reference to nonexistant IncorrectCredentialsError
mssalvatore Apr 3, 2023
a2ce7d3
UT: Strengthen the assertion in __fails_if_token_invalid()
mssalvatore Apr 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/).
- `GET /api/agent-otp`. #3076
- `POST /api/agent-otp-login` endpoint. #3076
- A smarter brute-forcing strategy for SMB exploiter. #3039
- `POST /api/refresh-authentication-token` endpoint that allows refreshing of
the access token. #3181

### Changed
- Migrated the hard-coded SMB exploiter to a plugin. #2952
Expand Down
8 changes: 5 additions & 3 deletions monkey/monkey_island/cc/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
from monkey_island.cc.services.authentication_service.configure_flask_security import (
configure_flask_security,
)
from monkey_island.cc.services.authentication_service.token import TokenGenerator, TokenValidator
from monkey_island.cc.services.authentication_service.token import TokenGenerator, TokenParser
from monkey_island.cc.services.representations import output_json

HOME_FILE = "index.html"
Expand Down Expand Up @@ -162,16 +162,18 @@ def init_app(
def _build_authentication_facade(container: DIContainer, security: Security):
repository_encryptor = container.resolve(ILockableEncryptor)
island_event_queue = container.resolve(IIslandEventQueue)

token_generator = TokenGenerator(security)
refresh_token_expiration = (
security.app.config["SECURITY_TOKEN_MAX_AGE"]
+ security.app.config["SECURITY_REFRESH_TOKEN_TIMEDELTA"]
)
refresh_token_validator = TokenValidator(security, refresh_token_expiration)
token_parser = TokenParser(security, refresh_token_expiration)

return AuthenticationFacade(
repository_encryptor,
island_event_queue,
security.datastore,
token_generator,
refresh_token_validator,
token_parser,
)
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from typing import Tuple

from flask_security import UserDatastore

from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic
from monkey_island.cc.models import IslandMode
from monkey_island.cc.server_utils.encryption import ILockableEncryptor
from monkey_island.cc.services.authentication_service.token.token_generator import TokenGenerator

from .token import Token, TokenValidator
from . import AccountRole
from .token import ParsedToken, Token, TokenParser
from .user import User


Expand All @@ -20,28 +23,53 @@ def __init__(
island_event_queue: IIslandEventQueue,
user_datastore: UserDatastore,
token_generator: TokenGenerator,
refresh_token_validator: TokenValidator,
token_parser: TokenParser,
):
self._repository_encryptor = repository_encryptor
self._island_event_queue = island_event_queue
self._datastore = user_datastore
self._token_generator = token_generator
self._refresh_token_validator = refresh_token_validator
self._token_parser = token_parser

def needs_registration(self) -> bool:
"""
Checks if a user is already registered on the Island

:return: Whether registration is required on the Island
"""
return not User.objects.first()
island_api_user_role = self._datastore.find_or_create_role(
name=AccountRole.ISLAND_INTERFACE.name
)
return not self._datastore.find_user(roles=[island_api_user_role])

def revoke_all_tokens_for_user(self, user: User):
"""
Revokes all tokens for a specific user
"""
self._datastore.set_uniquifier(user)

def generate_new_token_pair(self, refresh_token: Token) -> Tuple[Token, Token]:
"""
Generates a new access token and refresh, given a valid refresh token

:param refresh_token: Refresh token
:raise TokenValidationError: If the refresh token is invalid or expired
:return: Tuple of the new access token and refresh token
"""
parsed_refresh_token = self._token_parser.parse(refresh_token)
user = self._get_refresh_token_owner(parsed_refresh_token)

new_access_token = user.get_auth_token()
new_refresh_token = self._token_generator.generate_token(user.fs_uniquifier)

return new_access_token, new_refresh_token

def _get_refresh_token_owner(self, refresh_token: ParsedToken) -> User:
user = self._datastore.find_user(fs_uniquifier=refresh_token.user_uniquifier)
if not user:
raise Exception("Invalid refresh token")
return user

def generate_refresh_token(self, user: User) -> Token:
"""
Generates a refresh token for a specific user
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1 @@
from .register import Register
from .registration_status import RegistrationStatus
from .login import Login
from .logout import Logout
from .register_resources import register_resources
from .agent_otp import AgentOTP
from .agent_otp_login import AgentOTPLogin
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ def post(self):
returns an access token

:return: Access token in the response body
:raises IncorrectCredentialsError: If credentials are invalid
"""
try:
username, password = get_username_password_from_request(request)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import logging
from http import HTTPStatus

from flask import make_response, request

from monkey_island.cc.flask_utils import AbstractResource, responses
from monkey_island.cc.services.authentication_service.token import TokenValidationError

from ..authentication_facade import AuthenticationFacade
from .utils import ACCESS_TOKEN_KEY_NAME, REFRESH_TOKEN_KEY_NAME

logger = logging.getLogger(__name__)


class RefreshAuthenticationToken(AbstractResource):
"""
A resource for refreshing tokens
"""

urls = ["/api/refresh-authentication-token"]

def __init__(self, authentication_facade: AuthenticationFacade):
self._authentication_facade = authentication_facade

def post(self):
"""
Accepts a refresh token and returns a new token pair

:return: Response with new token pair or an invalid request response
"""
try:
old_refresh_token = request.json[REFRESH_TOKEN_KEY_NAME]
access_token, refresh_token = self._authentication_facade.generate_new_token_pair(
old_refresh_token
)
response = {
"response": {
"user": {
ACCESS_TOKEN_KEY_NAME: access_token,
REFRESH_TOKEN_KEY_NAME: refresh_token,
}
}
}
return response, HTTPStatus.OK
except TokenValidationError:
return make_response({}, HTTPStatus.UNAUTHORIZED)
except Exception:
return responses.make_response_to_invalid_request()
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .agent_otp_login import AgentOTPLogin
from .login import Login
from .logout import Logout
from .refresh_authentication_token import RefreshAuthenticationToken
from .register import Register
from .registration_status import RegistrationStatus

Expand All @@ -18,3 +19,8 @@ def register_resources(api: flask_restful.Api, authentication_facade: Authentica
api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_facade,))
api.add_resource(AgentOTP, *AgentOTP.urls)
api.add_resource(AgentOTPLogin, *AgentOTPLogin.urls)
api.add_resource(
RefreshAuthenticationToken,
*RefreshAuthenticationToken.urls,
resource_class_args=(authentication_facade,),
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

from monkey_island.cc.services.authentication_service.token import Token

REFRESH_TOKEN_KEY_NAME = "refresh_token"
ACCESS_TOKEN_KEY_NAME = "authentication_token"


def get_username_password_from_request(_request: Request) -> Tuple[str, str]:
"""
Expand Down Expand Up @@ -49,6 +52,6 @@ def add_refresh_token_to_response(response: Response, refresh_token: Token) -> R
:return: A Flask Response object
"""
new_data = deepcopy(response.json)
new_data["response"]["user"]["refresh_token"] = refresh_token
new_data["response"]["user"][REFRESH_TOKEN_KEY_NAME] = refresh_token
response.data = json.dumps(new_data).encode()
return response
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .token_generator import TokenGenerator
from .token_validator import TokenValidator
from .token_parser import TokenParser, ParsedToken, TokenValidationError
from .types import Token
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from flask_security import Security
from itsdangerous import BadSignature, Serializer, SignatureExpired
from pydantic import PrivateAttr

from common.base_models import InfectionMonkeyBaseModel

from .types import Token


class TokenValidationError(Exception):
"""Raise when an invalid token is encountered"""


class InvalidTokenSignatureError(TokenValidationError):
"""Raise when a token's signature is invalid"""


class ExpiredTokenError(TokenValidationError):
"""Raise when a token has expired"""


class ParsedToken(InfectionMonkeyBaseModel):
raw_token: Token
user_uniquifier: str
expiration_time: int
_token_serializer: Serializer = PrivateAttr()

def __init__(self, token_serializer: Serializer, *, raw_token: Token, expiration_time: int):
self._token_serializer = token_serializer

user_uniquifier = self._token_serializer.loads(raw_token, max_age=expiration_time)
super().__init__(
raw_token=raw_token, user_uniquifier=user_uniquifier, expiration_time=expiration_time
)

def is_expired(self) -> bool:
try:
self._token_serializer.loads(self.raw_token, max_age=self.expiration_time)
return False
except SignatureExpired:
return True


class TokenParser:
def __init__(self, security: Security, token_expiration: int):
self._token_serializer = security.remember_token_serializer
self._token_expiration = token_expiration # in seconds

def parse(self, token: Token) -> ParsedToken:
"""
Parses a token and returns a data structure with its components

:param token: The token to parse
:return: The parsed token
:raises TokenValidationError: If the token could not be parsed
"""
try:
return ParsedToken(
token_serializer=self._token_serializer,
raw_token=token,
expiration_time=self._token_expiration,
)
except SignatureExpired:
# NOTE: SignatureExpired is a subclass of BadSignature; this clause must come first.
raise ExpiredTokenError("Token has expired")
except BadSignature:
raise InvalidTokenSignatureError("Invalid token signature")

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,10 @@
import pytest
from tests.unit_tests.monkey_island.conftest import init_mock_security_app

from monkey_island.cc.services.authentication_service import register_resources
from monkey_island.cc.services.authentication_service.authentication_facade import (
AuthenticationFacade,
)
from monkey_island.cc.services.authentication_service.flask_resources import (
AgentOTP,
AgentOTPLogin,
Login,
Logout,
Register,
RegistrationStatus,
)

REFRESH_TOKEN = "refresh_token"

Expand All @@ -37,15 +30,7 @@ def inner():

def get_mock_auth_app(authentication_facade: AuthenticationFacade):
app, api = init_mock_security_app()
api.add_resource(Register, *Register.urls, resource_class_args=(authentication_facade,))
api.add_resource(Login, *Login.urls, resource_class_args=(authentication_facade,))
api.add_resource(Logout, *Logout.urls, resource_class_args=(authentication_facade,))
api.add_resource(
RegistrationStatus, *RegistrationStatus.urls, resource_class_args=(authentication_facade,)
)
api.add_resource(AgentOTP, *AgentOTP.urls)
api.add_resource(AgentOTPLogin, *AgentOTPLogin.urls)

register_resources(api, authentication_facade)
return app


Expand Down
Loading