summaryrefslogtreecommitdiff
path: root/macaroonbakery/httpbakery/_browser.py
diff options
context:
space:
mode:
Diffstat (limited to 'macaroonbakery/httpbakery/_browser.py')
-rw-r--r--macaroonbakery/httpbakery/_browser.py89
1 files changed, 89 insertions, 0 deletions
diff --git a/macaroonbakery/httpbakery/_browser.py b/macaroonbakery/httpbakery/_browser.py
new file mode 100644
index 0000000..a1ccbb0
--- /dev/null
+++ b/macaroonbakery/httpbakery/_browser.py
@@ -0,0 +1,89 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+import base64
+from collections import namedtuple
+
+import requests
+from ._error import InteractionError
+from ._interactor import (
+ WEB_BROWSER_INTERACTION_KIND,
+ DischargeToken,
+ Interactor,
+ LegacyInteractor,
+)
+from macaroonbakery._utils import visit_page_with_browser
+
+from six.moves.urllib.parse import urljoin
+
+
+class WebBrowserInteractor(Interactor, LegacyInteractor):
+ ''' Handles web-browser-based interaction-required errors by opening a
+ web browser to allow the user to prove their credentials interactively.
+ '''
+ def __init__(self, open=visit_page_with_browser):
+ '''Create a WebBrowserInteractor that uses the given function
+ to open a browser window. The open function is expected to take
+ a single argument of string type, the URL to open.
+ '''
+ self._open_web_browser = open
+
+ def kind(self):
+ return WEB_BROWSER_INTERACTION_KIND
+
+ def legacy_interact(self, ctx, location, visit_url):
+ '''Implement LegacyInteractor.legacy_interact by opening the
+ web browser window'''
+ self._open_web_browser(visit_url)
+
+ def interact(self, ctx, location, ir_err):
+ '''Implement Interactor.interact by opening the browser window
+ and waiting for the discharge token'''
+ p = ir_err.interaction_method(self.kind(), WebBrowserInteractionInfo)
+ if not location.endswith('/'):
+ location += '/'
+ visit_url = urljoin(location, p.visit_url)
+ wait_token_url = urljoin(location, p.wait_token_url)
+ self._open_web_browser(visit_url)
+ return self._wait_for_token(ctx, wait_token_url)
+
+ def _wait_for_token(self, ctx, wait_token_url):
+ ''' Returns a token from a the wait token URL
+ @param wait_token_url URL to wait for (string)
+ :return DischargeToken
+ '''
+ resp = requests.get(wait_token_url)
+ if resp.status_code != 200:
+ raise InteractionError('cannot get {}'.format(wait_token_url))
+ json_resp = resp.json()
+ kind = json_resp.get('kind')
+ if kind is None:
+ raise InteractionError(
+ 'cannot get kind token from {}'.format(wait_token_url))
+ token_val = json_resp.get('token')
+ if token_val is None:
+ token_val = json_resp.get('token64')
+ if token_val is None:
+ raise InteractionError(
+ 'cannot get token from {}'.format(wait_token_url))
+ token_val = base64.b64decode(token_val)
+ return DischargeToken(kind=kind, value=token_val)
+
+
+class WebBrowserInteractionInfo(namedtuple('WebBrowserInteractionInfo',
+ 'visit_url, wait_token_url')):
+ ''' holds the information expected in the browser-window interaction
+ entry in an interaction-required error.
+
+ :param visit_url holds the URL to be visited in a web browser.
+ :param wait_token_url holds a URL that will block on GET until the browser
+ interaction has completed.
+ '''
+ @classmethod
+ def from_dict(cls, info_dict):
+ '''Create a new instance of WebBrowserInteractionInfo, as expected
+ by the Error.interaction_method method.
+ @param info_dict The deserialized JSON object
+ @return a new WebBrowserInteractionInfo object.
+ '''
+ return WebBrowserInteractionInfo(visit_url=info_dict.get('VisitURL'),
+ wait_token_url=info_dict('WaitURL'))