Google Cloud Using a Google Cloud service account to authenticate to KeyCloak

Let’s assume we have an application that uses KeyCloak for authentication and exposes a set of APIs. To call one of these APIs, a client first has to obtain an access token from KeyCloak, and then pass this access token in the API call.

But how does the client application authenticate to KeyCloak? The most common approach is to let the application use the OAuth client_credentials grant in combination with a client secret. That solves the authentication problem, but creates a new problem: We now have to worry about storing that secret securely, and rotating it periodically. The client secret is, after all, nothing else than a password, and must be treated as such.

Avoiding secrets

When we deploy an application on Google Cloud, we can attach a service account to the underlying compute resource. That lets the app obtain access tokens and IdTokens for the service account.

But there’s something else an application can do with a service account: Sign data.

Each service account has a Google-managed RSA key pair which is:

[...] automatically generated and fully managed by Identity and Access Management. IAM maintains Google-managed key pairs for all service accounts, and rotates them periodically. You can download the public key of a Google-managed key pair, but you can't access its private key.
To use the private key of a Google-managed key pair, you must request IAM to perform operations for you. For example, you can invoke the signBlob or signJwt operations to let IAM use a service account's Google-managed private key to sign a piece of data for you. Similarly, by attaching a service account to a compute resource, you can authorize IAM to use the Google-managed key whenever the compute resource requests service account credentials.

The other day, we looked at whether we could use a service account’s Google-managed RSA key pair to sign a client assertion and use that to authenticate to Azure. But we had to conclude that it’s not possible:

Instead of using client secrets, Azure also allows clients to authenticate by using an assertion.
[...]
To be recognized by Azure, assertions must be signed by using an RSA private key, and the corresponding public key must be uploaded to Azure in the form of a X.509 certificate.
[...]
Using assertions and certificates to authenticate to Azure sounds like an interesting option: Service accounts on Google Cloud already have a (Google-managed) RSA private key, and the corresponding public key is available as a X.509 certificate on https://www.googleapis.com/service _accounts/v1/metadata/x509/[email protected]. We can just upload this certificate to Azure AD, and let the application use its attached service account to sign the assertion and authenticate!
Unfortunately, the approach doesn’t work in practice because of key rotation: The Google-managed keys are rotated every few days, and once they are, the setup breaks.

The problem was that Azure requires us to upload the signing certificate, but the certificate changes every few days.

Like Azure, KeyCloak also allows clients to authenticate by using the client_credentials grant and a signed assertion. But unlike Azure, KeyCloak doesn’t require us to upload the signing certificate – instead, we can point KeyCloak to the service account’s JSON Web Key Set (JWKS) endpoint.

This difference is crucial, because it solves the key rotation issue: Whenever Google Cloud rotates the Google-managed key of a service account, KeyCloak can fetch the latest signing keys by downloading the JWKS.

Using a service account to sign a client assertion

Here’s the idea:

  1. In GCP, we create a service account and attach it to the compute resource (VM, Cloud Run instance, Cloud Function, …) that runs our app.
  2. We grant the service account the Service Account Token Creator role on itself, so that the service account can use its own signing key to sign data.
  3. In KeyCloak, we create a confidential client, configure it to use signed JWTs for authentication. We then point KeyCloak to the service account’s JWKS endpoint.

With that setup in place, an app can do the following to authenticate to KeyCloak:

  1. Create a client assertion (which is nothing else than a small JSON document).
  2. Obtain an access token from the metadata server (for example, by using the client libraries).
  3. Use the access token to authenticate to the IAM Credentials API and call the signJwt method to sign the assertion.
  4. Authenticate to KeyCloak by using the client_credentials grant and the signed assertion.

KeyCloak will then:

  1. Parse the assertion and extract the client ID.
  2. Look up the JWKS endpoint for the client.
  3. Download the latest signing keys from the JWKS endpoint.
  4. Validate the assertion’s signature using the signing keys from the JWKS.

The following diagram illustrates the process:

Sequence

Arguably, the process is more complex than simply using a client secret, but it has some key advantages:

  • We don’t need to manage any secrets.
  • Signing keys are rotated automatically, and we don’t have to worry about it.

Now let’s go through the steps in more detail.

Creating a client in KeyCloak

First, we need to create a client in KeyCloak. This client corresponds to a specific Google Cloud service account:

  1. In the KeyCloak admin console, go to Clients and click Create.
  2. Enter a client ID, and click Save.
  3. On the Settings page, configure the following settings:
    1. Access type: confidential
    2. Service accounts enabled: on
    3. Valid redirect URIs: If we’re only use using the client_credentials grant, the value of this parameter is irrelevant, so we can use a dummy value like https://invalid.example/.
    4. Advanced settings > Access token lifespan: 10 minutes
  4. Click Save.
  5. On the Credentials page, configure the following settings:
    1. Client Authenticator: Signed Jwt
    2. Signature Algorithm: RS256 (That’s what signJwt uses).
  6. On the Keys page, configure the following settings:
    1. Use JWKS URL: on
    2. JWKS URL: https://www.googleapis.com/service_accounts/v1/[email protected].
  7. Click Save.

Note: To validate the client assertion, KeyCloak must be able to download the JWKS. If we run KeyCloak behind a proxy, that requires some extra configuration.

Authenticating

With that setup in place, let’s test the process with PowerShell:

  1. Initialize a few variables:

    $ClientId = 'KEYCLOAK-CLIENT-ID'
    $ServiceAccountEmail = '[email protected]'
    
  2. Obtain a Google access token:

    $AccessToken = (gcloud auth print-access-token)
    
  3. Create a client assertion:

    $Assertion = @{
      "sub" = $ClientId
      "iss" = $ClientId
      "aud" = "https://keycloak.example.com/auth/realms/master/protocol/openid-connect/token"
      "jti" = [Guid]::NewGuid().ToString()
      "iat" = [DateTimeOffset]::Now.ToUnixTimeSeconds()
      "exp" = [DateTimeOffset]::Now.AddMinutes(5).ToUnixTimeSeconds()
    } | ConvertTo-Json
    
  4. Call signJwt to sign the assertion:

    $SignedAssertion = (Invoke-RestMethod `
      -Method POST `
      -Uri "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$($ServiceAccountEmail):signJwt" `
      -ContentType "application/x-www-form-urlencoded" `
      -Header @{
          "Authorization" = "Bearer $AccessToken"
      } `
      -Body @{
          "payload" = $Assertion
      }).signedJwt
    

    When decoded, the resulting assertion (JWT) looks like this:

    {
      "alg": "RS256",
      "kid": "995f7747670906b8d7982d3fc3ced699f49014e5",
      "typ": "JWT"
    }.{
      "exp": 1635581143,
      "sub": "KEYCLOAK-CLIENT-ID",
      "jti": "beadd154-3543-4b28-85f5-1cc8f984ff70",
      "aud": "https://keycloak.example.com/auth/realms/master/protocol/openid-connect/token",
      "iss": "KEYCLOAK-CLIENT-ID",
      "iat": 1635580543
    }.[Signature]
    

    Notice the kid claim in the header, which lets KeyCloak find the right signing key in the JWKS.

  5. Authenticate to KeyCloak by using the signed assertion:

    Invoke-RestMethod `
        -Method POST `
        -Uri "https://keycloak.example.com/auth/realms/master/protocol/openid-connect/token" `
        -ContentType "application/x-www-form-urlencoded" `
        -Body "grant_type=client_credentials&client_assertion=$SignedAssertion&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
    

    Notice the two extra parameters:

    • client_assertion contains the signed assertion.
    • client_assertion_type indicates that the client assertion is a signed JWT.

    And, lo and behold, it works – KeyCloak returns an access token:

    access_token       : eyJhbGciOiJSUzI1NiIsInR5cCIgOiA...
    expires_in         : 600
    refresh_expires_in : 0
    token_type         : Bearer
    not-before-policy  : 0
    scope              : profile email
    
Any opinions expressed on this blog are Johannes' own. Refer to the respective vendor’s product documentation for authoritative information.
« Back to home