At Stack we’ve written and maintain a set of scripts named “Dev-Local-Setup” that our developers and designers can use to quickly provision their local environments. They assist in installing pre-requisites (things like Chocolatey, SQL Server, VS 2019, .NET Core SDKs), pulling down repos and provisioning databases and websites used by specific teams. They’re packaged as a PowerShell module and used by a menu-driven CLI interface that makes it easy to perform those tasks quickly and efficiently.
Dev-Local-Setup
We have a bunch of shared testing data that we store on a file share accessible over the VPN and we also synchronize to Google Drive to facilitate quick downloads for an audience that is spread all over the globe (downloading things from an NY-based data center can be sloooow when you’re in Australia!).
Google Drive requires authentication in order to allow things to be downloaded so we have configured our scripts as an application in Google and use OAuth to obtain an access token whenever somebody needs to download resources stored there.
If you search around the web you’ll find plenty of “solutions” to this issue, but most of them want you to obtain an access token and embed it in your script. I cannot stress what a terrible idea this is - never store secrets in source control, not to mention that the access token is tied to whoever writes the script. It’s probably worth mentioning that if you need to fully automate script access to Google APIs I’d suggest looking into service accounts and passing the relevant credentials from the environment that runs the script instead. Our approach is useful for when the script is used interactively.
Here’s how we used to do things:
- Import the WinForms types into our PowerShell script
- Construct a WinForms
Form
and embed aWebBrowser
object in it - Hook the
ContentLoaded
event on theWebBrowser
to detect URL changes - Navigate to the URI used to obtain an authorization token once a user has successfully authenticated.
- Configure the redirect URI to be some surrogate that we can intercept using the
DOMLoaded
event - When we hit the surrogate URI we extract the
code
querystring parameter and use it to obtain an access token - Use the access token to download the resources we need!
That has worked fine for a long time, but lately IE11 support has started to disappear across the web (including from Stack Exchange) and the Google authentication flows don’t work reliably in the embedded frame. Our first attempt to fix this was to use Microsoft’s WebView
component - this is an equivalent Winforms control that embeds Edge instead of IE11. Easy peasy, let’s do it!
Turns out it isn’t that easy! Given the nature of the scripts - installing software and other privileged operations - we need to run in an elevated security context. This is our first roadblock - WebView
has a real hard time doing anything when the security context is not that of a regular user. It appears that this is because some core infrastructure pieces of UWP applications (which Edge is) cannot run in an elevated security context.
If we run our scripts as a regular user we can get a modal to render the Edge-based browser but when we come to redirect to our surrogate URI (in our case it happens to be https://localhost/
) the browser won’t render anything. Eurgh, another roadblock! In this case UWP applications have additional security restrictions on accessing localhost in order to prevent port scanning - a common technique used by malware to determine whether a service is running or not. We can overcome this roadblock by running checknetisolation LoopbackExempt -a -n=""Microsoft.MicrosoftEdge_8wekyb3d8bbwe"
but that doesn’t feel like a good thing to do from scripts.
At this point we took a step back and re-considered our approach. The above roadblocks might well be solved by the new WebView2
component that embeds the preview version of Edge based upon Chromium, but it’s still in beta and requires us to install additional SDKs to work correctly.
We decided to take another approach - here’s the crazy we decided upon…
Create a Web Server
Create a bare-bones .NET Core 3.0 Kestrel-based application - a .csproj
and a .cs
contains everything we need to intercept a request to a surrogate URI. When we receive a valid request with an authorization code we output it to stdout and exit. If we receive an error we just exit. That looks a little like this:
OAuthListener.csproj
|
|
Program.cs
|
|
As part of our pre-requisites we install .NET Core 3.0 SDK so to execute this we simply dotnet run
the project passing the port number we want to listen to.
Open the Default Web Browser
Instead of opening a browser embedded in a Winforms modal we can simply use the user’s default browser. This has a couple of advantages - we don’t need to worry about elevation affecting things (this is effectively just performing a Shell.Open
call which executes in the same privilege level as Windows Explorer) and often the developer is already authenticated to Google in that browser so it’s less work overall.
When we construct the URL we configure our redirect URL to point at http://localhost:<port>
where <port>
is an arbitrary port number specified by our script in the next step…
Using from PowerShell
Finally we hook it all together…
|
|
Then to retrieve an access token and call an API using it (in our case to download a file from Google Drive):
|
|
And that’s it! This approach seems to be a lot more reliable than our previous approach; we get to use a modern browser that doesn’t have issues handling Google’s OAuth flow and a simple web server that is trivial to understand…