mpiekarz
mpiekarz

Reputation: 271

Authenticating user against app engine endpoints

I am creating my first app engine app and having problems with authenticating the users.

I have followed the https://cloud.google.com/appengine/docs/python/endpoints/consume_android#making_authenticated_calls - seems a bit magical, that I just "setAccountName" and it is supposed to work, but w/e, I guess that it should load the app scopes from Android Audience and then just check if the account name I passed has actually logged into the device.

The API call works, passes the authentication, but sadly - the "endpoints.get_current_user()" function on the backend returns None.

So I kept digging, but I can't seem to find anything on the topic. Best thing I have found is http://blog.notdot.net/2010/05/Authenticating-against-App-Engine-from-an-Android-app - but that's an article from 6 years ago and the author uses HTTP client and nothing related to endpoints libs.

All I can think of would be to follow some "non-endpoints" way of adding "login with Google" to my app and then try to pass the credentials I would get to my API builder, but that just feels wrong, like there should be an easier way to do that.

So, am I missing some step, that was not mentioned in https://cloud.google.com/appengine/docs/python/endpoints/consume_android#making_authenticated_calls ?

Actual code (slightly simplified) below:

Backend:

auth_api = endpoints.api(
    name='auth_api',
    version='v1.0',
    auth_level=endpoints.AUTH_LEVEL.REQUIRED,
    allowed_client_ids=[
        ANDROID_CLIENT_ID,
        WEB_CLIENT_ID,
        endpoints.API_EXPLORER_CLIENT_ID,
    ],
    audiences=[
        WEB_CLIENT_ID,
        endpoints.API_EXPLORER_CLIENT_ID,
    ],
    scopes=[
        'https://www.googleapis.com/auth/userinfo.profile',
        'https://www.googleapis.com/auth/userinfo.email',
    ],
)


@auth_api.api_class(resource_name='rating')
class RatingHandler(remote.Service):
    @endpoints.method(
        message_types.VoidMessage,
        RatingsMessage,
        path='rating/getRatings',
        http_method='GET',
    )
    def getRatings(self, request):
        rating_query = Rating.query(
            ancestor=ndb.Key(
                Account,
                endpoints.get_current_user().user_id(), // ERROR! endpoints.get_current_user() is None
            )
        ).order(-Rating.rating)

Client:

// Somewhere these lines exist
if (credential == null || credential.getSelectedAccountName() == null) {
    startActivityForResult(
        AuthUtils.getCredentials(getActivity()).newChooseAccountIntent(),
        AuthUtils.REQUEST_ACCOUNT_PICKER
    );
} else {
    LoadRatings();
}

@Override
public void onActivityResult(
    int requestCode,
    int resultCode,
    Intent data
) {
    super.onActivityResult(requestCode, resultCode, data);
    if (data != null && data.getExtras() != null) {
        String accountName =
            data.getExtras().getString(
                AccountManager.KEY_ACCOUNT_NAME
            );
        if (accountName != null) {
            credential = GoogleAccountCredential.usingAudience(
                getApplicationContext(),
                "server:client_id:" + Constants.ANDROID_AUDIENCE
            );
            credential.setSelectedAccountName(accountName);
            LoadRatings();
        }
    }
}

public void LoadRatings() {
    // AuthApi was auto-generated by Google App Engine based on my Backend
    AuthApi.Builder api = new AuthApi.Builder(
        AndroidHttp.newCompatibleTransport(),
        new AndroidJsonFactory(),
        credential
    ).setApplicationName(getPackageName());
    AuthApi service = api.build();
    try {
        ApiMessagesRatingRatedBeerListMessage result = service.rating().getRatings().
        // some stuff done with result, but the Exception is thrown in line above

Upvotes: 2

Views: 275

Answers (2)

mpiekarz
mpiekarz

Reputation: 271

OK, I figured it out. When I removed "scopes" from API declaration, it works. I'm not sure how I am going to access user's email / profile yet, but that's at least a step forward.

This issue has been actually brought up before - How to add more scopes in GoogleCloud Endpoints - sadly, without any answers

Upvotes: 2

Micro
Micro

Reputation: 10891

You won't have a User object (Entity that is) on the backend created for you. You have to do that yourself. For example:

@Entity
public class AppEngineUser {
    @Id
    private String email;
    private User user;
    private AppEngineUser() {}
    public AppEngineUser(User user) {
        this.user = user;
        this.email = user.getEmail();
    }
    public User getUser() {
        return user;
    }
    public Key<AppEngineUser> getKey() {
        return Key.create(AppEngineUser.class, email);
    }
}

When you create an API method and specify a User object like this:

 @ApiMethod(name = "insertRecord", path = "insert_record", httpMethod = HttpMethod.POST)
    public Record insertRecord(User user,  Record record)

        // check if google user is authenticated
        throws UnauthorizedException {
        if (user == null) {
            throw new UnauthorizedException("Authorization required");
        }

        // user is authenticated... do some stuff!
}

The User object is an injected type. It is actually: com.google.appengine.api.users.User. See "injected types" on https://cloud.google.com/appengine/docs/java/endpoints/paramreturn_types.

What this means is that GAE injects the Google user object if the user provided the correct credentials to their Google+ account. If they did not, then User will be null. If it is, you can throw an UnauthorizedException the way the above method does.

You can now get things like the User's gmail address among other things if the user object is not null. From there, you must store those values in your own custom entity, such as AppEngineUser and save it to the datastore. Then you can do other things with it later, such as load it, check if the user is registered and whatever else all by yourself.

Hope that helps!

Upvotes: 0

Related Questions