6

I'm trying to make an accesible cache of user data using Pyramid doc's "Making A “User Object” Available as a Request Attribute" example.

They're using this code to return a user object to set_request_property:

from pyramid.security import unauthenticated_userid

def get_user(request):
    # the below line is just an example, use your own method of
    # accessing a database connection here (this could even be another
    # request property such as request.db, implemented using this same
    # pattern).
    dbconn = request.registry.settings['dbconn']
    userid = unauthenticated_userid(request)
    if userid is not None:
        # this should return None if the user doesn't exist
        # in the database
        return dbconn['users'].query({'id':userid})

I don't understand why they're using unauthenticated_userid(request) to lookup user info from the database...isn't that insecure? That means that user might not be logged in, so why are you using that ID to get there private info from the database?

Shouldn't

    userid = authenticated_userid(request)

be used instead to make sure the user is logged in? What's the advantage of using unauthenticated_userid(request)? Please help me understand what's going on here.

zakdances
  • 22,285
  • 32
  • 102
  • 173

3 Answers3

3

The unauthenticated_userid call is a cheaper call; it looks up the user id from the request without going through the whole authentication process again.

The key concept there is the word again. You should only use the method in views that have already been authorized. In other words, by the time you reach code that uses unauthenticated_userid you've already verified the user, and specifically do not want to do this again for this particular call.

Authenticating users against a backend persistent storage can be expensive, especially if such a storage doesn't support caching. The unauthenticated_userid API method is an optimization where the request is basically your userid cache.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 1
    Doesn't authenticated_userid() do the same thing (just return an ID)? I don't see anywhere in the code where authenticated_userid is trying to re-authenticate or re-query my database. I thought that when I call `headers = remember(self.request, userID)` it just stores the static ID securely in a cookie and retrieves in when I call authenticated_userid(). Is it doing something more expensive in the background I'm unaware of? – zakdances Sep 22 '12 at 21:15
  • @yourfriendzak: The *default* implementation doesn't have to look up any users in a back-end database. The API exists for *specialized* implementations that do. – Martijn Pieters Sep 22 '12 at 21:16
  • @yourfriendzak: Note for examplethat iternally, `CallbackAuthenticationPolicy.authenticated_userid` calls `CallbackAuthenticationPolicyunauthenticated_userid` first, then uses the callback to verify that userid. The callback is potentially costly. – Martijn Pieters Sep 22 '12 at 21:18
  • When you say "CallbackAuthenticationPolicyunauthenticated_userid" do you mean AuthTktAuthenticationPolicy(secret='thesecret', callback=groupfinder)? – zakdances Sep 22 '12 at 21:21
  • @yourfriendzak: `AuthTktAuthenticationPolicy` is a subclass of `CallbackAuthenticationPolicy`; it inherits the implementation of `authenticated_userid`. – Martijn Pieters Sep 22 '12 at 21:25
  • Sorry for my questions, I'm just really curious: If config.set_request_property(get_user, 'user', reify=True) is set, doesn't that mean that get_user() will be called on every request **regardless** of whether the user is logged in or not? So if unauthenticated_userid() is being called from inside get_user(), how are you supposed to ensure the user has already been authorized? – zakdances Sep 22 '12 at 21:28
  • By using protecting your views with permissions. – Martijn Pieters Sep 22 '12 at 21:35
  • But I want most of my view to be public. The views only have different attributes when the user is logged in (such as a logout link on the page). Well thank you, I won't take more of your time. looks like I'm gonna have to hit up IRC or something to address my continuing confusion. – zakdances Sep 22 '12 at 21:39
  • 1
    Then in your case don't use `unauthenticated_userid`, only use `authenticated_userid`. – Martijn Pieters Sep 22 '12 at 21:47
  • As best I can understand, `AuthTktAuthenticationPolicy`'s `callback` (often called `groupfinder`) is the actual thing which does the authenticating. If the userid belongs to at least one group, it's authenticated. – labreuer Jan 29 '14 at 03:41
2

This is a late reply but it was linked as a source of confusion for some users of Pyramid.

The accepted answer here is not the actual reason that unauthenticated_userid is used for request.user. It has nothing to do with performance.

The reason that it uses unauthenticated_userid is because it makes it easier to reuse the authentication policy between applications with smaller modifications required. Your application needs a "source of truth" for whether the user is allowed to be considered authenticated and usually the policy's internal logic is not enough to make this determination. A valid cookie is nice, but you usually want to verify it with your backend before trusting it. Great, so where do we put that logic? Well unauthenticated_userid doesn't make sense because it is the reusable part of the policy that focuses specifically on parsing the request headers. You could put it into authenticated_userid but this method is not the one you normally care about in your application. You normally use request.user in your apps (rarely do you probably care about request.authenticated_userid directly) and lastly the request.user is a superset of functionality - it provides an entire user object, not just an id. It would be silly to verify the id without verifying the entire object in most cases. We can only have one "source of truth" and so the recipe declares it to be request.user. The groupfinder (and thus authenticated_userid) can now depend on request.user and trust that what it gets back from there has been properly verified with the backend. Also request.user is already reified and thus speeds up subsequent calls to request.authenticated_userid naturally.

Michael Merickel
  • 23,153
  • 3
  • 54
  • 70
1

Looks like Martijn Pieters is right.

My micro benchmark to test this (in my project I use Redis as DB for users and everything else):

print ('start test')
t1 = time()
authenticated_userid(self.request)
print ('authenticated: ' + str(time()-t1))
t1 = time()
unauthenticated_userid(self.request)
print ('unauthenticated: ' + str(time()-t1))
print ('test_stop')

Results:

start test
REDIS AUTH! # <-- actually this is query to groups finder in Redis
authenticated: 0.00032901763916
unauthenticated: 7.31945037842e-05
test_stop

It was tested for few times, results are constant :) Do you think I should add Martijn's answer to that article in Pyramid docs to make things more 'clear'? :)

Sapphire
  • 11
  • 1
  • Ok, then what exactly is authenticated_userid(self.request) doing that makes it more expensive? What functions is it calling? – zakdances Sep 22 '12 at 21:43
  • In my particular case (only one thing that I can say for sure) it calls my **groupfinder** function, which checks user's group. So I would say that maybe authenticated_userid checks user session and only if it is valid - returns username. – Sapphire Sep 22 '12 at 21:48
  • 1
    Check **groupfinder** here: http://pyramid.readthedocs.org/en/latest/tutorials/wiki/authorization.html#add-users-and-groups – Sapphire Sep 22 '12 at 21:50