googlesignin

package module
v0.0.0-...-c38336e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2023 License: Apache-2.0 Imports: 10 Imported by: 0

README

Google Sign-In

This library provides Go HTTP middleware to require Google Sign-In in a web application, and/or to use the Google Cloud Identity Aware Proxy. This library tries to be as easy to use as possible, and to use secure defaults. It verifies ID tokens on the server using Google's public keys and Square's go-jose library. It requires users to be logged in for all endpoints, except endpoints that are explicitly made public. This is particularly useful for "internal" applications that should be available to users in your domain, but not the public. Just use the RequiresSignIn wrapper for all your endpoints, set the HostedDomain argument, and you are done! This version cannot be used to request OAuth2 access tokens, at least as of 2021-08-13. Use the googlesignin/oauth2 version of this API if you need access tokens.

This library also supports the GCP Identity-Aware Proxy, and authenticating requests using Google Cloud Service accounts as both a client and server.

Google previously recommended web applications use the Google Sign-In Javascript Platform Library. I initially wrote this because I was curious how it compares to the older OAuth2 redirect approach. In August 2021, Google announced that this library will stop working after March 2023. This library was updated at that time to the newer Google Identity Services. Unfortunately, this seems to not permit a sign in without a click on Google's button at this time. It would probably be better to use the old-fashioned Google OAuth instead.

TODO/Bugs
  • Figure out how to fix the redirect behavior.
  • Correctly remember the page to return to on sign in. This breaks the IAP proxy demo.

Example

The example is running at https://gosignin-demo.appspot.com/. It has a main page that is not protected, then three sub-pages that print the user's email address and additional access token information.

To run it yourself:

  1. Create a new Google Sign-In OAuth client ID and secret. Follow Google's instructions to do this.
  2. Configure the OAuth client to permit https://YOURDOMAIN as an Authorized JavaScript origin and https://YOURDOMAIN/__start_signin as an Authorized redirect URI. For localhost testing, you must add both http://localhost and http://localhost:PORT to Authorized JavaScript origin.
  3. Run the example on YOURDOMAIN: CLIENT_ID=YOURID go run github.com/evanj/googlesignin/example
  4. Visit https://YOURDOMAIN/ in your browser and click the links.

Design Overview / Notes

This uses the Google Identity Service HTML API and saves the resulting ID token in a cookie. This is an HTTPOnly cookie, so it can only be accessed on the server. Even if it leaks, it is time limited, and scoped to the OAuth2 Client ID that was requested, so it can't really be used for much else.

The Go handler requires this cookie to be set, and validates the ID token on each request. If it is invalid, permission is denied or it redirects to the sign in page. The original URL is saved in a cookie so the sign in page can redirect when it receives the response from Google.

Identity-Aware Proxy

The Google Cloud Identity-Aware Proxy lets you control access to web applications using Google's built-in access control. This package provides HTTP middleware to verify the signed header and extract the email address. This performs the same function as the googlesignin package, but for applications using IAP. This repository contains an example. It is running at https://goiap-demo.uc.r.appspot.com, but you won't be able to access it (sorry!). This is simpler and probably more secure than relying on Google Sign-In, but only works on Google Cloud.

Simulate the Identity-Aware Proxy

There are many environments that don't support the Identity-Aware Proxy. One of the newest is Cloud Run, which has built-in authentication, but only using "Authorization: Bearer ..." headers, so it won't work for web applications. As a hack, I created a proxy server which simulates the Identity-Aware Proxy. It uses this package to redirect requests that are not authenticated to force a user to sign in. If the request is authenticated, then it is proxied through to the original backend.

Demo: https://proxytest-kgdmaenclq-ue.a.run.app/

You can set the HOSTED_DOMAIN environment variable to only allow users from a specified Google account domain.

Google's public key caching/rotation policy

The rotationcheck program periodically loads Google's public keys, and checks their cache expiration. I used this to reverse engineer their key caching and rotation policies.

  • When fetching public keys from the URL, Google appears to return a Cache-Control: max-page= parameter that is a random value between [5h, 7h] from the current time. It seems to be that one server has a specific cache expiration time, since multiple requests to the same IP return that value, but requests to DIFFERENT IPs return different values.

  • Google uses a single public key for a LONG time: it looks like 7 days.

  • After Google stops using a public key, it keeps publishing it for about 5 days.

  • Before using a new public key, Google publishes it well in advance: About 3 days before it uses it.

  • When transitioning to issue tokens with a new key, there is a period where both are being used. The transition takes about 30 minutes.

Specific timeline

This follows a new key (k2) through its entire lifecycle.

  • 2020/06/16 21:29:40: Publishing a new public key (serving 3 keys: old=k0, current=k1, next=k2)
  • 2020/06/17 04:44:40: Reorders the keys: current=k1, next=k2, old=k0
  • 2020/06/17 16:44:40: Removes old public key (serving 2 keys: current=k1, next=k2)
  • 2020/06/19 20:27:40 until 2020/06/19 20:53:40: Issuing tokens with both keys (current=k1, next=k2)
  • 2020/06/24 21:29:40: Publishing next key (serving 3 keys: old=k1, current=k2, next=k3)
  • 2020/06/25 04:44:40: Reorders the keys: current=k2, next=k3, old=k1
  • 2020/06/25 16:44:40: Removes old public key (serving 2 keys: current=k2, next=k3)
  • 2020/06/27 20:37:40 - 2020/06/27 20:49:40: Issuing tokens with both keys (current=k2, next=k3)
  • 2020/07/02 21:29:40: Publishing next key (serving 3 keys: old=k2, current=k3, next=k4)
  • 2020/07/03 04:44:40: Reorders the keys: current=k3, next=k4, old=k2
  • 2020/07/03 16:44:40: Removes old public key (serving 2 keys: current=k3, next=k4)

Service Account Authentication

To access resources on Google Cloud, humans use user accounts and software uses service accounts. You can re-use those accounts to authenticate other thing. The serviceaccount package contains code to make this easy. This lets you use service accounts to send requests to Identity-Aware Proxy protected pages, or to use Google Service Accounts to authenicate other things, like gRPC services. To create a service from Google credentials, use serviceaccount.NewSourceFromDefault. To require Google credentials to access server, use serviceaccount.NewAuthenticator.

Interesting note: Google's authentication seems to use a 5 minute "grace period." The tokens work for that much longer than the expiration time in the token.

The best reference I've seen on how this actually works is: https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_a_service_account

Audiences (aud)

If you are using IAP, the audience (aud) field is the Client ID provided by Google. However, if you want to use this yourself, you can use any https URL that you would like (e.g. https://www.example.com).

Demo

I have a demo service that will accept credentials from any Google service account. If you visit the page, it will tell you that you are not authenticated. Send it an authorized request with:

GOOGLE_APPLICATION_CREDENTIALS=[service account key.json] go run ./serviceaccount/exampleclient/exampleclient.go \
  --audience=https://example.evanjones.ca \
  --url=https://serviceaccount-dot-gosignin-demo.appspot.com
Notes about how this works
  1. The client signs a JWT with "target_audience": "(DESIRED AUDIENCE)" and sends it to Google. This proves that the client has access to the service account key.
  2. Google verifies the JWT and that the service account and key are still active. It returns a JWT signed by Google with "audience": "(DESIRED AUDIENCE)". This proves that the service account and key is currently valid.
  3. The client sends this JWT to the desired service.
  4. The service verifies that the JWT is correctly signed by Google and is not expired, and has the correct expected audience. This proves that the client had access to the key, and the service account was valid at the time the token was issued. The audience ensures other services can't maliciously reuse the token for other things.

This does mean that revocation takes up to an hour. If this is a concern, you'll need to do some other check with Google to ensure the account is still valid.

Documentation

Overview

Package googlesignin implements a Go API to sign in users with Google accounts. It attempts to use the most up to date "recommended" API from Google, since they seem to decide to change it every few years.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func InsecureMakeAuthenticated

func InsecureMakeAuthenticated(r *http.Request, idToken string) *http.Request

InsecureMakeAuthenticated makes a new *http.Request that is authenticated. It copies r and sets idToken and accessToken in the correct cookies, and marks the request as valid for MustGetEmail. This should only be called by tests.

Types

type Authenticator

type Authenticator struct {
	// If set, the Google accounts must belong to this domain. See:
	// https://developers.google.com/identity/protocols/OpenIDConnect#hd-param
	HostedDomain string
	// The path used to start and complete Google Sign In. Defaults to "/__start_signin".
	// Must start with /.
	SignInPath string
	// The path used to sign users out. Defaults to "/__signout". Must start with /.
	SignOutPath string
	// The path users will be redirected to after signing out, or when loading the sign in page
	// directly without a redirect (e.g. sometimes when hitting back). Defaults to "/".
	DefaultRedirect string
	// If true, users will be redirected to log in if they are not. Otherwise they get a failed
	// response.
	RedirectIfNotSignedIn bool

	// Gets keys to validate tokens. Should not be changed except in tests.
	CachedKeys jwkkeys.Set
	// contains filtered or unexported fields
}

Authenticator is an HTTP server middleware for requiring Google Sign-In.

func New

func New(clientID string) *Authenticator

New creates an Authenticator, configured with the provided OAuth configuration. The middleware will serve the page to start the sign in publicly at signInPath.

func (*Authenticator) GetEmail

func (a *Authenticator) GetEmail(r *http.Request) (string, error)

GetEmail returns the email for a request if it is signed in. This can be used on public pages. The error reports details that should not be returned to the client.

func (*Authenticator) GetIDToken

GetIDToken returns the valid ID token for an authenticated request. This can be used on public pages. The error reports details that should not be returned to the client.

func (*Authenticator) IsSignedIn

func (a *Authenticator) IsSignedIn(r *http.Request) bool

IsSignedIn returns true if the user is signed in to an accepted Google account. This can be used on public pages, for example to conditionally display content.

func (*Authenticator) MakePublic

func (a *Authenticator) MakePublic(path string)

MakePublic makes path accessible without signing in. This does path matching, unlike ServeMux, so "/" only permits the root page, and "/dir/" only permits the exact path "/dir/". It is currently not possible to permit subdirectories or any kind of pattern.

func (*Authenticator) MustGetEmail

func (a *Authenticator) MustGetEmail(r *http.Request) string

MustGetEmail returns the authenticated user's email address, or panics if the user is not signed in. The request must have been served by RequiresSignIn.

func (*Authenticator) PermitInsecureCookies

func (a *Authenticator) PermitInsecureCookies()

PermitInsecureCookies configures the Authenticator to allow sending cookies over HTTP connections (not setting the Secure cookie option). This should only be used for localhost testing. In production, you should only send cookies over HTTPS since they contain sensitive user data.

func (*Authenticator) RequireSignIn

func (a *Authenticator) RequireSignIn(handler http.Handler) http.Handler

RequireSignIn wraps an existing http.Handler to require a user to be signed in. It will fail the request, or will redirect the user to sign in.

Directories

Path Synopsis
iap
Package iap provides HTTP middleware for Google Cloud's Identity-Aware Proxy.
Package iap provides HTTP middleware for Google Cloud's Identity-Aware Proxy.
Package jwkkeys verifies JWTs using keys published at known URLs.
Package jwkkeys verifies JWTs using keys published at known URLs.
Checks how Google rotates its public keyss
Checks how Google rotates its public keyss
Package serviceaccount authenticates requests using Google Cloud service accounts, on both the client and server side.
Package serviceaccount authenticates requests using Google Cloud service accounts, on both the client and server side.
Package signintest provides shared test code for testing signing in with Google accounts.
Package signintest provides shared test code for testing signing in with Google accounts.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL