Pyramid authentication: Why does it work? - python

I'm just now getting into authentication in my app, and all of the pyramid examples that I can find explain the straightforward parts very well, but handwave over the parts that don't make any sense to me.
Most of the examples look something like this:
login = request.params['login']
password = request.params['password']
if USERS.get(login) == password:
headers = remember(request, login)
return HTTPFound(location = came_from,
headers = headers)
And from init:
session_factory = UnencryptedCookieSessionFactoryConfig(
settings['session.secret']
)
authn_policy = SessionAuthenticationPolicy()
authz_policy = ACLAuthorizationPolicy()
Trying to track down the point in which the login actually happens, I'm assuming it's this one:
headers = remember(request, login)
It appears to me that what is going on is we're storing the username in the session cookie.
If I put this line in my app, the current user is magically logged in, but why?
How does pyramid know that I'm passing a username? It looks like I'm just passing the value of login. Further, this variable is named differently in different examples.
Even if it does know that it's a username, how does it connect it with the user ID? If I run authenticated_userid(request) afterwards, it works, but how has the system connected the username with the userid? I don't see any queries as part of the remember() documentation.

Pyramid's security system revolves around principals; your login value is that principal. It is up to your code to provide remember() with a valid principal name; if your login name filled in the form is used as your principal, then that's great. If you are using an email address but use a database primary key as the principal string, then you'd have to map that yourself.
What exactly remember() does depends on your authentication policy; it is up to the policy to 'know' from request to request what principal you asked it to remember.
If you are using the AuthTktAuthenticationPolicy policy, then the principal value is stored in a cryptographically signed cookie; your next response will have a Set-Cookie header added. Then next time a request comes in with that cookie, provided it is still valid and the signature checks out, the policy now 'knows' what principle is making that request.
When that request then tries to access a protected resource, Pyramid sees that a policy is in effect, and asks that policy for the current authenticated principle.

Related

How do I authorize a Google user in Python backend with ID token coming from iOS application?

Solution
So I don't think its a surprise to anyone but Google's documentation is god awful. It's so scattered and the Python docs still reference their old depreciated library. Anyways.
So what I really needed to look at was this link "Enabling Server Side Access for your App". This is not linked to anywhere. Keep in mind this is entirely different than "Authenticating with a Backend Server"
This was a start. On the iOS side of things, we need to specify the server or backend's client_id.
...
GIDSignIn.sharedInstance().clientID = SBConstants.Google.IOS_CLIENT_ID
GIDSignIn.sharedInstance().serverClientID = SBConstants.Google.SERVER_CLIENT_ID
...
And capture serverAuthCode from the sign method inside your sign-in delegate.
...
self.googleUser.userID = user.userID
self.googleUser.token = user.authentication.idToken
self.googleUser.serverAuthCode = user.serverAuthCode
...
Now when you want to perform some action in the backend on behalf of the frontend, we pass the captured serverAuthCode and send it as a parameter.
That was the easy part. In the backend, Google seems to have 13 different OAuth2 libraries for Python documented. Their example uses oauth2client which of course is deprecated.
What we want to use is their 'new' library google-api-python-client.
When the auth_token is passed to the backend we need to check if the user already has an access token in our database. If it does, we need to refresh. Otherwise, we need to request a new access token based on the auth_code. After much trial and error, here is the code to do so:
# we have record of this user
# we have record of this user
if user.exists:
# create new credentials, and refresh
credentials = Credentials(
token=user.token,
refresh_token=user.refresh_token,
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
token_uri='https://oauth2.googleapis.com/token')
# now we have an access token
credentials.refresh(requests.Request())
else:
# get the auth_token
token_obj = json.loads(request.body)
code = token_obj.get('auth_code')
# request access token given auth_token
auth_flow = flow.Flow.from_client_secrets_file(creds, scopes=scopes)
auth_flow.fetch_token(code=code)
# now have access token
credentials = auth_flow.credentials
A warning: Pass or fail, the auth_token is only good for one request. This totally burned me. This also means once you have a successful backend interaction, you must store the user's token information to then request a refresh not a new access token.
Hope this helps someone.
Original Post
Following the documentation here, I am trying to authenticate a user in my iOS app and pass their ID token to my backend. The backend handles the Google API interactions for the iOS app.
I am missing how to actually authenticate that user in the backend. I read over the docs here regarding ID tokens but I am confused on where the service account comes into play.
Current endpoint:
#api_view(['POST'])
#authentication_classes([TokenAuthentication])
#permission_classes([IsAuthenticated])
def google_token_info(request):
try:
token_obj = json.loads(request.body)
token = token_obj['id_token']
id_info = id_token.verify_oauth2_token(token, requests.Request(), settings.IOS_CLIENT_ID)
# create session here - how?
This is all working fine. The ID info returns the expected decrypted JWT contents, and I have the user's unique Google ID at this point.
While testing I had authentication set up via my backend. I had code like this:
def google_auth(request):
web_flow = flow.Flow.from_client_secrets_file(creds, scopes=scopes)
web_flow.redirect_uri = request.build_absolute_uri(reverse('api.auth:oauth_callback'))
auth_url, state = web_flow.authorization_url(access_type='offline', include_granted_scopes='true', prompt='consent')
request.session['state'] = state
return redirect(auth_url)
def oauth_callback(request):
success_flow = flow.Flow.from_client_secrets_file(creds, scopes=scopes, state=request.session.get('state'))
success_flow.redirect_uri = request.build_absolute_uri(reverse('api.auth:oauth_callback'))
auth_response = request.build_absolute_uri()
success_flow.fetch_token(authorization_response=auth_response)
credentials = success_flow.credentials
if not request.session.get('google_credentials'):
request.session['google_credentials'] = _credentials_to_dict(credentials)
return redirect(reverse('api.auth:success'))
Which setup session credentials for the user. I'm assuming I need something similar, but I am unsure how to create a session without actual credentials.
Solution
So I don't think its a surprise to anyone but Google's documentation is god awful. It's so scattered and the Python docs still reference their old depreciated library. Anyways.
So what I really needed to look at was this link "Enabling Server Side Access for your App". This is not linked to anywhere. Keep in mind this is entirely different than "Authenticating with a Backend Server"
This was a start. On the iOS side of things, we need to specify the server or backend's client_id.
...
GIDSignIn.sharedInstance().clientID = SBConstants.Google.IOS_CLIENT_ID
GIDSignIn.sharedInstance().serverClientID = SBConstants.Google.SERVER_CLIENT_ID
...
And capture serverAuthCode from the sign method inside your sign-in delegate.
...
self.googleUser.userID = user.userID
self.googleUser.token = user.authentication.idToken
self.googleUser.serverAuthCode = user.serverAuthCode
...
Now when you want to perform some action in the backend on behalf of the frontend, we pass the captured serverAuthCode and send it as a parameter.
That was the easy part. In the backend, Google seems to have 13 different OAuth2 libraries for Python documented. Their example uses oauth2client which of course is deprecated.
What we want to use is their 'new' library google-api-python-client.
When the auth_token is passed to the backend we need to check if the user already has an access token in our database. If it does, we need to refresh. Otherwise, we need to request a new access token based on the auth_code. After much trial and error, here is the code to do so:
# we have record of this user
# we have record of this user
if user.exists:
# create new credentials, and refresh
credentials = Credentials(
token=user.token,
refresh_token=user.refresh_token,
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
token_uri='https://oauth2.googleapis.com/token')
# now we have an access token
credentials.refresh(requests.Request())
else:
# get the auth_token
token_obj = json.loads(request.body)
code = token_obj.get('auth_code')
# request access token given auth_token
auth_flow = flow.Flow.from_client_secrets_file(creds, scopes=scopes)
auth_flow.fetch_token(code=code)
# now have access token
credentials = auth_flow.credentials
A warning: Pass or fail, the auth_token is only good for one request. This totally burned me. This also means once you have a successful backend interaction, you must store the user's token information to then request a refresh not a new access token.

Azure graph api returning different token each time a login attempt is made.?

I have a python api which accepts email and password from a login webpage. This email and password is used to login to azure ad and in response we get a access token which is valid for 1hr. Below is the sample python code:
context = adal.AuthenticationContext(config_data['AUTHORITY_HOST_URL'] + '/' + config_data['TENANT'], validate_authority="cceaddik-1q7c5-997ad-6453-sduf9347asit8" != 'adfs')
token = context.acquire_token_with_username_password(config_data['RESOURCE'], email, password, config_data['CLIENT_ID'])
print(token['accessToken'])
Normally this token generated for a user should be valid for 1hr and if same user is logging again within the same 1hr, it should get the same access token. But what I have noticed is that, each time we login, we are getting different access token. Why is this happening. Can anyone please throw some lights on it. Thanks.
You will always get a new token when you call Azure AD. It does not cache tokens there.
A token is valid for one hour from the time you requested the token.
As a side note, handling passwords is a bad idea in general.
It would be much better to use one of the interactive login flows like authorization code flow.
You cannot enable MFA for users for example, as the login would fail.

Jupyterhub Custom Authenticator

I am a little stuck with writing a custom authenticator for jupyterhub. Most probably because I do not understand the inner workings of the available REMOTE_USER authenticator. I am not sure if it is applicable in my case... anyhow... this is what I'd like to do:
My general idea: I have a server that authenticates a user with his or her institutional login. After logging into the institution server/website, the users' data are encoded -- only some details to identify the user. They are then redirected to a the jupyterhub domain in the following way
https://<mydomain>/hub/login?data=<here go the encrypted data>
Now, if a request gets sent like this to my jupyterhub-domain, I'd like to decrypt the submitted data, and authenticate the user.
My trial:
I tried it with the following code. But it seems I am too nooby... :D
So please, pedantic comments are welcome :D
from tornado import gen
from jupyterhub.auth import Authenticator
class MyAuthenticator(Authenticator):
login_service = "my service"
authenticator_login_url="authentication url"
#gen.coroutine
def authenticate(self,handler,data=None):
# some verifications go here
# if data is verified the username is returned
My first problem... clicking the button on the login page, doesn't redirect me to my Authentication URL... it seems the variable authenticator_login_url from the login template is set somewhere else...
Second problem... a request made to .../hub/login?data=... is not evaluated by the authenticator (it seems...)
So: Has somebody any hints for me how to go about this?
As you see I followed the tutorials here:
https://universe-docs.readthedocs.io/en/latest/authenticators.html
So the following code does the job, however, I am always open to improvements.
So, what I did was redirect an empty login attempt to the login-url and deny access. If data is presented, check the validity of the data. If verified, user can login.
from tornado import gen, web
from jupyterhub.handlers import BaseHandler
from jupyterhub.auth import Authenticator
class MyAuthenticator(Authenticator):
login_service = "My Service"
#gen.coroutine
def authenticate(self,handler,data=None):
rawd = None
# If we receive no data we redirect to login page
while (rawd is None):
try:
rawd = handler.get_argument("data")
except:
handler.redirect("<The login URL>")
return None
# Do some verification and get the data here.
# Get the data from the parameters send to your hub from the login page, say username, access_token and email. Wrap everythin neatly in a dictionary and return it.
userdict = {"name": username}
userdict["auth_state"] = auth_state = {}
auth_state['access_token'] = verify
auth_state['email'] = email
#return the dictionary
return userdict
Simply add the file to the Python path, so that Jupyterhub is able to find it and make the necessary configurations in your jupyterhub_config.py file.

HTTP requests in python with basic authentication

I have a piece of code that includes several GET requests. I have a simple if statement to check the status code and provide auth if I receive a status code of 401. How do I avoid having a separate if statement for every single request line I have?
I'd like to be able to provide credentials only once if its protected instead of having to provide it with every single request.
EDIT - Added some code for reference
response = requests.get(self.uri + '/v2/apps')
if (response.status_code == 401):
print("It appears that your marathon instance is protected, please enter your username and password")
usr = input("Username: ")
pwd = getpass.getpass("Password: ")
response = requests.get(self.uri + '/v2/apps', auth=(usr,pwd)).json()
First, if you get a status code of 401, it means you're not authorized to use the resource.
Second, you have different options for authenticating the client once. Usually, I recommend looking into using HTTPS for transporting the data. You can use:
You can use a cookie with expiry
Using a token service in your web server
There multiple authentication standards (e.g. SAML, OAuth, OpenId, etc.) already existing, try not to reinvent the wheel.

Cherrypy Authenticate every request without caching

I wrote a cherrypy server to facilitate a file download and I used cherrypy auth digest to authenticate it. The configuration for this is:
conf = {
'/getXML': {
'tools.auth_digest.on': True,
'tools.auth_digest.realm': None,
'tools.auth_digest.get_ha1': auth_digest.get_ha1_dict_plain(USERS),
'tools.auth_digest.key': <some_key>
}
}
What is the role of that key?
Also, upon successful authentication, when I hit the server again it remembers login and does not prompt for credentials again. How can I ask for credentials for each and every request without remembering login?
Think of the key as a session id. You generate it once the user comes to your site...
cherrypy.session['_csrf_token'] = ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(16))
then you set that id in the user's cookies and you compare the two keys to be sure you have the same user. That is the concept behind using the 'tools.sessions.on': True, setting in cherrypy. This allows you to know a user from one page to another in a stateless environment such as http.
https://cherrypy.readthedocs.org/en/3.3.0/refman/lib/auth_digest.html#cherrypy.lib.auth_digest.HttpDigestAuthorization.validate_nonce
**
validate_nonce(s, key)
Validate the nonce. Returns True if nonce was generated by synthesize_nonce() and the timestamp is not spoofed, else returns False.
s
A string related to the resource, such as the hostname of the server.
key
A secret string known only to the server.
Both s and key must be the same values which were used to synthesize the nonce we are trying to validate.
**
looks like forcing a logout with auth digest is not possible...
https://groups.google.com/d/msg/cherrypy-users/M-GUFH2mU_M/45zHnA5Y6XMJ
Here's more details on the digest authentication...
What is digest authentication?
But this is a simple authenication where you can force a logout...
How to logout from a simple web appl. in CherryPy, Python
Hope this helps!

Categories