22
votes

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.

5

5 Answers

20
votes

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.

5
votes

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:

4
votes

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

1
votes

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

0
votes

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.