def validate_access_token_request(self): """ Override the parent method from authlib to not fail immediately for public clients. """ client = self.authenticate_token_endpoint_client() if not client.check_grant_type(self.GRANT_TYPE): raise UnauthorizedClientError(uri=self.uri) self._authenticated_client = client refresh_token = self.params.get("refresh_token") if refresh_token is None: raise InvalidRequestError( 'Missing "refresh_token" in request.', uri=self.uri ) refresh_claims = self.authenticate_refresh_token(refresh_token) if not refresh_claims: raise InvalidRequestError( 'Invalid "refresh_token" in request.', uri=self.uri ) scope = self.params.get("scope") if scope: original_scope = refresh_claims["scope"] if not original_scope: raise InvalidScopeError(uri=self.uri) original_scope = set(scope_to_list(original_scope)) if not original_scope.issuperset(set(scope_to_list(scope))): raise InvalidScopeError(uri=self.uri) self._authenticated_token = refresh_claims
def _validate_token_scope(self, token): """ OVERRIDES method from authlib. Why? Becuase our "token" is not a class with `get_scope` method. So we just need to treat it like a dictionary. """ scope = self.request.scope if not scope: return # token is dict so just get the scope, don't use get_scope() original_scope = token.get("scope") ##### begin refresh token patch block ##### # TODO: In the next release, remove this if block # Old refresh tokens are not compatible with new validation, so to smooth # the transition, allow old style refresh tokens with this patch; # remove patch in next tag. Refresh tokens have default TTL of 30 days. if not original_scope: original_scope = token.get("aud") ##### end refresh token patch block ##### if not original_scope: raise InvalidScopeError( "No scope claim found in original refresh token.") original_scope = set(scope_to_list(original_scope)) if not original_scope.issuperset(set(scope_to_list(scope))): raise InvalidScopeError( "Cannot request scopes that were not in original refresh token." )
def scope_insufficient(self, token: AzureToken, scope: str, operator: Union[str, Callable] = 'AND') -> bool: """ Determines whether a token has sufficient scopes to interact with a resource I.e. whether the token bearer has suitable permissions to perform their intended action. This method overloads the default method in the 'BearerTokenValidator' class to make it compatible with our AzureToken class. :type token: AzureToken :param token: JSON Web Token as an Azure Token object :type scope: str :param scope: space concatenated list of scopes required to interact with the current resource :type operator: str or Callable :param operator: Strategy of validating whether token scopes meet resource scopes (i.e. all represent, at least one present) :rtype bool :return: True if the token has insufficient scopes, False if ok """ if not scope: return False token_scopes = token.scopes resource_scopes = set(scope_to_list(scope)) if operator == 'AND': return not token_scopes.issuperset(resource_scopes) if operator == 'OR': for resource_scope in resource_scopes: if resource_scope in token_scopes: return False if callable(operator): return not operator(token_scopes, resource_scopes) raise ValueError(f"Invalid operator value [{ operator }], valid options are 'AND', 'OR' or <callable>")
def generate_id_token(key, token, request, alg, iss, exp, nonce=None, auth_time=None, code=None): scopes = scope_to_list(token['scope']) # TODO: merge scopes and claims user_info = _generate_user_info(request.user, scopes) client = request.client payload = _generate_id_token_payload( alg, iss, [client.get_client_id()], exp=exp, nonce=nonce, auth_time=auth_time, code=code, access_token=token.get('access_token'), ) payload.update(user_info) return _jwt_encode(alg, payload, key)
def get_allowed_scope(self, scope): """Get allowed scope. Has been slightly modified to accommodate parametric scopes. :param str scope: requested scope :return: str -- scopes """ if not isinstance(scope, six.string_types): scope = list_to_scope(scope) allowed = scope_to_list(super(Client, self).get_allowed_scope(scope)) for s in scope_to_list(scope): for def_scope in scope_to_list(self.scope): if s.startswith(def_scope) and s not in allowed: allowed.append(s) gLogger.debug('Try to allow "%s" scope:' % scope, allowed) return list_to_scope(list(set(allowed)))
def getGroupScopes(self, group: str) -> list: """Get group scopes :param group: DIRAC group """ idPScope = getGroupOption(group, "IdPRole") return scope_to_list(idPScope) if idPScope else []
def group(self): """Search DIRAC group in scopes :return: str """ groups = [s.split(":")[1] for s in scope_to_list(self.scope or "") if s.startswith("g:") and s.split(":")[1]] return groups[0] if groups else None
async def async_generate_user_info(self, user: UserWithRoles, scope: str): scope_list = scope_to_list(scope) includes = set() for scope in scope_list: if scope not in ('openid', 'offline_access'): includes.update(config.oauth2.user.scopes[scope].properties) user_data = user.user.dict(include=includes, by_alias=True, exclude_none=True) user_data['sub'] = user.user.id user_data['roles'] = user.roles if 'picture' in user_data: user_data[ 'picture'] = f"{config.oauth2.base_url}/picture/{user_data['picture']}" if 'groups' in user_data: # Only include visible groups user_data['groups'] = [ group['_id'] async for group in async_user_group_collection.find( { '_id': { '$in': user_data['groups'] }, 'visible': True }, projection={'_id': 1}) ] return UserInfo(**user_data)
def validate_requested_scope(self, scope, state=None): """See :func:`authlib.oauth2.rfc6749.authorization_server.validate_requested_scope`""" # We also consider parametric scope containing ":" charter extended_scope = list_to_scope( [re.sub(r":.*$", ":", s) for s in scope_to_list((scope or "").replace("+", " "))] ) super(AuthServer, self).validate_requested_scope(extended_scope, state)
def get_allowed_scope(self, scope: str) -> str: """Returns the allowed scope.""" if not scope: return '' allowed = {scope.scope for scope in self.scopes} scopes = scope_to_list(scope) return list_to_scope([scope for scope in scopes if scope in allowed])
def _validate_token_scope(self, token): """ OVERRIDES method from authlib. Why? Becuase our "token" is not a class with `get_scope` method. So we just need to treat it like a dictionary. """ scope = self.request.scope if not scope: return # token is dict so just get the scope, don't use get_scope() original_scope = token.get("aud") if not original_scope: raise InvalidScopeError() original_scope = set(scope_to_list(original_scope)) if not original_scope.issuperset(set(scope_to_list(scope))): raise InvalidScopeError()
def process_implicit_token(self, token, code=None): config = self.get_jwt_config() config['nonce'] = self.request.data.get('nonce') if code is not None: config['code'] = code scopes = scope_to_list(token['scope']) user_info = self.generate_user_info(self.request.user, scopes) id_token = generate_id_token(token, self.request, user_info, **config) token['id_token'] = id_token return token
def __call__(self, client, grant_type, user=None, scope=None, expires_in=None, include_refresh_token=True): if 'offline_access' not in scope_to_list(scope): include_refresh_token = False return super(BearerToken, self).__call__(client, grant_type, user, scope, expires_in, include_refresh_token)
def getGroupScopes(self, group): """Get group scopes :param str group: DIRAC group :return: list """ idPScope = getGroupOption(group, "IdPRole") if not idPScope: idPScope = "wlcg.groups:/%s/%s" % (getVOForGroup(group), group.split("_")[1]) return S_OK(scope_to_list(idPScope))
def _getScope(self, scope, param): """Get parameter scope :param str scope: scope :param str param: parameter scope :return: str or None """ try: return [s.split(":")[1] for s in scope_to_list(scope) if s.startswith("%s:" % param) and s.split(":")[1]][0] except Exception: return None
def exchangeToken(self, group=None, scope=None): """Get new tokens for group scope :param str group: requested group :param list scope: requested scope :return: dict -- token """ scope = scope or scope_to_list(self.scope) if group: if not (groupScopes := self.getGroupScopes(group)): return S_ERROR(f"No scope found for {group}") scope = list(set(scope + groupScopes))
def getGroupScopes(self, group): """Get group scopes :param str group: DIRAC group :return: list """ idPScope = getGroupOption(group, "IdPRole") if not idPScope: idPScope = "eduperson_entitlement?value=urn:mace:egi.eu:group:%s:role=%s#aai.egi.eu" % ( getVOForGroup(group), group.split("_")[1], ) return S_OK(scope_to_list(idPScope))
def __call__(self, client: Client, grant_type: str, user: UserWithRoles, scope: str): jwt_config = self.get_jwt_config() jwt_config['aud'] = [client.get_client_id()] jwt_config['auth_time'] = int(time.time()) user_info = {'sub': user.user.id, 'roles': user.roles} if 'groups' in scope_to_list(scope): user_info['groups'] = user.user.groups return generate_id_token({}, user_info, code=generate_token( config.oauth2.access_token_length), **jwt_config)
def process_token(self, grant, token): scope = token.get('scope') if not scope or not is_openid_scope(scope): # standard authorization code flow return token request = grant.request credential = request.credential scopes = scope_to_list(token['scope']) user_info = self.generate_user_info(request.user, scopes) config = self.get_jwt_config(grant) config['nonce'] = credential.get_nonce() config['auth_time'] = credential.get_auth_time() id_token = generate_id_token(token, request, user_info, **config) token['id_token'] = id_token return token
async def create_response(self, request: TypedRequest, user_id: str) -> Response: try: assert isinstance(request, OAuth2Request) request.token = await run_in_threadpool( resource_protector.validate_request, None, request) if request.token is None: raise HTTPException(403, "Invalid token") if 'users' not in scope_to_list(request.token.scope): raise InsufficientScopeError('Missing "users" scope', request.uri) user = await UserWithRoles.async_load(user_id, request.token.client_id) if user is None: raise HTTPException(404, "User not found") user_info = await self.async_generate_user_info(user, 'users') return JSONResponse(user_info) except OAuth2Error as error: return authorization.handle_error_response(request, error)
def generate_user_info(self, user, scope): # pragma: no cover """Provide user information for the given scope. Developers MUST implement this method in subclass, e.g.:: from authlib.oidc.core import UserInfo def generate_user_info(self, user, scope): user_info = UserInfo(sub=user.id, name=user.name) if 'email' in scope: user_info['email'] = user.email return user_info :param user: user instance :param scope: scope of the token :return: ``authlib.oidc.core.UserInfo`` instance """ deprecate('Missing "OpenIDCode.generate_user_info"', '1.0', 'fjPsV', 'oi') scopes = scope_to_list(scope) return _generate_user_info(user, scopes)
async def create_response(self, request: TypedRequest) -> Response: try: assert isinstance(request, OAuth2Request) request.token = await run_in_threadpool( resource_protector.validate_request, None, request) if request.token is None: raise HTTPException(403, "Invalid token") if 'users' not in scope_to_list(request.token.scope): raise InsufficientScopeError('Missing "users" scope', request.uri) user_infos = [] async for user in UserWithRoles.async_load_all( request.token.client_id): user_info = await self.async_generate_user_info( UserWithRoles(user=user, roles=[], last_modified=user.updated_at), 'users') del user_info['roles'] user_infos.append(user_info) return JSONResponse(user_infos) except OAuth2Error as error: return authorization.handle_error_response(request, error)
def groups(self): """Search DIRAC groups in scopes :return: list """ return [s.split(":")[1] for s in scope_to_list(self.scope or "") if s.startswith("g:") and s.split(":")[1]]
def is_openid_scope(scope): scopes = scope_to_list(scope) return scopes and scopes[0] == 'openid'
def test_scope_to_list(self): self.assertEqual(util.scope_to_list('a b'), ['a', 'b']) self.assertEqual(util.scope_to_list(['a', 'b']), ['a', 'b']) self.assertIsNone(util.scope_to_list(None))
def get_allowed_scope(self, scope): if not scope: return '' allowed = set(self.scope.split()) scopes = scope_to_list(scope) return list_to_scope([s for s in scopes if s in allowed])
def is_openid_scope(scope): scopes = scope_to_list(scope) return scopes and 'openid' in scopes
return S_OK((credDict, payload)) def submitDeviceCodeAuthorizationFlow(self, group=None): """Submit authorization flow :return: S_OK(dict)/S_ERROR() -- dictionary with device code flow response """ groupScopes = [] if group: if not (groupScopes := self.getGroupScopes(group)): return S_ERROR(f"No scope found for {group}") try: r = requests.post( self.get_metadata("device_authorization_endpoint"), data=dict(client_id=self.client_id, scope=list_to_scope(scope_to_list(self.scope) + groupScopes)), verify=self.verify, ) r.raise_for_status() deviceResponse = r.json() if "error" in deviceResponse: return S_ERROR("%s: %s" % (deviceResponse["error"], deviceResponse.get("description", ""))) # Check if all main keys are present here for k in ["user_code", "device_code", "verification_uri"]: if not deviceResponse.get(k): return S_ERROR("Mandatory %s key is absent in authentication response." % k) return S_OK(deviceResponse) except requests.exceptions.Timeout: return S_ERROR("Authentication server is not answer, timeout.")
def getScopeGroups(self, scope: str) -> list: """Get DIRAC groups related to scope""" groups = [] for group in getAllGroups(): if (g_scope := self.getGroupScopes(group)) and set(g_scope).issubset(scope_to_list(scope)): groups.append(group)
def addScopes(self, scopes): """Add new scopes to query :param list scopes: scopes """ self.setQueryArguments(scope=list(set(scope_to_list(self.scope or "") + scopes)))