OAuth2 Proxy login interface with Google authentication

Server-to-server authentication through oauth2-proxy has no obvious solution when using the Google provider. There is no built-in mechanism for API clients to authenticate without a browser-based OAuth2 flow. The workaround: exchange Google Service Account credentials for a JWT token that oauth2-proxy can validate directly.

Overview

In order to do server to server auth through oauth2-proxy when using Google Provider, you have to do the following:

  1. Setup a new Google Service Account
  2. Update OAuth2 Proxy settings
  3. Exchange SA credentials for JWT token

Google Service Account

Creating a Service Account is pretty standard fare, and no special permissions are needed for it. Grab the service-account.json file as you will need it to exchange for JWT Token

Update OAuth2 Proxy settings

When deploying oauth2 proxy, in addition to Google provider settings you need to set the following options:

  • --oidc-issuer-url=https://accounts.google.com, without this setting, as of v7.2.1, oauth2-proxy cannot validate jwt tokens when using the Google provider.
  • --skip-jwt-bearer-tokens=true - this tells oauth2 proxy not to do the exchange if Authorization: Bearer ey.... header is set.

We also set the --authenticated-emails-file=/path/to/file setting, where you will need to add the SA email address that you created in the first step.

Exchange SA credentials for JWT token

Last thing you will need to do is have a valid JWT token, which requires using the SA to get it from Google. Here’s some code that gives you the token:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
  #!/usr/bin/env bash
  set -euo pipefail

  get_token() {
    # Get the bearer token in exchange for the service account credentials.
    local service_account_key_file_path="${1}"
    local client_id="${2}"

    local iam_scope="https://www.googleapis.com/auth/iam"
    local oauth_token_uri="https://www.googleapis.com/oauth2/v4/token"

    local private_key_id="$(cat "${service_account_key_file_path}" | jq -r '.private_key_id')"
    local client_email="$(cat "${service_account_key_file_path}" | jq -r '.client_email')"
    local private_key="$(cat "${service_account_key_file_path}" | jq -r '.private_key')"
    local issued_at="$(date +%s)"
    local expires_at="$((issued_at + 3600))"
    local header="{'alg':'RS256','typ':'JWT','kid':'${private_key_id}'}"
    local header_base64="$(echo "${header}" | base64)"
    local payload="{'iss':'${client_email}','aud':'${oauth_token_uri}','exp':${expires_at},'iat':${issued_at},'sub':'${client_email}','target_audience':'${client_id}'}"
    local payload_base64="$(echo "${payload}" | base64)"
    local signature_base64="$(printf %s "${header_base64}.${payload_base64}" | openssl dgst -binary -sha256 -sign <(printf '%s\n' "${private_key}")  | base64)"
    local assertion="${header_base64}.${payload_base64}.${signature_base64}"
    local token_payload="$(curl -s \
      --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
      --data-urlencode "assertion=${assertion}" \
      ${oauth_token_uri})"
    local bearer_id_token="$(echo "${token_payload}" | jq -r '.id_token')"
    echo "${bearer_id_token}"
  }

  main() {
    # TODO: Replace the following variables: 
    SERVICE_ACCOUNT_KEY="path-to-service-account.json"
    IAM_CLIENT_ID="<your client id>.apps.googleusercontent.com"

    # Obtain the Bearer ID token.
    ID_TOKEN=$(get_token "${SERVICE_ACCOUNT_KEY}" "${IAM_CLIENT_ID}")
    # Access the application with the Bearer ID token.
    echo ${ID_TOKEN}
  }

  main "$@"

While get_token looks complicated, its not. All its doing is a valid jwt token request and you can read more about it here. Key details are the client_email which must match allowed email address and target_audience which must match the client id in OAuth2 Proxy.

And thats it! Now, in addition to secure, short lived access for your users, you can have service accounts exchange their credentials for a valid JWT token.