Service Account authentication to upload a file to Google Drive - c#

I need to upload a file to Google Drive from my Web Application. I need to use Service Account authentication. I have read Google Drive guide here: DotNetQuickStart and this tutorial for authentication Authentication Google Drive MVC but i don't know what i need to do.
In order to Google Drive guide i have set the parameter to Google Developers Page.
I have download and implemented secret.json file to my project and i have add this code to my project:
public static Google.Apis.Drive.v2.Data.File uploadFile(string _uploadFile, string _parent)
{
string[] scopes = new string[] { DriveService.Scope.Drive }; // Full access
var keyFilePath = #"c:\file.p12"; // Downloaded from https://console.developers.google.com
var serviceAccountEmail = "xx#developer.gserviceaccount.com"; // found https://console.developers.google.com
//loading the Key file
var certificate = new X509Certificate2(keyFilePath, "notasecret", X509KeyStorageFlags.Exportable);
var credential = new ServiceAccountCredential(new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = scopes
}.FromCertificate(certificate));
But i can't understand what to set in keyFilePath and serviceAccountEmail. How can i set this code?Thanks to all

You will need the P12 key file instead of secret.json
Go to https://console.developers.google.com/iam-admin/serviceaccounts --> select your project --> click on the 3 vertical dots located at the right side of the listed Service account --> Create key --> you will see the below dialog

Related

How can an authenticated user access the drive that is publicly accessible?

With this code, I am able to get the files that have been shared to the service account email.
But, when I shared the folder (that was not owned by me, but is publicly accessible) from a different email it is not displayed in the list.
Is there any way an authenticated user can access the drive folder that is publicly accessible, but which I won't own?
var serviceAccountEmail = "";
var certificate = new X509Certificate2(_credentialsService.GetCredentialPath(), "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = new[] { DriveService.Scope.Drive }
}.FromCertificate(certificate));
var service = new DriveService(new BaseClientService.Initializer
{
HttpClientInitializer = credential
});
Getting Drive Details
Google.Apis.Drive.v3.FilesResource.ListRequest FileListRequest = service.Files.List();
// for getting folders only.
//FileListRequest.Q = "mimeType='application/vnd.google-apps.folder'";
FileListRequest.Fields = "nextPageToken, files(id, name)";
FileListRequest.Corpora = "allDrives";
FileListRequest.Q = "sharedWithMe";
FileListRequest.IncludeItemsFromAllDrives = true;
FileListRequest.SupportsAllDrives = true;
// List files.
IList<Google.Apis.Drive.v3.Data.File> files = FileListRequest.Execute().Files;
List<GoogleDriveFile> FileList = new List<GoogleDriveFile>();
Sharing Process:
I shared the file to the service account email, from my personal google email.
And See it is still not available, the image that I owned only is shared. Which is my problem.
Sending the link of a publicly viewable file is not the same as sharing the file
Your screen looks like this:
This means that you have view-only access to the file
If you had edit access to the file, the screen would look as following:
Thus, since you are not an editor of the public file, you cannot add another viewer / editor to this file and consecuently it will not appear in the sharedWithMe of the user to whom you sent the email - no matter if this user is an actual suer or a service account
Solution:
If you need to explicitly share the file with the service account, your options are
to obtain edit access to the public file (if feasible)
create a copy of the respective file onto your own Drive (if copying is not disabled) and share your version explicitly with the service account

How to safely store OAuth response file of each user provided by google drive api

I am working on an asp.net application where I ask people to give the authorization (using OAuth) for accessing their google drive (to a particular folder) to be able to list the files within the application.
The following code enables users to provide authorization and creates a corresponding Google.Apis.Auth.OAuth2.Responses file in the server to be used for future requests. But, this request will happen for each user, which will create more OAuth response files. I am not sure how to design the application and store these files safely. Probably, I may create a new folder (using the Guid-based UserIds) for each user and save the file in that folder. Does this make sense? Or do you recommend another approach?
using (var stream =
new FileStream("Services/credentials.json", FileMode.Open, FileAccess.Read))
{
string credPath = "token.json";
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
Scopes,
"user",
CancellationToken.None,
new FileDataStore(credPath, true)).Result;
Console.WriteLine("Credential file saved to: " + credPath);
}
BaseClientService.Initializer bcs = new BaseClientService.Initializer();
bcs.HttpClientInitializer = credential;
DriveService service = new DriveService(bcs);
Correction
I am working on an asp.net application where I ask people to give the authorization (using OAuth) for accessing their google drive (to a particular folder) to be able to list the files within the application.
When you request access Its going to be for the users full drive account its not going to be just for one folder.
Storing credential files.
Your question is a bit hard to understand but I think you are asking where you should store your credential files. The way FileDataStore works is that it creates a new file for each "user" so depending upon what you have set for "user" a file will be created as you can see i tend to use a guid at the end and store that in a session var that way i know when the user comes back with this session var its that file i need to grab for them. Actually the client library does all that for you because as soon as it sees the id it will use that automatically.
Web vs installed application.
I do have one major comment if you are using Asp .net and intend to use a web application then you are using the wrong code. The following sample shows how to authenticate using Web appliations asp.net mvc note the session in the code below.
public class AppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "PUT_CLIENT_ID_HERE",
ClientSecret = "PUT_CLIENT_SECRET_HERE"
},
Scopes = new[] { DriveService.Scope.Drive },
DataStore = new FileDataStore("Drive.Api.Auth.Store")
});
public override string GetUserId(Controller controller)
{
// In this sample we use the session to store the user identifiers.
// That's not the best practice, because you should have a logic to identify
// a user. You might want to use "OpenID Connect".
// You can read more about the protocol in the following link:
// https://developers.google.com/accounts/docs/OAuth2Login.
var user = controller.Session["user"];
if (user == null)
{
user = Guid.NewGuid();
controller.Session["user"] = user;
}
return user.ToString();
}
public override IAuthorizationCodeFlow Flow
{
get { return flow; }
}
}

Get secure file download ticket via .NET API

Using the Microsoft.TeamFoundation .NET client APIs, how can I get a download ticket for a secure file from TFS 2017u2?
Using the Microsoft.TeamFoundation.DistributedTasks.WebApi nuget package, I puffed up the below code in a console app. It successfully retrieves all the metadata and associated properties entered in TFS for the secure file. However, despite passing the "includeDownloadTicket" argument as "true" to the GetSecureFileAsync method, the Ticket property is always null.
I thought perhaps it was a permissions issue but I am in the TFS Admin role and also specifically assigned myself as an admin to the file entry in the web interface.
var credentials = new VssCredentials();
var projectName = "{myProjectName}";
var secureFileId = new Guid("{theSecureFileId}");
var tfsUri = new Uri("https://{tfsBox}/{collection}");
var connection = new VssConnection(tfsUri, credentials);
var taskAgentClient = connection.GetClient<TaskAgentHttpClient>();
var projectClient = connection.GetClient<ProjectHttpClient>();
var project = await projectClient.GetProject(projectName, true);
var secureFile = await taskAgentClient.GetSecureFileAsync(project.Id, secureFileId, true);
var secureFileTicket = secureFile.Ticket;
if (secureFileTicket == null)
{
Console.WriteLine(
"No download ticket was provided by the TFS for the requested Secure File.");
return;
}
Your thought is right. This is related to the permission. For security reason, normal users are limited to download the secure files. The permission you can set is "Reader", "User" and "Administrator" for normal users while it requires "ViewSecrets" permission to include the download ticket.
So you cannot download the secure files for now.

Google Drive API - Transfer ownership from Service Account

I am attempting to transfer ownership from a Service Account created document to another user who resides within the same Google Apps account using the code below but am getting the following error
The resource body includes fields which are not directly writable. [403]
Errors [Message[The resource body includes fields which are not directly writable.] Location[ - ] Reason[fieldNotWritable] Domain[global]]
var service = GetService();
try
{
var permission = GetPermission(fileId, email);
permission.Role = "owner";
var updatePermission = service.Permissions.Update(permission, fileId, permission.Id);
updatePermission.TransferOwnership = true;
return updatePermission.Execute();
}
catch (Exception e)
{
Console.WriteLine("An error occurred: " + e.Message);
}
return null;
Commenting out // permission.Role = "owner"; returns the error below
The transferOwnership parameter must be enabled when the permission role is 'owner'. [403] Errors [Message[The transferOwnership parameter must be enabled when the permission role is 'owner'.] Location[transferOwnership - parameter] Reason[forbidden] Domain[global]]
Assigning any other permissions works fine. Therefore, is this a limitation of the Service Account not being able to transfer ownership to any other account that doesn't use the #gserviceaccount.com email address (i.e. our-project#appspot.gserviceaccount.com > email#domain.com)?
The email#domain.com email address has been created and is managed within Google Apps.
In the case, it is not achievable, any pointers on where to look next? We need multiple users to have the ability to create documents ad hoc and assign permissions and transfer ownership on the fly via the API.
Thanks
I have found the answer and am posting for anyone else who comes across this question.
You can not use the 'Service Account Key JSON file' as recommended by Google.
You need to use the p.12 certificate file for authentication.
The code to create a drive service for mimicking accounts is as follows.
public DriveService GetService(string certificatePath, string certificatePassword, string googleAppsEmailAccount, string emailAccountToMimic, bool allowWrite = true)
{
var certificate = new X509Certificate2(certificatePath, certificatePassword, X509KeyStorageFlags.Exportable);
var credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(googleAppsEmailAccount)
{
Scopes = new[] { allowWrite ? DriveService.Scope.Drive : DriveService.Scope.DriveReadonly },
User = emailAccountToMimic
}.FromCertificate(certificate));
// Create the service.
return new DriveService(new BaseClientService.Initializer
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName
});
}
You need to follow the steps listed here to delegate domain-wide authority to the service account.
Allow 5 to 10 minutes after completing step 4.
You can now create documents under the 'emailAccountToMimic' user which sets them to be the owner during creation.
I don't think it is possible to transfer the ownership from a non-ServiceAccount to a ServiceAccount, vice versa.
If you do that interactively, you will get the below error:
Typically, the document can be created and owned by the users and ownership transfer can be done using their own credentials. You will also have the option to impersonate as the owner if your Service Account is granted with the domain-wide delegation correctly.

How do I set return_uri for GoogleWebAuthorizationBroker.AuthorizeAsync?

I am trying to use the Google Calendar API in my non-MVC .NET Web Application. (This appears to be an important distinction.)
I’ve tried to use code from this example at Google and this example at Daimto along with some helpful hints from a number of related posts here.
I have written the following method:
public void GetUserCredential( String userName )
{
String clientId = ConfigurationManager.AppSettings[ "Google.ClientId" ]; //From Google Developer console https://console.developers.google.com
String clientSecret = ConfigurationManager.AppSettings[ "Google.ClientSecret" ]; //From Google Developer console https://console.developers.google.com
String[] scopes = new string[] {
Google.Apis.Calendar.v3.CalendarService.Scope.Calendar
};
// here is where we Request the user to give us access, or use the Refresh Token that was previously stored in %AppData%
UserCredential credential = GoogleWebAuthorizationBroker.AuthorizeAsync( new ClientSecrets
{
ClientId = clientId,
ClientSecret = clientSecret
}, scopes, userName, CancellationToken.None, new FileDataStore( "c:\\temp" ) ).Result;
// TODO: Replace FileDataStore with DatabaseDataStore
}
Problem is, when Google’s OAuth2 page is called, redirect_uri keeps getting set to http://localhost:<some-random-port>/authorize. I have no idea how to set this to something else, as in the following example URL generated by AuthorizeAsync:
https://accounts.google.com/o/oauth2/auth?access_type=offline
&response_type=code
&client_id=********.apps.googleusercontent.com
&redirect_uri=http:%2F%2Flocalhost:40839%2Fauthorize%2F
&scope=https:%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar
Google responds with a redirect_uri_mismatch error page with the message:
“The redirect URI in the request: http://localhost:XXXXX/authorize/ did not match a registered redirect URI”
I can only register so many Redirect URI’s in my Google Developer’s Console Credentials page. I’m not inclined to register 65535 ports, and I want to use a page other than /authorize on my site. Specifically, I want to use, during development, http://localhost:888/Pages/GoogleApiRedirect but have no clue as to where I would set this, beyond what I've done in the Developer’s Console.
How do I explicitly set the value of redirect_uri? I am also open to a response in the form “This approach is completely wrong.”
EDIT:
After playing with this over the past day, I've discovered that by using the Client ID/Client Secret for the Native Application rather than the Web Application, I can at least get to Google's web authorization page without it complaining about a redirect_uri_mismatch. This is still unacceptable, because it still returns to http://localhost:<some-random-port>/authorize, which is outside the control of my web application.
You can use this code: (original idea from http://coderissues.com/questions/27512300/how-to-append-login-hint-usergmail-com-to-googlewebauthorizationbroker)
dsAuthorizationBroker.RedirectUri = "my localhost redirect uri";
UserCredential credential = await dsAuthorizationBroker.AuthorizeAsync(...
dsAuthorizationBroker.cs
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Requests;
using Google.Apis.Util.Store;
namespace OAuth2
{
public class dsAuthorizationBroker : GoogleWebAuthorizationBroker
{
public static string RedirectUri;
public new static async Task<UserCredential> AuthorizeAsync(
ClientSecrets clientSecrets,
IEnumerable<string> scopes,
string user,
CancellationToken taskCancellationToken,
IDataStore dataStore = null)
{
var initializer = new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = clientSecrets,
};
return await AuthorizeAsyncCore(initializer, scopes, user,
taskCancellationToken, dataStore).ConfigureAwait(false);
}
private static async Task<UserCredential> AuthorizeAsyncCore(
GoogleAuthorizationCodeFlow.Initializer initializer,
IEnumerable<string> scopes,
string user,
CancellationToken taskCancellationToken,
IDataStore dataStore)
{
initializer.Scopes = scopes;
initializer.DataStore = dataStore ?? new FileDataStore(Folder);
var flow = new dsAuthorizationCodeFlow(initializer);
return await new AuthorizationCodeInstalledApp(flow,
new LocalServerCodeReceiver())
.AuthorizeAsync(user, taskCancellationToken).ConfigureAwait(false);
}
}
public class dsAuthorizationCodeFlow : GoogleAuthorizationCodeFlow
{
public dsAuthorizationCodeFlow(Initializer initializer)
: base(initializer) { }
public override AuthorizationCodeRequestUrl
CreateAuthorizationCodeRequest(string redirectUri)
{
return base.CreateAuthorizationCodeRequest(dsAuthorizationBroker.RedirectUri);
}
}
}
If you are trying to use GoogleWebAuthorizationBroker.AuthorizeAsync in a .NET application NON-web server application i.e. C# Console App command line program, it's crucial when creating the Google OAuth profile (https://console.developers.google.com/apis) in the credentials to select the following.
DESKTOP APP - This also known now as an installed application
It's hidden under "Help me choose" new credential. You must create a new credential which shows Desktop app and not WebApp.
CLICK "HELP ME CHOOSE"
CHOOSE YOUR INTENDED API LIBRARY
ie (Google Calendar API, YouTube) Select "User Data"
STEP 3 - OAUTH CLIENT ID - Desktop App!
Yeah - NO AUTHORIZATION REQUIRED FILEDS"
ie Javascript & Redirect Now you have a profile without the Web Application redirect authorization
Use the "Download JSON" and save it to your application
Reference in the code below. When you look inside this file, you will notice a different set of parameters as well to tell the broker this is an application. In this example, I am accessing the scope Calendar API. Just change the scope to whatever API you are trying to access.**
string[] Scopes = { CalendarService.Scope.Calendar }; //requires full scope to get ACL list..
string ApplicationName = "Name Of Your Application In Authorization Screen";
//just reference the namespaces in your using block
using (var stream = new FileStream("other_client_id.json", FileMode.Open, FileAccess.Read))
{
// The file token.json stores the user's access and refresh tokens, and is created
// automatically when the authorization flow completes for the first time.
string credPath = "other_token.json";
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
Scopes,
"user",
CancellationToken.None,
new FileDataStore(credPath, true)).Result;
}
// Create Google Calendar API service.
var service = new CalendarService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName,
});
//Then your ready to grab data from here using the methods mentioned in Google Calendar API docs
To run this in Docker
When you deploy this to docker you will get an exception that the operating system does not support running the process. Basically, its trying to open a browser on the Metal Box. which is not possible with docker.
To solve this. Modify the code to use full absolute path like this
var inputFolderAbsolute = Path.Combine(AppContext.BaseDirectory, "Auth.Store");
...
new FileDataStore(inputFolderAbsolute, true)
Run this application as a console app on your local machine so the browser opens.
Select the account you want to work with
In the bin folder, a new folder and file will be created.
Copy that folder to the root path
Set the file to copy if newer
Deploy to docker
Because the refresh token is saved for the account you selected it will get a new access token and work.
NB: It is possible the refresh token expires to whatever reason. You will have to repeat the steps above
selecting "other" while creating oAuth Client ID helped me resolve the redirection issue for me. (Having the "Web Application" option tries to redirect to some url with random port, which is very annoying)
Now my Gmail API works like a charm :)
If you are struggling to build a console app that would be able to authenticate to Gmail to send mail, here is what worked. LocalServerCodeReceiver was the key to listening to access token returned from the browser. This code also would obtain a refresh token if needed, and will cache the token on the file system for future use.
var clientSecrets = new ClientSecrets
{
ClientId = YOUR_CLIENTID,
ClientSecret = YOUR_CLIENT_SECRET
};
var codeFlow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
DataStore = new FileDataStore(AppDomain.CurrentDomain.BaseDirectory),
Scopes = new[] { GmailService.Scope.MailGoogleCom },
ClientSecrets = clientSecrets
});
var codeReceiver = new LocalServerCodeReceiver();
var authCode = new AuthorizationCodeInstalledApp(codeFlow, codeReceiver);
var credential = await authCode.AuthorizeAsync(EmailUser, CancellationToken.None);
if (authCode.ShouldRequestAuthorizationCode(credential.Token))
{
await credential.RefreshTokenAsync(CancellationToken.None);
}
Note:
in my experience, Firefox didn't handle the browser redirect correctly, so I had to switch my default Windows browser to Chrome in order for this to work
even though this code is part of a console app, it is not entirely set up to run unattended; unless the token is cached on the filesystem from prior runs, you would need to manually go through the authentication flow in the browser popup
the OAuth client ID configuration in the Developers console should be set to "Desktop"

Categories

Resources