pyramid icon indicating copy to clipboard operation
pyramid copied to clipboard

Pyramid auth_tkt cookie not compatible with Apache2's mod_auth_tkt

Open russellballestrini opened this issue 10 years ago • 14 comments
trafficstars

development.ini:

# http://docs.pylonsproject.org/docs/pyramid/en/latest/api/session.html
#session.hashalg = sha512
session.hashalg = md5
session.secret  = abc
session.timeout = 31104000
session.max_age = 31104000
session.reissue_time  = 15552000

# http://docs.pylonsproject.org/docs/pyramid/en/latest/api/authentication.html
#auth_tkt.hashalg = sha512
auth_tkt.hashalg = md5
auth_tkt.secret  = abc
auth_tkt.timeout = 31104000
auth_tkt.max_age = 31104000
auth_tkt.reissue_time = 15552000
auth_tkt.debug = true

**init.py main()**:

from pyramid.config import Configurator

# used to get userid from cookie
from pyramid.security import unauthenticated_userid

# builtin pyramid authen/author
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy

# cookie only session, not encrypted but signed to prevent tampering!
from pyramid.session import SignedCookieSessionFactory

def get_int_or_str(value):
    try:
        return int(value)
    except ValueError:
        return str(value)

def get_children_settings(settings, parent_key):
    """
    Accept a settings dict and parent key, return dict of children

    For example:

      auth_tkt.hashalg = md5

    Results to:

      {'auth_tkt.hashalg': 'md5'}

    This function returns the following:

      >>> get_children_settings({'auth_tkt.hashalg': 'md5'}, 'auth_tkt')
      {'hashalg': 'md5'}

    """
    # the +1 is the . between parent and child settings.
    parent_len = len(parent_key) + 1
    children = {}
    for key, value in settings.items():
        if parent_key in key:
            children[key[parent_len:]] = get_int_or_str(value)
    return children

def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application."""

    # setup session factory to use unencrypted but signed cookies
    session_factory = SignedCookieSessionFactory(
        **get_children_settings(settings, 'session')
    )

    authz_policy = ACLAuthorizationPolicy()
    authn_policy = AuthTktAuthenticationPolicy(
        **get_children_settings(settings, 'auth_tkt')
    )

    # build app config object from ini.
    config = Configurator(
        settings = settings,
        session_factory = session_factory,
        authorization_policy  = authz_policy,
        authentication_policy = authn_policy,
    )

apache2 vhost config:

#meta.example.com

<VirtualHost *:80>

    ServerName meta.example.com
    ServerAdmin [email protected]

    # Log Files
    LogLevel debug
    ErrorLog /var/log/apache2/error-meta-example.com.log
    CustomLog /var/log/apache2/access-meta-example.com.log combined

    # use cookie set from the other application in the cloud!
    # http://linux.die.net/man/3/mod_auth_tkt
    TKTAuthSecret "abc"
    #TKTAuthDigestType "SHA512"
    TKTAuthDigestType "MD5"

    DocumentRoot /www/example.com
    <Directory "/www/example.com">
    #<Directory />
        Order allow,deny
        Allow from all
        #Options Indexes FollowSymLinks
        #AllowOverride None
    </Directory>

    Alias /public /www/example.com/public
    <Directory "/www/example.com/public">
        Order allow,deny
        Allow from all
    </Directory>

    Alias /private /www/example.com/private
    <Directory "/www/example.com/private">
        Order allow,deny
        Allow from all

        AuthType None
        require valid-user

        TKTAuthDomain "example.com"
        TKTAuthLoginURL "http://example.com/login-or-register"
        TKTAuthTimeoutURL "http://example.com/login-or-register"
        TKTAuthPostTimeoutURL "http://example.com/login-or-register"
        TKTAuthUnauthURL "http://example.com/login-or-register"
        TKTAuthIgnoreIP on
        TKTAuthTimeout 31104000
        TKTAuthCookieExpires 31104000
        TKTAuthTimeoutRefresh .1
        TKTAuthDebug 3
    </Directory>

</VirtualHost>

apache2 + mod_auth_tkt with debug

==> /var/log/apache2/error-meta-ezqna.log <==
[Wed Mar 25 22:13:58.426245 2015] [authz_core:debug] [pid 14260] mod_authz_core.c(802): [client 127.0.0.1:60386] AH01626: authorization result of Require valid-user : denied (no authenticated user yet)
[Wed Mar 25 22:13:58.427573 2015] [authz_core:debug] [pid 14260] mod_authz_core.c(802): [client 127.0.0.1:60386] AH01626: authorization result of <RequireAny>: denied (no authenticated user yet)

==> /var/log/apache2/error.log <==
[ mod_auth_tkt config ]
URI: /private
Filename: /www/example.com/private
TKTAuthSecret: abc
TKTAuthSecretOld: (null)
TKTAuthDigestType: MD5
digest_sz: 32
directory: /www/example.com/private/
TKTAuthLoginURL: http://example.com/login-or-register
TKTAuthTimeoutURL: http://example.com/login-or-register
TKTAuthPostTimeoutURL: http://example.com/login-or-register
TKTAuthUnauthURL: http://example.com/login-or-register
TKTAuthCookieName: auth_tkt
TKTAuthDomain: example.com
TKTAuthCookieExpires: 31104000
TKTAuthBackCookieName: (null)
TKTAuthBackArgName: back
TKTAuthIgnoreIP: 1
TKTAuthRequireSSL: -1
TKTAuthCookieSecure: -1
TKTAuthTimeoutMin: 31104000
TKTAuthTimeoutRefresh: 0.100000
TKTAuthGuestLogin: -1
TKTAuthGuestCookie: -1
TKTAuthGuestUser: (null)
TKTAuthGuestFallback -1
TKTAuthDebug: 3

==> /var/log/apache2/error-meta-ezqna.log <==
[Wed Mar 25 22:13:58.429882 2015] [:debug] [pid 14260] mod_auth_tkt.c(601): [client 127.0.0.1:60386] TKT cookie_match, key Cookie against <auth_tkt="697d0b7e0453b8a8ad9311687269aedf55136ac4Zm94aG9wMUBnbWFpbC5jb20%3D!userid_type:b64unicode"> (name=auth_tkt)
[Wed Mar 25 22:13:58.430838 2015] [:debug] [pid 14260] mod_auth_tkt.c(627): [client 127.0.0.1:60386] TKT cookie_match: found '"697d0b7e0453b8a8ad9311687269aedf55136ac4Zm94aG9wMUBnbWFpbC5jb20%3D!userid_type:b64unicode"'
[Wed Mar 25 22:13:58.431373 2015] [:debug] [pid 14260] mod_auth_tkt.c(541): [client 127.0.0.1:60386] TKT parse_ticket decoded ticket: '697d0b7e0453b8a8ad9311687269aedf55136ac4Zm94aG9wMUBnbWFpbC5jb20%3D!userid_type:b64unicode'
[Wed Mar 25 22:13:58.432115 2015] [:debug] [pid 14260] mod_auth_tkt.c(555): [client 127.0.0.1:60386] TKT parse_ticket: no tokens
[Wed Mar 25 22:13:58.432638 2015] [:debug] [pid 14260] mod_auth_tkt.c(918): [client 127.0.0.1:60386] TKT valid_ticket: (parsed) uid 'Zm94aG9wMUBnbWFpbC5jb20%3D', tokens '', user_data 'userid_type:b64unicode', ts '1427335876'
[Wed Mar 25 22:13:58.433199 2015] [:debug] [pid 14260] mod_auth_tkt.c(820): [client 127.0.0.1:60386] TKT ticket_digest: using secret 'abc', ip '0.0.0.0', ts '1427335876'
[Wed Mar 25 22:13:58.433642 2015] [:debug] [pid 14260] mod_auth_tkt.c(865): [client 127.0.0.1:60386] TKT ticket_digest: digest0: 'a0e97753e45c48f0d6279d3c727ad5af' (input length 61)
[Wed Mar 25 22:13:58.434456 2015] [:debug] [pid 14260] mod_auth_tkt.c(887): [client 127.0.0.1:60386] TKT ticket_digest: digest: 'b878b8b1fc5abe0399f453f35d6a2ba4'  
[Wed Mar 25 22:13:58.434864 2015] [:warn] [pid 14260] [client 127.0.0.1:60386] TKT valid_ticket: ticket hash (current secret) is invalid, and no old secret set - digest 'b878b8b1fc5abe0399f453f35d6a2ba4', ticket '697d0b7e0453b8a8ad9311687269aedf55136ac4Zm94aG9wMUBnbWFpbC5jb20%3D!userid_type:b64unicode'
[Wed Mar 25 22:13:58.435243 2015] [:info] [pid 14260] [client 127.0.0.1:60386] TKT: no valid ticket found - redirecting to login url
[Wed Mar 25 22:13:58.435593 2015] [:debug] [pid 14260] mod_auth_tkt.c(1242): [client 127.0.0.1:60386] TKT: back url 'http://meta.example.com/private'
[Wed Mar 25 22:13:58.435868 2015] [:debug] [pid 14260] mod_auth_tkt.c(1270): [client 127.0.0.1:60386] TKT: redirect 'http://example.com/login-or-register?back=http%3a%2f%2fmeta.example.com%2fprivate'

==> /var/log/apache2/access-meta-ezqna.log <==
32.212.88.105 - - [25/Mar/2015:22:13:58 -0400] "GET /private HTTP/1.1" 307 624 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.94 Safari/537.36"

russellballestrini avatar Mar 27 '15 00:03 russellballestrini

I have no experience with apache's mod_auth_tkt so I wasn't aware that compatibility had been broken at some point. We should try to figure out when this happened and what we can do about it.

mmerickel avatar Jun 01 '15 21:06 mmerickel

I spent about 6 hours studying the code (when I was on IRC with Russell) on both sides and I can't find anything that jumps out at me that we are doing wrong that would be breaking compatibility.

digitalresistor avatar Jun 02 '15 03:06 digitalresistor

@bertjwregeer but you can confirm that it is not compatible? Can you enumerate anything you've already checked, such as what options you were using. I believe ipv6 pinning is explicitly not compatible, for example, due to it being undefined in apache. It'd be good to know what you tested.

mmerickel avatar Jun 02 '15 19:06 mmerickel

Actually I was doing the testing. I still have my testing env available. Good point about IPv6, I'm fairly certain I have that disabled on my OS.

The pasted output is all the settings to the test that I felt were relevant.

I'm available to test more if an idea is presented.

russellballestrini avatar Jun 03 '15 00:06 russellballestrini

@mmerickel I can only confirm that the two sides seem to be coming up with two entirely different hashes, however both sides seem to be doing the calculation the exact same way, and unfortunately there is no enough debug data coming out of the Apache side to know where exactly it is going wrong.

digitalresistor avatar Jun 03 '15 06:06 digitalresistor

I'd recommend comparing the apache and pyramid output to the paste output that we took the auth_tkt code from originally[1].

Finally, was this being tested on py2 or py3?

[1] https://bitbucket.org/ianb/paste/src/a159cd816e016984398a9f4332777fee6d0dce47/paste/auth/auth_tkt.py?at=default

mmerickel avatar Jun 03 '15 15:06 mmerickel

This was python 2. If we can get the initial commit where we added support for auth_tkt I can try to test and get output using that version.

EDIT: oops missed your link to https://bitbucket.org/ianb/paste/src/a159cd816e016984398a9f4332777fee6d0dce47/paste/auth/auth_tkt.py?at=default

On Wed, Jun 3, 2015, 11:48 AM Michael Merickel [email protected] wrote:

I'd recommend the apache and pyramid output to the paste output that we took the auth_tkt code from originally.

Finally, was this being tested on py2 or py3?

— Reply to this email directly or view it on GitHub https://github.com/Pylons/pyramid/issues/1620#issuecomment-108493273.

russellballestrini avatar Jun 03 '15 21:06 russellballestrini

After talking with @russellballestrini offline a bit it sounds like the raw auth_tkt token is still the same between Pyramid and Paste. However the encoded cookie value itself seems to differ (probably due to Pyramid now using updated cookie generation from webob 1.4 CookieProfile). Not much to say here but just keeping the thread up to date.

mmerickel avatar Jun 15 '15 04:06 mmerickel

just a random shot from a totally different project with similar issues... is your [local] server on a privately addressed ip and your host on a public ip (that's connecting to your auth?)

and do you have TKTAuthIgnoreIP not set?

that can wreak havok on the digest...

wesyoung avatar Aug 26 '15 15:08 wesyoung

Hey @wesyoung,

Yes, in my Apache2 config I have TKTAuthIgnoreIP on and Pyramid has an argument include_ip which defaults to False. (http://docs.pylonsproject.org/projects/pyramid/en/latest/api/authentication.html#pyramid.authentication.AuthTktCookieHelper)

I believe that is the proper orientation for those toggles to play nicely together.

russellballestrini avatar Aug 26 '15 16:08 russellballestrini

just double check your (apache?) debugging messages, some of the perl apps i saw were calculating it as "" and the c code appeared to be calculating it as 0.0.0.0. could be a totally diff issue, just citing what i found when i saw those types of errors...

wesyoung avatar Aug 26 '15 16:08 wesyoung

I ran into this issue with Apache 2.4. By setting LogLevel debug in the main apache config and TKTAuthDebug 3 in the protected Location in Apache, I was able to determine that the hash is checked against url_quote(self.userid) instead of self.userid. example%40example.com instead of [email protected]. I did not find any obvious difference between the older and newer mod_auth_tkt.c to explain this.

def digest(self):
    return calculate_digest(
        self.ip, self.time, self.secret, url_quote(self.userid), self.tokens,
        self.user_data)

Then REMOTE_USER probably always has %40 in it and must be unquoted in the app.

An alternative would be to not url_quote the username when writing the cookie value. The whole thing is base64 encoded anyway?

dholth avatar May 08 '18 12:05 dholth

I ran into another related issue. Pyramid's auth_tkt cookie doesn't work with mod_auth_tkt when using tokens. Here's why:

The mod_auth_tkt spec specifies that tokens are delimited by commas in the cookie. However, the cookie spec actually forbids this. WebOb helpfully escapes commas. However this invalidates the cookie as when mod_auth_tkt calculates the digest, it will differ from what is in the cookie.

This is exactly what mmerickel mentioned a few posts above.

Since most browsers are permissive enough to allow commas in cookies, I am manually editing the headers to unescape the commas. Of course I wouldn't recommend this if you are working on anything professional :).

retrogamer500 avatar Feb 21 '20 02:02 retrogamer500

The comma thing could be a real issue - but probably without a good solution.

I did dive into the url_quote situation mentioned by @dholth and I do not think it is related to this issue. Pyramid has urlquoted the value ever since it was copied into the codebase from Paste in bd0c7a6cb3d0539283b27e068671ba074e63b4e4.

mmerickel avatar Feb 21 '20 03:02 mmerickel