javagoogle-oauthgoogle-oauth-java-client

implement remote oauth server and exchange token with client without save token on server


I'm developing a small Java application to access the chat of my yt live streams. Other than twitch yt doesn't have an irc server to access the chat so I'm forced to use the yt API. Basically I plan to only use it by myself, but I maybe will offer it for some friends or maybe even make it public.

I already successfully gained access to the the API, but only if I have the client secret and token stored on my system. When I want to open it public I have to setup an auth server and then transfer the token the client. The main issue is the method com.google.api.client.auth.oauth2.AuthorizationCodeFlow.createAndStoreCredential(TokenResponse, String) which is needed to actually create the token. This will store the token where ever the code runs - so in the planed environment on the server, not on the client.

I also found this: OAuth-server, storing user tokens - so if I understand this correctly whenever a service use google oauth it stores the access token on it's own storage and then someone interact with the client. So, I have these two questions:

  1. How do I implement such an auth service if I want to open a project to the public so it can be used by others?
  2. How do I send the token to the client? Or has all communication has to be done over my server? 2a) If the answer to the previous question is yes, how do I secure the access so each client can only access it's own token when it's only identified by a simple string?

Currently, this is the code to get a token:

public class Main
{
    public final static void main(final String... args)
    {
        (new Main()).start();
    }
    private Main() {}
    private void start()
    {
        try
        {
            File DATA_STORE_DIR=new File(System.getProperty("user.home"), "yta");
            JsonFactory JSON_FACTORY=JacksonFactory.getDefaultInstance();
            List<String> SCOPES=Arrays.asList(YouTubeScopes.YOUTUBE_READONLY, YouTubeScopes.YOUTUBE, YouTubeScopes.YOUTUBE_FORCE_SSL, YouTubeScopes.YOUTUBE_UPLOAD);
            DataStoreFactory DATA_STORE_FACTORY=new FileDataStoreFactory(DATA_STORE_DIR);
            NetHttpTransport transport=GoogleNetHttpTransport.newTrustedTransport();
            GoogleClientSecrets clientSecrets=GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(new FileInputStream(new File(DATA_STORE_DIR, "client_secret.json"))));
            GoogleAuthorizationCodeFlow googleAuthorizationCodeFlow=new GoogleAuthorizationCodeFlow.Builder(transport, JSON_FACTORY, clientSecrets, SCOPES).setDataStoreFactory(DATA_STORE_FACTORY).build();
            String authURL=googleAuthorizationCodeFlow.newAuthorizationUrl().setRedirectUri("urn:ietf:wg:oauth:2.0:oob").build();
            Desktop.getDesktop().browse(new URI(authURL));
            String authToken=System.console().readLine();
            GoogleTokenResponse googleTokenResponse=googleAuthorizationCodeFlow.newTokenRequest(authToken).setRedirectUri("urn:ietf:wg:oauth:2.0:oob").execute();
            Credential credential=googleAuthorizationCodeFlow.createAndStoreCredential(googleTokenResponse, "userId");
        }
        catch(IOException|GeneralSecurityException|URISyntaxException ex)
        {
            Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
        }
        
    }
}

Which ends up in a file "StoredCredential" is created in the DATA_STORE_DIR directory which contains the auth token identified by "userId". To transfer the token to the client I can just call getRefreshToken and getAccessToken, send them to the client, store them on the client, build a new Credential instance from those two strings and "activated" it by refreshing it. To get rid of the token on the server side I then could just call delete on the datastore - and to increase security I could use a UUID for this one flow so it would be hard for some attacker to intercept the token - but a risk is still there. So I'm searching for a way to create the token without it stored on the server (not even for a short time) to prevent possible attack vector. Any of you know how to do this?

// update So, I tinkered with it to see how I would create a token on the client - which still require my client-id and client-secret used in the auth code flow (otherwise you get an exception telling you to set these). This ended up in these lines:

credential=(new Credential.Builder(BearerToken.authorizationHeaderAccessMethod()))
    .setTransport(transport)
    .setJsonFactory(JSON_FACTORY)
    .setTokenServerEncodedUrl("https://oauth2.googleapis.com/token")
    .setClientAuthentication(new ClientParametersAuthentication("client-id", "client-secret"))
    .build()
.setAccessToken("access-token")
.setRefreshToken("refresh-token");

Well, as far as I can tell it isn't even possible to transfer a Credential object to a remote client without also exposing the API secret which has to be kept secret. This would mean that any call has to be gone through the server, which has to somehow uniquely identify the client over several sessions/connections, and that each reply has to be forwarded back to the client.

So - this brings me to the question: Is what I want to do * even possible? *= Write a program in java which access the chat of my live stream - which stores its token local but request an external server to get it

I also tried to use an API key - but it seems for what I want to do this doesn't have sufficient permissions - even a simple channel id lookup for my given username fails ... so I have to use OAuth

// update 2 Well - it seems no to matter - I hit the max limit of a normal free user account of 10k per day within 5 minutes - guess I have to switch to twitch then ...


Solution

  • So, I found a solution: https://github.com/cryptearth/YouTubeLiveChat/commit/b1ce15400688b6907600b006463ce538132bd807 It boils down to two things I didn't understand until now:

    1. The Credential.Builder works fine with just give "null" as the client-secret.
    2. The AuthorizationCodeFlow doesn't need a DataStoreFactory.

    So, when not setting a DataStoreFactory for the AuthorizationCodeFlow the Credential gets created, but just not stored anywhere (in the source there's a simple if(null) to check for if a DataStoreFactory was set). This also the client id doesn't really matter as there's no risk that another thread could access the newly created Credential. As AccessTokens only have a limited validity (about an hour) I didn't test what happens when the client runs for longer than this, but as the doc hints there're internal checks so I guess it would try to refresh itself - which would fail. Or, if the refresh didn't happen the next call will fail just with an 401 - Unauthorized reply from Google. So by disable the auto-refresh mechanism I guess I have to somehow check it myself and let a refresh happen on the server at the right timing. To refresh I just sent the refresh token to the server wich then respond with a new access token and the new lifetime of it. So, the server doesn't have to store anything and the client only the refresh token - DONE! Re-Implement the auto-refresh stuff could be done dirty by abusing the Exception caused by the 401 reply after the validity ran out - would work but is considered bad code style - have to figure out how to write it somewhat not-bad.

    About hitting the 10k limit in 5min: I set the timeout to 10sec - gave me about 2h30m on a test - still not enough for my own daily streams - and if I would share it I had to lengthen the timeout even further. So, if 10sec get me 2h30m I would need a poll timeout of 100sec to get about 24h - for one uses(!) - that's 1m40s - multiplied by the number of users. Guess I would have to make a proper project page and somehow had to increase my max quota ... but that's a story for another day.

    Question solved - project dev paused.