diff --git a/docs/api/wallet-rpc.yaml b/docs/api/wallet-rpc.yaml index e3f9a2c50..5a37d34d8 100644 --- a/docs/api/wallet-rpc.yaml +++ b/docs/api/wallet-rpc.yaml @@ -10,6 +10,26 @@ servers: - url: https://none description: This API is called locally to a jmwalletd instance, acting as server, for each wallet owner, it is not public. paths: + /token/refresh: + post: + security: + - bearerAuth: [] + summary: refresh an access token + operationId: refreshtoken + description: Give a refresh token and get back both an access and refresh token. Access token, valid for 30 min, must be used for authenticated endpoints. refreshtoken endpoint can be called with an expired access token and a refresh token, valid 4 hours after issuance. The newly issued tokens must be used in subsequent calls since operation invalidates previously issued tokens. + responses: + '200': + $ref: '#/components/responses/RefreshToken-200-OK' + '400': + $ref: '#/components/responses/400-BadRequest' + '401': + $ref: '#/components/responses/401-Unauthorized' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RefreshTokenRequest' + description: token refresh parameters /wallet/create: post: summary: create a new wallet @@ -652,6 +672,26 @@ components: destination: type: string example: "bcrt1qujp2x2fv437493sm25gfjycns7d39exjnpptzw" + RefreshTokenRequest: + type: object + required: + - refresh_token + properties: + refresh_token: + type: string + format: byte + RefreshTokenResponse: + type: object + required: + - token + - refresh_token + properties: + token: + type: string + format: byte + refresh_token: + type: string + format: byte RunScheduleRequest: type: object required: @@ -893,27 +933,31 @@ components: type: string extradata: type: string - CreateWalletResponse: type: object required: - walletname - - token - seedphrase + - token + - refresh_token properties: walletname: type: string example: wallet.jmdat + seedphrase: + type: string token: type: string format: byte - seedphrase: + refresh_token: type: string + format: byte UnlockWalletResponse: type: object required: - walletname - token + - refresh_token properties: walletname: type: string @@ -921,6 +965,9 @@ components: token: type: string format: byte + refresh_token: + type: string + format: byte DirectSendResponse: type: object required: @@ -1100,6 +1147,12 @@ components: application/json: schema: $ref: "#/components/schemas/ListWalletsResponse" + RefreshToken-200-OK: + description: "access token refreshed successfully" + content: + application/json: + schema: + $ref: "#/components/schemas/RefreshTokenResponse" Session-200-OK: description: "successful heartbeat response" content: diff --git a/jmclient/jmclient/auth.py b/jmclient/jmclient/auth.py new file mode 100644 index 000000000..10c01cbe7 --- /dev/null +++ b/jmclient/jmclient/auth.py @@ -0,0 +1,94 @@ +import datetime +import os +from typing import Optional + +import jwt + +from jmbase.support import bintohex + + +class InvalidScopeError(Exception): + pass + + +class JMSessionValidity: + access = datetime.timedelta(minutes=30) + refresh = datetime.timedelta(hours=4) + + +class JMTokenSignatureKey: + algorithm = "HS256" + + def __init__(self): + self.access = self.get_random_key() + self.refresh = self.get_random_key() + + @staticmethod + def get_random_key(size: int = 16) -> str: + """Create a random key has an hexadecimal string.""" + return bintohex(os.urandom(size)) + + def reset(self, refresh_token_only: bool): + """Invalidate previously issued token(s) by creating new signature key(s).""" + self.refresh = self.get_random_key() + if not refresh_token_only: + self.access = self.get_random_key() + + +class JMTokenAuthority: + """Manage authorization tokens.""" + + validity = JMSessionValidity() + + def __init__(self, wallet_name: Optional[str] = None): + self.signature = JMTokenSignatureKey() + self.wallet_name = wallet_name + + def verify( + self, + token: str, + scope: str = "walletrpc", + *, + is_refresh: bool = False, + verify_exp: bool = True, + ): + """Verify JWT token. + + Token must have a valid signature and its scope must contain both scopes in + arguments and wallet_name property. + """ + token_type = "refresh" if is_refresh else "access" + claims = jwt.decode( + token, + getattr(self.signature, token_type), + algorithms=self.signature.algorithm, + options={"verify_exp": verify_exp}, + leeway=10, + ) + token_claims = set(claims.get("scope", []).split()) + if not set(scope.split()) | {self.wallet_name} <= token_claims: + raise InvalidScopeError + + def _issue(self, scope: str, token_type: str) -> str: + return jwt.encode( + { + "exp": datetime.datetime.utcnow() + getattr(self.validity, token_type), + "scope": f"{scope} {self.wallet_name}", + }, + getattr(self.signature, token_type), + algorithm=self.signature.algorithm, + ) + + def issue(self, scope: str = "walletrpc") -> dict: + """Issue a new access and refresh token for said scope. + Previously issued refresh token is invalidated. + """ + self.signature.reset(refresh_token_only=True) + return { + "token": self._issue(scope, "access"), + "refresh_token": self._issue(scope, "refresh"), + } + + def reset(self): + """Invalidate all previously issued tokens by creating new signature keys.""" + self.signature.reset(refresh_token_only=False) diff --git a/jmclient/jmclient/wallet_rpc.py b/jmclient/jmclient/wallet_rpc.py index d844f5f8c..84d946cc2 100644 --- a/jmclient/jmclient/wallet_rpc.py +++ b/jmclient/jmclient/wallet_rpc.py @@ -1,5 +1,3 @@ -from jmbitcoin import * -import datetime import os import json from io import BytesIO @@ -9,9 +7,9 @@ from twisted.application.service import Service from autobahn.twisted.websocket import listenWS from klein import Klein -import jwt import pprint +from .auth import JMTokenAuthority from jmbitcoin import human_readable_transaction from jmclient import Taker, jm_single, \ JMClientProtocolFactory, start_reactor, \ @@ -134,7 +132,7 @@ def make_jmwalletd_response(request, status=200, **kwargs): class JMWalletDaemon(Service): """ This class functions as an HTTP/TLS server, - with acccess control, allowing a single client(user) + with access control, allowing a single client(user) to control functioning of encapsulated Joinmarket services. """ @@ -146,7 +144,8 @@ def __init__(self, port, wss_port, tls=True): websocket connections for clients to subscribe to updates. """ # cookie tracks single user's state. - self.cookie = None + self.token = JMTokenAuthority() + self.active_session = False self.port = port self.wss_port = wss_port self.tls = tls @@ -225,7 +224,7 @@ def startService(self): # wallet service, since the client must actively request # that with the appropriate credential (password). # initialise the web socket service for subscriptions - self.wss_factory = JmwalletdWebSocketServerFactory(self.wss_url) + self.wss_factory = JmwalletdWebSocketServerFactory(self.wss_url, self.token) self.wss_factory.protocol = JmwalletdWebSocketServerProtocol if self.tls: cf = get_ssl_context(os.path.join(jm_single().datadir, "ssl")) @@ -251,11 +250,9 @@ def stopSubServices(self): - shuts down any other running sub-services, such as yieldgenerator. - shuts down (aborts) any taker-side coinjoining happening. """ - # Currently valid authorization tokens must be removed - # from the daemon: - self.cookie = None - if self.wss_factory: - self.wss_factory.valid_token = None + self.token.reset() + self.active_session = False + self.wss_factory.active_session = False self.wallet_name = None # if the wallet-daemon is shut down, all services # it encapsulates must also be shut down. @@ -361,19 +358,13 @@ def yieldgenerator_report_unavailable(self, request, failure): request.setResponseCode(404) return self.err(request, "Yield generator report not available.") - def check_cookie(self, request): - #part after bearer is what we need + def check_cookie(self, request, *, verify_exp: bool = True): try: - auth_header=((request.getHeader('Authorization'))) - request_cookie = None - if auth_header is not None: - request_cookie=auth_header[7:] - except Exception: - # deliberately catching anything - raise NotAuthorized() - if request_cookie==None or self.cookie != request_cookie: - jlog.warn("Invalid cookie: " + str( - request_cookie) + ", request rejected.") + # Token itself is stated after `Bearer ` prefix, it must be removed + access_token = request.getHeader("Authorization")[7:] + self.token.verify(access_token, verify_exp=verify_exp) + except Exception as e: + jlog.debug(e) raise NotAuthorized() def check_cookie_if_present(self, request): @@ -381,23 +372,6 @@ def check_cookie_if_present(self, request): if auth_header is not None: self.check_cookie(request) - def set_token(self, wallet_name): - """ This function creates a new JWT token and sets it as our - 'cookie' for API and WS. Note this always creates a new fresh token, - there is no option to manually set it, intentionally. - """ - # any random secret is OK, as long as it is not deducible/predictable: - secret_key = bintohex(os.urandom(16)) - encoded_token = jwt.encode({"wallet": wallet_name, - "exp" :datetime.datetime.utcnow( - )+datetime.timedelta(minutes=30)}, - secret_key) - self.cookie = encoded_token.strip() - # We want to make sure that any websocket clients use the correct - # token. The wss_factory should have been created on JMWalletDaemon - # startup, so any failure to exist here is a logic error: - self.wss_factory.valid_token = self.cookie - def get_POST_body(self, request, required_keys, optional_keys=None): """ given a request object, retrieve values corresponding to keys in a dict, assuming they were encoded using JSON. @@ -468,6 +442,8 @@ def initialize_wallet_service(self, request, wallet, wallet_name, **kwargs): def dummy_restart_callback(msg): jlog.warn("Ignoring rescan request from backend wallet service: " + msg) self.services["wallet"].add_restart_callback(dummy_restart_callback) + self.active_session = True + self.wss_factory.active_session = True self.wallet_name = wallet_name self.services["wallet"].register_callbacks( [self.wss_factory.sendTxNotification], None) @@ -475,20 +451,20 @@ def dummy_restart_callback(msg): # now that the WalletService instance is active and ready to # respond to requests, we return the status to the client: - # First, prepare authentication for the calling client: - self.set_token(wallet_name) # return type is different for a newly created OR recovered # wallet, in this case we use the 'seedphrase' kwarg as trigger: - if('seedphrase' in kwargs): - return make_jmwalletd_response(request, - status=201, - walletname=self.wallet_name, - token=self.cookie, - seedphrase=kwargs.get('seedphrase')) + if "seedphrase" in kwargs: + return make_jmwalletd_response( + request, + status=201, + walletname=self.wallet_name, + seedphrase=kwargs.get("seedphrase"), + **self.token.issue(), + ) else: - return make_jmwalletd_response(request, - walletname=self.wallet_name, - token=self.cookie) + return make_jmwalletd_response( + request, walletname=self.wallet_name, **self.token.issue() + ) def taker_finished(self, res, fromtx=False, waittime=0.0, txdetails=None): if not self.tumbler_options: @@ -582,6 +558,26 @@ def preflight(self, request): request.setHeader("Access-Control-Allow-Methods", "POST") with app.subroute(api_version_string) as app: + @app.route('/token/refresh', methods=['POST']) + def refresh(self, request): + try: + self.check_cookie(request, verify_exp=False) + + assert isinstance(request.content, BytesIO) + refresh_token = self.get_POST_body(request, ["refresh_token"])["refresh_token"] + self.token.verify(refresh_token, is_refresh=True) + + return make_jmwalletd_response( + request, + walletname=self.wallet_name, + **self.token.issue(), + ) + except: + return make_jmwalletd_response( + request, + status=401, + ) + @app.route('/wallet//display', methods=['GET']) def displaywallet(self, request, walletname): print_req(request) @@ -625,9 +621,6 @@ def session(self, request): #this lets caller know if cookie is invalid or outdated self.check_cookie_if_present(request) - #if no wallet loaded then clear frontend session info - #when no wallet status is false - session = not self.cookie==None maker_running = self.coinjoin_state == CJ_MAKER_RUNNING coinjoin_in_process = self.coinjoin_state == CJ_TAKER_RUNNING @@ -663,14 +656,17 @@ def session(self, request): else: wallet_name = "None" - return make_jmwalletd_response(request,session=session, - maker_running=maker_running, - coinjoin_in_process=coinjoin_in_process, - schedule=schedule, - wallet_name=wallet_name, - offer_list=offer_list, - nickname=nickname, - rescanning=rescanning) + return make_jmwalletd_response( + request, + session=self.active_session, + maker_running=maker_running, + coinjoin_in_process=coinjoin_in_process, + schedule=schedule, + wallet_name=wallet_name, + offer_list=offer_list, + nickname=nickname, + rescanning=rescanning, + ) @app.route('/wallet//taker/direct-send', methods=['POST']) def directsend(self, request, walletname): @@ -976,10 +972,11 @@ def unlockwallet(self, request, walletname): # this also shouldn't happen so raise: raise NoWalletFound() # no exceptions raised means we just return token: - self.set_token(self.wallet_name) - return make_jmwalletd_response(request, - walletname=self.wallet_name, - token=self.cookie) + return make_jmwalletd_response( + request, + walletname=self.wallet_name, + **self.token.issue(), + ) # This is a different wallet than the one currently open; # try to open it, then initialize the service(s): diff --git a/jmclient/jmclient/websocketserver.py b/jmclient/jmclient/websocketserver.py index 1c126cbad..f160487bb 100644 --- a/jmclient/jmclient/websocketserver.py +++ b/jmclient/jmclient/websocketserver.py @@ -1,6 +1,8 @@ import json from autobahn.twisted.websocket import WebSocketServerFactory, \ WebSocketServerProtocol + +from .auth import JMTokenAuthority from jmbitcoin import human_readable_transaction from jmbase import get_log @@ -8,19 +10,18 @@ class JmwalletdWebSocketServerProtocol(WebSocketServerProtocol): def onOpen(self): - self.token = None + self.active_session = False self.factory.register(self) def sendNotification(self, info): """ Passes on an object (json encoded) to the client, if currently authenticated. """ - if not self.token: - # gating by token means even if this client - # is erroneously in a broadcast list, it won't get - # any data if it hasn't authenticated. + if not self.active_session: + # not sending any data if the session is + # not active, i.e. client hasn't authenticated. jlog.warn("Websocket not sending notification, " - "the connection is not authenticated.") + "the session is not active.") return self.sendMessage(json.dumps(info).encode()) @@ -37,22 +38,22 @@ def onMessage(self, payload, isBinary): other message will drop the connection. """ if not isBinary: - self.token = payload.decode('utf8') + token = payload.decode('utf8') # check that the token set for this protocol - # instance is the same as the one that the - # JMWalletDaemon instance deems is valid. - if not self.factory.check_token(self.token): + # instance is valid. + try: + self.factory.token.verify(token) + self.active_session = True + except Exception as e: + jlog.debug(e) self.dropConnection() class JmwalletdWebSocketServerFactory(WebSocketServerFactory): - def __init__(self, url): + def __init__(self, url, token_authority = JMTokenAuthority()): WebSocketServerFactory.__init__(self, url) - self.valid_token = None + self.token = token_authority self.clients = [] - def check_token(self, token): - return self.valid_token == token - def register(self, client): if client not in self.clients: self.clients.append(client) diff --git a/jmclient/test/test_auth.py b/jmclient/test/test_auth.py new file mode 100644 index 000000000..f9f5e2d70 --- /dev/null +++ b/jmclient/test/test_auth.py @@ -0,0 +1,100 @@ +"""test auth module.""" + +import copy +import datetime + +import jwt +import pytest + +from jmclient.auth import ( + InvalidScopeError, + JMTokenAuthority, + JMTokenSignatureKey, +) + +class TestJMTokenSignatureKey: + @pytest.mark.parametrize("execution_number", range(4)) + def test_get_random_key(self, execution_number): + """Results must always be different.""" + assert ( + JMTokenSignatureKey.get_random_key() != JMTokenSignatureKey.get_random_key() + ) + + @pytest.mark.parametrize("refresh_token_only", [True, False]) + def test_reset(self, refresh_token_only): + sig = JMTokenSignatureKey() + self.access_sig = copy.copy(sig.access) + self.refresh_sig = copy.copy(sig.refresh) + sig.reset(refresh_token_only) + assert sig.refresh != self.refresh_sig + assert ( + sig.access == self.access_sig + if refresh_token_only + else sig.access != self.access_sig + ) + + +class TestJMTokenAuthority: + token_auth = JMTokenAuthority("dummywallet") + + access_sig = copy.copy(token_auth.signature.access) + refresh_sig = copy.copy(token_auth.signature.refresh) + + validity = datetime.timedelta(hours=1) + scope = "walletrpc dummywallet" + + @pytest.mark.parametrize( + "sig, is_refresh", [(access_sig, False), (refresh_sig, True)] + ) + def test_verify_valid(self, sig, is_refresh): + token = jwt.encode( + {"exp": datetime.datetime.utcnow() + self.validity, "scope": self.scope}, + sig, + algorithm=self.token_auth.signature.algorithm, + ) + + try: + self.token_auth.verify(token, is_refresh=is_refresh) + except Exception as e: + print(e) + pytest.fail("Token verification failed, token is valid.") + + def test_verify_expired(self): + token = jwt.encode( + {"exp": datetime.datetime.utcnow() - self.validity, "scope": self.scope}, + self.access_sig, + algorithm=self.token_auth.signature.algorithm, + ) + + try: + self.token_auth.verify(token, verify_exp=False) + except jwt.exceptions.ExpiredSignatureError: + pytest.fail("Token verification failed, expiration should not be verified.") + + with pytest.raises(jwt.exceptions.ExpiredSignatureError): + self.token_auth.verify(token) + + def test_verify_non_scoped(self): + token = jwt.encode( + {"exp": datetime.datetime.utcnow() + self.validity, "scope": "wrong"}, + self.access_sig, + algorithm=self.token_auth.signature.algorithm, + ) + + with pytest.raises(InvalidScopeError): + self.token_auth.verify(token) + + def test_issue(self): + try: + for k, v in self.token_auth.issue().items(): + claims = jwt.decode( + v, + self.token_auth.signature.refresh + if k == "refresh_token" + else self.token_auth.signature.access, + algorithms=self.token_auth.signature.algorithm, + ) + assert claims.get("scope") == self.scope + assert self.token_auth.signature.refresh != self.refresh_sig + except jwt.exceptions.InvalidTokenError: + pytest.fail("An invalid token was issued.") diff --git a/jmclient/test/test_wallet_rpc.py b/jmclient/test/test_wallet_rpc.py index 44e05a898..012a6bead 100644 --- a/jmclient/test/test_wallet_rpc.py +++ b/jmclient/test/test_wallet_rpc.py @@ -1,11 +1,12 @@ -import os, json +import datetime +import json +import os +import jwt import pytest from twisted.internet import reactor, defer, task - from twisted.web.client import readBody, Headers from twisted.trial import unittest - from autobahn.twisted.websocket import WebSocketClientFactory, \ connectWS @@ -20,7 +21,7 @@ from test_coinjoin import make_wallets_to_list, sync_wallets from test_websocket import (ClientTProtocol, test_tx_hex_1, - test_tx_hex_txid, encoded_token) + test_tx_hex_txid, test_token_authority) pytestmark = pytest.mark.usefixtures("setup_regtest_bitcoind") @@ -31,10 +32,14 @@ jlog = get_log() class JMWalletDaemonT(JMWalletDaemon): - def check_cookie(self, request): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.token = test_token_authority + + def check_cookie(self, request, *args, **kwargs): if self.auth_disabled: return True - return super().check_cookie(request) + return super().check_cookie(request, *args, **kwargs) class WalletRPCTestBase(object): """ Base class for set up of tests of the @@ -80,6 +85,7 @@ def setUp(self): # (and don't use wallet files yet), we won't have set a wallet name, # so we set it here: self.daemon.wallet_name = self.get_wallet_file_name(1) + self.daemon.token.wallet_name = self.daemon.wallet_name r, s = self.daemon.startService() self.listener_rpc = r self.listener_ws = s @@ -115,7 +121,7 @@ def get_wallet_file_name(self, i, fullpath=False): @defer.inlineCallbacks def do_request(self, agent, method, addr, body, handler, token=None): if token: - headers = Headers({"Authorization": ["Bearer " + self.jwt_token]}) + headers = Headers({"Authorization": ["Bearer " + token]}) else: headers = None response = yield agent.request(method, addr, headers, bodyProducer=body) @@ -173,9 +179,9 @@ class TrialTestWRPC_WS(WalletRPCTestBase, unittest.TestCase): """ def test_notif(self): # simulate the daemon already having created - # a valid token (which it usually does when + # an active session (which it usually does when # starting the WalletService: - self.daemon.wss_factory.valid_token = encoded_token + self.daemon.wss_factory.protocol.active_session = True self.client_factory = WebSocketClientFactory( "ws://127.0.0.1:"+str(self.wss_port)) self.client_factory.protocol = ClientTProtocol @@ -665,6 +671,88 @@ def process_get_seed_response(self, response, code): json_body = json.loads(response.decode('utf-8')) assert json_body["seedphrase"] +class TrialTestWRPC_JWT(WalletRPCTestBase, unittest.TestCase): + @defer.inlineCallbacks + def test_refresh_token(self): + """Test refresh token endpoint + + 1. Valid access, valid refresh -> Success + 2. Valid access, invalid refresh -> Failed + 3. Valid access, expired refresh -> Failed + 4. Expired access, valid refresh -> Success + 5. Expired access, invalid refresh -> Failed + 6. Expired access, expired refresh -> Failed + 7. Invalid access, valid refresh -> Failed + """ + + agent = get_nontor_agent() + addr = self.get_route_root() + addr += "/token/refresh" + addr = addr.encode() + + for access_token, refresh_token, is_successful_response in [ + ("valid", "valid", True), + ("valid", "invalid", False), + ("valid", "expired", False), + ("expired", "valid", True), + ("expired", "invalid", False), + ("expired", "expired", False), + ("invalid", "valid", False), + ]: + access_token, refresh_token, is_successful_response = "valid", "valid", True + body = BytesProducer( + json.dumps( + { + "refresh_token": self.get_token( + expired=refresh_token == "expired", + invalid=refresh_token == "invalid", + refresh=True, + ) + } + ).encode() + ) + response_handler = ( + self.successful_refresh_response_handler + if is_successful_response + else self.failed_refresh_response_handler + ) + yield self.do_request( + agent, + b"POST", + addr, + body, + response_handler, + self.get_token( + expired=access_token == "expired", + invalid=access_token == "invalid", + ), + ) + + def failed_refresh_response_handler(self, response, code): + assert code == 401 + + def successful_refresh_response_handler(self, response, code): + assert code == 200 + json_body = json.loads(response.decode("utf-8")) + assert json_body.get("token") + assert json_body.get("refresh_token") + + def get_token(self, *, expired=False, invalid=False, refresh=False): + if invalid: + return jwt.encode({"scope": "invalid"}, "12345678") + + if expired: + exp = datetime.datetime.utcnow() - datetime.timedelta(hours=1) + else: + exp = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + + t = jwt.encode( + {"exp": exp, "scope": f"walletrpc {self.daemon.wallet_name}"}, + getattr(test_token_authority.signature, "refresh" if refresh else "access"), + algorithm=test_token_authority.signature.algorithm, + ) + return t + """ Sample listutxos response for reference: diff --git a/jmclient/test/test_websocket.py b/jmclient/test/test_websocket.py index cf89bcf56..38ba9d87c 100644 --- a/jmclient/test/test_websocket.py +++ b/jmclient/test/test_websocket.py @@ -1,17 +1,16 @@ import os import json -import datetime from twisted.internet import reactor, task from twisted.trial import unittest from autobahn.twisted.websocket import WebSocketClientFactory, \ WebSocketClientProtocol, connectWS, listenWS -import jwt from jmbase import get_log, hextobin from jmbase.support import get_free_tcp_ports from jmclient import (JmwalletdWebSocketServerFactory, JmwalletdWebSocketServerProtocol) +from jmclient.auth import JMTokenAuthority from jmbitcoin import CTransaction testdir = os.path.dirname(os.path.realpath(__file__)) @@ -21,11 +20,8 @@ test_tx_hex_1 = "02000000000102578770b2732aed421ffe62d54fd695cf281ca336e4f686d2adbb2e8c3bedb2570000000000ffffffff4719a259786b4237f92460629181edcc3424419592529103143090f07d85ec330100000000ffffffff0324fd9b0100000000160014d38fa4a6ac8db7495e5e2b5d219dccd412dd9bae24fd9b0100000000160014564aead56de8f4d445fc5b74a61793b5c8a819667af6c208000000001600146ec55c2e1d1a7a868b5ec91822bf40bba842bac502473044022078f8106a5645cc4afeef36d4addec391a5b058cc51053b42c89fcedf92f4db1002200cdf1b66a922863fba8dc1b1b1a0dce043d952fa14dcbe86c427fda25e930a53012102f1f750bfb73dbe4c7faec2c9c301ad0e02176cd47bcc909ff0a117e95b2aad7b02483045022100b9a6c2295a1b0f7605381d416f6ed8da763bd7c20f2402dd36b62dd9dd07375002207d40eaff4fc6ee219a7498abfab6bdc54b7ce006ac4b978b64bff960fbf5f31e012103c2a7d6e44acdbd503c578ec7d1741a44864780be0186e555e853eee86e06f11f00000000" test_tx_hex_txid = "ca606efc5ba8f6669ba15e9262e5d38e745345ea96106d5a919688d1ff0da0cc" -# example (valid) JWT token for test: -encoded_token = jwt.encode({"wallet": "dummywallet", - "exp" :datetime.datetime.utcnow( - )+datetime.timedelta(minutes=30)}, "secret") -encoded_token = encoded_token.strip() +# Shared JWT token authority for test: +test_token_authority = JMTokenAuthority("dummywallet") class ClientTProtocol(WebSocketClientProtocol): """ @@ -37,7 +33,7 @@ def sendAuth(self): """ Our server will not broadcast to us unless we authenticate. """ - self.sendMessage(encoded_token.encode('utf8')) + self.sendMessage(test_token_authority.issue()["token"].encode('utf8')) def onOpen(self): # auth on startup @@ -69,9 +65,8 @@ def setUp(self): free_ports = get_free_tcp_ports(1) self.wss_port = free_ports[0] self.wss_url = "ws://127.0.0.1:" + str(self.wss_port) - self.wss_factory = JmwalletdWebSocketServerFactory(self.wss_url) + self.wss_factory = JmwalletdWebSocketServerFactory(self.wss_url, test_token_authority) self.wss_factory.protocol = JmwalletdWebSocketServerProtocol - self.wss_factory.valid_token = encoded_token self.listeningport = listenWS(self.wss_factory, contextFactory=None) self.test_tx = CTransaction.deserialize(hextobin(test_tx_hex_1))