Пример #1
0
 def get_auth_code_from_callback(self, request):
     """
     This function processes the callback from the OAuth2 provider server; in particular, it gets the
     authorization code out of the request and checks the state parameter as well, if applicable.
     :param request: the request object made by the 3rd party OAuth2 provider server.
     :return:
     """
     # first, check for the state parameter and, if passed, compare it to the state in the session
     logger.debug(
         f"top of get_auth_code_from_callback; request.args: {request.args}; request: {request}"
     )
     req_state = request.args.get('state')
     if req_state:
         state = session.get('state')
         if not state == req_state:
             logger.error(
                 f"ERROR! state stored in the session ({state}) did not match the state passed in"
                 f"the callback ({req_state}")
             raise errors.ServiceConfigError(
                 "Error processing provider callback -- state mismatch.")
     req_code = request.args.get('code')
     if not req_code:
         logger.error(
             f"ERROR! did not receive an authorization code in the callback."
         )
         raise errors.ServiceConfigError(
             "Error processing provider callback -- code missing.")
     self.authorization_code = req_code
Пример #2
0
 def get_token_using_auth_code(self):
     """
     Exchange the authorization code for an access token from the provider server.
     :return:
     """
     logger.debug("top of get_token_using_auth_code")
     # todo -- it is possible these body parameters will need to change for different oauth2 servers
     # note -- this function is not called by CII because it does non-standard OAuth.
     body = {
         "client_id": self.client_id,
         "client_secret": self.client_key,
         "code": self.authorization_code,
         "redirect_uri": self.callback_url
     }
     logger.debug(
         f"making POST to token url {self.oauth2_token_url}...; body: {body}"
     )
     try:
         rsp = requests.post(self.oauth2_token_url,
                             data=body,
                             headers={'Accept': 'application/json'})
     except Exception as e:
         logger.error(
             f"Got exception from POST request to OAuth server attempting to exchange the"
             f"authorization code for a token. Debug data:"
             f"request body: {body}"
             f"exception: {e}")
         raise errors.ServiceConfigError(
             "Error requesting access token. Contact server administrator.")
     logger.debug(
         f"successfully made POST to token url {self.oauth2_token_url}; rsp: {rsp};"
         f"rsp.content: {rsp.content}")
     # todo -- it is possible different provider servers will not pass JSON
     try:
         self.access_token = rsp.json().get('access_token')
     except Exception as e:
         logger.error(
             f"Got exception trying to process response from POST request to exchange the"
             f"authorization code for a token. Debug data:"
             f"request body: {body};"
             f"response: {rsp}"
             f"exception: {e}")
         raise errors.ServiceConfigError(
             "Error parsing access token. Contact server administrator.")
     logger.debug(f"successfully got access_token: {self.access_token}")
     return self.access_token
Пример #3
0
 def get_config(self, tenant_id):
     """
     Returns the config for a specific tenant from the cache.
     :param tenant_id:
     :return:
     """
     logger.debug(f"top of get_config for tenant: {tenant_id}")
     tries = 0
     # first, check if the cache is older than the configured max cache lifetime.
     if datetime.datetime.now() > self.last_update + self.cache_lifetime:
         self.load_tenant_config_cache()
         # if we just reloaded the cache, we don't need the check below
         tries = 1
     while tries < 2:
         for t in self.tenant_config_models:
             if t.tenant_id == tenant_id:
                 return t
         # the first pass through, if we didn't find the tenant_id, reload the cache and try again
         if tries==0:
             self.load_tenant_config_cache()
             tries = 1
             continue
         tries = 2
     raise errors.ServiceConfigError(f"tenant id {tenant_id} not found in tenant configurations.")
Пример #4
0
    def __init__(self, tenant_id, is_local_development=False):
        # the tenant id for this OAuth2 provider
        self.tenant_id = tenant_id
        # the custom tenant config for this tenant
        self.tenant_config = tenant_configs_cache.get_config(
            tenant_id=tenant_id)
        # the type of OAuth2 provider, such as github
        self.ext_type = tenant_configs_cache.get_custom_oa2_extension_type(
            tenant_id)
        # the actual custom_idp_configuration object, as a python dictionary
        self.custom_idp_config_dict = json.loads(
            self.tenant_config.custom_idp_configuration)
        # whether or not this authenticator is running in local development mode (i.e., on localhost)
        self.is_local_development = is_local_development
        # validate that this tenant should be using the OAuth2 extension module.
        if not self.ext_type:
            raise errors.ResourceError(
                f"Tenant {tenant_id} not configured for a custom OAuth2 extension."
            )
        tenant_base_url = t.tenant_cache.get_tenant_config(tenant_id).base_url
        if self.is_local_development:
            self.callback_url = f'http://localhost:5000/v3/oauth2/extensions/oa2/callback'
        else:
            self.callback_url = f'{tenant_base_url}/v3/oauth2/extensions/oa2/callback'
        # These attributes get computed later, as a result of the OAuth flow ----------
        self.authorization_code = None
        self.access_token = None
        self.username = None

        # Custom configs for each provider ---------------
        if self.ext_type == 'github':
            # github calls the client_key the "client_secret"
            self.client_id = self.custom_idp_config_dict.get('github').get(
                'client_id')
            self.client_key = self.custom_idp_config_dict.get('github').get(
                'client_secret')
            # initial redirect URL; used to start the oauth flow and log in the user
            self.identity_redirect_url = 'https://github.com/login/oauth/authorize'
            # URL to use to exchange the code for an qccess token
            self.oauth2_token_url = 'https://github.com/login/oauth/access_token'
        elif self.ext_type == 'cii':
            # we configure the CII redirect URL directly in the config because there are different CII environments.
            self.identity_redirect_url = self.custom_idp_config_dict.get(
                'cii').get('login_url')
            if not self.identity_redirect_url:
                raise errors.ServiceConfigError(
                    f"Missing required cii config, identity_redirect_url. "
                    f"Config: {self.custom_idp_config_dict}")
            self.jwt_decode_key = self.custom_idp_config_dict.get('cii').get(
                'jwt_decode_key')
            if not self.jwt_decode_key:
                raise errors.ServiceConfigError(
                    f"Missing required cii config, jwt_decode_key. "
                    f"Config: {self.custom_idp_config_dict}")
            self.check_jwt_signature = self.custom_idp_config_dict.get(
                'check_jwt_signature')
            # note that CII does not implement standard OAuth2; they do not require a client id and key and they do not
            # create an authorization code to be exchanged for a token.
            self.client_id = 'not_used'
            self.client_key = 'not_used'
        #
        # NOTE: each provider type must implement this check
        # elif self.ext_type == 'google'
        #     ...
        else:
            logger.error(
                f"ERROR! OAuth2ProviderExtension constructor not implemented for OAuth2 provider "
                f"extension {self.ext_type}.")
            raise errors.ServiceConfigError(
                f"Error processing callback URL: extension type {self.ext_type} not "
                f"supported.")
Пример #5
0
 def get_user_from_token(self):
     """
     Determines the username for the user once an access token has been obtained.
     :return:
     """
     logger.debug("top of get_user_from_token")
     # todo -- each OAuth2 provider will have a different mechanism for determining the user's identity
     if self.ext_type == 'github':
         user_info_url = 'https://api.github.com/user'
         headers = {
             'Authorization': f'token {self.access_token}',
             'Accept': 'application/vnd.github.v3+json'
         }
         try:
             rsp = requests.get(user_info_url, headers=headers)
         except Exception as e:
             logger.error(
                 f"Got exception from request to look up user's identity with github. Debug data:"
                 f"exception: {e}")
             raise errors.ServiceConfigError(
                 "Error determining user identity. Contact server administrator."
             )
         if not rsp.status_code == 200:
             logger.error(
                 "Did not get 200 from request to look up user's identity with github. Debug data:"
                 f"status code: {rsp.status_code};"
                 f"rsp content: {rsp.content}")
             raise errors.ServiceConfigError(
                 "Error determining user identity. Contact server administrator."
             )
         username = rsp.json().get('login')
         if not username:
             logger.error(
                 f"username was none after processing the github response. Debug data:"
                 f"response: {rsp}")
             raise errors.ServiceConfigError(
                 "Error determining user identity: username was empty. "
                 "Contact server administrator.")
         self.username = f'{username}@github.com'
         logger.debug(
             f"Successfully determined user's identity: {self.username}")
         return self.username
     elif self.ext_type == 'cii':
         # the CII token is a JWT; we only need to decode it and get the username out of the payload.
         # todo -- we should verify the signature if that is working...
         try:
             claims = jwt.decode(self.access_token,
                                 self.jwt_decode_key,
                                 verify=self.check_jwt_signature)
         except Exception as e:
             msg = f"got exception trying to decode the CII jwt; exception: {e}"
             logger.error(msg)
             raise errors.ResourceError(
                 f"Unable to decode third-party JWT. Contact system administrator."
                 f"(Debug message:{msg})")
         self.username = claims.get('username')
         if not self.username:
             msg = f"Did not get a username from the CII jwt; full claims: {claims}"
             logger.error(msg)
             raise errors.ResourceError(
                 f"Unable to determine username from third-party JWT. Contact system "
                 f"administrator."
                 f"(Debug message:{msg})")
         logger.debug(
             f"Successfully determined user's identity: {self.username}")
         return self.username
     # elif self.ext_type == 'google':
     #     ...
     else:
         logger.error(
             f"ERROR! OAuth2ProviderExtension.get_user_from_token not implemented for OAuth2 provider "
             f"extension {self.ext_type}.")
         raise errors.ServiceConfigError(
             f"Error determining user identity: extension type {self.ext_type} not "
             f"supported.")
Пример #6
0
def authentication():
    """
    Entry point for checking authentication for all requests to the authenticator.
    :return:
    """
    # The authenticator uses different authentication methods for different endpoints. For example, the service
    # APIs such as clients and profiles use pure JWT authentication, while the OAuth endpoints use Basic Authentication
    # with OAuth client credentials.
    logger.debug(f"base_url: {request.base_url}; url_rule: {request.url_rule}")
    if not hasattr(request, 'url_rule') or not hasattr(
            request.url_rule, 'rule') or not request.url_rule.rule:
        raise common_errors.ResourceError(
            "The endpoint and HTTP method combination "
            "are not available from this service.")

    # the metadata endpoint is publicly available
    if '/v3/oauth2/.well-known/' in request.url_rule.rule:
        logger.debug(
            ".well-known endpoint; request is allowed to be made unauthenticated."
        )
        auth.resolve_tenant_id_for_request()
        return True
    # only the authenticator's own service token and tenant admins for the tenant can retrieve or modify the tenant
    # config
    if '/v3/oauth2/admin' in request.url_rule.rule:
        logger.debug(
            "admin endpoint; checking for authentictor service token or tenant admin role..."
        )
        # admin endpoints always require tapis token auth
        auth.authentication()
        # we'll need to use the request's tenant_id, so make sure it is resolved now
        auth.resolve_tenant_id_for_request()
        # first, make sure this request is for a tenant served by this authenticator
        if g.request_tenant_id not in conf.tenants:
            raise common_errors.PermissionsError(
                f"The request is for a tenant ({g.request_tenant_id}) that is not "
                f"served by this authenticator.")
        # we only want to honor tokens from THIS authenticator; i.e., not some other authenticator. therefore, we need
        # to check that the tenant_id associated with the token (g.tenant_id) is the same as THIS authenticator's tenant
        # id;
        if g.username == conf.service_name and g.tenant_id == conf.service_tenant_id:
            logger.info(
                f"allowing admin request because username was {conf.service_name} "
                f"and tenant was {conf.service_tenant_id}")
            return True
        logger.debug(
            f"request token does not represent THIS authenticator: token username: {g.username};"
            f" request tenant: {g.tenant_id}. Now checking for tenant admin..."
        )
        # all other service accounts are not allowed to update authenticator
        if g.account_type == 'service':
            raise common_errors.PermissionsError(
                "Not authorized -- service accounts are not allowed to access the"
                "authenticator admin endpoints.")
        # sanity check -- the request tenant id should be the same as the token tenant id in the remaining cases because
        # they are all user tokens
        if not g.request_tenant_id == g.tenant_id:
            logger.error(
                f"program error -- g.request_tenant_id: {g.request_tenant_id} not equal to "
                f"g.tenant_id: {g.tenant_id} even though account type was user!"
            )
            raise common_errors.ServiceConfigError(
                f"Unexpected program error checking permissions. The tenant id of"
                f"the request ({g.request_tenant_id})  did not match the tenant id "
                f"of the access token ({g.tenant_id}). Please contact server "
                f"administrators.")
        # check SK for tenant admin --
        try:
            rsp = t.sk.isAdmin(tenant=g.tenant_id, user=g.username)
        except Exception as e:
            logger.error(
                f"Got exception trying to check tenant admin role for tenant: {g.tenant_id} "
                f"and user: {g.username}; exception: {e}")
            raise common_errors.PermissionsError(
                "Could not check tenant admin role with SK; this role is required for "
                "accessing the authenticator admin endpoints.")
        try:
            if rsp.isAuthorized:
                logger.info(
                    f"user {g.username} had tenant admin role for tenant {g.tenant_id}; allowing request."
                )
                return True
            else:
                logger.info(
                    f"user {g.username} DID NOT have tenant admin role for tenant {g.tenant_id}; "
                    f"NOT allowing request.")
                raise common_errors.PermissionsError(
                    "Permission denied -- Tenant admin role required for accessing "
                    "the authenticator admin endpoints.")
        except Exception as e:
            logger.error(
                f"got exception trying to check isAuthorized property from isAdmin() call to SK."
                f"username: {g.username}; tenant: {g.tenant_id}; rsp: {rsp}; e: {e}"
            )
            logger.info(
                f"user {g.username} DID NOT have tenant admin role for tenant {g.tenant_id}; "
                f"NOT allowing request.")
            raise common_errors.PermissionsError(
                "Permission denied -- Tenant admin role required for accessing the "
                "authenticator admin endpoints.")

    # no credentials required on the authorize, login and oa2 extenion pages
    if '/v3/oauth2/authorize' in request.url_rule.rule or '/v3/oauth2/login' in request.url_rule.rule \
            or '/oauth2/extensions' in request.url_rule.rule:
        # always resolve the request tenant id based on the URL:
        logger.debug(
            "authorize, login or oa2 extension page. Resolving tenant_id")
        auth.resolve_tenant_id_for_request()
        try:
            logger.debug(f"request_tenant_id: {g.request_tenant_id}")
        except AttributeError:
            raise common_errors.BaseTapisError(
                "Unable to resolve tenant_id for request.")
        return True

    # the profiles endpoints always use standard Tapis Token auth -
    if '/v3/oauth2/profiles' in request.url_rule.rule or \
            '/v3/oauth2/userinfo' in request.url_rule.rule:
        auth.authentication()
        # always resolve the request tenant id based on the URL:
        auth.resolve_tenant_id_for_request()
        return True

    # the clients endpoints need to accept both standard Tapis Token auth and basic auth,
    if '/v3/oauth2/clients' in request.url_rule.rule:
        # first check for basic auth header:
        parts = get_basic_auth_parts()
        if parts:
            logger.debug("oauth2 clients page, with basic auth header.")
            # do basic auth against the ldap
            # always resolve the request tenant id based on the URL:
            auth.resolve_tenant_id_for_request()
            try:
                logger.debug(f"request_tenant_id: {g.request_tenant_id}")
            except AttributeError:
                raise common_errors.BaseTapisError(
                    "Unable to resolve tenant_id for request.")
            check_username_password(parts['tenant_id'], parts['username'],
                                    parts['password'])
            return True
        else:
            logger.debug("oauth2 clients page, no basic auth header.")
            # check for a Tapis token
            auth.authentication()
            # always resolve the request tenant id based on the URL:
            auth.resolve_tenant_id_for_request()
            try:
                logger.debug(f"request_tenant_id: {g.request_tenant_id}")
            except AttributeError:
                raise common_errors.BaseTapisError(
                    "Unable to resolve tenant_id for request.")
            return True

    if '/v3/oauth2/tokens' in request.url_rule.rule:
        logger.debug("oauth2 tokens URL")
        # the tokens endpoint uses basic auth with the client; logic handled in the controller. # however, it does
        # require the request tenant id:

        # first, check if an X-Tapis-Token header appears in the request. We do not honor JWT authentication for
        # generating new tokens, but we also don't want to fail for an expired token. So, we remove the token header
        # if it
        if 'X-Tapis-Token' in request.headers:
            logger.debug("Got an X-Tapis-Token header.")
            try:
                auth.add_headers()
                auth.validate_request_token()
            except:
                # we need to set the token claims because the resolve_tenant_id_for_request method depends on it:
                g.token_claims = {}
        # now, resolve the tenant_id
        try:
            auth.resolve_tenant_id_for_request()
        except:
            # we need to catch and swallow permissions errors having to do with an invalid JWT; if the JWT is invalid,
            # its claims (including its tenant claim) will be ignored, but then resolve_tenant_id_for_request() will
            # throw an error because the None tenant_id claim will not match the tenant_id of the URL.
            pass
        try:
            logger.debug(f"request_tenant_id: {g.request_tenant_id}")
        except AttributeError:
            raise common_errors.BaseTapisError(
                "Unable to resolve tenant_id for request.")
        return True

    if '/v3/oauth2/logout' in request.url_rule.rule \
        or '/v3/oauth2/login' in request.url_rule.rule \
        or '/v3/oauth2/tenant' in request.url_rule.rule \
        or '/v3/oauth2/webapp' in request.url_rule.rule \
        or '/v3/oauth2/portal-login' in request.url_rule.rule:
        # or '/v3/oauth2/webapp/callback' in request.url_rule.rule \
        # or '/v3/oauth2/webapp/token-display' in request.url_rule.rule \
        logger.debug("call is for some token webapp page.")
        auth.resolve_tenant_id_for_request()
        try:
            logger.debug(f"request_tenant_id: {g.request_tenant_id}")
        except AttributeError:
            raise common_errors.BaseTapisError(
                "Unable to resolve tenant_id for request.")
        #  make sure this tenant allows the token web app
        config = tenant_configs_cache.get_config(g.request_tenant_id)
        logger.debug(f"got tenant config: {config.serialize}")
        if not config.use_token_webapp:
            logger.info(
                f"tenant {g.request_tenant_id} not configured for the token webapp. Raising error"
            )
            raise common_errors.PermissionsError(
                "This tenant is not configured to use the Token Webapp.")

        return True
Пример #7
0
def validate_config():
    """
    Validates the config provided in the config.json file.
    """
    print(f"top of validate_config; found this config: {conf}")
    # we must be running as the Tokens API or this is not going to work.
    if not conf.service_name == 'tokens':
        raise errors.ServiceConfigError(
            f"Invalid config: conf.service_name must be 'tokens' not {conf.service_name}."
            f"This program must run as the Tokens API to be able to interact with SK."
        )
    # this keys management program leverages the Tokens API code for various calls
    if conf.use_sk:
        raise errors.ServiceConfigError(
            f"Invalid config: conf.use_sk must be False so that the tokens code running for "
            f"this program does not try to retrieve the private keys from the SK at"
            f"start up (they may not exist yet).")
    if not conf.dev_jwt_private_key:
        raise errors.ServiceConfigError(
            f"Invalid config: conf.dev_jwt_private_key required and must be the site admin "
            f"tenant private key.")
    # check that all tenant id's are valid
    for tn in conf.tenants:
        found = False
        for valid_tenant in t.tenant_cache.tenants:
            if valid_tenant.tenant_id == tn:
                found = True
                # check that all tenants in the list are owned by the site_id
                if not valid_tenant.site_id == conf.site_id:
                    raise errors.ServiceConfigError(
                        f"Invalid tenant '{tn}' configured: tenant owned by {valid_tenant.site_id}, "
                        f"not owned by the configured site ({conf.site_id}.)")
        if not found:
            raise errors.ServiceConfigError(
                f"Invalid tenant {tn} configured: tenant not found; available "
                f"tenants: {valid_tenants}")

    if conf.running_at_primary_site:
        # first check for all required configs:
        if not hasattr(conf, 'update_associate_site'):
            raise errors.ServiceConfigError(
                "running_at_primary_site was 'true' so update_associate_site (t/f) config "
                "required.")
        if conf.update_associate_site:
            # when updating an associate site at the primary site, the associate site id is also required:
            if not hasattr(conf, 'associate_site_id'):
                raise errors.ServiceConfigError(
                    "running_at_primary_site was 'true' and 'update_associate_site' was "
                    "true so associate_site_id config required.")
            # check that there is a directory for each tenant in the list of tenants
            for tn in conf.tenants:
                tn_dir_path = os.path.join(DATA_DIR, tn)
                if not os.path.isdir(tn_dir_path):
                    raise errors.ServiceConfigError(
                        f"Did not find data directory for tenant {tn}"
                        f"Expected a directory at {tn_dir_path}.")
                # check for the existence of a public key file
                pub_key_path = os.path.join(DATA_DIR, tn, 'pub.key')
                if not os.path.isfile(pub_key_path):
                    raise errors.ServiceConfigError(
                        f"Did not find public key for tenant {tn}"
                        f"Expected a public key file at {pub_key_path}.")

    # check that we can talk to the sk and that the tokens user has the tenant_definition_updater role
    try:
        has_role = t.sk.hasRole(roleName='tenant_definition_updater',
                                user='******',
                                tenant=conf.service_tenant_id)
    except Exception as e:
        raise errors.ServiceConfigError(
            f"Got an exception checking that tokens has the tenant_definition_updater role;"
            f"exception: {e}")
    if not has_role.isAuthorized:
        raise errors.ServiceConfigError(
            f"Got FALSE checking that tokens has the tenant_definition_updater role;"
            f"has_role: {has_role}")
    print("tokens user has necessary role.")