google oauth2 impersonate service account with user#gmail.com - java

I wanted to access some google api services:
GDrive API
Contact API
People API
And I'm struggeling with the oauth2 impersonate service account flow (you know that one: Google Oauth v2 - service account description. For impersonification you need to apply the "Delegating domain-wide authority" in the google apps console, download the correspoding pk12 file and activate the api in a google console project.
At the moment I always get:
com.google.api.client.auth.oauth2.TokenResponseException: 401 Unauthorized
at com.google.api.client.auth.oauth2.TokenResponseException.from(TokenResponseException.java:105)
at com.google.api.client.auth.oauth2.TokenRequest.executeUnparsed(TokenRequest.java:287)
at com.google.api.client.auth.oauth2.TokenRequest.execute(TokenRequest.java:307)
at com.google.api.client.googleapis.auth.oauth2.GoogleCredential.executeRefreshToken(GoogleCredential.java:384)
at com.google.api.client.auth.oauth2.Credential.refreshToken(Credential.java:489)
at oauthsample.GDriveAPI.<init>(GDriveAPI.java:50)
at oauthsample.GDriveAPI.main(GDriveAPI.java:85)
Here is my code:
HttpTransport httpTransport = new NetHttpTransport();
JacksonFactory jsonFactory = new JacksonFactory();
Set<String> scopes = new HashSet<String>();
scopes.add("https://www.google.com/m8/feeds");
GoogleCredential credential = new GoogleCredential.Builder()
.setTransport(httpTransport)
.setJsonFactory(JSON_FACTORY)
.setServiceAccountId("myserviceuser#xxxxxx.iam.account.com")
.setServiceAccountPrivateKeyFromP12File(new File("somep12key.p12"))
.setServiceAccountScopes(scopes)
.setServiceAccountUser("my_user_name#gmail.com")
.build();
credential.refreshToken();
ContactsService service = new ContactsService("MYAPP");
service.getRequestFactory().setHeader("User-Agent", "MYAPP");
service.setHeader("GData-Version", "3.0");
service.setOAuth2Credentials(credential);
URL feedUrl = new URL("https://www.google.com/m8/feeds/contacts/default/full");
ContactFeed resultFeed = service.getFeed(feedUrl, ContactFeed.class);
I also searched heavily through stackoverflow (can't list all references and checked the responses and solutions). But one question was never clearly answered - nor in googles documentaiont nor on all the stackoverflow posts:
Is it realy possible to impersonate a serviceaccount with a normal user#gmail.com user (I mean a normal gmail account with no access to the mentioned admin console in the chapter "Delegating domain-wide authority to the service account" and withouth having a own domain )?
Some say yes, some say no. So what's the absolute truth?
As far as I understand when reading the google docs: The service account can only impersonate on users when you in charge of a own domain and you need to have a google work account with your own domain registered. Then you're able to access the admin console and can grant access to the service account.
Thanks for your patience and for your time to answer.
Best regards
Matt

The short answer is no, it's not possible to perform service-account impersonate of a #gmail.com account. The key reason is that although the service account OAuth flow doesn't involve an authorization screen, at the end of the day someone must still say "I authorize this application to impersonate this user."
In the case of a Google Apps domain that person is the domain administrator, who has the authority to approve apps for all users in the domain. For an #gmail.com account, there is no other authority that can approve this on your behalf. And if you have to ask the user for authorization anyway, they it just makes sense to use the regular 3-legged OAuth flow to prompt the user for authorization, get a refresh token, etc.
Now for a while there was a trick where you could take an #gmail.com user through the regular 3-legged flow, and once they approved it use the service account flow from then on. This lead to some strange problems however, so we've disabled that option. This may be why there was disagreement in the past about if this is possible.

Related

Scope Issue in Azure Graph Rest API Java

I am new using Azure Graph Rest API Java using this repo.
My aim is to list all of the users in the AAD tenant
So far I was only able to get to this:
List<String> scopes= Arrays.asList("https://graph.microsoft.com/User.Read.All");
AzureProfile profile = new AzureProfile(tenantId, subscriptionId, AzureEnvironment.AZURE);
final ClientSecretCredential credential = new ClientSecretCredentialBuilder()
.clientId(clientId)
.clientSecret(clientSecret)
.tenantId(tenantId)
//.httpClient(client)
.authorityHost(profile.getEnvironment().getActiveDirectoryEndpoint())
.build();
TokenCredentialAuthProvider tokenCredentialAuthProvider = new TokenCredentialAuthProvider(scopes, credential);
GraphServiceClient<Request> graphClient =
GraphServiceClient
.builder()
.authenticationProvider(tokenCredentialAuthProvider)
.buildClient();
UserCollectionPage users = graphClient.users()
.buildRequest()
.get();
for(User user: users.getCurrentPage()){
System.out.println(user.displayName);
System.out.println(user.id);
System.out.println(user.userPrincipalName);
}
However, I run into this error instead:
Caused by: java.io.IOException:
java.util.concurrent.ExecutionException:
com.microsoft.aad.msal4j.MsalServiceException:
AADSTS1002012: The
provided value for scope https://graph.microsoft.com/User.Read.All
openid profile offline_access is not valid. Client credential flows
must have a scope value with /.default suffixed to the resource
identifier (application ID URI).
It seems the Scope that I have used is wrong/insufficient, but I am not too sure what should I use the scope with. Any idea?
It is written in the documentation that:
Client credentials requests in your client service must include
scope={resource}/.default. Here, {resource} is the web API that your
app intends to call, and wishes to obtain an access token for. Issuing
a client credentials request by using individual application
permissions (roles) is not supported. All the app roles (application
permissions) that have been granted for that web API are included in
the returned access token.
The Client Credential flow is best suited for situations where you have a Deamon App that will have to authenticate and get access to some kind of a resource through a Non-Interactive way, which in sequence means that the permissions for this Deamon App have been configured and consented from a step done prior to the auth request.
The /.default scope can be translated as the request of the Background App that runs unattended, to get the bulk of the permissions that it has been configured with and access the resource that it asks.
In plain english, the use of the above scope in the Client Credentials flow is a convention that has to be implemented always when this flow is chosen :P.
I tried to reproduce the same in my environment via Postman and got below results:
I registered one Azure AD application and added API permissions like below:
When I tried to generate access token with same scope as you via Postman using client credentials flow, I got same error as below:
POST https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token
grant_type:client_credentials
client_id: <appID>
client_secret: <secret_value>
scope: https://graph.microsoft.com/User.Read.All openid profile offline_access
Response:
To resolve the above error, you must change your scope to https://graph.microsoft.com/.default if you are using client credentials flow.
After changing the scope, I'm able to generate access token successfully like below:
POST https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token
grant_type:client_credentials
client_id: <appID>
client_secret: <secret_value>
scope: https://graph.microsoft.com/.default
Response:
When I used the above token to call below Graph query, I got the list of users with display name, id and user principal name successfully like below:
GET https://graph.microsoft.com/v1.0/users?$select=displayName,id,userPrincipalName
Response:
In your case, change scope value in your code like below:
List<String> scopes= Arrays.asList("https://graph.microsoft.com/.default");

400 Bad Request when refreshing token via Gmail Java API

Hi I'm trying to implement sending/receiving email using Google's gmail api on my server:
private GoogleCredential authorize(HttpTransport httpTransport, JsonFactory jsonFactory ) {
try{
Resource resource = new ClassPathResource("my_key_in_json_format.");
InputStream input = resource.getInputStream();
GoogleCredential credential = GoogleCredential.fromStream(input);
credential.createScoped(GmailScopes.all());
credential.refreshToken();
return credential;
}catch(IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
But I'm getting the following exception when the credential tries to refresh token:
com.google.api.client.auth.oauth2.TokenResponseException: 400 Bad Request
{
"error" : "invalid_scope",
"error_description" : "Bad Request"
}
at com.google.api.client.auth.oauth2.TokenResponseException.from(TokenResponseException.java:105)
at com.google.api.client.auth.oauth2.TokenRequest.executeUnparsed(TokenRequest.java:287)
at com.google.api.client.auth.oauth2.TokenRequest.execute(TokenRequest.java:307)
at com.google.api.client.googleapis.auth.oauth2.GoogleCredential.executeRefreshToken(GoogleCredential.java:394)
at com.google.api.client.auth.oauth2.Credential.refreshToken(Credential.java:493)
at com.snobo.util.GmailService.authorize(GmailService.java:79)
I've tried changing the scope parameters to:
Collection<String> SCOPES = Collections.unmodifiableCollection(Arrays.asList(new String[]{GmailScopes.GMAIL_READONLY}));
And it also failed the same when refreshing token. Google's online document is not really Java friendly. Anyone run into similar issues?
I found the answer to my problem based on this thread after searching around:
400 Bad Request on Gmail API with php
"You should not be using a service account if you just want to access one account (your own). Service accounts are their own account and they're not Gmail accounts. They work well for APIs that don't need a user (e.g. maps, search) or when you are using a Google Apps for Work domain and want delegation enabled for all users in the domain (by domain admin, so you don't need individual user authorization)."
I have modified my implementation to use oauth web flow now. I'm really disappointed on Google's documentation as this matter should be addressed outright and as concise as possible. I'm sure "Service Account" and "domain wide delegation" mis-led many developers to use the Service Account approach for many types of personal/individual account application.

Get user information Youtube v3 api after oauth

I am trying to get the user email, that I used to log in with when performing Oauth2 through Youtube in my application. The code is similar to this:
YouTube client = new YouTube.Builder(GoogleNetHttpTransport.newTrustedTransport(), JacksonFactory
.getDefaultInstance(), credential)
.setApplicationName("app_name").build();
YouTube.Channels.List channelListByIdRequest = client.channels().list("snippet,contentDetails,statistics");
channelListByIdRequest.setMine(true);
ChannelListResponse channelListResponse = channelListByIdRequest.execute();
Here I pull the channel api, that, according to the doc is some kind of similar to the user api in v3. However, neither in Channel nor in any other API I cannot find how to get the email I logged in with. How can that information be accessed?
I have done some research and, unfortunately, there is indeed no possibility to fetch the email. As far as I understand, it is against the concept of Youtube and its data API - it deals with channels and multiple of those can be associated with a single email.
Used this for reference:
https://developers.google.com/youtube/v3/getting-started
However, you can use Google plus API scopes along with Youtube scopes to fetch the user info (but we need to take into account that it will be discarded by March 7, 2019)
GoogleNetHttpTransport.newTrustedTransport(), JacksonFactory.getDefaultInstance(), clientSecrets,
Arrays.asList(YouTubeScopes.YOUTUBE_FORCE_SSL, PlusScopes.PLUS_ME, PlusScopes.USERINFO_EMAIL))
.setAccessType("offline").setApprovalPrompt("force").build();
We will be prompted to select email and channel in Oauth window and will be able to fetch the info.

Do I need G Suite account to make requests impersonating an user with a service account?

I'm trying to make requests to Drive API impersonating an user using a service account but I'm getting a TokenResponseException: 401 Unauthorized exception.
I've followed what is described in https://developers.google.com/identity/protocols/OAuth2ServiceAccount:
In https://console.developers.google.com, created a Project > Credentials > Service account > P12 file;
Enabled Drive API;
Enabled domain-wide delegation;
In https://console.developers.google.com/iam-admin/iam, added Project > Editor permission to email test#gmail.com (example).
Then, in my code:
File p12 = new File("p12FileFromDevelopersConsole.p12");
HttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
GoogleCredential c = new GoogleCredential.Builder()
.setTransport(transport)
.setJsonFactory(jsonFactory)
.setServiceAccountId("MY_ID#MY-ID.iam.gserviceaccount.com")
.setServiceAccountUser("test#gmail.com")
.setServiceAccountScopes(Arrays.asList(DriveScopes.DRIVE, DriveScopes.DRIVE_APPDATA, DriveScopes.DRIVE_FILE, DriveScopes.DRIVE_METADATA_READONLY))
.setServiceAccountPrivateKeyFromP12File(p12)
.build();
List<com.google.api.services.drive.model.File> files = drive.files()
.list()
.setSpaces("drive")
.setFields("nextPageToken, files(id, name, webViewLink)")
.setPageSize(10)
.execute() // < --- exception is thrown here
.getFiles();
for(com.google.api.services.drive.model.File f : files) {
System.out.println(f.getName());
}
The stack trace is:
com.google.api.client.auth.oauth2.TokenResponseException: 401 Unauthorized
at com.google.api.client.auth.oauth2.TokenResponseException.from(TokenResponseException.java:105)
at com.google.api.client.auth.oauth2.TokenRequest.executeUnparsed(TokenRequest.java:287)
at com.google.api.client.auth.oauth2.TokenRequest.execute(TokenRequest.java:307)
at com.google.api.client.googleapis.auth.oauth2.GoogleCredential.executeRefreshToken(GoogleCredential.java:384)
at com.google.api.client.auth.oauth2.Credential.refreshToken(Credential.java:489)
at com.google.api.client.auth.oauth2.Credential.intercept(Credential.java:217)
at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:868)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:419)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:352)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.execute(AbstractGoogleClientRequest.java:469)
I could note that if I comment out the line .setServiceAccountUser("test#gmail.com"), I am able to execute code and it prints "Getting started" in my System.out.println.
I've seen many posts with this problem but none with a concrete solution. Then, I looked at documentation referenced above and it says many times about G Suite and then I realize it could be that these requests only work with a G Suite account. Am I right? If not, how can I make it work?
Yes, domain-wide delegation for service accounts is only for GSuite users because GSuite admins must grant service accounts domain-wide authority -- see Step 4 here: https://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority
Consider using regular OAuth (https://developers.google.com/identity/protocols/OAuth2WebServer) to grant access to your web app from your gmail account.

Upload videos to Youtube from my web server in Java

My goal is to upload videos that are uploaded to my web server to Youtube on my own channel, not the users' Youtube account (my web server is acting as a proxy).
I found the sample code for uploading video to Youtube here with the credential acquired this way. The problem that I have with this sample is that it writes to disk the credential, and it opens an http server. Since my web server can potentially have a lot of users uploading their videos concurrently, the credential file location has to be dynamic, and multiple binding to the same http port is not possible. Further more, after searching through other writing about uploading to Youtube, I think this approach is for users uploading to their Youtube account.
Could you share your experiences/code sample/solutions for my scenario? In short I am just trying to automate the process of me opening up Youtube dashboard, and uploading videos to a channel in my Youtube.
In general, starting at API V3, Google prefers OAuth2 over other mechanism, and uploading a video (or any other action that modifies user data) requires OAuth2.
Fortunately, there is a special kind of token called refresh token to the rescue. Refresh token does not expire like normal access token, and is used to generate normal access token when needed. So, I divided my application into 2 parts:
The 1st part is for generating refresh token, which is a Java desktop app, meant to be run by a user on a computer. See here for sample code from Google.
The 2nd part is is part of my web application, which uses a given refresh token to create a credential object.
Here is my implementation in Scala, which you can adapt to Java version easily:
For generating a refresh token, you should set the accessType to offline for the authorization flow. Note: if a token already exists on your system, it won't try to get new token, even if it does not have refresh token, so you also have to set approval prompt to force:
def authorize(dataStoreName: String, clientId: String, clientSecret: String): Credential = {
val builder = new GoogleAuthorizationCodeFlow.Builder(
HTTP_TRANSPORT,
JSON_FACTORY,
clientId,
clientSecret,
Seq(YouTubeScopes.YOUTUBE_UPLOAD)
)
val CREDENTIAL_DIRECTORY = s"${System.getProperty("user.home")}/.oauth-credentials"
val fileDataStoreFactory = new FileDataStoreFactory(new java.io.File(CREDENTIAL_DIRECTORY))
val dataStore: DataStore[StoredCredential] = fileDataStoreFactory.getDataStore(dataStoreName)
builder.setCredentialDataStore(dataStore).setAccessType("offline").setApprovalPrompt("force")
val flow = builder.build()
val localReceiver = new LocalServerReceiver.Builder().setPort(8000).build()
new AuthorizationCodeInstalledApp(flow, localReceiver).authorize("user")
}
val credential = authorize(dataStore, clientId, clientSecret)
val refreshToken = credential.getRefreshToken
For using the refresh token on the server, you can build a credential from a refresh token:
def getCredential = new GoogleCredential.Builder()
.setJsonFactory(JSON_FACTORY)
.setTransport(HTTP_TRANSPORT)
.setClientSecrets(clientId, clientSecret)
.build()
.setRefreshToken(refreshToken)
I have have bypassed the whole AuthorizationCodeInstalledApp authorize() method and created a new subclass which bypasses the jetty server implementation process.
The methods are as follows
getAuthorizationFromStorage : Get access token from stored credentials.
getAuthorizationFromGoogle : Get the authentication with the credentials from Google creates the url that will lead the user to the authentication page and creating a custom defined name-value pair in the state parameter. The value should be encoded with base64 encoder so we can receive the same code redirected from google after authentication.
saveAuthorizationFromGoogle : Save the credentials that we get from google.
Create the GoogleAuthorizationCodeFlow object from the credentialDatastorfrom the response received from the google after authentication.
Hit google to get the permanent refresh-token that can be used to get the accesstoken of the user any time .
Store the tokens like accesstoken and refreshtoken in the filename as userid
Checkout the code Implementation here

Categories