This article will describe how PowerShell interacts with the Google API to manipulate G Suite users.
In the organization, we use several internal and cloud services. For the most part, authorization in them comes down to Google or Active Directory, between which we cannot maintain a replica, respectively, when a new employee leaves, you need to create / enable an account in these two systems. To automate the process, we decided to write a script that collects information and sends it to both services.
Authorization
When compiling the requirements, we decided to use real human administrators for authorization, this simplifies the analysis of actions in case of accidental or intentional massive changes.
Google APIs use the OAuth 2.0 protocol for authentication and authorization. Usage scenarios and a more detailed description can be found here: Using OAuth 2.0 to Access Google APIs.
I chose the script that is used for authorization in desktop applications. There is also an option to use a service account that does not require unnecessary movements from the user.
The picture below is a schematic description of the selected scenario from the Google page.
First, we send the user to the Google account authentication page, specifying the GET parameters:
application ID
areas that the application needs access to
the address to which the user will be redirected after the procedure is completed
the way we will update the token
Security Code
verification code transmission format
After authorization is completed, the user will be redirected to the page specified in the first request, with an error or authorization code passed by GET parameters
The application (script) will need to get these parameters and, if the code is received, execute the following request to get tokens
When correctly requested, the Google API returns:
Access token with which we can make requests
The expiration date of this token
Refresh token required to refresh the Access token.
First you need to go to the Google API console: Credentials - Google API Console, select the desired application, and in the Credentials section, create an OAuth client ID. In the same place (or later, in the properties of the created identifier), you need to specify the addresses to which redirection is allowed. In our case, this will be several localhost entries with different ports (see below).
To make it easier to read the script algorithm, you can output the first steps into a separate function that will return Access and refresh tokens for the application:
We set the Client ID and Client Secret obtained in the OAuth Client ID properties, and the code verifier is a 43 to 128 character string that should be randomly generated from unreserved characters: [AZ] / [az] / [0-9 ] / "-" / "." / "_" / "~".
This code will then be retransmitted. It eliminates a vulnerability in which an attacker can intercept the response returned by the redirect after the user is authorized.
You can send the code verifier in the current request in cleartext (which makes it meaningless - this is only suitable for systems that do not support SHA256), or by creating a hash using the SHA256 algorithm, which must be encoded in BASE64Url (different from Base64 by two table characters) and remove the character line endings: =.
Next, we need to start listening to http on the local machine in order to receive a response after authorization, which will be returned by a redirect.
Administrative tasks are performed on a dedicated server, we can't rule out the possibility that multiple administrators will run the script at the same time, so it will randomly choose a port for the current user, but I specified predefined ports, because. they must also be added as trusted in the API console.
access_type=offline means that the application can renew an expired token on its own without user interaction with the browser, response_type=code sets the format of how the code will be returned (a reference to the old authorization method, when the user copied the code from the browser into the script), scope specifies scopes and type of access. They must be separated by spaces or %20 (according to URL Encoding). The list of access areas with types can be seen here: OAuth 2.0 Scopes for Google APIs.
After receiving the authorization code, the application will return a close message to the browser, stop listening on the port and send a POST request to receive the token. We specify in it the previously set id and secret from the console API, the address to which the user will be redirected and the grant_type in accordance with the protocol specification.
In response, we will receive an Access token, its validity period in seconds, and a Refresh token, with which we can update the Access token.
The application must store the tokens in a secure place with a long shelf life, so until we revoke the access received, the refresh token will not be returned to the application. At the end, I added a request to revoke the token, if the application was not completed successfully and the refresh token did not return, it will start the procedure again (we considered it unsafe to store tokens locally on the terminal, and we donβt want to complicate it with cryptography or open the browser often).
As you have already noticed, when revoking a token, Invoke-WebRequest is used. Unlike Invoke-RestMethod, it does not return the received data in a usable format and show the status of the request.
Next, the script asks to enter the user's first and last name, generating a login + email.
Inquiries
The next requests will be - first of all, you need to check if there is already a user with such a login in order to get a decision on the formation of a new one or the inclusion of the current one.
I decided to implement all requests in the format of a single function with a selection using switch:
In each request, you need to send an Authorization header containing the token type and the Access token itself. Currently, the token type is always Bearer. Because we need to check that the token is not expired and update it after an hour from the moment of issue, I specified a request for another function that returns an Access token. The same piece of code is at the beginning of the script when receiving the first Access token:
The email:$query request will ask the API to look for a user with exactly that email, including aliases. You can also use wildcard: =, :, :{PREFIX}*.
To obtain data, the GET request method is used, to insert data (creating an account or adding a member to a group) - POST, to update existing data - PUT, to delete a record (for example, a member from a group) - DELETE.
The script will also ask for a phone number (non-validated string) and for inclusion in a regional distribution group. It decides which organizational unit the user should have based on the selected Active Directory OU and will come up with a password:
The update and account creation functions have a similar syntax, not all additional fields are required, in the section with phone numbers you need to specify an array that can contain at least one record with a number and its type.
In order not to get an error when adding a user to a group, we can first check if he is already in this group by getting a list of group members or the composition from the user himself.
Querying the group membership of a particular user will not be recursive and will only show immediate membership. Including a user in a parent group that already has a child group of which the user is a member will succeed.
Conclusion
It remains to send the user a password for a new account. We do this via SMS, and send general information with instructions and login to personal mail, which, along with a phone number, was provided by the recruitment department. As an alternative, you can save money and send the password to the secret telegram chat, which can also be considered the second factor (macbooks will be an exception).
Thank you for reading to the end. I will be glad to see suggestions for improving the style of writing articles and I wish you to catch fewer mistakes when writing scripts =)
List of links that may be thematically useful or just answer questions: