Omniauth session lost during authentication - mongoid

I'm working on an application which uses Discord OAuth as a authentication method. Because this app needs a Discord account to have, I just use omniauth-discord, and not any local authentication methodology.
But during the authentication after the Discord callback I loose the session. The user will be created in the MongoDB, but after that, the current_user will give back a nil value.
I can call the Discord OAuth without any issues, and get the information from the callback, store in the MongoDB, but than if I try to authorize the user to use the site, there isn't an available session, so the logininfo will give back 401 every time. If I try to sign in again, I always get the Discord consent page.
I've already tried many different codes from tutorials, but most of them uses Devise for local login, and authentication, then connect the local user with the Discord one.
Also tried to init the session in very different part of the code, without any luck.
I've created a github repo for the current situation of the code. You can find it here:
https://github.com/montyx99/ppapi
My sessions_contoller.rb:
require 'json'
class SessionsController < ApplicationController
def new
redirect_to '/auth/discord'
end
def current_user
super
end
def create
session[:init] = true
auth = request.env["omniauth.auth"]
user = User.where(:discord_id => auth.extra.raw_info.id).first || User.create_with_omniauth(auth)
p user
p user.discord_id
# Reset the session after successful login, per
# 2.8 Session Fixation – Countermeasures:
# http://guides.rubyonrails.org/security.html#session-fixation-countermeasures
reset_session
session[:user_id] = user.discord_id
p session[:user_id]
redirect_to "http://localhost:3000", :notice => 'Signed in!'
end
def destroy
reset_session
redirect_to "http://localhost:3000", :notice => 'Signed out!'
end
def failure
redirect_to "http://localhost:3000", :alert => "Authentication error: #{params[:message].humanize}"
end
def logininfo
p current_user
if current_user
render :json => true, :status => 200
else
render :json => false, :status => 401
end
end
end
The application_controller.rb
class ApplicationController < ActionController::API
def current_user
begin
#current_user ||= User.find(session[:user_id]) if session[:user_id]
rescue Exception => e
nil
end
end
def user_signed_in?
return true if current_user
end
def correct_user?
#user = User.find(params[:id])
unless current_user == #user
redirect_to root_url, :alert => "Access denied."
end
end
def authenticate_user!
if !current_user
redirect_to root_url, :alert => 'You need to sign in for access to this page.'
end
end
def logging
id = ENV["DISCORD_CLIENT_ID"]
secret = ENV["DISCORD_CLIENT_SECRET"]
end
end
Expected:
After the first handshake with Discord, the user need to be able to login again next time without the Discord Consent page

At first You’re assigning the discord_id as your user ID in session[:user_id]. Then when you call User.find in current_user, you’re not then searching against your discord_id user property, find will search by user.id by default. Your actual user.id is probably an Integer assigned by the database.
Also your begin is redundant in current_user. Take it out and the first end and indent back. Begin at the start of a method is implied so you only need to rescue before you end.

Related

How can I get a more specific 400 validation error instead of a 500 error when using params.require?

I'm working through a full-stack project using Rails on the back end and React on the front end. I have a Sign Up form designed to accept a new user's information and avatar and pass it to the Users controller on the back end. For whatever reason though, if the form is empty or incomplete, it throws a 500 Internal Server Error instead of a more informative 400 error.
User Controller is :
``
class UsersController < ApplicationController
rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity_response
def index
users = User.all
render json: users
end
def create
user = User.create(user_params)
session[:user_id] = user.id
render json: user, status: :created
end
private
def user_params
params.require(:user).permit(:first_name, :last_name, :email, :birthday, :username, :password, :password_confirmation, :avatar)
end
def render_unprocessable_entity_response(invalid)
render json: { errors: invalid.record.errors.full_messages }, status: :unprocessable_entity
end
end
Sign Up Form in React (specifically, my Submit function)
function handleSubmit(event) {
event.preventDefault();
const data = new FormData();
data.append("[user][first_name]", firstName);
data.append("[user][last_name]", lastName);
data.append("[user][email]", email);
data.append("[user][username]", username);
data.append("[user][password]", password);
data.append("[user][password_confirmation]", passwordConfirmation);
data.append("[user][avatar]", avatar);
fetch("/signup", {
method: "POST",
body: data
})
.then((response) => {
if (response.ok) {
response.json().then((user) => onLogin(user));
} else {
response.json().then((errorData) => contextData.setErrors(errorData.errors));
}
})
};
Response I'm getting in the console when I submit an empty form:
Parameters: {"user"=>{"first_name"=>"", "last_name"=>"", "email"=>"", "username"=>"", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "avatar"=>""}}
Completed 500 Internal Server Error in 97ms (ActiveRecord: 49.6ms | Allocations: 9234)
When I remove the "params.require(:user)" portion, it gives me the validation errors I was expecting, but then I'm unable to create a new user due to have ActiveStorage works and requires data.
There is a lot wrong in the premise of this question so please bear with me.
First lets look at the docs for ActionController::Parameters#require:
Ensures that a parameter is present. If it's present, returns the
parameter at the given key, otherwise raises an
ActionController::ParameterMissing error.
Rails will rescue this error on the framework level but you can rescue it on a per controller level with rescue_from 'ActionController::ParameterMissing'. But remember that the purpose of require isn't validation. Its just to bail early if the request is missing the correct structure and should not be processed anyways - because classical Rails applications nest their parameters in a key with the same name as the resource by default.
In an API application you don't even have to follow that pattern - you could just as well use "flat parameters" or standards like JSONAPI.org.
Then there is the use of:
rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity_response
This pops up from time to time as a "clever" way to DRY out API's. In your case it won't even work because User.create does not raise - User.create! does.
It's also not a good use of exceptions. Exceptions should be used for exceptional events that should cause an immediate halt of the execution. A user passing invalid input is not exceptional in any way - in fact its completely mundane.
If you want to DRY your API in a good way instead do it the way that the Responders gem does:
class UsersController < ApplicationController
def create
user = User.create(user_params)
session[:user_id] = user.id
respond_with(user)
end
end
class ApplicationController
# ...
private
def respond_with(resource)
if resource.valid?
resource.new_record? ? render_created_response(resource) : render_updated_response(resource)
else
render_unprocessable_entity_response(resource)
end
end
def render_created_response(resource)
render json: resource, status: :created
end
def render_updated_response(resource)
render json: resource, status: :ok
end
end
This creates an explicit flow of methods that can be overridden in subclasses. Instead of an implicit flow created by what is actually a callback.
class UsersController < ApplicationController
private
def render_updated_response
# do something wild and craazy
end
end
Reserve the use of create! for contexts where creating the record should not be expected to fail (like in a seed file or non-interactive contexts) or to cause rollbacks inside of a transaction.

how does get_current_user work

I'm really confused how Google App Engine's User's get_current_user() works. I've looked around the internet at a bunch of different guides and tutorials about login and authentication, and many of them mention similar methods.
If there are a million users logged in to my application at the same time, how can that method possibly work? Does each user get their own instance of the server? How does the server know which client it is talking to?
It doesn't make sense to me at all.
When logging in (by clicking on the URL generated by create_login_url()) a cookie containing user identifying information is prepared and pushed on the client side, then used in subsequent requests until the user logs out or the cookie expires. Calling get_current_user() simply checks the cookie existance/information and responds accordingly.
On the development server the cookie is named dev_appserver_login. I can no longer check the cookie name on GAE as I switched away from the Users API.
The actual handling of the cookie seems to happen somewhere on the Users service backend, for example, by looking at the google/appengine/api/users.py file in the python SDK:
def create_login_url(dest_url=None, _auth_domain=None,
federated_identity=None):
...
req = user_service_pb.CreateLoginURLRequest()
resp = user_service_pb.CreateLoginURLResponse()
try:
apiproxy_stub_map.MakeSyncCall('user', 'CreateLoginURL', req, resp)
...
The end point (at least for the development server) seems to somehow land somewhere in google/appengine/tools/appengine_rpc.py, for example:
#staticmethod
def _CreateDevAppServerCookieData(email, admin):
"""Creates cookie payload data.
Args:
email: The user's email address.
admin: True if the user is an admin; False otherwise.
Returns:
String containing the cookie payload.
"""
if email:
user_id_digest = hashlib.md5(email.lower()).digest()
user_id = "1" + "".join(["%02d" % ord(x) for x in user_id_digest])[:20]
else:
user_id = ""
return "%s:%s:%s" % (email, bool(admin), user_id)
def _DevAppServerAuthenticate(self):
"""Authenticates the user on the dev_appserver."""
credentials = self.auth_function()
value = self._CreateDevAppServerCookieData(credentials[0], True)
self.extra_headers["Cookie"] = ('dev_appserver_login="%s"; Path=/;' % value)

Facebook login in Google Cloud Endpoints

Can someone explain steps to implement login process with other OAuth2 providers
This link Google Cloud Endpoints with another oAuth2 provider gives little info about writing custom authentication, but I guess for beginner like me that's not enough, please give detailed steps.
Especially, interested in Facebook.
You need to implement Facebook's client side APIs according to their documentation and the environment you are deploying your client app to (Browser vs iOS vs Android). This includes registering your app with them. Your registered app will direct the user to go through an authentication flow and at the end of it your client app will have access to a short-lived access token. Facebook has multiple types of access tokens, but the one it sounds like you're interested in is called a User Access Token since it identifies an authorized user.
Pass the access token to your Cloud Endpoints API via a field or header. Inside of your API code receive the access token and implement Facebook's API that checks the validity of the access token. The first answer on this SO question makes it look rather easy, but you probably want to reference their documentation again. If that check passes then you would run your API code, otherwise throw an exception.
You will typically also want to implement a caching mechanism to prevent calling the Facebook server side validation API for each Cloud Endpoints request.
Finally, I mentioned that your client app has a short lived token. If you have a client app that is browser-based then you will probably want to upgrade that to a long lived token. Facebook has a flow for that as well which involves your API code requesting a long lived token with the short lived one. You would then need to transfer that long lived token back to the client app to use for future Cloud Endpoints API calls.
If your client app is iOS or Android based then your tokens are managed by Facebook code and you simply request access tokens from the respective APIs when you need them.
So I actually tried to implement that custom authentication flow. It seems working fine although there might be further consideration on security side.
First, user go to my application and authenticate with facebook, the application got his user_id and access_token. Then the application call auth API to the server with these info.
class AuthAPI(remote.Service):
#classmethod
def validate_facebook_user(cls, user_id, user_token):
try:
graph = facebook.GraphAPI(user_token)
profile = graph.get_object("me", fields='email, first_name, last_name, username')
except facebook.GraphAPIError, e:
return (None, None, str(e))
if (profile is not None):
# Check if match user_id
if (profile.get('id', '') == user_id):
# Check if user exists in our own datastore
(user, token) = User.get_by_facebook_id(user_id, 'auth', user_token)
# Create new user if not
if user is None:
#print 'Create new user'
username = profile.get('username', '')
password = security.generate_random_string(length=20)
unique_properties = ['email_address']
if (username != ''):
(is_created, user) = User.create_user(
username,
unique_properties,
email_address = profile.get('email', ''),
name = profile.get('first_name', ''),
last_name = profile.get('last_name', ''),
password_raw = password,
facebook_id = user_id,
facebook_token = user_token,
verified=False,
)
if is_created==False:
return (None, None, 'Cannot create user')
token_str = User.create_auth_token(user.get_id())
#print (user, token_str)
# Return if user exists
if token is not None:
return (user, token.token, 'Successfully logged in')
else:
return (None, None, 'Invalid token')
return (None, None, 'Invalid facebook id and token')
# Return a user_id and token if authenticated successfully
LOGIN_REQ = endpoints.ResourceContainer(MessageCommon,
type=messages.StringField(2, required=True),
user_id=messages.StringField(3, required=False),
token=messages.StringField(4, required=False))
#endpoints.method(LOGIN_REQ, MessageCommon,
path='login', http_method='POST', name='login')
def login(self, request):
type = request.type
result = MessageCommon()
# TODO: Change to enum type if we have multiple auth ways
if (type == "facebook"):
# Facebook user validation
user_id = request.user_id
access_token = request.token
(user_obj, auth_token, msg) = self.validate_facebook_user(user_id, access_token)
# If we can get user data
if (user_obj is not None and auth_token is not None):
print (user_obj, auth_token)
result.success = True
result.message = msg
result.data = json.dumps({
'user_id': user_obj.get_id(),
'user_token': auth_token
})
# If we cannot
else:
result.success = False
result.message = msg
return result
In addition to this, you might want to implement the normal user authentication flow following instruction here: http://blog.abahgat.com/2013/01/07/user-authentication-with-webapp2-on-google-app-engine/ .
This is because the user_id and user_token that I obtain was provided by webapp2_extras.appengine.auth.
Implementation of User.get_by_facebook_id:
class User(webapp2_extras.appengine.auth.models.User):
#classmethod
def get_by_facebook_id(cls, fb_id, subj='auth', fb_token=""):
u = cls.query(cls.facebook_id==fb_id).get()
if u is not None:
user_id = u.key.id()
# TODO: something better here, now just append the facebook_token to a prefix
token_str = "fbtk" + str(fb_token)
# get this token if it exists
token_key = cls.token_model.get(user_id, subj, token_str)
print token_key, fb_token
if token_key is None:
# return a token that created from access_token string
if (fb_token == ""):
return (None, None)
else:
token = cls.token_model.create(user_id, subj, token_str)
else:
token = token_key
return (u, token)
return (None, None)
Server verify if the user is authenticated with facebook once more time. If it passes, user is considered logged in. In this case, server pass back a user_token (generated based on facebook_token) and user_id from our datastore.
Any further API calls should use this user_id and user_token
def get_request_class(messageCls):
return endpoints.ResourceContainer(messageCls,
user_id=messages.IntegerField(2, required=False),
user_token=messages.StringField(3, required=False))
def authenticated_required(endpoint_method):
"""
Decorator that check if API calls are authenticated
"""
def check_login(self, request, *args, **kwargs):
try:
user_id = request.user_id
user_token = request.user_token
if (user_id is not None and user_token is not None):
# Validate user
(user, timestamp) = User.get_by_auth_token(user_id, user_token)
if user is not None:
return endpoint_method(self, request, user, *args, **kwargs )
raise endpoints.UnauthorizedException('Invalid user_id or access_token')
except:
raise endpoints.UnauthorizedException('Invalid access token')
#endpoints.api(name='blah', version='v1', allowed_client_ids = env.CLIENT_IDS, auth=AUTH_CONFIG)
class BlahApi(remote.Service):
# Add user_id/user_token to the request
Blah_Req = get_request_class(message_types.VoidMessage)
#endpoints.method(Blah_Req, BlahMessage, path='list', name='list')
#authenticated_required
def blah_list(self, request, user):
newMessage = BlahMessage(Blah.query().get())
return newMessage
Note:
I am using this library to handle facebook authentication checking on server: https://github.com/pythonforfacebook/facebook-sdk
I implemented this use case by adding a webapp2 handler to exchange the Facebook access token for one generated by my own application, using the SimpleAuth mixin for verification:
class AuthHandler(webapp2.RequestHandler, SimpleAuthHandler):
"""Authenticates a user to the application via a third-party provider.
The return value of this request is an OAuth token response.
Only a subset of the PROVIDERS specified in SimpleAuthHandler are currently supported.
Tested providers: Facebook
"""
def _on_signin(self, data, auth_info, provider):
# Create the auth ID format used by the User model
auth_id = '%s:%s' % (provider, data['id'])
user_model = auth.get_auth().store.user_model
user = user_model.get_by_auth_id(auth_id)
if not user:
ok, user = user_model.create_user(auth_id)
if not ok:
logging.error('Unable to create user for auth_id %s' % auth_id)
self.abort(500, 'Unable to create user')
return user
def post(self):
# Consider adding a check for a valid endpoints client ID here as well.
access_token = self.request.get('x_access_token')
provider = self.request.get('x_provider')
if provider not in self.PROVIDERS or access_token is None:
self.abort(401, 'Unknown provider or access token')
auth_info = {'access_token': access_token}
fetch_user_info = getattr(self, '_get_%s_user_info' % provider)
user_info = fetch_user_info(auth_info)
if 'id' in user_info:
user = self._on_signin(user_info, auth_info, provider)
token = user.create_bearer_token(user.get_id())
self.response.content_type = 'application/json'
self.response.body = json.dumps({
'access_token': token.token,
'token_type': 'Bearer',
'expires_in': token.bearer_token_timedelta.total_seconds(),
'refresh_token': token.refresh_token
})
else:
self.abort(401, 'Access token is invalid')
The exchanged access token can be passed on each endpoints request in the Authorization header, or as part of the RPC message if you prefer. Here's an example of reading it from the header:
def get_current_user():
token = os.getenv('HTTP_AUTHORIZATION')
if token:
try:
token = token.split(' ')[1]
except IndexError:
pass
user, _ = User.get_by_bearer_token(token)
return user
I posted the complete example on Github: https://github.com/loudnate/appengine-endpoints-auth-example
So no body has thrown a light on the android client side stuff. Since, you do not require Google login in this case hence the code for getting api handle will look like:
private Api getEndpointsApiHandle() {
Api.Builder api = new Api.Builder(HTTP_TRANSPORT, JSON_FACTORY, null);
api.setRootUrl(yourRootUrl);
return api.build();
}
If you notice; You will require to pass null as the Credential. This code works like a charm
I too have written my own solution for this problem. You can check out the code here: https://github.com/rggibson/Authtopus
Authtopus is a python library for custom authentication with Google Cloud Endpoints. It supports basic username and password registrations + logins, as well as logins via Facebook and Google (and could probably be extended to support other social providers without too much hassle). I know this doesn't directly answer the original question, but it seems related enough that I thought I'd share.

GAE put object, redirect, then query for object returns null?

All,
I am in the process of learning Google App Engine / Webapp2 and i'm having trouble saving an object to the datastore, redirecting to another page/handler, then fetching that object from the datastore. Forgive me if there is an easy answer to this question. The following is a description of the code I have.
I have a base handler:
class BaseHandler(webapp2.RequestHandler):
def set_secure_cookie(self, name, val):
cookie_val = make_secure_val(val)
self.response.headers.add_header(
'Set-Cookie',
'%s=%s; Path=/' % (name, cookie_val))
def get_secure_cookie(self, name):
cookie_val = self.request.cookies.get(name)
return cookie_val and check_secure_val(cookie_val)
def login(self, user):
self.set_secure_cookie('user', str(user.name))
# Called before every request and stores user object
def initialize(self, *a, **kw):
webapp2.RequestHandler.initialize(self, *a, **kw)
username = self.get_secure_cookie('user')
self.user = username and User.by_name(str(username))
I have a Signup page which inherits from BaseHandler:
class Signup(BaseHandler):
def get(self):
# Get the page
def post(self):
has_error = False
# Extract and validate the input
if has_error:
#Re-render the form
else:
new_user = User.register(self.username, self.password, self.email)
new_user.put()
self.login(new_user)
self.redirect("/blog/welcome")
If the user is a new user, the User db.Model object is created, the user is stored to the datastore, a user cookie is set and we are redirected to the Welcome handler:
class Welcome(BaseHandler):
def get(self):
if self.user:
self.render('welcome.html', username = self.user.name)
else:
self.redirect('/blog/signup')
The intent here is that upon redirect, BaseHandler.initialize() would get called and would set self.user of the new user I just created.
Here is what I know:
- When signing up a new user, I am redirected back to the signup page.
- If I then manually navigate to /blog/welcome, the page loads correctly with the new username populated.
If I add the following logging statements into Welcome.get():
username = self.get_secure_cookie('user')
logging.info("Cookie %r obtained inside of Welcome.get().", username)
logging.info("Found user %r", User.by_name(str(username)))
The cookie is obtained for the new username but no User object is found. Again, if I navigate directly to /blog/welcome, the logs report that the cookie is obtained and the User object is found for the new user.
The User object looks like so:
def users_key(group = 'default'):
return db.Key.from_path('users', group)
class User(db.Model):
name = db.StringProperty(required = True)
password = db.StringProperty(required = True)
email = db.StringProperty()
#classmethod
def by_name(cls, name):
u = User.all().filter('name =', name).get()
return u
#classmethod
def register(cls, name, password, email = None):
return User(parent = users_key(),
name = name,
password = password,
email = email)
Is there something about the datastore that is causing this first query to get the new user to return nothing? How should I proceed in debugging this? Is there additional reading I should do? (I have tried to provide all necessary code snippets but I can provide additional code if required.)
Just guessing, but I suspect self.username, self.password and self.email in your RequestHandler are not set to anything. I'm assuming you're getting those paramters from the request POST data, but that's not happening in the code shown.
The other potential problem is that your query is eventually consistent, and may not reflect recent changes (ie new User entity). It would be much better if you fetch the user by it's key or id with a get() call instead of a query via filter().

Detecting first time login of user into application (Google Appengine)

My app requires users to login using their google account.
I have this set in my App.yamp file:
url: /user/.*
script: user.py
login: required
Now when any user tries to access files under /user/secret.py he will need to authenticate via google, which will redirect the user back to /user/secret.py after successful authentication. Now the problem I am facing is when the user is redirected back to the app, I cannot be sure if this is the first time the user has logged in or is it a regular user to my site who has come back again from just the user object which google passes using users.get_current_user() .
I thus need to maintain state in the datastore to check if the user already exists or not everytime. If he does not exist i need to create a new entry with other application specific settings.
My question is: Is there some easier way to handle this? without having to query the datastore to figure if this is a first time user or a regular one?
No, Google doesn't keep track of if a user has logged in to your app before. Since you presumably need to store some sort of state against the user, the simplest way is to try and retrieve the user's record from the datastore. If they don't have one, you can send them to the registration screen to gather this information. You can use memcache to cache a user's information and avoid extra datastore round-trips.
I tend to use my own user and session manangement
For my web handlers I will attach a decorator called session and one called authorize. The session decorator will attach a session to every request, and the authorize decorator will make sure that the user is authorised
(A word of caution, the authorize decorator is specific to how I develop my applications - the username being the first parameter in most requests)
So for example a web handler may look like:
class UserProfile(webapp.RequestHandler):
#session
#authorize
def get(self, user):
# Do some funky stuff
# The session is attached to the self object.
someObjectAttachedToSession = self.SessionObj.SomeStuff
self.response.out.write("hello %s" % user)
In the above code, the session decorator attaches some session stuff that I need based on the cookies that are present on the request. The authorize header will make sure that the user can only access the page if the session is the correct one.
The decorators code are below:
import functools
from model import Session
import logging
def authorize(redirectTo = "/"):
def factory(method):
'Ensures that when an auth cookie is presented to the request that is is valid'
#functools.wraps(method)
def wrapper(self, *args, **kwargs):
#Get the session parameters
auth_id = self.request.cookies.get('auth_id', '')
session_id = self.request.cookies.get('session_id', '')
#Check the db for the session
session = Session.GetSession(session_id, auth_id)
if session is None:
self.redirect(redirectTo)
return
else:
if session.settings is None:
self.redirect(redirectTo)
return
username = session.settings.key().name()
if len(args) > 0:
if username != args[0]:
# The user is allowed to view this page.
self.redirect(redirectTo)
return
result = method(self, *args, **kwargs)
return result
return wrapper
return factory
def session(method):
'Ensures that the sessions object (if it exists) is attached to the request.'
#functools.wraps(method)
def wrapper(self, *args, **kwargs):
#Get the session parameters
auth_id = self.request.cookies.get('auth_id', '')
session_id = self.request.cookies.get('session_id', '')
#Check the db for the session
session = Session.GetSession(session_id, auth_id)
if session is None:
session = Session()
session.session_id = Session.MakeId()
session.auth_token = Session.MakeId()
session.put()
# Attach the session to the method
self.SessionObj = session
#Call the handler.
result = method(self, *args, **kwargs)
self.response.headers.add_header('Set-Cookie', 'auth_id=%s; path=/; HttpOnly' % str(session.auth_token))
self.response.headers.add_header('Set-Cookie', 'session_id=%s; path=/; HttpOnly' % str(session.session_id))
return result
return wrapper
def redirect(method, redirect = "/user/"):
'When a known user is logged in redirect them to their home page'
#functools.wraps(method)
def wrapper(self, *args, **kwargs):
try:
if self.SessionObj is not None:
if self.SessionObj.settings is not None:
# Check that the session is correct
username = self.SessionObj.settings.key().name()
self.redirect(redirect + username)
return
except:
pass
return method(self, *args, **kwargs)
return wrapper
Can you not just set a Cookie the first time the user logs in and check for this? If they're a new user it won't be there and but if they're an old user it will be. It's not 100% accurate since some users might clear their cookies but it might do depending on what it is you want to achieve.
If you're using Django in your application managing Cookies is pretty straightforward.
I agree that managing your own authenticated users is the best way to approach this problem. Depending on your application scope obviously but at the very least an AuthUser(Model) class that contains the UserProperty for the users that have logged in with your account.
...
class AuthUser(db.Model):
user = UserProperty(required=True)
...
Then when a user logs in just
...
user = users.get_current_user()
user_exists = AuthUser.gql('where user = :1', user) # or easy check db.GqlQuery("select __key__ from AuthUser where user = :1", user)
if user_exists:
# do user has been before stuff
else:
# do first time user stuff
...
Alternately a super easy way to do this is have a Model for your site that has a ListProperty(users.User) and then you can easily check the list to see if the user has been into your app before.
...
class SiteStuff(db.Model):
auth_users = ListProperty(users.User)
...
and when they log in: check if they are in the list; if not, you add them to the list, put() it and do whatever you need to do for first time users. If you find them in there then do the other stuff.

Resources