I'm looking for a Google Drive API authentication scheme that will give a service application (on a server) rights to create shared documents in a Drive folder, without user interaction.
Google's current unique name for the specific authentication scheme I should use for this is probably a sufficient answer to this question.
Although the document creation will occur in response to a user action, the documents will not be permanently associated with those users and I do not wish to require any user to present a Google account. Instead, I wish the user to be able to access the document via an "Anyone with the link can edit"-type URL displayed on a web page after the document is created.
This is intended to automatically generate documents for multiple generally anonymous people to collaborate, and all documents will be stored in a single folder.
There's a good chance this is a duplicate of this question: Google Drive API username + password authentication. Unfortunately, the accepted answer doesn't contain enough information for me to find my way now that the links it references are dead.
It may also be a duplicate of other questions that have accepted but unclear answers, such as: .NET Google Docs API Authentication (without user interaction), How do I authenticate Google Calendar API v3 without user interaction?, and Drive API doc upload from a server without user interaction.
Authenticating as a service account was the approach I needed.
The Google SDK actions were simply misleading. When I provided some incorrect values it fell back to user-based authentication (automatically opening a web browser to request interactive credentials). I incorrectly interpreted this to mean that the service account functionality was implemented as a long-term key approved by and in the context of a specific interactive user, or something similar.
No user interaction was necessary, however the .p12 certificate was required, rather than whatever credentials the default .json file provided (which I had tried using in a number of ways). Here's the code I used:
using Google.Apis.Auth.OAuth2;
using Google.Apis.Drive.v2;
using Google.Apis.Drive.v2.Data;
using Google.Apis.Http;
using Google.Apis.Services;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using GData = Google.Apis.Drive.v2.Data;
public class Drive
{
private const string GoogleDocMimeType = "application/vnd.google-apps.document";
/// <summary>
/// Creates a drive service, authenticated using information found in the Google Developers Console under "APIs & auth / Credentials / OAuth / Service account"
/// </summary>
/// <param name="svcEmail">The service account "Email address"</param>
/// <param name="certPath">The path to the service account "P12 key" file</param>
public Drive(string svcEmail, string certPath)
{
Service = AuthenticateService(svcEmail, certPath);
}
private DriveService Service
{
get;
set;
}
/// <summary>
/// Creates a "Google Doc" and shares it with anyone with the link
/// </summary>
/// <param name="title"></param>
/// <returns>The drive FileId, accessible at https://docs.google.com/document/d/FileId </returns>
public async Task<string> CreateShared(string title)
{
var fileId = await CreateDocument(title);
await ShareFile(fileId);
return fileId;
}
private async Task<string> CreateDocument(string title)
{
var file = new GData.File
{
Title = title,
MimeType = GoogleDocMimeType
};
file = await Service.Files.Insert(file).ExecuteAsync();
return file.Id;
}
private async Task ShareFile(string fileId)
{
Permission permission = new Permission
{
Type = "anyone",
Role = "writer",
WithLink = true
};
var a = Service.Permissions.Insert(permission, fileId);
await a.ExecuteAsync();
}
private static DriveService AuthenticateService(string svcEmail, string certPath)
{
string[] scopes = new[] { DriveService.Scope.DriveFile };
X509Certificate2 certificate = new X509Certificate2(certPath, "notasecret", X509KeyStorageFlags.Exportable);
var init = new ServiceAccountCredential.Initializer(svcEmail) { Scopes = scopes };
IConfigurableHttpClientInitializer credential = new ServiceAccountCredential(init.FromCertificate(certificate));
return new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Document Management Service",
});
}
}
And here's an experimental consumer:
internal class Program
{
private const string svcEmail = "serviceaccountid#developer.gserviceaccount.com";
private const string certPath = #"credentials\projectname-fingerprintprefix.p12";
private readonly static Drive drive = new Drive(svcEmail, certPath);
private static void Main(string[] args)
{
string id = drive.CreateShared("my title").Result;
Console.WriteLine(id);
}
}
This seems to use Google Drive storage in an isolated, application/project-specific data repository. According to other posts, there is no way to get an interactive Drive UI view on that. I don't know if if it uses my personal storage quota, etc. But, this is the best approach I have so-far and I'll answer those questions for myself (not here) next.
Related
I'm using the Google Fit Android API to retrieve fitness data and it all works like a charm. I want to also access the name of the currently logged in user , which should be accessible by GoogleSignInAccount .getDisplayName();
I already asked this question but unfortunately didn't get any replies, and I cant figure it out with the documentation.
Example code:
//Create a FitnessOptions instance, declaring the data types and access type (read and/or write) your app needs:
FitnessOptions fitnessOptions = FitnessOptions.builder()
.addDataType(DataType.TYPE_STEP_COUNT_DELTA, FitnessOptions.ACCESS_READ)
.addDataType(DataType.TYPE_SLEEP_SEGMENT, FitnessOptions.ACCESS_READ)
.addDataType(DataType.TYPE_HEART_RATE_BPM, FitnessOptions.ACCESS_READ)
.addDataType(DataType.AGGREGATE_HEART_RATE_SUMMARY, FitnessOptions.ACCESS_READ)
.build();
//Get an instance of the Account object to use with the API:
GoogleSignInAccount account = GoogleSignIn.getAccountForExtension(this, fitnessOptions);
GoogleSignInAccount acct = GoogleSignIn.getLastSignedInAccount(this);
if (acct != null) {
loggedInUser = account.getDisplayName();
}
The problem is acct.getDisplayname().getGrantedScopes works like a charm, and I see the granted scope. When I try to read .getDisplayName I always get NULL.
I decided to use another way of logging in...
I now use this to configure sign in options and access :
// Configure sign-in to request the user's ID, email address, and basic
// profile. ID and basic profile are included in DEFAULT_SIGN_IN.
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.requestProfile()
.build();
mGoogleSignInClient = GoogleSignIn.getClient(this, gso);
Then we start the sign in intent:
Intent signInIntent = mGoogleSignInClient.getSignInIntent();
startActivityForResult(signInIntent, 000000);
And now we handle the result:
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// Result returned from launching the Intent from GoogleSignInClient.getSignInIntent(...);
if (requestCode == 000000) {
// The Task returned from this call is always completed, no need to attach
// a listener.
Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);
handleSignInResult(task);
}
}
private void handleSignInResult(Task<GoogleSignInAccount> completedTask) {
try {
GoogleSignInAccount account = completedTask.getResult(ApiException.class);
// Signed in successfully, show authenticated UI.
updateUI(account);
} catch (ApiException e) {
// The ApiException status code indicates the detailed failure reason.
// Please refer to the GoogleSignInStatusCodes class reference for more information.
Log.w(TAG, "signInResult:failed code=" + e.getStatusCode());
updateUI(null);
}
}
Tips: Make sure to use ApiException.class from Google and not AWS
About 6 months ago I set up a web application in the google developers console so that employees of our internal web site could initiate emails which would read a template doc in a google account, merge some fields and then download a pdf version of it to email out.
Now we have to move those template docs to a different google managed domain/user account so I've made copies of the documents in the new account and updated our references with the new doc ids.
In addition, the email I had when I originally created this application in the google dev console is going away as of the first of the year. So I also have to recreate the app under a new account.
I've done that and matched all the settings of the original app. However, when I try to access a document I get the error Google.Apis.Auth.OAuth2.Responses.TokenResponseException: 'Error:"unauthorized_client", Description:"Unauthorized", Uri:""'
I had followed this page in setting up the original user authentication. I know there was a ton of trial and error before I actually got it working and I must be forgetting something. I'm wondering if it's tied to needing to reauthenticate the new app. Although I'm specifying the new clientid and clientsecret from the new app, I don't get the popup asking me to give permission to the app. I would expect with the new credential info that it would open that window asking me to give permission. Here's that file for reference. Any ideas?
public class AuthCallbackController : Google.Apis.Auth.OAuth2.Mvc.Controllers.AuthCallbackController
{
protected override FlowMetadata FlowData => new AppFlowMetadata();
}
public class AppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = AwsSecrets.GoogleCreds.ClientId,
ClientSecret = AwsSecrets.GoogleCreds.ClientSecret
},
Scopes = new[] {DriveService.Scope.Drive},
DataStore = new FileDataStore("Drive.Api.Auth.Store")
});
public override IAuthorizationCodeFlow Flow => flow;
public override string GetUserId(Controller controller)
{
return "userid";
}
}
public class GoogleController : TECWareControllerBase
{
private readonly IGoogleCredentialService _gservice;
public GoogleController(IGoogleCredentialService gservice)
{
_gservice = gservice;
}
public async Task<ActionResult> IndexAsync(CancellationToken cancellationToken)
{
var result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).AuthorizeAsync(cancellationToken);
if (result.Credential != null)
{
_gservice.SaveRefreshToken(result.Credential.Token.RefreshToken);
return View();
}
return new RedirectResult(result.RedirectUri);
}
}
I finally found a way to get this working.
First off in this method
private static readonly IAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = AwsSecrets.GoogleCreds.ClientId,
ClientSecret = AwsSecrets.GoogleCreds.ClientSecret
},
Scopes = new[] {DriveService.Scope.Drive},
DataStore = new FileDataStore("Drive.Api.Auth.Store")
});
I had to change the FileDataStore("Drive.Api.Auth.Store") key to something else like FileDataStore("GoogleAuth")
That forced the authentication to fire up.
Unfortunately, google then complained about an invalid redirect uri. The following code returned a redirect uri of http://localhost:11224/AuthCallback/IndexAsync which didn't even exist in my web application's Authorized redirect uris. It should have been http://localhost:11224/MVC/AuthCallback/IndexAsync. So in the url result's redirect url I changed it to what it should have been which allowed me to complete the authorization. Now I can access the documents in the authenticated account.
var result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).AuthorizeAsync(cancellationToken);
I'm trying to connect to my Azure Mysql via http rest api (https://learn.microsoft.com/en-us/rest/api/mysql/) without success. The problem is that i can't get the JSON Web Token from my Web App. Situation:
Azure Web App ----- rest api ----> Azure MySql
I guess i need to 'register' this Mysql Server resource in active directory but seems i can't do it.
I followed this tutorial (https://blogs.msdn.microsoft.com/jpsanders/2017/03/17/accessing-azure-app-services-using-azure-ad-bearer-token-2) but i have the same problem : i can't register MySql in Azure Active Directory .
So, how can i obtain a JSON Web Token for Mysql HTTP REST API ?
Thanks!
-------- AD PROPIETARY ROLE FOR MYSQL RESOURCE (NOT MYSQL SERVER) --
---------------- CODE ----------------------------------------------
//
// https://blogs.msdn.microsoft.com/jpsanders/2017/03/17/accessing-azure-app-services-using-azure-ad-bearer-token-2/
//
public static class AzureActiveDirectory
{
// the AD Authority used for login. For example: https://login.microsoftonline.com/myadnamehere.onmicrosoft.com
public static string authority = "";
// the Application ID of this app. This is a guid you can get from the Advanced Settings of your Auth setup in the portal
public static string clientId = "";
// the key you generate in Azure Active Directory for this application
public static string clientSecret = "";
// the Application ID of the app you are going to call.This is a guid you can get from the Advanced Settings of your Auth setup for the targetapp in the portal
public static string resource = "";
static public async Task<AuthenticationResult> GetS2SAccessTokenForProdMSAAsync()
{
var task = await GetS2SAccessToken(authority, resource, clientId, clientSecret);
return task;
}
static async Task<AuthenticationResult> GetS2SAccessToken(string authority, string resource, string clientId, string clientSecret)
{
var clientCredential = new ClientCredential(clientId, clientSecret);
AuthenticationContext context = new AuthenticationContext(authority, false);
AuthenticationResult authenticationResult = await context.AcquireTokenAsync(
resource, // the resource (app) we are going to access with the token
clientCredential); // the client credentials
return authenticationResult;
}
}
AzureActiveDirectory.authority = "https://login.microsoftonline.com/********/";
AzureActiveDirectory.clientId = "********";
AzureActiveDirectory.clientSecret = "********";
AzureActiveDirectory.resource = "https://management.azure.com/";
try
{
AuthenticationResult token = await AzureActiveDirectory.GetS2SAccessTokenForProdMSAAsync();
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Authorization", "Bearer " + token.AccessToken);
var resp = await client.GetAsync("https://management.azure.com/subscriptions/*******/resourceGroups/MYSQL/providers/Microsoft.DBforMySQL/servers/shoplister/firewallRules?api-version=2017-12-01");
Console.WriteLine(resp.StatusCode.ToString());
Console.WriteLine();
}
catch (Exception e) { Console.WriteLine(e); }
--------------- AFTER CHANGES NOW GETTING UNAUTHORIZED ------------
I'm compiling the important points from our discussion in the comments that led to a solution:
Use https://management.azure.com as the resource identifier when acquiring the access token
Use https://login.microsoftonline.com/tenant-id-here/ as the authority (you can also use a verified domain name instead of the id). This defines which AAD tenant you authenticate against
The access token must be attached with new AuthenticationHeaderValue("Bearer", token.AccessToken) in C#, so that the resulting header is Authorization: Bearer tokengoeshere
Finally make sure you have granted permissions to the right app. There can be apps with an identical or similar name.
I am building feature to upload files from my web application to my clients google drive. I don't want application to authenticate the user. Who ever is uploading any file from my file i want that to be uploaded to my client's google drive.
I am assuming that Service Account is the One which is used for this purpose. am I correct ?
When i upload the files using following code it gets uploaded to successfully but they are not visible under "My Drive". Then i added a permission which basically shares that file with my gmail account. and then it is visible under "Shared With Me" section.
I want those files to be available under "My Drive" section just like normal files.
Can any one point out what i am doing wrong here ?
private DriveService GetServiceInstance()
{
try
{
var certificate = new X509Certificate2(Server.MapPath("path to .p12 key"), "notasecret", X509KeyStorageFlags.Exportable);
string[] scopes = new string[] {
DriveService.Scope.Drive,
DriveService.Scope.DriveFile,
DriveService.Scope.DriveAppdata,
DriveService.Scope.DriveMetadata
};
var credential = new ServiceAccountCredential(new ServiceAccountCredential.Initializer("Client Email")
{
Scopes = scopes
}.FromCertificate(certificate));
// Create the service.
var service = new DriveService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Drive API Sample",
});
return service;
}
catch (Exception ex)
{
throw;
}
}
protected void btnSave_Click(object sender, EventArgs e)
{
try
{
if (fu.HasFile)
{
DriveService service = GetServiceInstance();
Google.Apis.Drive.v2.Data.File body = new Google.Apis.Drive.v2.Data.File();
body.Title = fu.FileName;
body.MimeType = fu.PostedFile.ContentType;
FilesResource.InsertMediaUpload request = service.Files.Insert(body, fu.PostedFile.InputStream, fu.PostedFile.ContentType);
request.Upload();
// I want to eliminate this part. where i do not have to give permission. As i want these files to get stored on same google drive account for which i have generated this service account and client keys
Google.Apis.Drive.v2.Data.File file = request.ResponseBody;
Google.Apis.Drive.v2.Data.Permission newPermission = new Google.Apis.Drive.v2.Data.Permission();
newPermission.Value = "my email address";
newPermission.Type = "user";
newPermission.Role = "writer";
Google.Apis.Drive.v2.PermissionsResource.InsertRequest insertRequest = service.Permissions.Insert(newPermission, file.Id);
insertRequest.SendNotificationEmails = false;
insertRequest.Execute();
}
}
catch (Exception ex)
{
throw;
}
}
Don't use a Service Account. Embed a refresh token for the target account in your server. See How do I authorise an app (web or installed) without user intervention? (canonical ?)
I have created a specific folder and shared the folder to my email address. Uploaded the document to that folder. This resolved my issue.
I always get this error when trying to add files under a folder in google drive, the folder is marked shared with mode "anyone in abc.com can view and edit"
com.google.api.client.auth.oauth2.TokenResponseException: 400 Bad Request
{
"error" : "invalid_grant"
}
I followed this procedure from Google Developers Guide
Delegating domain-wide authority to the service account
If your application accesses user data, the service account that you created needs to be granted access to the Google Apps domain’s user data that you want to access.
The following steps must be performed by an administrator of the Google Apps domain:
Go to your Google Apps domain’s Admin console.
Select Security from the list of controls. If you don't see Security listed, select More controls from the gray bar at the bottom of the page, then select Security from the list of controls. If you can't see the controls, make sure you're signed in as an administrator for the domain.
Select Show more and then Advanced settings from the list of options.
Select Manage API client access in the Authentication section.
In the Client Name field enter the service account's Client ID.
In the One or More API Scopes field enter the list of scopes that your application should be granted access to. For example, if your application needs domain-wide access to the Google Drive API and the Google Calendar API, enter: https://www.googleapis.com/auth/drive, https://www.googleapis.com/auth/calendar.
Click Authorize.
Your application now has the authority to make API calls as users in your domain (to "impersonate" users). When you prepare to make authorized API calls, you specify the user to impersonate.
added the keys in application.properties
#Google Drive
google.drive.service.account=AAAAAAAAAAAA-i8jfnhhug5uoo9rm8ek2sf06b9452vb9#developer.gserviceaccount.com
google.drive.impersonate.account=my.user#abc.com
google.drive.pkey.file=abcdefgh-0aafb873fcc9.p12
google.drive.application.name=myapp-web
And then created a configuration class for drive
#Configuration
public class GoogleDriveAPIConfiguration {
#Resource
private Environment environment;
private final org.springframework.core.io.Resource pkeyFile = new ClassPathResource("abcdefgh-0aafb873fcc9.p12");
private static final String GOOGLE_RIVE_SERVICE_ACCOUNT = "google.drive.service.account";
private static final String GOOGLE_DRIVE_APPLICATION_NAME = "google.drive.application.name";
private static final String GOOGLE_DRIVE_IMPERSONATE_ACCOUNT = "google.drive.impersonate.account";
#Bean
#Autowired
public GoogleCredential googleCredential(JsonFactory jsonFactory, HttpTransport httpTransport)
throws GeneralSecurityException, IOException {
GoogleCredential credential = new GoogleCredential.Builder().setTransport(httpTransport)
.setJsonFactory(jsonFactory).setServiceAccountId(environment.getProperty(GOOGLE_RIVE_SERVICE_ACCOUNT))
.setServiceAccountPrivateKeyFromP12File(getPrivateKeyFile())
.setServiceAccountScopes(Collections.singleton(DriveScopes.DRIVE))
.setServiceAccountId(environment.getProperty(GOOGLE_DRIVE_IMPERSONATE_ACCOUNT))
.build();
return credential;
}
#Bean
public JsonFactory getJsonFactory() {
JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
return JSON_FACTORY;
}
#Bean
public HttpTransport getHttpTransport() throws GeneralSecurityException, IOException {
HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
return httpTransport;
}
#Bean
#Autowired
public Drive getDrive(HttpTransport httpTransport, JsonFactory JSON_FACTORY, GoogleCredential credential) {
Drive drive = new Drive.Builder(httpTransport, JSON_FACTORY, credential).setApplicationName(
GOOGLE_DRIVE_APPLICATION_NAME).build();
return drive;
}
private File getPrivateKeyFile() throws IOException {
return pkeyFile.getFile();
}
}
The app starts up without any error, here's file upload code (full file in this gist)
#Autowired
public void setDrive(Drive drive) {
this.drive = drive;
}
#Async
public void uploadResume(UploadData data, byte[] uploadFile) throws GeneralSecurityException, IOException {
LOGGER.info("Uploading file type {} of size {} to Drive",data.getContentType(),uploadFile.length);
com.google.api.services.drive.model.File file = new com.google.api.services.drive.model.File();
List<ParentReference> referneces = new ArrayList<ParentReference>();
referneces.add(buildParentReference(data.getApplication().getRole()));
file.setTitle(data.getFileName());
file.setParents(referneces);
file.setCopyable(true);
file.setEditable(true);
file.setWritersCanShare(true);
file.setTitle(data.getFileName());
ByteArrayInputStream inputBytes = new ByteArrayInputStream(uploadFile);
InputStreamContent fileUpload = new InputStreamContent(data.getContentType(), new BufferedInputStream(inputBytes));
fileUpload.setLength(uploadFile.length);
Drive.Files.Insert request = drive.files().insert(file, fileUpload);
request.getMediaHttpUploader().setProgressListener(getProgressListener());
request.execute();
}
The error message really doesn't help too, any ideas where I am going wrong?