def validate_refresh_scopes(self, request, prior_tokens, requested_scope): """ Ensure requested scopes are in client's prior authorized scopes or within the limits of client's default scopes """ if prior_tokens: original_scopes = set( scope for token in prior_tokens for scope in utils.scope_to_list(token.access_token.scope)) else: original_scopes = self.get_default_scopes(request.client_id, request) if requested_scope: request.scopes = utils.scope_to_list(requested_scope) else: request.scopes = original_scopes # scope request is invalid if not within the client's available scopes if not self.validate_scopes(request.client_id, request.scopes, request.client, request): valid_scope_request = False # scope request is invalid if not in the client's previously # authorized scopes elif not set(request.scopes).issubset(set(original_scopes)): valid_scope_request = False else: valid_scope_request = True return valid_scope_request
def validate_scopes(self, request): if not request.scopes: request.scopes = utils.scope_to_list(request.scope) or utils.scope_to_list( self.request_validator.get_default_scopes(request.client_id, request)) log.debug('Validating access to scopes %r for client %r (%r).', request.scopes, request.client_id, request.client) if not self.request_validator.validate_scopes(request.client_id, request.scopes, request.client, request): raise errors.InvalidScopeError(state=request.state, request=request)
def test_scope_to_list(self): expected = ['foo', 'bar', 'baz'] string_scopes = 'foo bar baz' self.assertEqual(scope_to_list(string_scopes), expected) string_list_scopes = ['foo', 'bar', 'baz'] self.assertEqual(scope_to_list(string_list_scopes), expected) obj_list_scopes = [ScopeObject('foo'), ScopeObject('bar'), ScopeObject('baz')] self.assertEqual(scope_to_list(obj_list_scopes), expected)
def validate_scopes(self, request): """ :param request: OAuthlib request. :type request: oauthlib.common.Request """ if not request.scopes: request.scopes = utils.scope_to_list(request.scope) or utils.scope_to_list( self.request_validator.get_default_scopes(request.client_id, request)) log.debug('Validating access to scopes %r for client %r (%r).', request.scopes, request.client_id, request.client) if not self.request_validator.validate_scopes(request.client_id, request.scopes, request.client, request): raise errors.InvalidScopeError(request=request)
def validate_token_request(self, request): if request.grant_type != JWT_BEARER: raise errors.UnsupportedGrantTypeError(request=request) if request.assertion is None: raise errors.InvalidRequestError('Missing assertion parameter.', request=request) for param in ('grant_type', 'scope'): if param in request.duplicate_params: raise errors.InvalidRequestError('Duplicate %s parameter.' % param, request=request) # Since the JSON Web Token is signed by its issuer client # authentication is not strictly required when the token is used as # an authorization grant. However, if client credentials are provided # they should be validated as describe in Section 3.1. # https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12#section-3.1 if self.request_validator.client_authentication_required(request): log.debug('Authenticating client, %r.', request) if not self.request_validator.authenticate_client(request): log.debug('Invalid client (%r), denying access.', request) raise errors.InvalidClientError(request=request) # REQUIRED. The web token issued by the client. log.debug('Validating assertion %s.', request.assertion) if not self.request_validator.validate_bearer_token( request.assertion, request.scopes, request): log.debug('Invalid assertion, %s, for client %r.', request.assertion, request.client) raise errors.InvalidGrantError('Invalid assertion.', request=request) original_scopes = utils.scope_to_list( self.request_validator.get_original_scopes(request.assertion, request)) if request.scope: request.scopes = utils.scope_to_list(request.scope) if (not all((s in original_scopes for s in request.scopes)) and not self.request_validator.is_within_original_scope( request.scopes, request.refresh_token, request)): log.debug('Refresh token %s lack requested scopes, %r.', request.refresh_token, request.scopes) raise errors.InvalidScopeError(request=request) else: request.scopes = original_scopes
def validate_token_request(self, request): if request.grant_type != JWT_BEARER: raise errors.UnsupportedGrantTypeError(request=request) if request.assertion is None: raise errors.InvalidRequestError('Missing assertion parameter.', request=request) for param in ('grant_type', 'scope'): if param in request.duplicate_params: raise errors.InvalidRequestError( 'Duplicate %s parameter.' % param, request=request) # Since the JSON Web Token is signed by its issuer client # authentication is not strictly required when the token is used as # an authorization grant. However, if client credentials are provided # they should be validated as describe in Section 3.1. # https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12#section-3.1 if self.request_validator.client_authentication_required(request): log.debug('Authenticating client, %r.', request) if not self.request_validator.authenticate_client(request): log.debug('Invalid client (%r), denying access.', request) raise errors.InvalidClientError(request=request) # REQUIRED. The web token issued by the client. log.debug('Validating assertion %s.', request.assertion) if not self.request_validator.validate_bearer_token( request.assertion, request.scopes, request): log.debug('Invalid assertion, %s, for client %r.', request.assertion, request.client) raise errors.InvalidGrantError('Invalid assertion.', request=request) original_scopes = utils.scope_to_list( self.request_validator.get_original_scopes( request.assertion, request)) if request.scope: request.scopes = utils.scope_to_list(request.scope) if (not all((s in original_scopes for s in request.scopes)) and not self.request_validator.is_within_original_scope( request.scopes, request.refresh_token, request)): log.debug('Refresh token %s lack requested scopes, %r.', request.refresh_token, request.scopes) raise errors.InvalidScopeError(request=request) else: request.scopes = original_scopes
def post(self, request, *args, **kwargs): grant_type = request.POST.get('grant_type', 'password') username = request.POST.get('username') # pre-validate scopes requested client_id = request.POST.get('client_id', None) requested_scopes = [s for s in scope_to_list(request.POST.get('scope', '')) if s] if client_id: try: oauth_app = Application.objects.get(client_id=client_id) except Application.DoesNotExist: return HttpResponse(json.dumps({"error": "invalid client_id"}), status=HTTP_400_BAD_REQUEST) try: allowed_scopes = oauth_app.applicationinfo.scope_list except ApplicationInfo.DoesNotExist: allowed_scopes = ['r:profile'] # handle rw:issuer:* scopes if 'rw:issuer:*' in allowed_scopes: issuer_scopes = filter(lambda x: x.startswith(r'rw:issuer:'), requested_scopes) allowed_scopes.extend(issuer_scopes) filtered_scopes = set(allowed_scopes) & set(requested_scopes) if len(filtered_scopes) < len(requested_scopes): return HttpResponse(json.dumps({"error": "invalid scope requested"}), status=HTTP_400_BAD_REQUEST) # let parent method do actual authentication response = super(TokenView, self).post(request, *args, **kwargs) if grant_type == "password" and response.status_code == 401: badgrlogger.event(badgrlog.FailedLoginAttempt(request, username, endpoint='/o/token')) return response
def __parse_token_response(self, body, scope=None): """Parse the JSON token response body into a dict. """ try: params = json.loads(body) except ValueError: # Fall back to URL-encoded string, to support old implementations, # including (at time of writing) Facebook. See: # https://github.com/idan/oauthlib/issues/267 params = dict(urlparse.parse_qsl(body)) for key in ('expires_in', 'expires'): if key in params: # cast a couple things to int params[key] = int(params[key]) if 'scope' in params: params['scope'] = utils.scope_to_list(params['scope']) if 'expires' in params: params['expires_in'] = params.pop('expires') if 'expires_in' in params: params['expires_at'] = time() + int(params['expires_in']) params = tokens.OAuth2Token(params, old_scope=scope) self.__validate_token_parameters(params) return params
def create_token_response(self, uri, http_method='POST', body=None, headers=None, credentials=None, grant_type_for_scope=None, claims=None): """Extract grant_type and route to the designated handler.""" request = Request( uri, http_method=http_method, body=body, headers=headers) self.validate_token_request(request) # 'scope' is an allowed Token Request param in both the "Resource Owner Password Credentials Grant" # and "Client Credentials Grant" flows # https://tools.ietf.org/html/rfc6749#section-4.3.2 # https://tools.ietf.org/html/rfc6749#section-4.4.2 request.scopes = utils.scope_to_list(request.scope) request.extra_credentials = credentials if grant_type_for_scope: request.grant_type = grant_type_for_scope # OpenID Connect claims, if provided. The server using oauthlib might choose # to implement the claims parameter of the Authorization Request. In this case # it should retrieve those claims and pass them via the claims argument here, # as a dict. if claims: request.claims = claims grant_type_handler = self.grant_types.get(request.grant_type, self.default_grant_type_handler) log.debug('Dispatching grant_type %s request to %r.', request.grant_type, grant_type_handler) return grant_type_handler.create_token_response( request, self.default_token_type)
def get_authorization_code_scopes(self, client_id, code, redirect_uri, request): scopes = Grant.objects.filter(code=code).values_list( "scope", flat=True).first() if scopes: return utils.scope_to_list(scopes) return []
def create_token_response(self, uri, http_method='GET', body=None, headers=None, credentials=None, grant_type_for_scope=None, claims=None): """Extract grant_type and route to the designated handler.""" request = Request( uri, http_method=http_method, body=body, headers=headers) # 'scope' is an allowed Token Request param in both the "Resource Owner Password Credentials Grant" # and "Client Credentials Grant" flows # https://tools.ietf.org/html/rfc6749#section-4.3.2 # https://tools.ietf.org/html/rfc6749#section-4.4.2 request.scopes = utils.scope_to_list(request.scope) request.extra_credentials = credentials if grant_type_for_scope: request.grant_type = grant_type_for_scope # OpenID Connect claims, if provided. The server using oauthlib might choose # to implement the claims parameter of the Authorization Request. In this case # it should retrieve those claims and pass them via the claims argument here, # as a dict. if claims: request.claims = claims grant_type_handler = self.grant_types.get(request.grant_type, self.default_grant_type_handler) log.debug('Dispatching grant_type %s request to %r.', request.grant_type, grant_type_handler) return grant_type_handler.create_token_response( request, self.default_token_type)
def authorize(request): uri, http_method, body, headers = extract_params(request) try: login_req, two_factor = should_show_login_form(request) if login_req: return validate_and_redirect_to_login(request, two_factor, uri, http_method, body, headers) else: oauth_request = Request(uri, http_method=http_method, body=body, headers=headers) scopes = scope_to_list(oauth_request.scope) credentials = { 'user': request.user, 'session_state': get_oidc_session_state(request), } headers, _, _ = oidc_server.create_authorization_response( uri, http_method, body, headers, scopes, credentials) return HttpOAuth2ResponseRedirect(headers['Location']) except oauth2.FatalClientError as e: logger.warning(f'Fatal client error, redirecting to error page. {e}') error_uri = reverse('oauth2:oauth2_error') return HttpResponseRedirect(e.in_uri(error_uri)) except oauth2.OAuth2Error as e: # Less grave errors will be reported back to client logger.warning(f'OAuth2Error, redirecting to error page. {e}') redirect_uri = get_request_param(request, 'redirect_uri', reverse('oauth2:oauth2_error')) return HttpResponseRedirect(e.in_uri(redirect_uri))
def test_scope_to_list(self): expected = ['foo', 'bar', 'baz'] string_scopes = 'foo bar baz ' self.assertEqual(scope_to_list(string_scopes), expected) string_list_scopes = ['foo', 'bar', 'baz'] self.assertEqual(scope_to_list(string_list_scopes), expected) tuple_list_scopes = ('foo', 'bar', 'baz') self.assertEqual(scope_to_list(tuple_list_scopes), expected) obj_list_scopes = [ScopeObject('foo'), ScopeObject('bar'), ScopeObject('baz')] self.assertEqual(scope_to_list(obj_list_scopes), expected) set_list_scopes = set(string_list_scopes) set_list = scope_to_list(set_list_scopes) self.assertEqual(sorted(set_list), sorted(string_list_scopes))
def validate_authorization_request(self, uri, http_method='GET', body=None, headers=None): """Extract response_type and route to the designated handler.""" request = Request( uri, http_method=http_method, body=body, headers=headers) request.scopes = utils.scope_to_list(request.scope) response_type_handler = self.response_types.get( request.response_type, self.default_response_type_handler) return response_type_handler.validate_authorization_request(request)
def get_authorization_code_scopes(self, client_id, code, redirect_uri, request): scopes = [] fields = { "code": code, } if client_id: fields["application__client_id"] = client_id if redirect_uri: fields["redirect_uri"] = redirect_uri grant = Grant.objects.filter(**fields).values() if grant.exists(): grant_dict = dict(grant[0]) scopes = utils.scope_to_list(grant_dict["scope"]) return scopes
def validate_offline_access(self, request, user, client, by_scope=BY_SCOPE): """Ensure client is authorized for offline access to resources. Client credentials grant type applications are automatically authorized for offline access since the client is the resource owner. For all other grant types: The existence of refresh tokens granted to the client for the resource owner creates an implicit offline access authorization. Trusted clients can be granted explicit offline access by_scope and either setting the client's 'skip_authorization' attribute to True or adding the client app owner to the TRUSTED_APP_GROUP. """ if client.authorization_grant_type == GRANT_CLIENT_CREDENTIALS: valid_offline_auth = True else: refresh_tokens = RefreshToken.objects.filter(user=user, application=client) if refresh_tokens: request.refresh_tokens = refresh_tokens request.original_scopes = set( scope for token in refresh_tokens for scope in utils.scope_to_list(token.access_token.scope)) valid_offline_auth = True elif by_scope: client_user_groups = client.user.groups.values_list("name", flat=True) skip_auth = getattr(client, 'skip_authorization', False) available_scopes = get_scopes_backend().get_available_scopes( application=client, request=request) if TRUSTED_APP_GROUP in client_user_groups or skip_auth: valid_offline_auth = any('offline' in scope for scope in available_scopes) else: valid_offline_auth = False else: valid_offline_auth = False return valid_offline_auth
def authorize(): """ First endpoint used in authentication flow Example of request received: GET /authorize ?response_type=code &client_id=CLIENT_ID &redirect_uri=given_by_the_client &scope=openid%20profile &state=OPAQUE_VALUE """ uri, http_method, body, headers = extract_params(request) try: scopes, credentials = server.validate_authorization_request( uri, http_method, body, headers) session["credentials"] = { k: credentials["request"]._params[k] for k in ["audience", "client_id", "redirect_uri", "response_type"] } session["scopes"] = scope_to_list(scopes) msg = "Redirecting to twitter for authorization" current_app.logger.info(msg) return redirect(url_for("auth.authorize_twitter")) except errors.FatalClientError as err: current_app.logger.debug(err) raise err except errors.OAuth2Error as err: msg = "{} {}\nbody:\n{}\nheaders:\n{}".format(http_method, uri, json.dumps(body), headers) current_app.logger.debug(msg) return redirect(err.in_uri(err.redirect_uri))
def post(self, request, *args, **kwargs): _backoff_period = getattr(settings, 'TOKEN_BACKOFF_PERIOD_SECONDS', 2) _max_backoff = getattr(settings, 'TOKEN_BACKOFF_MAXIMUM_SECONDS', 3600) # max is 1 hour grant_type = request.POST.get('grant_type', 'password') username = request.POST.get('username') client_ip = client_ip_from_request(request) if grant_type == 'password' and _backoff_period is not None: # check for existing backoff for password attempts backoff = cache.get(backoff_cache_key(username, client_ip)) if backoff is not None: backoff_until = backoff.get('until', None) backoff_count = backoff.get('count', 1) if backoff_until > timezone.now(): backoff_count += 1 backoff_seconds = min(_max_backoff, _backoff_period ** backoff_count) backoff_until = timezone.now() + datetime.timedelta(seconds=backoff_seconds) cache.set(backoff_cache_key(username, client_ip), dict(until=backoff_until, count=backoff_count), timeout=None) # return the same error as a failed login attempt return HttpResponse(json.dumps({ "error_description": "Too many login attempts. Please wait and try again.", "error": "login attempts throttled", "expires": backoff_seconds, }), status=HTTP_401_UNAUTHORIZED) # pre-validate scopes requested client_id = request.POST.get('client_id', None) requested_scopes = [s for s in scope_to_list(request.POST.get('scope', '')) if s] if client_id: try: oauth_app = Application.objects.get(client_id=client_id) except Application.DoesNotExist: return HttpResponse(json.dumps({"error": "invalid client_id"}), status=HTTP_400_BAD_REQUEST) try: allowed_scopes = oauth_app.applicationinfo.scope_list except ApplicationInfo.DoesNotExist: allowed_scopes = ['r:profile'] # handle rw:issuer:* scopes if 'rw:issuer:*' in allowed_scopes: issuer_scopes = filter(lambda x: x.startswith(r'rw:issuer:'), requested_scopes) allowed_scopes.extend(issuer_scopes) filtered_scopes = set(allowed_scopes) & set(requested_scopes) if len(filtered_scopes) < len(requested_scopes): return HttpResponse(json.dumps({"error": "invalid scope requested"}), status=HTTP_400_BAD_REQUEST) # let parent method do actual authentication response = super(TokenView, self).post(request, *args, **kwargs) if grant_type == "password" and response.status_code == 401: # failed password login attempt username = request.POST.get('username', None) badgrlogger.event(badgrlog.FailedLoginAttempt(request, username, endpoint='/o/token')) if _backoff_period is not None: # update backoff for failed logins backoff = cache.get(backoff_cache_key(username, client_ip)) if backoff is None: backoff = {'count': 0} backoff['count'] += 1 backoff['until'] = timezone.now() + datetime.timedelta(seconds=_backoff_period ** backoff['count']) cache.set(backoff_cache_key(username, client_ip), backoff, timeout=None) elif response.status_code == 200: # successful login cache.set(backoff_cache_key(username, client_ip), None) # clear any existing backoff return response
def default_scopes(self): return utils.scope_to_list(self.default_scope)
def post(self, request, *args, **kwargs): def _request_identity(request): username = request.POST.get('username', None) if username: return username return client_ip_from_request(self.request) def _backoff_cache_key(request): return "failed_token_backoff_{}".format(_request_identity(request)) _backoff_period = getattr(settings, 'TOKEN_BACKOFF_PERIOD_SECONDS', 2) grant_type = request.POST.get('grant_type', 'password') if grant_type == 'password' and _backoff_period is not None: # check for existing backoff for password attempts backoff = cache.get(_backoff_cache_key(request)) if backoff is not None: backoff_until = backoff.get('until', None) backoff_count = backoff.get('count', 1) if backoff_until > timezone.now(): backoff_count += 1 backoff_seconds = min( 86400, _backoff_period** backoff_count) # maximum backoff is 24 hours backoff_until = timezone.now() + datetime.timedelta( seconds=backoff_seconds) cache.set(_backoff_cache_key(request), dict(until=backoff_until, count=backoff_count), timeout=None) # return the same error as a failed login attempt return HttpResponse(json.dumps({ "error_description": "Invalid credentials given.", "error": "invalid_grant" }), status=HTTP_401_UNAUTHORIZED) # pre-validate scopes requested client_id = request.POST.get('client_id', None) requested_scopes = [ s for s in scope_to_list(request.POST.get('scope', '')) if s ] if client_id: try: oauth_app = Application.objects.get(client_id=client_id) except Application.DoesNotExist: return HttpResponse(json.dumps({"error": "invalid client_id"}), status=HTTP_400_BAD_REQUEST) try: allowed_scopes = oauth_app.applicationinfo.scope_list except ApplicationInfo.DoesNotExist: allowed_scopes = ['r:profile'] # handle rw:issuer:* scopes if 'rw:issuer:*' in allowed_scopes: issuer_scopes = [ x for x in requested_scopes if x.startswith(r'rw:issuer:') ] allowed_scopes.extend(issuer_scopes) filtered_scopes = set(allowed_scopes) & set(requested_scopes) if len(filtered_scopes) < len(requested_scopes): return HttpResponse(json.dumps( {"error": "invalid scope requested"}), status=HTTP_400_BAD_REQUEST) # let parent method do actual authentication response = super(TokenView, self).post(request, *args, **kwargs) if grant_type == "password" and response.status_code == 401: # failed password login attempt username = request.POST.get('username', None) badgrlogger.event( badgrlog.FailedLoginAttempt(request, username, endpoint='/o/token')) if _backoff_period is not None: # update backoff for failed logins backoff = cache.get(_backoff_cache_key(request)) if backoff is None: backoff = {'count': 0} backoff['count'] += 1 backoff['until'] = timezone.now() + datetime.timedelta( seconds=_backoff_period**backoff['count']) cache.set(_backoff_cache_key(request), backoff, timeout=None) elif response.status_code == 200: # successful login cache.set(_backoff_cache_key(request), None) # clear any existing backoff return response
def post(self, request, *args, **kwargs): if len(request.GET): return HttpResponse(json.dumps({ "error": "Token grant parameters must be sent in post body, not query parameters" }), status=HTTP_400_BAD_REQUEST) grant_type = request.POST.get('grant_type', 'password') username = request.POST.get('username') client_id = None try: auth_header = request.META['HTTP_AUTHORIZATION'] credentials = auth_header.split(' ') if credentials[0] == 'Basic': client_id, client_secret = base64.b64decode( credentials[1].encode('ascii')).decode('ascii').split(':') except (KeyError, IndexError, ValueError, TypeError): client_id = request.POST.get('client_id', None) client_secret = None # pre-validate scopes requested requested_scopes = [ s for s in scope_to_list(request.POST.get('scope', '')) if s ] oauth_app = None if client_id: try: oauth_app = Application.objects.get(client_id=client_id) if client_secret and oauth_app.client_secret != client_secret: return HttpResponse(json.dumps( {"error": "invalid client_secret"}), status=HTTP_400_BAD_REQUEST) except Application.DoesNotExist: return HttpResponse(json.dumps({"error": "invalid client_id"}), status=HTTP_400_BAD_REQUEST) try: allowed_scopes = oauth_app.applicationinfo.scope_list except ApplicationInfo.DoesNotExist: allowed_scopes = ['r:profile'] # handle rw:issuer:* scopes if 'rw:issuer:*' in allowed_scopes: issuer_scopes = [ x for x in requested_scopes if x.startswith(r'rw:issuer:') ] allowed_scopes.extend(issuer_scopes) filtered_scopes = set(allowed_scopes) & set(requested_scopes) if len(filtered_scopes) < len(requested_scopes): return HttpResponse(json.dumps( {"error": "invalid scope requested"}), status=HTTP_400_BAD_REQUEST) # let parent method do actual authentication response = super(TokenView, self).post(request, *args, **kwargs) if oauth_app and not oauth_app.applicationinfo.issue_refresh_token: data = json.loads(response.content) try: del data['refresh_token'] except KeyError: pass response.content = json.dumps(data) if grant_type == "password" and response.status_code == 401: badgrlogger.event( badgrlog.FailedLoginAttempt(request, username, endpoint='/o/token')) return response
def scopes(self): return utils.scope_to_list(self.scope)