Google API Auth Scopes Incorrect - laravel

Bit of an odd one. I have an endpoint to log the user into Google and grant the Google Drive authentication scope for access to a user's drive account. This is said function
if(Auth::check() && $profile->abilities()->contains('manage_docs') && $request->input('redirect_uri') && $request->input('community')) {
$scopes = [
'https://www.googleapis.com/auth/drive',
];
return Socialite::driver('google')->scopes($scopes)->with([
'state' => "sso.redirect.uri=" . $request->input('redirect_uri'). "&type=documents",
"access_type" => "offline",
"prompt" => "consent select_account"
])->redirect();
}
When a user hits this endpoint, they get redirected to Google. Users are reporting they see that they are being asked to grant access to Google Drive from my application, however their credentials do not have the above listed scopes. However, if they remove the Google Account and resign in (using the same method), they get the required scopes..
Tldr, first pass through this method doesn't add the scope, 2nd time through grants correct scopes.

If in doubt, doubt the user's ability to press the tick box to grant the auth scope...

Related

Google Admin API with service account -- bad credentials

I'm trying to write console app code to update Google Directory with values pulled from a SQL database. I can't seem to make the API connect successfully when using a service account. Any ideas? I've whittled the code down to just the essentials here.
static void Main(string[] args)
{
try
{
// Create a ServiceAccountCredential credential
var xCred = new ServiceAccountCredential(new ServiceAccountCredential.Initializer("saxxxxxxxx#directorysync-xxxxxx.iam.gserviceaccount.com")
{
Scopes = new[] {
DirectoryService.Scope.AdminDirectoryUser,
DirectoryService.Scope.AdminDirectoryUserReadonly
}
}.FromPrivateKey("-----BEGIN PRIVATE KEY-----\nMI...p9XnI4DZFO/QQJc=\n-----END PRIVATE KEY-----\n"));
// Create the service
DirectoryService service = new DirectoryService(
new BaseClientService.Initializer()
{
HttpClientInitializer = xCred,
}
);
var listReq = service.Users.List();
listReq.Domain = "mycompany.com";
listReq.MaxResults = 100;
listReq.OrderBy = UsersResource.ListRequest.OrderByEnum.Email;
Users results = listReq.Execute();
// process the users list here...
}
catch (Exception e)
{ Console.WriteLine(e.Message); }
}
The error happens at the .Execute() line:
Google.Apis.Requests.RequestError
Not Authorized to access this resource/api [403]
Errors [
Message[Not Authorized to access this resource/api] Location[ - ] Reason[forbidden] Domain[global]
]
I've tried code seen elsewhere (How to login to Google API with Service Account in C# - Invalid Credentials) to bring in the whole contents of the .JSON file that contains the credentials for the service account; that made no difference. I'm not the google domain admin, but the admin built the credential and promises that it does, indeed, have rights to the user resources. I'm utterly lost at what's not right.
Either:
You're missing the email address of an admin user to impersonate.
An Admin of the domain needs to assign user management privileges to the service account.
Background
There's two HTTP requests involved in making a Google API request with a service account:
Using the service account's credentials to request an access token from the Google OAuth 2.0 server. A HTTP POST is sent with a JWT signed with the private key of the service account. If successful, an access token is returned which is valid for one hour.
Making the service API request (Admin SDK Directory API in this case) using the OAuth access token obtained from the last step.
The error message you provided is not listed in the JWT error codes page so step 1 is working, and the error is coming from step 2 - the request to Directory API.
You should be able to confirm this using an HTTPS request interceptor like mitmproxy.
You'd get a 403 error for the Directory API users.list method for a few reasons:
You've authenticated as a service account, but that service account has no admin privileges which are sufficient for the request.
You've authenticated as a user in the domain (either using a service account with impersonation with the sub parameter in the JWT request, or using three-legged interactive OAuth), but that user has no admin privileges which are sufficient for the request.
You've authenticated as a service account or user with sufficient privileges, but the domain parameter is not owned by the customer for which the service account or user is associated with.
In your sample code, there is no email address of a domain user (admin) specified (the email address for ServiceAccountCredential.Initializer is actually the OAuth client name of the service account, not a real email address), so it is either case 1 or 3.
To address case 1
A service account has no association with a domain or access privileges by default.
An admin can open Admin console, go to "Account > Admin roles > "User Management Admin" > Admins > Assign service accounts", then:
Add the service account name (looks like an email address, ending #gserviceaccount.com).
Click "Assign role".
Now, the service account does not need to impersonate another admin in the domain, it will directly have admin privileges.
A couple of side-effects of this:
The service account cannot be "suspended" as such, only removed from the admin role.
Audit logging of the actions will show the service account name in the "Reporting > Audit > Admin" section of Admin console.
To address case 2
You may have meant to impersonate an admin user in the domain, which you could do by adding:
, User = "admin#example.com"
- after the Scopes array passed to ServiceAccountCredential.Initializer.
This adds the email address of a user to the JWT request to retrieve an access token for that user (by adding the sub field to the claim-set of the JWT assertion).
To address case 3
Replace the "mycompany.com" on the line:
listReq.Domain = "mycompany.com";
- with a domain that is associated with the customer, or instead, remove that line and add:
listReq.Customer = "my_customer";
(literally my_customer - see users.list query-parameters)
- Which will list users on all domains associated with the customer (Google Workspace and Cloud Identity customers can have many secondary domains).

Google Domains API issue when post on user profile

I got issue to post on users profile (Not got error on all users).
Error Code: 400
Error Message: Calls to this method must be made by or on behalf of a Google+ Page.
$options = array( "headers" => array( 'content-type' => 'application/json;' ), "body" => json_encode($activity) );
$httpClient = $this->gplus->client->authorize();
$request = $httpClient->post("googleapis.com/plusPages/v2/people/$Id/activities";, $options);
$response = $request->getBody();
$googlePostResponse = $response->getContents();
$googlePostResponse = json_decode($googlePostResponse, TRUE);
The code you are currently using is to the Google Domains api. The error means that the user you have currently authenticated with doesnt have access to the domains account. Make sure to grant the user access and they will be able to post to the Domains google+ page authentication
Note:
There is a difference between the Google+ API (Socialmedia website) and the Google Domains API (Gsuite)
The google+ api is read only and does not allow for programiticly inserting posts into Google+.
How authentication works
When you authenticate your application using these scopes
$this->client->addScope('googleapis.com/auth/plus.me');
$this->client->addScope('googleapis.com/auth/plus.stream.write');
$this->client->addScope('googleapis.com/auth/plus.stream.read');
$this->client->addScope('googleapis.com/auth/plus.pages.manage');
$this->client->addScope('googleapis.com/auth/plus.media.readwrite');
You are asking the user can I do these things on your behalf. However if the user does not have permission to do something your not going to be able to do it.
calls to this method must be made by or on behalf of a Google+ Page.
The user you are authenticating with does not have access to a business page. So your application cant write to a business page. You can only post to a domain account if you have a gsuite account. If you don't have one then you cant post to it. you cant just give them the id of your business page because again they dont have access to write to your business page.

Laravel Passport - Authenticate using postman

I have setup Passport in Laravel 5 and am creating a token when a user registers by using this..
$user->createToken('My Token')->accessToken;
When I create a new user then I can see an entry in the oauth_access_tokens table that looks like this, I have shortened the id for display...
id | user_id | client_id | name | scopes | revoked
-----------------------------------------------------------
765765 | 193 | 1 | My Token | [] | 0
The API route I want to allow access to is protected, now I am trying to allow access by passing the following headers in Postman...
Authorization | Bearer 765765
Accept | application/json
But this is returning a response of unauthenticated, where am I going wrong?
Laravel Passport uses oAuth2. It's not as simple as generating a user token and being able to use it to authenticate. oAuth2 requires another step, which is called a token exchange.
You will have seen the oAuth2 process in action when you log into a website with Facebook. You click the login with Facebook button and you are sent to Facebook and you are presented with a dialog where you're asked to confirm or deny an app access to your account (Usually, specific parts of your account, a.k.a scopes).
That website will have it's own client account with Facebook and will have its own client ID and client secret. When you click that button, the website sends you to Facebook in order to gain your permission and an authorization code from Facebook. The website passes its client ID, requested permissions (scopes), a randomly generated session state (So it can verify later) and a URL to redirect to Facebook where you are shown the dialog.
When you accept, Facebook generates what is called an authorization code and sends you back on your way to the website (The redirect URL specified) along with the sessions state (So the website is able to verify the request) and the authorization code.
The website, on its back end will then ask Facebook to exchange your authorization code for an access token and will provide its client ID and client secret so Facebook is able to verify its authenticity. Facebook then responds with an access token and an expiry time.
Now, the website is able to access your account using the access token to be able to grab the requested information (Such as your email address for login).
It's also possible to do skip a lot of this process and not require the user to have to follow the whole redirection flow. To do this, (In Passport at least), you will need a password grant client. This is usually what you would do if you are using oAuth2 to authenticate an API.
The process here would be to generate a password grant client:
php artisan passport:client --password
In your database, the you will find in the oauth_clients table, a password grant client with a client ID and secret. You would need to give this to whoever is consuming your API (Such as a mobile/cellphone app).
Now when your user wants to log in, the consumer of your API (In this case Postman) would need to provide the user's credentials (username/password) as well as the client ID and secret for your password grant client. It's also necessary to tell Passport that you want to authorize via password grant.
The example given in the docs looks like this:
$response = $http->post('http://your-app.com/oauth/token', [
'form_params' => [
'grant_type' => 'password',
'client_id' => 'client-id',
'client_secret' => 'client-secret',
'username' => 'taylor#laravel.com',
'password' => 'my-password',
'scope' => '',
],
]);
When successful, Passport will return a 200 response and will return some JSON containing an access token and a refresh token. You use the access token to access the user's account and you use the refresh token to get a new access token (without requiring the user to log in again).
It is this access token that you need to provide as the Bearer in your Authorization header.

Auth0 and Google API, access token expired, howto write files to Google Drive

I've written an online converter and integrated Auth0 to my website. What I'm trying to achieve is to auto-upload the converted file to the Google Drive of a logged in user. I set up Google oauth in Auth0 and everything seemed to work fine.
The problem is, Google's access_token expires after 60min and I don't have a refresh_token. Therefore, the user needs to log in via the Google Login-page again. That is not, what I want, because the user is in fact logged in way longer than just 60min on my site, but Google refuses API-calls (because the Google token expired).
I know I can request a refresh_token by setting access_type=offline but this will add the permission Have offline access. I don't want that, I just want to upload data to the user's Drive, if he clicked the convert button on my page. I don't want to ask the users for permissions I don't need. If I (as a user) would link my Google account on a similar page and the tool asks for offline access I wouldn't approve, to be honest - the permission sounds like the tool creator can do whatever he wants with your account whenever he wants... There are many tools out there that have write access to a user's Drive without asking for offline access and with one single login until the user revokes the permission. How is that done?
Is there a way to make Google API calls without asking for offline access and without forcing the user to approve the app (that is already approved by him) again and again every 60min?
Thanks in advance,
phlo
Is there a way to make Google API calls without asking for offline access and without forcing the user to approve the app (that is already approved by him) again and again every 60min?
Yes there are ways, but it depends on the specifics of your use case. For example, is your code in Java/php/etc running on a server, or is it JavaScript running in the browser?
Running your auth in the browser is probably the simplest solution as the Google library (https://developers.google.com/api-client-library/javascript/features/authentication) does all the work for you.
By asking for offline access you are requesting a refresh token. Google is going to tell the user that you are requesting offline access. You can request something without telling the user what they are authorizing.
No there is no way to request a refresh token without displaying that message. Nor is there a way for you to change the message it's a standard Google thing.
I found the solution!
Prerequirements
Enable Use Auth0 instead of the IdP to do Single Sign On in your client's Dashboard
Create a new Angular-route to handle the silent login callback (e.g. /sso)
Add
$rootScope.$on("$locationChangeStart", function() {
if ($location.path().indexOf("sso") == -1) {
authService.relogin(); //this is your own service
}
});
to your run-function and set the callbackURL in angularAuth0Provider.init() to your new Angular-route (<YOUR_DOMAIN>/sso). Add this URL to your accepted callbacks in the Auth0 dashboard - this won't end in an infinite loop, because the locationChangeStart-event won't call authService.relogin() for this route
Add $window.close(); to the controller of the Angular-route (/sso) to auto-close the popup
Authenticate the user via Auth0 and save the timestamp and the Auth0-access_token somewhere
On reload:
Check, if the Auth0-token is still valid in authService.relogin(). If not, the user has to login again either way. If the token is valid and the Google token is about to expire (check this with the saved timestamp to prevent unnecessary API calls) check for SSO-data and login silently, if present
/* ... */
if (validToken && googleExpired) {
angularAuth0.getSSOData(function (err, data) {
var lastUsedConnection = data.lastUsedConnection;
var connectionName = (_.isUndefined(lastUsedConnection) ? undefined : lastUsedConnection.name);
var isGoogle = (_.isUndefined(connectionName) ? false : connectionName == "google-oauth2");
if (!err && data.sso && isGoogle) {
authManager.authenticate();
localStorage.setItem("last-relogin", new Date().getTime());
angularAuth0.signin({
popup: true,
connection: data.lastUsedConnection.name
});
}
});
}
Now you will find a fresh Google access_token for this user (without asking for offline access)
Resources:
https://auth0.com/docs/quickstart/spa/angularjs/03-session-handling
https://auth0.com/docs/quickstart/spa/angularjs/11-sso

Google Drive API - Transfer ownership as delegated admin

I am trying to transfer ownership from a normal user to a delegated admin user which has both the Drive service privilege and the Data Transfer privilege, which is required according to the documentation.
I'm trying to do this on behalf of the delegated admin user.
The reason for this is, that I want to make sure that every file that gets created by any user in our domain, will transfer its ownership to one admin user.
So I have an (offline) access token and refresh token stored in my backend. I use the Push Notification service from the Google Drive API to respond to new files created by users in my domain using this (stripped down) code:
$client = new \Google_Client();
$client->setAuthConfigFile('client_config.json');
// This is the access token json for the delegated admin user.
$client->setAccessToken('{access_token_json_string}');
$drive = new \Google_Service_Drive($client);
// This is the file created by the user. It has two permissions: The user is the 'owner' and the admin user is the 'writer'
$fileId = 'XXX_XXxXXXXxxXXXxxX';
// this is the admin user's permission id, of role 'writer'. This is the one I am trying to change to 'owner'.
$permissionId = '1234567890';
// I've tried setting all values, but that doesn't help
$permission = new \Google_Service_Drive_Permission();
$permission->setId($permissionId);
$permission->setEmailAddress('admin#domain.com');
$permission->setValue('admin#domain.com');
$permission->setType('user');
$permission->setRole('owner');
// Both 'put' and 'update' raise the same error
$drive->permissions->update($fileId, $permission->getId(), $permission, array(
'transferOwnership' => true
));
The response I am getting:
Error calling PUT https://www.googleapis.com/drive/v2/files/XXX_XXxXXXXxxXXXxxX/permissions/1234567890?transferOwnership=true: (403) Insufficient permissions for this file
Note that all other API calls work properly, so the access token is valid. There's also no option available within Google Drive itself to transfer the ownership (as being the admin user). Transferring it from the normal user to the admin user works as expected.

Resources