Play-games-plugin-for-unity: Re-authentication with server using auth code

Created on 29 Jun 2017  路  8Comments  路  Source: playgameservices/play-games-plugin-for-unity

As @claywilkinson stated here https://github.com/playgameservices/play-games-plugin-for-unity/issues/1637#issuecomment-304988328

The best practice is to pass the auth code to the backendserver, which exchanges the code for an access token and a refresh token. The server should persist the refresh token so when the access token expires, the refresh token can be used to get a new token.

The force flag on RequestServerAuthCode is used when the server loses the refresh token. This flag causes the player to re-consent to the token, so its use is discouraged.

You can get a new server auth code every time the player signs in, the only difference is that when it is exchanged for an access token, there will be no refresh token associated with the access token.

I currently have an application where Google is used as one of the primary authentication providers, the client passes the google id and a one-time use auth code to the server for login, the server exchanges the auth code for an access token using https://www.googleapis.com/oauth2/v4/token and then uses https://www.googleapis.com/games/v1/applications/%s/verify to compare the google id sent by the client against the result payload..

    protected BoundRequestBuilder buildVerifyRequest(String accessToken) {
        return asyncHttpClient.prepareGet(verifyUrl).addHeader("Authorization", String.format("Bearer %s", accessToken));
    }

    protected BoundRequestBuilder buildAuthorizeRequest(List<Param> params) {
        return asyncHttpClient.preparePost(AUTHORIZE_URL).setFormParams(params);
    }

    private AsyncHttpClientConfig buildClientConfig() {
        return new DefaultAsyncHttpClientConfig.Builder()
                .setConnectTimeout((int) TimeUnit.SECONDS.toMillis(connectTimeout))
                .setReadTimeout((int) TimeUnit.SECONDS.toMillis(connectTimeout))
                .build();
    }

    public boolean isAuthenticated(@NotNull String googlePlayId, @NotNull String authenticationCode) {
        long start = System.currentTimeMillis();
        boolean success = false;

        // TODO: replace prototype code with async api
        try {
            AuthorizationResult authorizationResult = getAuthorizationResult(authenticationCode);

            if (logger.isDebugEnabled()) {
                logger.debug("Auth result for {} is {}", authenticationCode, authorizationResult);
            }
            if (authorizationResult.getError() != null) {
                logger.warn("Failed verifying access token {}, {}", authorizationResult.getError(), authorizationResult.getErrorDescription());
                return false;
            }

            if (authorizationResult.getRefreshToken() != null) {
                // TODO: persist refresh token?
            } else {
                if (logger.isDebugEnabled()) {
                    logger.debug("No authentication token and refresh token is null for {} {}", googlePlayId, authenticationCode);
                }
            }

            VerifyResult verifyResult = getVerifyResult(authorizationResult.getAccessToken());
            if (logger.isDebugEnabled()) {
                logger.debug("Verify result for {} is {}", authorizationResult.getAccessToken(), verifyResult);
            }
            if (verifyResult != null) {
                if (verifyResult.getError() != null) {
                    logger.warn("Failed verifying access token, " + verifyResult.getError());
                    return false;
                }
                success = true;
                boolean idMatches = googlePlayId.equals(verifyResult.getPlayerId()) || googlePlayId.equals(verifyResult.getAlternatePlayerId());
                if (!idMatches) {
                    logger.warn("Id mismatch {}, {}", googlePlayId, verifyResult);
                }
                return idMatches;
            }
        } catch (InterruptedException | ExecutionException | IOException e) {
            logger.error("Failed verifying access token", e);
        } finally {
            externalAPILatencyMeasurementHelper.addMeasurement(start, success);
        }
        return false;
    }

    private VerifyResult getVerifyResult(String accessToken) throws InterruptedException, ExecutionException, IOException {
        Response response = buildVerifyRequest(accessToken).execute().get();
        byte[] responseBytes = response.getResponseBodyAsBytes();
        try {
            return JsonUtils.getObjectMapper().readValue(responseBytes, VerifyResult.class);
        } catch (JsonParseException e) {
            logger.error("Failed parsing response " + new String(responseBytes, StandardCharsets.UTF_8));
            throw e;
        }
    }

    private AuthorizationResult getRefreshResult(String refreshToken) throws InterruptedException, ExecutionException, IOException {
        List<Param> params = new ArrayList<>(4);
        params.add(new Param("client_id", clientId));
        params.add(new Param("client_secret", clientSecret));
        params.add(new Param("refresh_token", refreshToken));
        params.add(new Param("grant_type", "refresh_token"));
        Response response = buildAuthorizeRequest(params).execute().get();
        byte[] responseBytes = response.getResponseBodyAsBytes();
        try {
            return JsonUtils.getObjectMapper().readValue(responseBytes, AuthorizationResult.class);
        } catch (JsonParseException e) {
            logger.error("Failed parsing response " + new String(responseBytes, StandardCharsets.UTF_8));
            throw e;
        }
    }

    private AuthorizationResult getAuthorizationResult(String authenticationCode) throws InterruptedException, ExecutionException, IOException {
        List<Param> params = new ArrayList<>(4);
        params.add(new Param("client_id", clientId));
        params.add(new Param("client_secret", clientSecret));
        params.add(new Param("code", authenticationCode));
        params.add(new Param("grant_type", "authorization_code"));
        Response response = buildAuthorizeRequest(params).execute().get();
        byte[] responseBytes = response.getResponseBodyAsBytes();
        try {
            return JsonUtils.getObjectMapper().readValue(responseBytes, AuthorizationResult.class);
        } catch (JsonParseException e) {
            logger.error("Failed parsing response " + new String(responseBytes, StandardCharsets.UTF_8));
            throw e;
        }
    }

If the client is required to login a second time with the server eg. the client to server TCP connection drops what is the best practice / solution for re-authentication? I don't see how exchanging the access token for a refresh token would help the server authenticate a client if they resend the same (used!) one-time use auth code, persisting the auth code feels like an awful work-around / hack.. Should the server be sending the access token back to the client after the first success auth and using that for re-auth or is there another method / way for my use case?

Most helpful comment

The thing to do is to call silent signin while already authenticated. This will return another server auth token. Unfortunately, the current implementation of the plugin does not allow calling authenticate when already authenticated.

I am working on a fix for this by adding another method called "GetAnotherServerAuthCode()" which will only work if you are already authenticated and it will return another server auth code without signing out.

I can't commit to an exact date, but hopefully it will be this week, definitely next week.

All 8 comments

I was about to create a very similar issue. Signing out and back in again should work around this (with the latest fixes to fetching server auth codes - although I haven't tried it yet), but is a poor experience for the user as they will no longer be using the silent authentication flow since their selected account will have been cleared. I would like to be able to get a new server auth code for an already authenticated user in order to authenticate them again against my server.

1821 is the same issue. We really need the way to refresh auth token without relogin hacks.

Hello. I'm having the same question/issue. Would be very happy to know more details (currently I have to call SignOut on each disconnect from the game server and it's a pain for the user to see the account selection popup every time).

The thing to do is to call silent signin while already authenticated. This will return another server auth token. Unfortunately, the current implementation of the plugin does not allow calling authenticate when already authenticated.

I am working on a fix for this by adding another method called "GetAnotherServerAuthCode()" which will only work if you are already authenticated and it will return another server auth code without signing out.

I can't commit to an exact date, but hopefully it will be this week, definitely next week.

@claywilkinson for increased robustness could this new method fall back to displaying the permission popup if obtaining the new server token silently fails for whatever reason eg. device state out of sync or if the player revokes permission?

Thanks for the suggestion @johnou. I'll add a flag to allow re-authentication. I think there are some cases where if it failed, you may not want to interrupt the game to re-authenticate.

Fix verified with 0.9.41, thanks again!

Hello i am facing an issue with the GetAnotherServerAuthCode. v0.9.42
@johnou Can you state the optimal way to fetch the AuthCode using this function.

We have implemented silent login in our game.
We are using the GetAnotherServerAuthCode to fetch the AuthCode for server validation.
The callback provided is getting called for the first time.
However when the game is kept idle for a long time we are requesting for a new AuthCode using the same function. But this time the callback is not getting called.
In the logs i found this message : No connected Games API, waiting for onConnected

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Only4Gamers picture Only4Gamers  路  4Comments

parkJeongOck picture parkJeongOck  路  4Comments

ivribalko picture ivribalko  路  3Comments

RafikTSG picture RafikTSG  路  4Comments

hippogamesunity picture hippogamesunity  路  5Comments