How to Log Out Users From a Flask App and Test Authentication

Project Source Code

Get the project source code below, and follow along with the lesson material.

Download Project Source Code

To set up the project on your local machine, please follow the directions provided in the README.md file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.

Requiring Logins

You might want to make certain routes only accessible to logged in users. To do that you'll first need to tell Flask-Login what to do with requests to those routes that are not from logged in users.

The most common action would be send users who try to access to the login page. In order to send users back to the page they were trying to visit before after the login, we'll store the page the were trying to access in their cookies (through the use Flask's session. session is the interface Flask provides for storing and reading writing data in cookies).

from flask import ..., request, session

@login_manager.unauthorized_handler
def unauthorized():
    session['after_login'] = request.url
    return redirect(url_for('user.login'))

Then on the login (and/or signup) route, we'll want to make the redirect attempt to go to the page we stored in the session before. If there's no value stored in the session, we can redirect Users to the Products home page.

    login_user(user)
    return redirect(session.get('after_login') or url_for("products.index"))

To mark that a route requires a user to be logged in, Flask-Login provides a decorator we can add to each route called login_required. To make the product creation route in blueprints/products.py require a login,

from flask_login import login_required
...
@products.route('/create', methods=['GET', 'POST'])
@login_required
def create():
    form = ProductForm()
    ...

Logging out

Implementing log outs requires using the logout_user function from Flask-Login. To ensure that only logged in users can access it, we can use the @login_required decorator.

from flask_login import login_user, logout_user, login_required
...

@user_bp.route('/logout', methods=["GET", "POST"])
@login_required
def logout():
    logout_user()
    return redirect(url_for('products.index'))
    # You may want to only allow access through a valid POST request

Testing

We added a few forms as well as a model here. The first part to test, is our User.create model as well as our password validations.

In tests/test_user.py, we can define two unit tests.

import pytest

from yumroad.models import db, User

EXAMPLE_EMAIL = "[email protected]"
EXAMPLE_PASSWORD = "test"

# Unit Tests
def test_user_creation(client, init_database):
    assert User.query.count() == 0
    user = create_user()
    assert User.query.count() == 1
    assert user.password is not EXAMPLE_PASSWORD

def test_email_password_validation(client, init_database):
    assert User.query.count() == 0
    with pytest.raises(ValueError):
        create_user('', EXAMPLE_PASSWORD)
    with pytest.raises(ValueError):
        create_user(EXAMPLE_EMAIL, '')
    assert User.query.count() == 0

To add functional tests, we can fill out the sign up and login forms with the same credentials. There are many scenarios here, and many of them will depend on having a logged in user, for which we can define a fixture in conftest.py

@pytest.fixture
def authenticated_request(client):
    new_user = User.create("[email protected]", "examplepass")
    db.session.add(new_user)
    db.session.commit()

    response = client.post(url_for('user.login'), data={
        'email': "[email protected]",
        'password': "examplepass"
    }, follow_redirects=True)
    yield client

This lesson preview is part of the Fullstack Flask: Build a Complete SaaS App with Flask course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to Fullstack Flask: Build a Complete SaaS App with Flask with a single-time purchase.

Thumbnail for the \newline course Fullstack Flask: Build a Complete SaaS App with Flask
  • [00:00 - 00:13] Welcome back. In this part of our login authentication segment, we're going to work on making some pages require having logins. We're also going to build logout and test all of our login functionality.

    [00:14 - 00:29] Okay, in our code here, we're going to want to have wait to market certain routes as having to require a login. Plus login provides us with a way we can mark certain routes as requiring a login.

    [00:30 - 00:40] The way we do that is by adding this as a decorator to the route. But in order to support that, we need to tell the last login what to do if a user is not logged in.

    [00:41 - 00:54] And so what we can do there is we need to tell the login manager what we do for unauthorized requests. And so we would mark this as login manager.unauthorized handler.

    [00:55 - 01:04] And so what we can do here is we can actually store the URL they were trying to visit. So in our session, we can actually store values as well.

    [01:05 - 01:13] So here we can say after login, and we can store the URL of the current request . So that's going to be in request.url.

    [01:14 - 01:27] And that's going to be something we're going to have to import from Flask as well. Now that we have that value, what we can do is we can return a redirect to the login page.

    [01:28 - 01:40] So we can say user.login. Okay, now that we have this handled in the login section, we're going to want to send them to the page that's in after login.

    [01:41 - 01:46] Or they're a default page here. All right.

    [01:47 - 01:58] And now we can mark certain routes as login required. For example, if we had an edit route, we can go in here and say, this is requiring a login.

    [01:59 - 02:16] Then we can go ahead and import this. So we say from Flask login, import login required. Okay.

    [02:17 - 02:30] Next up is a, next up is a logout page. So to implement a logout page, what we're going to do is we're going to add a function here called logout.

    [02:31 - 02:40] And we're just going to call a function from Flask login called logout user. And logout user is just something we can import.

    [02:41 - 02:51] It operates similarly to how a login user works. It just unsets that value. So we can route this as logout.

    [02:52 - 03:03] And sometimes you might want to make this a post request instead of a get request. That depends on your security needs. Sometimes it can be fine to just leave this as a get request.

    [03:04 - 03:19] But either way you do that, you can logout the user and then redirect urlfor product.index. For our case, I think it's fine if we just leave it as a get request.

    [03:20 - 03:35] So technically this does open the possibility for a cross site request for our tree where someone could logout a user. That's pretty low impact though, so I'm fine with that for our application.

    [03:36 - 03:43] Another place we can mark something as login required is the ability to create a product. So we can mark this as login required.

    [03:44 - 03:56] Okay. Once we fix that redirect, we can go back and see if we've been logged out. Okay, let's go ahead and go to /logout now.

    [03:57 - 04:08] Great. So now that we went to /logout and everything got logged out and we try and create a product and see what happens. It's going to tell us that it's time that we need to be logged in.

    [04:09 - 04:17] Now this is a great place where we can use our flashes. So going back here, what we can do is we can say flash.

    [04:18 - 04:25] You need to log in. We can say this is a warning.

    [04:26 - 04:31] So I tried creating an account there and it wouldn't let me. So what I can do is I can go ahead and select "all right".

    [04:32 - 04:41] So it logged in successfully, which is great. Now we can also notice that it actually brought us back to product/create.

    [04:42 - 04:50] So it's remembering where we went before, which is exactly what we wanted. Next up, we're going to talk about how to test this application.

    [04:51 - 05:06] Going back in our code base, what we're going to do is we're going to go back into our test folder and we're going to create a new file called test user.pl. One thing we're going to need is a way to run a test as a logged in user.

    [05:07 - 05:21] And the easy way to set that up is to create a fixture. So we can have a fixture that actually creates a user and logs us in by submitting a post request to the actual application.

    [05:22 - 05:33] Over in test user.py, I've gone ahead and imported some things here. So we have a function that will allow us to create users, which is something we 're going to do a lot.

    [05:34 - 05:41] Our first thing we can do is test user creation. So we can make sure that our users are actually being created.

    [05:42 - 05:52] Next up, we can test that the register page is actually loading. Now we can also try submitting an actual register query.

    [05:53 - 06:00] And to do that, we're going to set up some params here for a valid registration . Then we're going to post that into our page.

    [06:01 - 06:11] So what we do is make a post request with that data here. And we want to assert that the flash is showing up and the user looks like they 're logged in.

    [06:12 - 06:21] We can also assert that an infiled registration raises an error accordingly. All right.

    [06:22 - 06:27] I'll be over to our terminal. If we run pytest, let's see what happens.

    [06:28 - 06:36] So it looks like a lot of things failed in test product. And the reason they're going to fail is because all of these pages require a log.

    [06:37 - 06:52] So to fix that, let's go and test product and add authentication to all of these tests. Now going back into our template and our contest fixture, you can see what authentic request does again.

    [06:53 - 07:06] We're going to also have to import user from our models. And then we're also going to have to import URL for to ensure that everything here works.

    [07:07 - 07:16] Okay. Now that we're back here, essentially what we want to do is for any page that requires creating a product, we're going to want to add the authenticated request fixture.

    [07:17 - 07:35] So the index page does not require that, nor does the detail page, but the create page requires that. So does the creation page submission here, as well as this, and a few other tests as well.

    [07:36 - 07:40] Okay. Now let's go ahead and try our thing in the terminal.

    [07:41 - 07:42] Okay. Here we are.

    [07:43 - 07:47] Awesome. So all of our tests pass, which is a nice site to see.

    [07:48 - 07:57] Let's go back in our tests and go and check out our user tests here. So so far we've test registration, but we haven't test some of the other cases around registration.

    [07:58 - 08:07] Like, for example, what happens if a user has an existing account? Here I've created a test that checks with the same user.

    [08:08 - 08:18] So create user is going to set up a user and then we're going to try and register that same user again. And then that should mean it should pop up a few things, and these should not already pop up.

    [08:19 - 08:33] Similarly, we should test what happens if you try and register, but you're already logged in. So I had a test here that if we try and register with someone who's already logged in, it should let us and should just take us back to the login page.

    [08:34 - 08:42] Let's go ahead and try that in our terminal and see what happens. Okay, looks like that's going to fail.

    [08:43 - 09:01] And if we go back to our code and check out our user's logic here, our registration field doesn't actually look at if someone's already logged in. So what we can do is we can copy this code and add it to register here.

    [09:02 - 09:13] If you do this a lot, you might find that it might be helpful to write your own decorator that effectively does that. For now, I think it's fine to replicate this code here.

    [09:14 - 09:19] Going back in our terminal, let's go and run a PyTest and see what happens now. Cool.

    [09:20 - 09:30] So tests are a good way to sanity check that your assumptions are coming true and you haven't forgot to implement a feature, for example. They can also be a really good way to test for bugs.

    [09:31 - 09:39] Back in our code, let's go ahead and add some tests for the login case. So first off, here's a simple one for just testing that the login page works.

    [09:40 - 09:50] Next is we want to test our login form as well. So let's set up some valid login parameters similar to how we set up valid register parameters earlier.

    [09:51 - 10:07] And let's go ahead and add a test that actually makes a request to the endpoint . So here I've added a test that creates a user and then logs in with those details there.

    [10:08 - 10:30] Now we don't have a logout page in our nav bar yet, but you can go ahead and add one if you want to in the nav bar. Another thing we can do is check what happens if we try and log in with an invalid email or one that doesn't exist yet.

    [10:31 - 10:44] So these tests will pass so far. We can try log in with the user that doesn't exist yet.

    [10:45 - 10:55] But when we try to run this test, we're actually going to stumble upon a bug. So here we see that this is complaining about our error.

    [10:56 - 11:08] And now that I look at the code again, I see that this should be "or not ant". So let's go back in our code and let's fix that.

    [11:09 - 11:24] In forms.py, this should be "or here". So if the user doesn't exist or if the password is not valid, we're going to go ahead and return false. Now we can run PyTest and see what happens.

    [11:25 - 11:39] Great. So as you can see, PyTest is great for helping us catch bugs. Let's go ahead and add one more test to ensure that logging in with a bad password also does not work.

    [11:40 - 11:51] And then we can add one more test to ensure that the user is actually logged out when you go to the logout page. Okay. Here I've added one to test for logout.

    [11:52 - 12:06] So it essentially just makes a request to the logout page and then it starts that it looks like the user is logged out. Let's go ahead and run our test now with some coverage metrics.

    [12:07 - 12:14] So it looks like we've tested almost everything, but we've forgotten a few lines of code. So let's go ahead and look at those routes.

    [12:15 - 12:23] Okay. Looking at our layouts, here was one of them that it was complaining about. It didn't test the case where we were trying to log in when we were already authenticated.

    [12:24 - 12:29] So we should try that one. The other one was complaining about is that we never tried the unauthorized handler.

    [12:30 - 12:36] We were always logged in when we tried to authenticate it routes. So let's go ahead and add those in our test suite.

    [12:37 - 12:49] In order to test this, we want to add a test that looks something like this, but for the login route. So we can actually just duplicate this test and we can say already logged in, login.

    [12:50 - 13:03] And if we can do a login params there and then we can assert that after following redirects, it'll still give us a 200 code that says you're already logged in. Okay. So that covers one of them.

    [13:04 - 13:13] The other thing we might want to check is that a redirect for a page that requires authentication is there. So you might be wondering which test switch should this go in?

    [13:14 - 13:22] I'm going to say it goes in test product. So what we can do is add a new test here that's similar to test a new page, but it will not be authenticated here.

    [13:23 - 13:37] And then we can assert that the redirect will be a 302 and we can assert that the location it's sending us to is the login page. Okay. Going back into our code, we can try and run all of our tests.

    [13:38 - 13:48] All right. We're back at 100% test coverage. In the next video, we're going to talk about adding some more advanced things to our database like relationships.