user1184088
user1184088

Reputation: 51

Endpoints Google ID JWT, and Golang oauth2: have id_token, need access_token?

I have an app engine app behind endpoints and I'm having trouble following the documentation around adding authentication. My preference is to allow service accounts within the project through, then perform more granular app-side authorization.

In my case, most clients will be outside GCP and will be automated programs not people, so I'm using JSON key files thinking that is the way to go (correct me if I'm wrong please). I also don't want to have to redeploy the app to change user configs, so I follow the "GOOGLE ID JWT" information in the documentation from here:

https://cloud.google.com/endpoints/docs/openapi/service-to-service-auth

This is the security sections of my swagger JSON:

  "securityDefinitions": {
    "api_key": {
      "type": "apiKey",
      "name": "key",
      "in": "query"
    },
    "google_id_token": {
      "authorizationUrl": "",
      "flow": "implicit",
      "type": "oauth2",
      "x-google-issuer": "https://accounts.google.com"
    }
  },
  "security": [
    {
      "api_key": [],
      "google_id_token": []
    }
  ]

This deploys OK, but I am stumped on what to do from the client side to make use of the service account JSON.

In Go, I use the following, based on my understanding of the oauth2/google documentation:

package main

import (
    "context"
    "fmt"
    "io/ioutil"
    "log"
    "os"

    "golang.org/x/oauth2/google"
)

func main() {
    ctx := context.Background()
    apiKey := os.Getenv("API_KEY")
    basePath := "https://SERVICE_NAME-dot-PROJECT_NAME.appspot.com" // changed for privacy

    creds, _ := google.FindDefaultCredentials(ctx)
    jwt, _ := google.JWTConfigFromJSON(creds.JSON, basePath)

    client := jwt.Client(ctx)
    res, _ := client.Get(fmt.Sprintf("%s/REQUEST_PATH?key=%s", basePath, apiKey)) // changed for privacy

    body, _ := ioutil.ReadAll(res.Body)
    log.Printf("### http status: %d %s", res.StatusCode, res.Status)
    log.Printf("### http body: %s", body)
}

I've removed error-checking for this code paste for brevity, there are no errors when I run this. The result of this being:

2017/10/16 07:32:38 ### http status: 401 401 Unauthorized
2017/10/16 07:32:38 ### http body: {
 "code": 16,
 "message": "JWT validation failed: Missing or invalid credentials",
 "details": [
  {
   "@type": "type.googleapis.com/google.rpc.DebugInfo",
   "stackEntries": [],
   "detail": "auth"
  }
 ]
}

Upon inspection of the internals of what's happening, such as calling jwt.TokenSource(ctx).Token(), I see that Google is returning an id_token, but no access_token (spotted by adding some debug logs to the jwt package):

2017/10/16 07:38:47 ### oauth2/jwt -> tokenURL: https://accounts.google.com/o/oauth2/token, form: url.Values{"grant_type":[]string{"urn:ietf:params:oauth:grant-type:jwt-bearer"}, "assertion":[]string{"...cut..."}}
2017/10/16 07:38:47 ### oauth2/jwt <- response: {
  "id_token" : "...cut..."
}
2017/10/16 07:38:47 ### oauth2/jwt <- token: &oauth2.Token{AccessToken:"", TokenType:"", RefreshToken:"", Expiry:time.Time{wall:0x0, ext:63643740079, loc:(*time.Location)(0x1424160)}, raw:map[string]interface {}{"id_token":"...cut..."}}

So when the oauth2 HTTP transport tries to add the auth header -- which is tied to the AccessToken field above -- it results in a string like Authorization: Bearer, thus the result fails.

I feel like I'm missing a step here which isn't obvious to me in the documentation.

Thanks for your time.

Upvotes: 1

Views: 1762

Answers (1)

saiyr
saiyr

Reputation: 2595

It would be expected for the oauth client to return id_token instead of access_token. You're using JWTConfigFromJSON, not ConfigFromJSON, which is correct. I think the second argument to JWTConfigFromJSON is incorrect, and you should be specifying scopes instead. In this case, try "email" instead of basePath.

Upvotes: 1

Related Questions