I have a web app made with PHP (Laravel more specifically, but doesn't really matter) where I am implementing Google OAuth 2.0 authentication for users in customer Google Workspaces.
I have created an app in Google Console and added OAuth 2.0 client with client id, client secret, redirect URI and the scopes below. The app's publishing status is currently "Testing", and User type is "External".
Scopes:
I have access to an admin account in a customer's Google Workspace for development and testing, and at admin.google.com I navigated to Security -> Access and data control -> API controls -> Manage Domain Wide Delegation, where I added my OAuth client id and added all the same scopes as above.
When logging in using a regular (non-admin) user account from that same customer's Google Workspace into my app (I have added this user as a test user in my Google Auth Platform), the login process goes through succesfully with 3 different screens:
After the user is redirected back into my web app, I am getting the following error while trying to retrieve the user's attributes using directory API after fetching the access token using the auth code:
Google\Service\Exception
{ "error": { "code": 403, "message": "Not Authorized to access this resource/api", "errors": [ { "message": "Not Authorized to access this resource/api", "domain": "global", "reason": "forbidden" } ] } }
Until now my web app has supported only Microsoft Entra ID tenants and its users, where I have a similar OAuth 2.0 app. When Microsoft tenant customers start using my app, their admin user needs to grant admin consent on behalf of the whole organization for the scopes my web app needs. After that, none of the users (even regular non-admin users) ever need to grant any permissions or see any consent screens (not even the first time using the app).
I thought the domain-wide delegation was similar to Microsoft's admin consent, but is it not enough to grant the permission for all users in the Workspace to use the app?
Code where I redirect the user from my web app to Google's authentication (only the relevant parts):
$client = new Google\Client();
$client->setClientId(env('GOOGLE_APP_CLIENT_ID'));
$client->setClientSecret(env('GOOGLE_APP_CLIENT_SECRET'));
$client->setRedirectUri('http://localhost/login/google-callback'); // Local testing
$client->setAccessType('offline');
$client->setPrompt('consent');
$client->setScopes([
'openid',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/admin.directory.user.readonly',
'https://www.googleapis.com/auth/admin.directory.group.readonly',
]);
$client->setHostedDomain($tenant->domainName);
$authUrl = $client->createAuthUrl();
return redirect()->away($authUrl);
Code when Google redirects the user back into my web app (only the relevant parts):
$client = new Google\Client();
$client->setClientId(env('GOOGLE_APP_CLIENT_ID'));
$client->setClientSecret(env('GOOGLE_APP_CLIENT_SECRET'));
$client->setRedirectUri('http://localhost/login/google-callback'); // Local testing
$code = $request->query('code');
$token = $client->fetchAccessTokenWithAuthCode($code);
$client->setAccessToken($token);
$idToken = $token['id_token'];
$payload = $client->verifyIdToken($idToken);
$email = $payload['email'] ?? null;
$directoryService = new Google\Service\Directory($client);
// This is the line which causes the exception!
$user = $directoryService->users->get($email, [
'fields' => 'id,primaryEmail,name',
]);
dd($user); // Debug
Ok, it seems I have fundamentally misunderstood how Google's scopes/permissions work and are used, and Domain-wide delegation is not in any way similar to Microsoft Entra ID admin consent.
I had to create a service account in my own Google Cloud, download the key JSON-file, and then in customer's Google Workspace I had to add the service account as Domain-wide delegation with the admin directory API scopes (user.readonly and group.readonly), along with the OAuth 2.0 client I added earlier.
Then I had to impersonate an admin user (from customer's Google Workspace) in my code because the service account only handles the delegation but does still not have any access to any user data.
I used 2 separate clients in code, one for OAuth 2.0 and one for the admin directory API.
Working code when Google redirects the user back into my web app (only the relevant parts):
$client = new Google\Client();
$client->setClientId(env('GOOGLE_APP_CLIENT_ID'));
$client->setClientSecret(env('GOOGLE_APP_CLIENT_SECRET'));
$client->setRedirectUri('http://localhost/login/google-callback'); // Local testing
$code = $request->query('code');
$token = $client->fetchAccessTokenWithAuthCode($code);
$client->setAccessToken($token);
$idToken = $token['id_token'];
$payload = $client->verifyIdToken($idToken);
$email = $payload['email'] ?? null;
$adminClient = new Google\Client();
$adminClient->setAuthConfig('/path/to/key.json');
$adminClient->setScopes([
'https://www.googleapis.com/auth/admin.directory.user.readonly',
'https://www.googleapis.com/auth/admin.directory.group.readonly',
]);
$adminClient->setSubject('admin@customerworkspace.com');
$directoryService = new Google\Service\Directory($adminClient);
$user = $directoryService->users->get($email, [
'fields' => 'id,primaryEmail,name',
]);
dd($user); // Debug