def callback(): """Retrieving an access token. After you've redirected from our provider to your callback URL, you'll have access to the auth code in the redirect URL, which we'll be using to get an access token. """ client = WebApplicationClient(client_id=CLIENT_ID) # Parse the response URI after the callback, with the same state we initially sent client.parse_request_uri_response(request.url, state=session["oauth_state"]) # Now we've access to the auth code code = client.code # Prepare request body to get the access token body = client.prepare_request_body( code=code, redirect_uri=REDIRECT_URI, include_client_id=False, scope=scope, ) # Basic HTTP auth by providing your client credentials auth = requests.auth.HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET) # Making a post request to the TOKEN_URL endpoint r = requests.post(TOKEN_URL, data=dict(urldecode(body)), auth=auth) # Parse the response to get the token and store it in session token = client.parse_request_body_response(r.text, scope=scope) session["access_token"] = token return redirect("/home")
def test_oauth2(app, user, oauth2_client): client_id, client_secret = oauth2_client client = WebApplicationClient(client_id) host = "https://localhost" state = "randomly_text" with app.test_client() as provider: # login forcefully. with provider.session_transaction() as sess: sess["user_id"] = user sess["_fresh"] = True uri = client.prepare_request_uri(host + "/oauth2/authorize", redirect_uri=redirect_uri, state=state) uri = uri[len(host) :] # step 1: redirect to provider response = provider.get(uri, follow_redirects=True) assert response.status_code == 200 # step 2: redirect to client response = provider.post(uri, data={"scope": "user", "confirm": "yes"}) assert response.location.startswith(redirect_uri) data = client.parse_request_uri_response(response.location, state=state) assert "code" in data # step 3: get the token body = client.prepare_request_body(code=data["code"], redirect_uri=redirect_uri) response = provider.post("/oauth2/token", content_type="application/x-www-form-urlencoded", data=body) assert response.status_code == 200 data = client.parse_request_body_response(response.data) assert "access_token" in data assert data["token_type"] == "Bearer" assert data["scope"] == ["user"] # step 4: using token pass
class TestOauthlib(ApprovalTestCase): def setUp(self): super(TestOauthlib, self).setUp() self.libclient = WebApplicationClient(self.oauth_client.id) self.authorization_code.response_type = "code" self.authorization_code.save() def test_flow(self): self.client.login(username="******", password="******") request_uri = self.libclient.prepare_request_uri( "https://localhost" + reverse("oauth2_authorize"), redirect_uri=self.redirect_uri.url, scope=["test", ], state="test_state", ) response = self.client.get(request_uri[17:]) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "doac/authorize.html") approval_url = reverse("oauth2_approval") + "?code=" + self.authorization_code.token response = self.client.post(approval_url, { "code": self.authorization_code.token, "code_state": "test_state", "approve_access": None, }) response_uri = response.get("location", None) if not response_uri: response_uri = response.META["HTTP_LOCATION"] response_uri = response_uri.replace("http://", "https://") data = self.libclient.parse_request_uri_response(response_uri, state="test_state") authorization_token = data["code"] request_body = self.libclient.prepare_request_body( client_secret=self.oauth_client.secret, code=authorization_token, ) post_dict = {} for pair in request_body.split('&'): key, val = pair.split('=') post_dict[key] = val response = self.client.post(reverse("oauth2_token"), post_dict) data = self.libclient.parse_request_body_response(response.content)
class OAuthAuthorizationCodeGrantRequest: def __init__(self, redirect_uri: str, settings: OAuthSettings): from aiohttp import ClientSession from oauthlib.oauth2 import WebApplicationClient self.settings = settings self.future = get_event_loop().create_future() self.state = uuid.uuid4().hex self.client = WebApplicationClient(settings.client_id) self.redirect_uri = redirect_uri self.session = ClientSession() async def start(self) -> None: uri = self.client.prepare_request_uri( self.settings.token_uri, redirect_uri=self.redirect_uri, state=self.state) body = self.client.prepare_request_body() async with self.session.post(uri, data=body) as response: print(response.status) print(await response.text()) async def handle_redirect(self, uri: str, code: str, state: str) -> OAuthSettings: self.client.parse_request_uri_response(uri, state) kwargs = {} if self.settings.client_secret: kwargs['client_secret'] = self.settings.client_secret uri = self.settings.token_uri body = self.client.prepare_request_body( code=code, redirect_uri=self.redirect_uri, **kwargs) # fetch token self.client.parse_request_body_response(response_body) new_settings = OAuthSettings( ) self.future.set_result(new_settings) return new_settings async def close(self): await self.session.close()
def test_request_body(self): client = WebApplicationClient(self.client_id, code=self.code) # Basic, no extra arguments body = client.prepare_request_body(body=self.body) self.assertFormBodyEqual(body, self.body_code) rclient = WebApplicationClient(self.client_id) body = rclient.prepare_request_body(code=self.code, body=self.body) self.assertFormBodyEqual(body, self.body_code) # With redirection uri body = client.prepare_request_body(body=self.body, redirect_uri=self.redirect_uri) self.assertFormBodyEqual(body, self.body_redirect) # With extra parameters body = client.prepare_request_body(body=self.body, **self.kwargs) self.assertFormBodyEqual(body, self.body_kwargs)
def verify_token(request): dataporten_oauth_client = WebApplicationClient(settings.DATAPORTEN_ID) code = request.GET.get('code') redirect_uri = settings.DATAPORTEN_REDIRECT_URI token_request_body = dataporten_oauth_client.prepare_request_body( code=code, redirect_uri=redirect_uri, client_secret=settings.DATAPORTEN_SECRET) token_request_response = requests.post( settings.DATAPORTEN_OAUTH_TOKEN_URL, data=token_request_body, headers={ 'content-type': 'application/x-www-form-urlencoded', 'authorization': 'Basic {}'.format(settings.DATAPORTEN_SECRET), }) if token_request_response.status_code != 200: raise Exception('invalid code') response_json = token_request_response.json() session = requests.Session() session.headers.update( {'authorization': 'bearer {}'.format(response_json['access_token'])}) user_info = session.get(settings.DATAPORTEN_USER_INFO_URL).json()['user'] user_mail = user_info['email'] user, created = User.objects.get_or_create(email=user_mail, username=user_mail) ExpiringToken.objects.filter(user=user).delete() token = ExpiringToken.objects.create(user=user) access_token = response_json['access_token'] has_token = False try: user_auth = UserAuth.objects.filter(user_email=user_mail).first() if user_auth: has_token = True except: print("User has no token") if has_token: UserAuth.objects.filter(user_email=user_mail).first().delete() UserAuth.objects.create(user_email=user_mail, expiring_token="Token " + str(token), access_token=access_token) return Response(({'token': token.key, 'email': user_mail}))
async def get_session(client_id: str, client_secret: str) -> AuthToken: """ Use the Authorization Code Grant flow to get a token. This opens a browser tab. """ refresh_token_file = os.path.join(config.config_dir(), '.refresh.token') base_url = 'https://bitbucket.org/site/oauth2' # If we have a refresh token, use that existing_token = None if os.path.isfile(refresh_token_file): with open(refresh_token_file) as f: existing_token = json.load(f) now = arrow.utcnow() if existing_token and arrow.get(existing_token['expires_at']) - now > timedelta(minutes=5): log.info('Found existing token') return existing_token # Otherwise, send the user to the browser flow redirect_uri = 'https://localhost:8888' client = WebApplicationClient(client_id) auth_url = client.prepare_request_uri(f'{base_url}/authorize', redirect_uri=redirect_uri) print(f'Please go to the following link, then copy the redirected URL back here.\n\n\t{auth_url}\n') code = client.parse_request_uri_response(input('URL: '))['code'] token_reqest_params = parse_qs(client.prepare_request_body(code=code, redirect_uri=redirect_uri)) async with aiohttp.ClientSession() as session: resp = await session.post( f'{base_url}/access_token', headers={'Authorization': aiohttp.BasicAuth(client_id, client_secret).encode()}, data=token_reqest_params ) if resp.status != 200: log.error(await resp.text()) raise Exception('Could not authenticate with the Bitbucket API') token: AuthToken = await resp.json() token['expires_at'] = now.shift(seconds=token['expires_in']).format(arrow.FORMAT_RFC3339) with open(refresh_token_file, 'w') as f: json.dump(token, f) return token
def test_oauth2(app, user, oauth2_client): client_id, client_secret = oauth2_client client = WebApplicationClient(client_id) host = 'https://localhost' state = 'randomly_text' with app.test_client() as provider: # login forcefully. with provider.session_transaction() as sess: sess['user_id'] = user sess['_fresh'] = True uri = client.prepare_request_uri(host + '/oauth2/authorize', redirect_uri=redirect_uri, state=state) uri = uri[len(host):] # step 1: redirect to provider response = provider.get(uri, follow_redirects=True) assert response.status_code == 200 # step 2: redirect to client response = provider.post(uri, data={ 'scope': 'user', 'confirm': 'yes', }) assert response.location.startswith(redirect_uri) data = client.parse_request_uri_response(response.location, state=state) assert 'code' in data # step 3: get the token body = client.prepare_request_body( code=data['code'], redirect_uri=redirect_uri, ) response = provider.post( '/oauth2/token', content_type='application/x-www-form-urlencoded', data=body) assert response.status_code == 200 data = client.parse_request_body_response(response.data) assert 'access_token' in data assert data['token_type'] == 'Bearer' assert data['scope'] == ['user'] # step 4: using token pass
def index(request): client = WebApplicationClient(settings.GITHUB_CLIENT_ID) if not request.GET.has_key('code'): # todo - also setup hook for repo # todo - check http referrer = 'https://github.com/' uri = client.prepare_request_uri(AUTHORIZATION_URL, redirect_uri=request.build_absolute_uri(reverse('index')), scope=['repo']) context = {'uri': uri} else: code = request.GET['code'] body = client.prepare_request_body(code=code, client_secret=settings.GITHUB_CLIENT_SECRET) headers = {'Accept': 'application/json'} response = requests.post(REQUEST_TOKEN_URL, body, headers=headers) response = response.json() owner = Github(response['access_token']).get_user() # todo - check if user already exists => show repo settings token = GithubToken(owner=owner.login, access_token=response['access_token'], scope=response['scope']) token.save() context = {'owner': owner.name} return render(request, 'benchmarks/index.html', context)
class GoogleMusicSession(httpx.Client): authorization_base_url = AUTHORIZATION_BASE_URL redirect_uri = REDIRECT_URI token_url = TOKEN_URL def __init__(self, client_id, client_secret, scope, *, token=None, **kwargs): # httpx sets a default timeout on the Client class. # requests did not. # Disable timeout by default as too low a value # can cause issues with upload calls. timeout = kwargs.pop('timeout', None) super().__init__(timeout=timeout, **kwargs) self.params = {} self.headers.update({'User-Agent': f'{__title__}/{__version__}'}) self.client_id = client_id self.client_secret = client_secret self.scope = scope self.token = token or {} self.oauth_client = WebApplicationClient(self.client_id, token=self.token) @property def access_token(self): return self.token.get('access_token') @property def authorized(self): return bool(self.access_token) def authorization_url(self): state = generate_token() return (self.oauth_client.prepare_request_uri( self.authorization_base_url, redirect_uri=self.redirect_uri, scope=self.scope, state=state, access_type='offline', prompt='select_account')) def fetch_token(self, code): body = self.oauth_client.prepare_request_body( code=code, body='', redirect_uri=self.redirect_uri, include_client_id=None) response = self.request( 'POST', self.token_url, headers={ 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', }, data=dict(urldecode(body)), auth=httpx.BasicAuth(self.client_id, self.client_secret)) self.token = self.oauth_client.parse_request_body_response( response.text, scope=self.scope) return self.token def refresh_token(self): refresh_token = self.token.get('refresh_token') body = self.oauth_client.prepare_refresh_body( body='', refresh_token=refresh_token, scope=self.scope, client_id=self.client_id, client_secret=self.client_secret) response = self.request( 'POST', self.token_url, headers={ 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', }, data=dict(urldecode(body)), auth=httpx.BasicAuth(self.client_id, self.client_secret), withhold_token=True) self.token = self.oauth_client.parse_request_body_response( response.text, scope=self.scope) if 'refresh_token' not in self.token: self.token['refresh_token'] = refresh_token return self.token def request(self, method, url, data=None, headers=None, withhold_token=False, **kwargs): if self.token and not withhold_token: try: url, headers, data = self.oauth_client.add_token( url, http_method=method, body=data, headers=headers) except TokenExpiredError: self.refresh_token() url, headers, data = self.oauth_client.add_token( url, http_method=method, body=data, headers=headers) return super().request(method, url, headers=headers, data=data, **kwargs)
def test_prepare_request_body(self): """ see issue #585 https://github.com/oauthlib/oauthlib/issues/585 `prepare_request_body` should support the following scenarios: 1. Include client_id alone in the body (default) 2. Include client_id and client_secret in auth and not include them in the body (RFC preferred solution) 3. Include client_id and client_secret in the body (RFC alternative solution) 4. Include client_id in the body and an empty string for client_secret. """ client = WebApplicationClient(self.client_id) # scenario 1, default behavior to include `client_id` r1 = client.prepare_request_body() self.assertEqual(r1, 'grant_type=authorization_code&client_id=%s' % self.client_id) r1b = client.prepare_request_body(include_client_id=True) self.assertEqual(r1b, 'grant_type=authorization_code&client_id=%s' % self.client_id) # scenario 2, do not include `client_id` in the body, so it can be sent in auth. r2 = client.prepare_request_body(include_client_id=False) self.assertEqual(r2, 'grant_type=authorization_code') # scenario 3, Include client_id and client_secret in the body (RFC alternative solution) # the order of kwargs being appended is not guaranteed. for brevity, check the 2 permutations instead of sorting r3 = client.prepare_request_body(client_secret=self.client_secret) r3_params = dict(urlparse.parse_qsl(r3, keep_blank_values=True)) self.assertEqual(len(r3_params.keys()), 3) self.assertEqual(r3_params['grant_type'], 'authorization_code') self.assertEqual(r3_params['client_id'], self.client_id) self.assertEqual(r3_params['client_secret'], self.client_secret) r3b = client.prepare_request_body(include_client_id=True, client_secret=self.client_secret) r3b_params = dict(urlparse.parse_qsl(r3b, keep_blank_values=True)) self.assertEqual(len(r3b_params.keys()), 3) self.assertEqual(r3b_params['grant_type'], 'authorization_code') self.assertEqual(r3b_params['client_id'], self.client_id) self.assertEqual(r3b_params['client_secret'], self.client_secret) # scenario 4, `client_secret` is an empty string r4 = client.prepare_request_body(include_client_id=True, client_secret='') r4_params = dict(urlparse.parse_qsl(r4, keep_blank_values=True)) self.assertEqual(len(r4_params.keys()), 3) self.assertEqual(r4_params['grant_type'], 'authorization_code') self.assertEqual(r4_params['client_id'], self.client_id) self.assertEqual(r4_params['client_secret'], '') # scenario 4b, `client_secret` is `None` r4b = client.prepare_request_body(include_client_id=True, client_secret=None) r4b_params = dict(urlparse.parse_qsl(r4b, keep_blank_values=True)) self.assertEqual(len(r4b_params.keys()), 2) self.assertEqual(r4b_params['grant_type'], 'authorization_code') self.assertEqual(r4b_params['client_id'], self.client_id) # scenario Warnings with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") # catch all # warning1 - raise a DeprecationWarning if a `client_id` is submitted rWarnings1 = client.prepare_request_body(client_id=self.client_id) self.assertEqual(len(w), 1) self.assertIsInstance(w[0].message, DeprecationWarning) # testing the exact warning message in Python2&Python3 is a pain # scenario Exceptions # exception1 - raise a ValueError if the a different `client_id` is submitted with self.assertRaises(ValueError) as cm: client.prepare_request_body(client_id='different_client_id')
def get(self, request, *args, **kwargs): # Retrieve these data from the URL data = self.request.GET code = data['code'] state = data['state'] print("code=%s, state=%s" % (code, state)) # For security purposes, verify that the # state information is the same as was passed # to github_login() if self.request.session['state'] != state: messages.add_message(self.request, messages.ERROR, "State information mismatch!") return HttpResponseRedirect(reverse('github:welcome')) else: del self.request.session['state'] # fetch the access token from GitHub's API at token_url token_url = 'https://github.com/login/oauth/access_token' client_id = settings.GITHUB_OAUTH_CLIENT_ID client_secret = settings.GITHUB_OAUTH_SECRET # Create a Web Applicantion Client from oauthlib client = WebApplicationClient(client_id) # Prepare body for request data = client.prepare_request_body( code=code, redirect_uri=settings.GITHUB_OAUTH_CALLBACK_URL, client_id=client_id, client_secret=client_secret) # Post a request at GitHub's token_url # Returns requests.Response object response = requests.post(token_url, data=data) """ Parse the unicode content of the response object Returns a dictionary stored in client.token { 'access_token': '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', 'scope': ['read:user'], 'token_type': 'bearer' } """ client.parse_request_body_response(response.text) # Prepare an Authorization header for GET request using the 'access_token' value # using GitHub's official API format header = { 'Authorization': 'token {}'.format(client.token['access_token']) } # Retrieve GitHub profile data # Send a GET request # Returns requests.Response object response = requests.get('https://api.github.com/user', headers=header) # Store profile data in JSON json_dict = response.json() ''' Fields that are of interest: 'login' => json_dict['login'], 'name' => json_dict['name'], 'bio' => json_dict['bio'], 'blog' => json_dict['blog'], 'email' => json_dict['email'], # not public data 'avatar_url' => json_dict['avatar_url'], ''' # save the user profile in a session self.request.session['profile'] = json_dict # retrieve or create a Django User for this profile try: user = User.objects.get(username=json_dict['login']) messages.add_message( self.request, messages.DEBUG, "User %s already exists, Authenticated? %s" % (user.username, user.is_authenticated)) print("User %s already exists, Authenticated %s" % (user.username, user.is_authenticated)) # remember to log the user into the system login(self.request, user) except: # create a Django User for this login user = User.objects.create_user(json_dict['login'], json_dict['email']) messages.add_message( self.request, messages.DEBUG, "User %s is created, Authenticated %s?" % (user.username, user.is_authenticated)) print("User %s is created, Authenticated %s" % (user.username, user.is_authenticated)) # remember to log the user into the system login(self.request, user) # Redirect response to hide the callback url in browser return HttpResponseRedirect(reverse('github:welcome'))
class OAuthClient: """ Helper class to handle the OAuth authentication flow the logic is divided in 2 steps: - open the browser on GitGuardian login screen and run a local server to wait for callback - handle the oauth callback to exchange an authorization code against a valid access token """ def __init__(self, config: Config, instance: str) -> None: self.config = config self.instance = instance self._oauth_client = WebApplicationClient(CLIENT_ID) self._state = "" # use the `state` property instead self._handler_wrapper = RequestHandlerWrapper(oauth_client=self) self._access_token: Optional[str] = None self._port = USABLE_PORT_RANGE[0] self.server: Optional[HTTPServer] = None self._generate_pkce_pair() def oauth_process(self, token_name: Optional[str] = None, lifetime: Optional[int] = None) -> None: """ Handle the whole oauth process which includes - opening the user's webbrowser to GitGuardian login page - open a server and wait for the callback processing """ # enable redirection to http://localhost os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = str(True) if token_name is None: token_name = "ggshield token " + datetime.today().strftime( "%Y-%m-%d") self._token_name = token_name if lifetime is None: lifetime = self.default_token_lifetime self._lifetime = lifetime self._prepare_server() self._redirect_to_login() self._wait_for_callback() message = f"Created Personal Access Token {self._token_name} " expire_at = self.instance_config.account.expire_at if expire_at is not None: message += "expiring on " + get_pretty_date(expire_at) else: message += "with no expiry" click.echo(message) def process_callback(self, callback_url: str) -> None: """ This function runs within the request handler do_GET method. It takes the url of the callback request as argument and does - Extract the authorization code - Exchange the code against an access token with GitGuardian's api - Validate the new token against GitGuardian's api - Save the token in configuration Any error during this process will raise a OAuthError """ authorization_code = self._get_code(callback_url) self._claim_token(authorization_code) token_data = self._validate_access_token() self._save_token(token_data) def _generate_pkce_pair(self) -> None: """ Generate a code verifier (random string) and its sha encoded version to be used for the pkce checking process """ self.code_verifier = self._oauth_client.create_code_verifier( 128) # type: ignore self.code_challenge = (urlsafe_b64encode( sha256(self.code_verifier.encode()).digest()).decode().rstrip("=")) def _redirect_to_login(self) -> None: """ Open the user's browser to the GitGuardian ggshield authentication page """ static_params = { "auth_mode": "ggshield_login", "utm_source": "cli", "utm_medium": "login", "utm_campaign": "ggshield", } request_uri = self._oauth_client.prepare_request_uri( uri=urlparse.urljoin(self.dashboard_url, "auth/login"), redirect_uri=self.redirect_uri, scope=SCOPE, code_challenge=self.code_challenge, code_challenge_method="S256", state=self.state, **static_params, ) click.echo( f"To complete the login process, follow the instructions from {request_uri}.\n" "Opening your web browser now...") webbrowser.open_new_tab(request_uri) def _prepare_server(self) -> None: for port in range(*USABLE_PORT_RANGE): try: self.server = HTTPServer( # only consider requests from localhost on the predetermined port ("127.0.0.1", port), # attach the wrapped request handler self._handler_wrapper.request_handler, ) self._port = port break except OSError: continue else: raise click.ClickException("Could not find unoccupied port.") def _wait_for_callback(self) -> None: """ Wait to receive and process the authorization callback on the local server. This actually catches HTTP requests made on the previously opened server. The callback processing logic is implementend in the request handler class and the `process_callback` method """ try: while not self._handler_wrapper.complete: # Wait for callback on localserver including an authorization code # any matchin request will get processed by the request handler and # the `process_callback` function self.server.handle_request() # type: ignore except KeyboardInterrupt: raise click.ClickException("Aborting") if self._handler_wrapper.error_message is not None: # if no error message is attached, the process is considered successful raise click.ClickException(self._handler_wrapper.error_message) def _get_code(self, uri: str) -> str: """ Extract the authorization from the incoming request uri and verify that the state from the uri match the one stored internally. if no code can be extracted or the state is invalid, raise an OAuthError else return the extracted code """ try: authorization_code = self._oauth_client.parse_request_uri_response( uri, self.state).get("code") except OAuth2Error: authorization_code = None if authorization_code is None: raise OAuthError( "Invalid code or state received from the callback.") return authorization_code # type: ignore def _claim_token(self, authorization_code: str) -> None: """ Exchange the authorization code with a valid access token using GitGuardian public api. If no valid token could be retrieved, exit the authentication process with an error message """ request_params = {"name": self._token_name} if self._lifetime is not None: request_params["lifetime"] = str(self._lifetime) request_body = self._oauth_client.prepare_request_body( code=authorization_code, redirect_uri=self.redirect_uri, code_verifier=self.code_verifier, body=urlparse.urlencode(request_params), ) response = requests.post( urlparse.urljoin(self.api_url, "oauth/token"), request_body, headers={"Content-Type": "application/x-www-form-urlencoded"}, ) if not response.ok: raise OAuthError("Cannot create a token.") self._access_token = response.json()["key"] self.config.auth_config.current_token = self._access_token def _validate_access_token(self) -> Dict[str, Any]: """ Validate the token using GitGuardian public api. If the token is not valid, exit the authentication process with an error message. """ response = retrieve_client(self.config).get(endpoint="token") if not response.ok: raise OAuthError("The created token is invalid.") return response.json() # type: ignore def _save_token(self, api_token_data: Dict[str, Any]) -> None: """ Save the new token in the configuration. """ account_config = AccountConfig( account_id=api_token_data["account_id"], token=self._access_token, # type: ignore expire_at=api_token_data.get("expire_at"), token_name=api_token_data.get("name", ""), type=api_token_data.get("type", ""), ) self.instance_config.account = account_config self.config.save() @property def instance_config(self) -> InstanceConfig: return self.config.auth_config.instances[self.instance] @property def default_token_lifetime(self) -> Optional[int]: """ return the default token lifetime saved in the instance config. if None, this will be interpreted as no expiry. """ default_lifetime = self.instance_config.default_token_lifetime if default_lifetime is not None: return default_lifetime.days return None @property def redirect_uri(self) -> str: return f"http://localhost:{self._port}" @property def state(self) -> str: """ Return the state used to verify the auth process. The state is included in the redirect_uri and is expected in the callback url. Then, if both states don't match, the process fails. The state is an url-encoded string dict containing the token name and lifetime It is cached to prevent from altering its value during the process """ if not self._state: self._state = urlparse.quote( json.dumps({ "token_name": self._token_name, "lifetime": self._lifetime })) return self._state @property def dashboard_url(self) -> str: return self.config.dashboard_url @property def api_url(self) -> str: return self.config.api_url
def callback(request): #parse the incomming answer from oauth client = WebApplicationClient(settings.SHEN_RING_CLIENT_ID) try: response = client.parse_request_uri_response(request.build_absolute_uri()) except MissingCodeError: if 'error' in request.GET: raise PermissionDenied(request.GET.get('error')) else: raise PermissionDenied("Authentication failed") if not settings.SHEN_RING_NO_CSRF: if 'state' not in response: raise PermissionDenied("csrf_state_check_failed") if request.session.get(csrf.CSRF_SESSION_KEY, '') != response['state']: raise PermissionDenied("csrf_state_check_failed") #upgrade grant code to access code session = requests.Session() session.headers['User-Agent'] = 'BEP Marketplace ELE' #get parameters data = client.prepare_request_body(code=response['code'], client_secret=settings.SHEN_RING_CLIENT_SECRET, include_client_id=True) # convert to requests dictionary data_dict = {} for itm in data.split("&"): data_dict[itm.split('=')[0]] = itm.split('=')[1] #request accesstoken try: access_code_data = requests.post(settings.SHEN_RING_URL + "oauth/token/", data=data_dict).json() except: raise PermissionDenied("invalid_json_data") if 'access_token' not in access_code_data: raise PermissionDenied(access_code_data['error']) #request account information ## this assumes that timeslot pk is identical on both shen and local db! r = session.get(settings.SHEN_RING_URL + "info/", headers={"Authorization" : "Bearer {}".format(access_code_data["access_token"])}) if r.status_code != 200: raise PermissionDenied("shen_link_failed") try: value = json.dumps(signing.loads(r.text, settings.SHEN_RING_CLIENT_SECRET)) except signing.BadSignature: raise PermissionDenied("shen_signing_failed") #login or create the user try: user, usermeta = serializers.deserialize('json', value) except: raise PermissionDenied('corrupted_user_info_retrieved') ## data from info is directly saved to db, this means that the appointed shen system is fully trusted ## this is breached when the shen server is man in the middled, but then an attacker needs to steal both the domain as well as the secret keys if User.objects.filter(Q(username=user.object.username) & Q(email=user.object.email)).count() == 1: #user exists existent_user = User.objects.filter(Q(username=user.object.username) & Q(email=user.object.email))[0] user.object.pk = existent_user.pk existent_usermeta = existent_user.usermeta usermeta.object.pk = existent_usermeta.pk groups = list(existent_user.groups.all()) timeslots = list(existent_usermeta.TimeSlot.all()) # for fields that do not exist on shen but do exist on local, port the value over otherwise data is lost for local_field in settings.USERMETA_LOCAL_FIELDS: setattr(usermeta.object, local_field, getattr(existent_usermeta, local_field)) try: user.save() usermeta.object.User = user.object usermeta.save() except: raise PermissionDenied("Authentication failed") #overwrite the timeslots, this needs to be done after usermeta save due to begin a m2m relation usermeta.object.TimeSlot.clear() for ts in timeslots: usermeta.object.TimeSlot.add(ts) usermeta.object.save() #foreignkeys on the user to other models are wiped with this method, foreignkeys from other models to user keep working # has to be done after save because its an m2m relation for group in groups: user.object.groups.add(group) user.object.save() elif User.objects.filter(Q(username=user.object.username) & Q(email=user.object.email)).count() == 0: #user does not exist user.object.pk = None usermeta.object.pk = None try: user.save() usermeta.object.User = user.object usermeta.save() # clear timeslots as this is handled internally usermeta.object.TimeSlot.clear() usermeta.save() except: raise PermissionDenied("Authentication failed") else: #more then one user found with this combination, db corrupted, abort return HttpResponseNotFound() user = user.object usermeta = usermeta.object response = check_user(request, user) if response is not True: return response #login user auth.login(request, user) #TODO: fix that next parameter is taken into account return HttpResponseRedirect("/")
def callback(request): # parse the incoming answer from oauth client = WebApplicationClient(settings.SHEN_RING_CLIENT_ID) try: response = client.parse_request_uri_response( request.build_absolute_uri()) except MissingCodeError: if 'error' in request.GET: raise PermissionDenied(request.GET.get('error')) else: raise PermissionDenied("Authentication failed") if not settings.SHEN_RING_NO_CSRF: if 'state' not in response: raise PermissionDenied( "Authentication failed. (csrf state not available)") if '-' in response['state']: csrf_token, next_url = response['state'].split('-') next_url = base64.b64decode(next_url.encode()).decode() else: csrf_token = response['state'] next_url = None if request.session.get(csrf.CSRF_SESSION_KEY, '') != csrf_token: raise PermissionDenied( "Authentication failed. (csrf token failed)") else: if '-' in response.get('state', ""): next_url = base64.b64decode( response['state'].strip('-').encode()).decode() else: next_url = None # upgrade grant code to access code session = requests.Session() session.headers['User-Agent'] = settings.NAME_PRETTY # get parameters data = client.prepare_request_body( code=response['code'], client_secret=settings.SHEN_RING_CLIENT_SECRET, include_client_id=True) # convert to requests dictionary data_dict = {} for itm in data.split("&"): data_dict[itm.split('=')[0]] = itm.split('=')[1] # request accesstoken try: access_code_data = requests.post(settings.SHEN_RING_URL + "oauth/token/", data=data_dict).json() except: raise PermissionDenied("Authentication failed. (invalid_json_data)") if 'access_token' not in access_code_data: raise PermissionDenied(access_code_data['error']) # request account information # this assumes that timeslot pk is identical on both shen and local db! r = session.get(settings.SHEN_RING_URL + "info/", headers={ "Authorization": "Bearer {}".format(access_code_data["access_token"]) }) if r.status_code != 200: raise PermissionDenied("Authentication failed. (shen_link_failed)") try: value = json.dumps( signing.loads(r.text, settings.SHEN_RING_CLIENT_SECRET)) except signing.BadSignature: raise PermissionDenied("Authentication failed. (shen_signing_failed)") # login or create the user try: user, usermeta = serializers.deserialize('json', value) except: raise PermissionDenied( 'Authentication failed. (corrupted_user_info_retrieved)') # data from info is directly saved to db, this means that the appointed shen system is fully trusted # this is breached when the shen server is man in the middled, but then an attacker needs to steal both the domain as well as the secret keys # find user and map shen user to local user existent_user = get_user(user.object.email, user.object.username) if existent_user: if not existent_user.is_active: raise PermissionDenied( "Your user account is disabled. Please contact support.") user.object.pk = existent_user.pk existent_usermeta = existent_user.usermeta usermeta.object.pk = existent_usermeta.pk groups = list(existent_user.groups.all()) perms = list(existent_user.user_permissions.all()) timeslots = list(existent_usermeta.TimeSlot.all()) user.object.is_staff = existent_user.is_staff # keep the is_staff flag. # for fields that do not exist on shen but do exist on local, port the value over otherwise data is lost for local_field in settings.USERMETA_LOCAL_FIELDS: setattr(usermeta.object, local_field, getattr(existent_usermeta, local_field)) try: user.save() usermeta.object.User = user.object usermeta.save() except Exception as e: logger.exception( 'User save failed with Shen login for existing user {}. Exception {}' .format(user, e)) raise PermissionDenied( "Something went wrong while logging you in. Please contact support at {} to get this resolved." .format(settings.CONTACT_EMAIL)) # overwrite the timeslots, this needs to be done after usermeta save due to begin a m2m relation usermeta.object.TimeSlot.clear() for ts in timeslots: usermeta.object.TimeSlot.add(ts) usermeta.object.save() # foreignkeys on the user to other models are wiped with this method, foreignkeys from other models to user keep working # has to be done after save because its an m2m relation for group in groups: user.object.groups.add(group) for perm in perms: # copy custom user permissions to have individual admin access for users. user.object.user_permissions.add(perm) user.object.save() elif existent_user is None: # user does not exist user.object.pk = None usermeta.object.pk = None try: user.save() usermeta.object.User = user.object usermeta.save() # clear timeslots as this is handled internally usermeta.object.TimeSlot.clear() usermeta.save() except Exception as e: logger.exception( 'User save failed with Shen login for new user {}. Exception {}' .format(user, e)) raise PermissionDenied( "Something went wrong while logging you in. Please contact support at {} to get this resolved." .format(settings.CONTACT_EMAIL)) else: # more then one user found with this combination, db corrupted, abort # this will not happen, as get_user already raises exception return HttpResponseNotFound() user = user.object usermeta = usermeta.object response = check_user(request, user) if response is not True: return response # login user auth.login(request, user) if next_url is not None: if is_safe_url(next_url, None): return HttpResponseRedirect(next_url) return HttpResponseRedirect("/")