web-dev-qa-db-fra.com

Accès par programme depuis un compte de service à une ressource protégée par Google IAP refusé avec une erreur de signature non valide

J'ai récemment activé IAP dans le cluster GKE.

  • Version du cluster: 1.15.11-gke.11

J'ai suivi les instructions ici: https://cloud.google.com/iap/docs/enabling-kubernetes-howto

La configuration du service est la suivante:

---
apiVersion: cloud.google.com/v1beta1
kind: BackendConfig
metadata:
  name: foo-bc-iap
  namespace: foo-test
spec:
  iap:
    enabled: true
    oauthclientCredentials:
      secretName: iap-client-secret
---
apiVersion: v1
kind: Service
metadata:
  name: foo-internal-service
  namespace: foo-test
  annotations:
    cloud.google.com/backend-config: '{"ports":{"80":"foo-bc-iap"}}'
spec:
  type: NodePort # To create Ingress using the service.
  selector:
    app: foo-test
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8081

Les informations d'identification que j'ai utilisées étaient OAuth 2.0 ID client (Type: Application Web).

Après m'être assuré que le point de terminaison d'API protégé par IAP fonctionne différemment lorsque j'active IAP sur le service Kubernetes, j'ai écrit le programme de test suivant pour m'assurer que le point de terminaison est accessible à partir du compte de service indiqué dans le fichier JSON 'account.json'.

En écrivant cet exemple d'application, j'ai consulté ce document: https://cloud.google.com/iap/docs/authentication-howto#iap_make_request-go

  • google.golang.org/api v0.23.0
  • aller 1.12
func (m *myApp) testAuthz(ctx *cli.Context) error {
    audience := "<The client ID of the credential mentioned above>"
    serviceAccountOption := idtoken.WithCredentialsFile("account.json")

    client, err := idtoken.NewClient(ctx.Context, audience, serviceAccountOption)
    if err != nil {
        return fmt.Errorf("idtoken.NewClient: %v", err)
    }

    requestBody := `{
        <some JSON payload>
    }`

    request, err := http.NewRequest("POST", "https://my.iap.protected/endpoint",
        bytes.NewBuffer([]byte(requestBody)))
    if err != nil {
        return fmt.Errorf("http.NewRequest: %v", err)
    }

    request.Header.Add("Content-Type", "application/json")

    response, err := client.Do(request)
    if err != nil {
        return fmt.Errorf("client.Do: %v", err)
    }
    defer response.Body.Close()

    fmt.Printf("request header = %#v\n", response.Request.Header)
    fmt.Printf("response header = %#v\n", response.Header)

    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        return fmt.Errorf("ioutil.ReadAll: %v", err)
    }

    fmt.Printf("%d: %s\n", response.StatusCode, string(body))

    return nil
}

Mais quand je lance ceci, je ne pouvais voir que la réponse suivante.

request header = http.Header{"Authorization":[]string{"Bearer <jwt token>"}, "Content-Type":[]string{"application/json"}, "X-Cloud-Trace-Context":[]string{"c855757f20d155da1140fad1508ae3e5/17413578722158830486;o=0"}}

response header = http.Header{"Alt-Svc":[]string{"clear"}, "Content-Length":[]string{"49"}, "Content-Type":[]string{"text/html; charset=UTF-8"}, "Date":[]string{"Wed, 06 May 2020 22:17:43 GMT"}, "X-Goog-Iap-Generated-Response":[]string{"true"}}

401: Invalid IAP credentials: JWT signature is invalid

Comme vous pouvez le voir ici, l'accès a été refusé .

J'ai donc pensé que la signature utilisée pour signer le jeton JWT dans l'en-tête pouvait être fausse.

Mais je me suis assuré ce qui suit en utilisant jwt.io:

  • Le jeton JWT utilisé dans l'en-tête est signé par la clé privée du compte de service de l'appelant
  • Le jeton JWT utilisé dans l'en-tête peut être vérifié par la clé publique du compte de service de l'appelant
  • Le jeton JWT a été signé à l'aide de l'algorithme RS256

Et j'ai aussi regardé dans le jeton:

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "<the service account's private key ID>"
}

{
  "iss": "<email address of the service account>",
  "aud": "",
  "exp": 1588806087,
  "iat": 1588802487,
  "sub": "<email address of the service acocunt>"
}

Rien de bien étrange.

Je ne suis donc pas sûr de ce qui se passe ici. Si je désactive IAP, le point final renvoie la bonne réponse .

Quelqu'un peut-il me donner une idée de ce que je fais mal?

7
Byungjoon Lee

Comme @Dirbaio l'a souligné, je pense que c'est un problème spécifique à la v0.23.0. Comme je ne peux pas mettre à niveau la dépendance pour le moment, j'ai choisi de créer un nouveau client IAP qui n'utilise pas idtoken.NewClient. Au lieu de cela, il utilise simplement idtoken.NewTokenSource pour créer un jeton OIDC. L'ajout du jeton à l'en-tête d'autorisation est facile afin que je puisse contourner le problème dans le client créé par idtoken.NewClient.

package main

import (
    "context"
    "crypto/tls"
    "fmt"
    "io"
    "net/http"

    "golang.org/x/oauth2"
    "google.golang.org/api/idtoken"
    "google.golang.org/api/option"
)

// IAPClient is the default HTTPS client with Morse-Code KMS integration.
type IAPClient struct {
    client      *http.Client
    tokenSource oauth2.TokenSource
}

// NewIAPClient returns an HTTP client with TLS transport, but not doing the CA checks.
func NewIAPClient(audience string, opts ...option.ClientOption) *IAPClient {
    tokenSource, err := idtoken.NewTokenSource(context.Background(), audience, opts...)
    if err != nil {
        panic(fmt.Errorf("cannot create a new token source: %s", err.Error()))
    }

    return &IAPClient{
        client: &http.Client{
            Transport: &http.Transport{
                TLSClientConfig: &tls.Config{
                    InsecureSkipVerify: true,
                },
            },
        },
        tokenSource: tokenSource,
    }
}

// Do sends the http request to server with a morse-code JWT Authorization: Bearer header.
func (c *IAPClient) Do(request *http.Request) (*http.Response, error) {
    err := c.addAuthorizationHeader(request)
    if err != nil {
        return nil, fmt.Errorf("couldn't override the request with the new auth header: %s", err.Error())
    }

    return c.client.Do(request)
}

// Get sends the http GET request to server with a morse-code JWT Authorization: Bearer header.
func (c *IAPClient) Get(url string) (*http.Response, error) {
    request, err := http.NewRequest(http.MethodGet, url, nil)

    if err != nil {
        return nil, err
    }

    return c.Do(request)
}

// Post sends the http POST request to server with a morse-code JWT Authorization: Bearer header.
func (c *IAPClient) Post(url, contentType string, body io.Reader) (*http.Response, error) {
    request, err := http.NewRequest(http.MethodPost, url, body)
    if err != nil {
        return nil, err
    }

    request.Header.Add("Content-Type", contentType)

    return c.Do(request)
}

func (c *IAPClient) addAuthorizationHeader(request *http.Request) error {
    tkn, err := c.tokenSource.Token()
    if err != nil {
        return fmt.Errorf("cannot create a token: %s", err.Error())
    }

    tkn.SetAuthHeader(request)

    return nil
}
0
Byungjoon Lee