def _create_access_token_cookie(app, session, response, user): keypair = app.keypairs[0] scopes = config["SESSION_ALLOWED_SCOPES"] now = int(time.time()) expiration = now + config.get("ACCESS_TOKEN_EXPIRES_IN") # try to get from current session, if it's not there, we have to hit db linked_google_email = session.get("linked_google_email") if not linked_google_email: linked_google_email = get_linked_google_account_email(user.id) access_token = generate_signed_access_token( keypair.kid, keypair.private_key, user, config.get("ACCESS_TOKEN_EXPIRES_IN"), scopes, forced_exp_time=expiration, linked_google_email=linked_google_email, ).token domain = app.session_interface.get_cookie_domain(app) response.set_cookie( config["ACCESS_TOKEN_COOKIE_NAME"], access_token, expires=expiration, httponly=True, domain=domain, ) return response
def _get_users_without_access(db, auth_ids, user_emails, check_linking): """ Build list of users without access to projects identified by auth_ids Args: db (str): database instance auth_ids (list(str)): list of project auth_ids to check access against user_emails (list(str)): list of emails to check access for check_linking (bool): flag to check for linked google email Returns: dict{str : (list(str))} : dictionary where keys are user emails, and values are list of project_ids they do not have access to """ no_access = {} for user_email in user_emails: user = get_user_by_email(user_email, db) or get_user_by_linked_email( user_email, db) logger.info("Checking access for {}.".format(user.email)) if not user: logger.info("Email ({}) does not exist in fence database.".format( user_email)) continue if check_linking: link_email = get_linked_google_account_email(user.id, db) if not link_email: logger.info( "User ({}) does not have a linked google account.".format( user_email)) continue no_access_auth_ids = [] for auth_id in auth_ids: project = get_project_from_auth_id(auth_id, db) if project: if not user_has_access_to_project(user, project.id, db): logger.info( "User ({}) does NOT have access to project (auth_id: {})" .format(user_email, auth_id)) # add to list to send email no_access_auth_ids.append(auth_id) else: logger.info( "User ({}) has access to project (auth_id: {})".format( user_email, auth_id)) else: logger.warning( "Project (auth_id: {}) does not exist.".format(auth_id)) if no_access_auth_ids: no_access[user_email] = no_access_auth_ids return no_access
def _get_optional_userinfo(user, claims): info = {} for claim in claims: if claim == "linked_google_account": google_email = get_linked_google_account_email(user.id) info["linked_google_account"] = google_email if claim == "linked_google_account_exp": google_account_exp = get_linked_google_account_exp(user.id) info["linked_google_account_exp"] = google_account_exp return info
def _link_google_account(): provided_redirect = flask.request.args.get("redirect") if not provided_redirect: raise UserError({"error": "No redirect provided."}) user_id = current_token["sub"] google_email = get_users_linked_google_email(user_id) proxy_group = get_or_create_proxy_group_id() # Set session flag to signify that we're linking and not logging in # Save info needed for linking in session since we need to AuthN first flask.session["google_link"] = True flask.session["user_id"] = user_id flask.session["google_proxy_group_id"] = proxy_group flask.session["linked_google_email"] = google_email if not google_email: # save off provided redirect in session and initiate Google AuthN flask.session["redirect"] = provided_redirect flask.redirect_url = flask.current_app.google_client.get_auth_url() # Tell Google to let user select an account flask.redirect_url = append_query_params(flask.redirect_url, prompt="select_account") else: # double check that the token isn't stale by hitting db linked_email_in_db = get_linked_google_account_email(user_id) if linked_email_in_db: # skip Google AuthN, already linked, error redirect_with_errors = append_query_params( provided_redirect, error="g_acnt_link_error", error_description= "User already has a linked Google account.", ) flask.redirect_url = redirect_with_errors _clear_google_link_info_from_session() else: # TODO can we handle this error? redirect_with_errors = append_query_params( provided_redirect, error="g_acnt_link_error", error_description="Stale access token, please refresh.", ) flask.redirect_url = redirect_with_errors _clear_google_link_info_from_session() return flask.redirect(flask.redirect_url)
def test_google_id_token_linked(db_session, encoded_creds_jwt, oauth_test_client): """ Test google email and link expiration are in id_token for a linked account """ user_id = encoded_creds_jwt["user_id"] proxy_group_id = encoded_creds_jwt["proxy_group_id"] original_expiration = 1000 google_account = "*****@*****.**" # add google account and link existing_account = UserGoogleAccount(email=google_account, user_id=user_id) db_session.add(existing_account) db_session.commit() g_account_access = UserGoogleAccountToProxyGroup( user_google_account_id=existing_account.id, proxy_group_id=proxy_group_id, expires=original_expiration, ) db_session.add(g_account_access) db_session.commit() # get google account info with utility function assert get_linked_google_account_email(user_id) == google_account assert get_linked_google_account_exp(user_id) == original_expiration # get the id token through endpoint data = {"confirm": "yes"} oauth_test_client.authorize(data=data) tokens = oauth_test_client.token() id_token = jwt.decode(tokens.id_token, verify=False) assert "google" in id_token["context"]["user"] assert (id_token["context"]["user"]["google"].get("linked_google_account") == google_account) assert (id_token["context"]["user"]["google"].get( "linked_google_account_exp") == original_expiration)
def _link_google_account(): provided_redirect = flask.request.args.get("redirect") # will raise UserError if invalid validate_redirect(provided_redirect) if not provided_redirect: raise UserError({"error": "No redirect provided."}) user_id = current_token["sub"] google_email = get_users_linked_google_email(user_id) proxy_group = get_or_create_proxy_group_id() # Set session flag to signify that we're linking and not logging in # Save info needed for linking in session since we need to AuthN first flask.session["google_link"] = True flask.session["user_id"] = user_id flask.session["google_proxy_group_id"] = proxy_group flask.session["linked_google_email"] = google_email if not google_email: # save off provided redirect in session and initiate Google AuthN flask.session["redirect"] = provided_redirect # requested time (in seconds) during which the link will be valid requested_expires_in = get_valid_expiration_from_request() if requested_expires_in: flask.session["google_link_expires_in"] = requested_expires_in # if we're mocking Google login, skip to callback if config.get("MOCK_GOOGLE_AUTH", False): flask.redirect_url = (config["BASE_URL"].strip("/") + "/link/google/callback?code=abc") response = flask.redirect(flask.redirect_url) # pass-through the authorization header. The user's username # MUST be a Google email for MOCK_GOOGLE_AUTH to actually link that # email correctly response.headers["Authorization"] = flask.request.headers.get( "Authorization") return response flask.redirect_url = flask.current_app.google_client.get_auth_url() # Tell Google to let user select an account flask.redirect_url = append_query_params(flask.redirect_url, prompt="select_account") else: # double check that the token isn't stale by hitting db linked_email_in_db = get_linked_google_account_email(user_id) if linked_email_in_db: # skip Google AuthN, already linked, error redirect_with_errors = append_query_params( provided_redirect, error="g_acnt_link_error", error_description= "User already has a linked Google account.", ) flask.redirect_url = redirect_with_errors _clear_google_link_info_from_session() else: # TODO can we handle this error? redirect_with_errors = append_query_params( provided_redirect, error="g_acnt_link_error", error_description="Stale access token, please refresh.", ) flask.redirect_url = redirect_with_errors _clear_google_link_info_from_session() return flask.redirect(flask.redirect_url)
def generate_implicit_response(client, grant_type, include_access_token=True, expires_in=None, user=None, scope=None, nonce=None, **kwargs): # prevent those bothersome "not bound to session" errors if user not in current_session: user = current_session.query(User).filter_by(id=user.id).first() if not user: raise OIDCError("user not authenticated") keypair = flask.current_app.keypairs[0] linked_google_email = get_linked_google_account_email(user.id) linked_google_account_exp = get_linked_google_account_exp(user.id) if not isinstance(scope, list): scope = scope.split(" ") if not "user" in scope: scope.append("user") # ``expires_in`` is just the token expiration time. expires_in = config["ACCESS_TOKEN_EXPIRES_IN"] response = { "token_type": "Bearer", "expires_in": expires_in, # "state" handled in authlib } # don't provide user projects access in access_tokens for implicit flow # due to issues with "Location" header size during redirect (and b/c # of general deprecation of user access information in tokens) if include_access_token: access_token = generate_signed_access_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=config["ACCESS_TOKEN_EXPIRES_IN"], scopes=scope, client_id=client.client_id, linked_google_email=linked_google_email, include_project_access=False, ).token response["access_token"] = access_token # don't provide user projects access in id_tokens for implicit flow # due to issues with "Location" header size during redirect (and b/c # of general deprecation of user access information in tokens) id_token = generate_signed_id_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=config["ACCESS_TOKEN_EXPIRES_IN"], client_id=client.client_id, audiences=scope, nonce=nonce, linked_google_email=linked_google_email, linked_google_account_exp=linked_google_account_exp, include_project_access=False, auth_flow_type=AuthFlowTypes.IMPLICIT, access_token=access_token if include_access_token else None, ).token response["id_token"] = id_token return response
def generate_token_response(client, grant_type, expires_in=None, user=None, scope=None, include_refresh_token=True, nonce=None, refresh_token=None, refresh_token_claims=None, **kwargs): # prevent those bothersome "not bound to session" errors if user not in current_session: user = current_session.query(User).filter_by(id=user.id).first() if not user: # Find the ``User`` model. # The way to do this depends on the grant type. if grant_type == "authorization_code": # For authorization code grant, get the code from either the query # string or the form data, and use that to look up the user. if flask.request.method == "GET": code = flask.request.args.get("code") else: code = flask.request.form.get("code") user = (current_session.query(AuthorizationCode).filter_by( code=code).first().user) if grant_type == "refresh_token": # For refresh token, the user ID is the ``sub`` field in the token. user = (current_session.query(User).filter_by( id=int(refresh_token_claims["sub"])).first()) keypair = flask.current_app.keypairs[0] linked_google_email = get_linked_google_account_email(user.id) linked_google_account_exp = get_linked_google_account_exp(user.id) if not isinstance(scope, list): scope = scope.split(" ") access_token = generate_signed_access_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=config["ACCESS_TOKEN_EXPIRES_IN"], scopes=scope, client_id=client.client_id, linked_google_email=linked_google_email, ).token id_token = generate_signed_id_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=config["ACCESS_TOKEN_EXPIRES_IN"], client_id=client.client_id, audiences=scope, nonce=nonce, linked_google_email=linked_google_email, linked_google_account_exp=linked_google_account_exp, auth_flow_type=AuthFlowTypes.CODE, access_token=access_token, ).token # If ``refresh_token`` was passed (for instance from the refresh # grant), use that instead of generating a new one. if refresh_token is None: refresh_token = generate_signed_refresh_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=config["REFRESH_TOKEN_EXPIRES_IN"], scopes=scope, client_id=client.client_id, ).token # ``expires_in`` is just the access token expiration time. expires_in = config["ACCESS_TOKEN_EXPIRES_IN"] return { "token_type": "Bearer", "id_token": id_token, "access_token": access_token, "refresh_token": refresh_token, "expires_in": expires_in, }
def generate_implicit_response(client, grant_type, include_access_token=True, expires_in=None, user=None, scope=None, nonce=None, **kwargs): # prevent those bothersome "not bound to session" errors if user not in current_session: user = current_session.query(User).filter_by(id=user.id).first() if not user: raise OIDCError("user not authenticated") keypair = flask.current_app.keypairs[0] linked_google_email = get_linked_google_account_email(user.id) linked_google_account_exp = get_linked_google_account_exp(user.id) if not isinstance(scope, list): scope = scope.split(" ") if not "user" in scope: scope.append("user") id_token = generate_signed_id_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=ACCESS_TOKEN_EXPIRES_IN, client_id=client.client_id, audiences=scope, nonce=nonce, linked_google_email=linked_google_email, linked_google_account_exp=linked_google_account_exp, ).token # ``expires_in`` is just the token expiration time. expires_in = ACCESS_TOKEN_EXPIRES_IN response = { "token_type": "Bearer", "id_token": id_token, "expires_in": expires_in, # "state" handled in authlib } if include_access_token: access_token = generate_signed_access_token( kid=keypair.kid, private_key=keypair.private_key, user=user, expires_in=ACCESS_TOKEN_EXPIRES_IN, scopes=scope, client_id=client.client_id, linked_google_email=linked_google_email, ).token response["access_token"] = access_token return response