Get access token for a google cloud service account in Golang? - go

I'm using a service account on google cloud. For some reason, I want to get the access token programmatically in golang. I can do gcloud auth application-default print-access-token on the command line.
There is a library by google that seems to allow me to get the token. Here is how I try to use it:
credentials, err := auth.FindDefaultCredentials(ctx)
if err == nil {
glog.Infof("found default credentials. %v", credentials)
token, err2 := credentials.TokenSource.Token()
fmt.Printf("token: %v, err: %v", token, err2)
if err2 != nil {
return nil, err2
}
However, I get an error saying token: <nil>, err: oauth2: cannot fetch token: 400 Bad Request.
I already have GOOGLE_APPLICATION_CREDENTIALS env variable defined and pointing to the json file.

Running your code as-is, returns an err:
Invalid OAuth scope or ID token audience provided
I added the catch-all Cloud Platform writable scope from Google's OAuth scopes:
https://www.googleapis.com/auth/cloud-platform
Doing so, appears to work. See below:
package main
import (
"context"
"log"
"golang.org/x/oauth2"
auth "golang.org/x/oauth2/google"
)
func main() {
var token *oauth2.Token
ctx := context.Background()
scopes := []string{
"https://www.googleapis.com/auth/cloud-platform",
}
credentials, err := auth.FindDefaultCredentials(ctx, scopes...)
if err == nil {
log.Printf("found default credentials. %v", credentials)
token, err = credentials.TokenSource.Token()
log.Printf("token: %v, err: %v", token, err)
if err != nil {
log.Print(err)
}
}
}
I had some challenges using this library recently (to access Cloud Run services which require a JWT audience). On a friend's recommendation, I used google.golang.org/api/idtoken instead. The API is very similar.

Related

How to get the decoded token for jwt auth0 and gofiber?

Im using next.js auth0 and a custom golang api backend and I'm
having trouble getting the decoded token on the backend side.
On the frontend side I followed this tutorial -
https://auth0.com/docs/quickstart/webapp/nextjs/01-login
and I managed to send the accessToken to my backend API successfully
on the backend side I followed this tutorial -
https://auth0.com/docs/quickstart/backend/golang/01-authorization
The middleware has successfully verified the token
Example middleware from auth0 implementation
func EnsureValidToken(next http.Handler) http.Handler {
// EnsureValidToken is a middleware that will check the validity of our JWT.
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
issuerURL, err := url.Parse("https://" + os.Getenv("AUTH0_DOMAIN") + "/")
if err != nil {
log.Fatalf("Failed to parse the issuer url: %v", err)
}
provider := jwks.NewCachingProvider(issuerURL, 5*time.Minute)
jwtValidator, err := validator.New(
provider.KeyFunc,
validator.RS256,
issuerURL.String(),
[]string{os.Getenv("AUTH0_AUDIENCE")},
validator.WithCustomClaims(
func() validator.CustomClaims {
return &CustomClaims{}
},
),
validator.WithAllowedClockSkew(time.Minute),
)
if err != nil {
log.Fatalf("Failed to set up the jwt validator")
}
errorHandler := func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("Encountered error while validating JWT: %v", err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"message":"Failed to validate JWT."}`))
}
middleware := jwtmiddleware.New(
jwtValidator.ValidateToken,
jwtmiddleware.WithErrorHandler(errorHandler),
)
return middleware.CheckJWT(next)
}
Example token
I'm using https://docs.gofiber.io/ to handle the HTTP methods
Main function
func main() {
// This is to translate the net/http -> fiber http
var ensureValidToken = adaptor.HTTPMiddleware(EnsureValidToken)
app := fiber.New()
app.Use(cors.New())
app.Use(logger.New())
// routes
app.Use(ensureValidToken)
app.Get("/api/books", getAll)
app.Listen(":8080")
}
func getAll(c *fiber.Ctx) error {
token := c.Context().Value(jwtmiddleware.ContextKey{}).(*validator.ValidatedClaims)
// The above code will always panic, I'm assuming that it already stored in the context since it passes the validation
}
Panic example
panic: interface conversion: interface {} is nil, not
*validator.ValidatedClaims
I dig deeper into the auth0 golang implementation, it does store in the context, I think the translation between http.Request to fiber HTTP failed
r = r.Clone(context.WithValue(r.Context(), ContextKey{}, validToken))
Seems like more people have faced the same issue when they used the gofiber adaptor. The way others have solved it was to create their own implementation of HTTPMiddleware middleware adaptor with the only change being that they set the context to the fiber.Ctx.
You can find an the thread on the gofiber/adaptor github page here: https://github.com/gofiber/adaptor/issues/27#issuecomment-1120428400
I got the same panic in the gin framework, I resolved the panic error by changing the code snippet to c.Request.Context().Value() but this is not available in fiber framework. If you want the decoded jwt token either you can get it from the header of the fiber context and decode it appropriately inside the controller, and pass the token you get from the header to the below function and decode.
import (
extract "github.com/golang-jwt/jwt"
"fmt"
)
func Extractor(tokenString string) {
token, _, err := new(extract.Parser).ParseUnverified(tokenString, extract.MapClaims{})
if err != nil {
fmt.Printf("Error %s", err)
}
if claims, ok := token.Claims.(extract.MapClaims); ok {
// obtains claims
subId := fmt.Sprint(claims["sub"])
fmt.Println(subId)
}
}
Implement your logic after this and pass the values you needed to the next handler.

Google Cloud Function Calling Gmail API Using Service Account Domain-Wide Access

// Package p contains an HTTP Cloud Function.
package p
import (
"log"
"fmt"
"net/http"
"context"
"google.golang.org/api/gmail/v1"
)
func HelloWorld(wx http.ResponseWriter, rx *http.Request) {
log.Println("start")
srv, err := gmail.NewService(context.Background())
if err != nil {
log.Fatalf("Unable to retrieve Gmail client: %v", err)
}
user := "me"
r, err := srv.Users.Labels.List(user).Do()
if err != nil {
log.Fatalf("Unable to retrieve labels: %v", err)
}
if len(r.Labels) == 0 {
fmt.Println("No labels found.")
return
}
fmt.Println("Labels:")
for _, l := range r.Labels {
fmt.Printf("- %s\n", l.Name)
}
log.Println("end")
}
I'm writing a Google Cloud function in Go that needs to access the Gmail of my admin userid that owns the project, the gmail account, and created the cloud function.
Using the information on the page Choose the best way to use and authenticate service accounts on Google Cloud, I determined I should authenticate using "Attach Service Account" option.
Following the instructions on page Using OAuth 2.0 for Server to Server Applications, I created a new service account and delegated to it domain-wide access to scope https://mail.google.com/
I assigned the new service account as the Runtime Service Account for the Cloud Function.
The gmail.NewService statement seems to execute successfully but the srv.Users.Labels.List(user) statement fails with "Error 400: Precondition check".
The log file is below.
2022-07-09T03:34:14.575564Z testgogmail hy9add8bqppl 2022/07/09 03:34:14 start
2022-07-09T03:34:14.785116Z testgogmail hy9add8bqppl 2022/07/09 03:34:14 Unable to retrieve labels: googleapi: Error 400: Precondition check failed., failedPrecondition
2022-07-09T03:34:14.799538102Z testgogmail hy9add8bqppl Function execution took 553 ms. Finished with status: connection error
So what am I missing? What have I done wrong?
From what I determined, currently it is not possible to impersonate another account using the Gmail API for Go when you are relying on the basic domain-wide access/authentication. The gmail.NewService(context.Background()) statement executes successfully but you are authenticated as the gmail address of the service account. Since that does not actually exist, the subsequent Users.Labels.List fails even if you pass a different/valid email account as the user parameter.
However, it does work if you create an authentication token based on the service account with domain-wide access by using google.JWTConfigFromJSON and then use the WithTokenSource option when creating the Gmail service. Note - the sample code below is based on creating and executing the cloud function from the online UI, not the cloud shell.
pathWD, err := os.Getwd()
if err != nil {
log.Println("error getting working directory:", err)
}
log.Println("pathWD: ", pathWD)
jsonPath := pathWD + "/serverless_function_source_code/"
serviceAccountFile := "service_account.json"
serviceAccountFullFile := jsonPath + serviceAccountFile
os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", serviceAccountFullFile)
serviceAccountJSON, err := ioutil.ReadFile(serviceAccountFullFile)
if err != nil {
log.Fatal(err)
}
config, err := google.JWTConfigFromJSON(serviceAccountJSON,
"https://mail.google.com/", "https://www.googleapis.com/auth/cloud-platform",
)
config.Subject = "admin#awarenet.us"
ctx := context.Background()
srv, err := gmail.NewService(ctx, option.WithTokenSource(config.TokenSource(ctx)))
if err != nil {
log.Fatalf("Unable to retrieve Gmail client: %v", err)
}

Why can I not use a DefaultClient to access Calendar API?

I'd like to use Google's quickstart code to access my Google Calendar. It works when I follow the steps, but changing it to use a DefaultClient results in a 403 error:
$ go run quickstart.go
2021/02/09 14:03:19 Unable to retrieve next ten of the user's events: googleapi: Error 403: Request had insufficient authentication
scopes.
More details:
Reason: insufficientPermissions, Message: Insufficient Permission
exit status 1
This is my current code to retrieve local Default Credentials and use them for generating an httpClient:
client, err := google.DefaultClient(oauth2.NoContext, calendar.CalendarReadonlyScope)
if err != nil {
log.Fatalf("auth broke: %v", err)
}
srv, err := calendar.New(client)
if err != nil {
log.Fatalf("Unable to retrieve Calendar client: %v", err)
}
t := time.Now().Format(time.RFC3339)
events, err := srv.Events.List("primary").ShowDeleted(false).
SingleEvents(true).TimeMin(t).MaxResults(10).OrderBy("startTime").Do()
if err != nil {
log.Fatalf("Unable to retrieve next ten of the user's events: %v", err)
}
fmt.Println("Upcoming events:")
if len(events.Items) == 0 {
fmt.Println("No upcoming events found.")
} else {
for _, item := range events.Items {
date := item.Start.DateTime
if date == "" {
date = item.Start.Date
}
fmt.Printf("%v (%v)\n", item.Summary, date)
}
}
The code does work if I create an OAuth2 credential in my GCP Project and use that, but that's not the approach I'd like to use here.
I understand I may need to use a 3-legged approach to get an OAuth2 token, but I can't find any examples on how to do this with Default Credentials.
Can I not use an approach similar to gcloud CLI's built-in token generation?

Renew Access Token using Golang Oauth2 library

I am working in a Golang application, this one is connected to a Oauth2 service, right now I have the refresh token and I need to get a new access token with it, I am using golang.org/x/oauth2 but it wans't successful, so there's something that I am missing, currently I have:
refresh_token := "some_refresh_token"
var conf = oauth2.Config{
ClientID:MY_CLIENT,
ClientSecret:MY_CLIENT_SECRET,
Scopes:[]string{"refresh_token"},
RedirectURL:"https://mydomain/callback",
Endpoint: oauth2.Endpoint{
AuthURL:"myoauth2Cluster.com/oauth2/auth",
TokenURL: "myoauth2Cluster.com/oauth2/token",
},
}
t := new (oauth2.Token)
t.RefreshToken=refresh_token
myclient := conf.Client(context.Background(),t)
req, err := http.NewRequest("GET",DontKnowWhichURLhere , nil)
if err != nil {
fmt.Println("error:",err.Error())
}
mrr, er := myclient.Do(req)
if(er!=nil){
fmt.Println(er.Error())
}else{
fmt.Println("status code:",mrr.StatusCode)
}
But I am getting a 404 status, I checked the logs of the Oauth2 server and there I have
msg="completed handling request" measure#https://myOauth2Cluster.latency=100648 method=GET remote=xxx.xxx.xx.xxx request="/" status=404 text_status="Not Found" took=100.648µs
Also, I am not really sure which URL should I stablish when I create the http.NewRequest should it be a callback? or the url of the Oauth2 Server?
If there's some example of how to renew the access token using this library would be nice, but at the moment I haven't found it
Normally you just use your old token and it is refreshed by the oauth2 library implicitly.
Example:
In the code below conf is *oauth2.Config.
Say I'm exchanging the code for the token (first-time auth):
token, err := conf.Exchange(ctx, code)
if err != nil {
log.Fatalln(err)
}
SaveToken(token)
Now I have my token and I can use it to make requests.
Later, before I use my token, I let oauth2 automatically refresh it if need:
tokenSource := conf.TokenSource(context.TODO(), token)
newToken, err := tokenSource.Token()
if err != nil {
log.Fatalln(err)
}
if newToken.AccessToken != token.AccessToken {
SaveToken(newToken)
log.Println("Saved new token:", newToken.AccessToken)
}
client := oauth2.NewClient(context.TODO(), tokenSource)
resp, err := client.Get(url)

Authentication methods using the vault API package

I am trying to use the Vault Golang Package to authenticate using the API.
I created a new client, and then can set my token:
client, err := api.NewClient(&api.Config{Address: vaultAddr, HttpClient: httpClient})
if err != nil {
return nil, errors.Wrap(err, "could not create vault client")
}
client.SetToken(token)
That's great and all, but I want to auth against the API using one of the other auth methods, (LDAP, Userpass etc)
Is this even possible? How can I retrieve a token using the API?
I guess I could just use net/http to retrieve the token using an API call, but is there any method to actually auth in another way?
I managed to figure this out, eventually. It's not totally obvious, but makes sense.
Vault has a generic write method it uses to write data. You can utilise this to perform a login with the API by simply building the URL and sending a PUT request to that endpoint
It looks a bit like this:
// create a vault client
client, err := api.NewClient(&api.Config{Address: url, HttpClient: httpClient})
if err != nil {
panic(err)
}
// to pass the password
options := map[string]interface{}{
"password": password,
}
// the login path
// this is configurable, change userpass to ldap etc
path := fmt.Sprintf("auth/userpass/login/%s", username)
// PUT call to get a token
secret, err := client.Logical().Write(path, options)
Without having enough reputation adding an extra hint here instead of a comment to the accepted answer. The token can be extracted from the auth response something like this:
client.SetToken(secret.Auth.ClientToken)
The relevant interfaces are documented here:
https://godoc.org/github.com/hashicorp/vault/api#Secret
https://godoc.org/github.com/hashicorp/vault/api#SecretAuth
I wrote a GitHub gist that contains a function to authenticate your Go program using an AWS IAM role. Here's a link.
Note: authenticating to Vault only gets you a token. The token may later be used to read secrets, if the AWS IAM role allows.
You'll need to create an AWS STS request, extract some information from it, then send it to Vault instead.
Here's the function from the GitHub gist. It will consume an AWS IAM role and some Vault environment variables such as VAULT_ADDR. Here's some other environment variables that the Vault Golang library may read. It produces an authenticated Vault *api.Client, token, and response from the authentication request.
It's based off of this GitHub project.
package vault
import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/hashicorp/vault/api"
)
// AWSLogin will create a Vault client, login via an AWS role, and return a valid Vault token and client that can be
// used to get secrets.
// The authProvider is likely "aws". It's the "Path" column as described in these docs:
// https://www.vaultproject.io/api/auth/aws#login.
// The serverID is an optional value to be placed in the X-Vault-AWS-IAM-Server-ID header of the HTTP request.
// The role is an AWS IAM role. It needs to be able to read secrets from Vault.
func AWSLogin(authProvider, serverID, role string) (client *api.Client, token string, secret *api.Secret, err error) {
// Create the Vault client.
//
// Configuration is gathered from environment variables by upstream vault package. Environment variables like
// VAULT_ADDR and VAULT_SKIP_VERIFY are relevant. The VAULT_TOKEN environment variable shouldn't be needed.
// https://www.vaultproject.io/docs/commands#environment-variables
if client, err = api.NewClient(nil); err != nil {
return nil, "", nil, fmt.Errorf("failed to create Vault client: %w", err)
}
// Acquire an AWS session.
var sess *session.Session
if sess, err = session.NewSession(); err != nil {
return nil, "", nil, fmt.Errorf("failed to create AWS session: %w", err)
}
// Create a Go structure to talk to the AWS token service.
tokenService := sts.New(sess)
// Create a request to the token service that will ask for the current host's identity.
request, _ := tokenService.GetCallerIdentityRequest(&sts.GetCallerIdentityInput{})
// Add an server ID IAM header, if present.
if serverID != "" {
request.HTTPRequest.Header.Add("X-Vault-AWS-IAM-Server-ID", serverID)
}
// Sign the request to the AWS token service.
if err = request.Sign(); err != nil {
return nil, "", nil, fmt.Errorf("failed to sign AWS identity request: %w", err)
}
// JSON marshal the headers.
var headers []byte
if headers, err = json.Marshal(request.HTTPRequest.Header); err != nil {
return nil, "", nil, fmt.Errorf("failed to JSON marshal HTTP headers for AWS identity request: %w", err)
}
// Read the body of the request.
var body []byte
if body, err = ioutil.ReadAll(request.HTTPRequest.Body); err != nil {
return nil, "", nil, fmt.Errorf("failed to JSON marshal HTTP body for AWS identity request: %w", err)
}
// Create the data to write to Vault.
data := make(map[string]interface{})
data["iam_http_request_method"] = request.HTTPRequest.Method
data["iam_request_url"] = base64.StdEncoding.EncodeToString([]byte(request.HTTPRequest.URL.String()))
data["iam_request_headers"] = base64.StdEncoding.EncodeToString(headers)
data["iam_request_body"] = base64.StdEncoding.EncodeToString(body)
data["role"] = role
// Create the path to write to for Vault.
//
// The authProvider is the value referenced in the "Path" column in this documentation. It's likely "aws".
// https://www.vaultproject.io/api/auth/aws#login
path := fmt.Sprintf("auth/%s/login", authProvider)
// Write the AWS token service request to Vault.
if secret, err = client.Logical().Write(path, data); err != nil {
return nil, "", nil, fmt.Errorf("failed to write data to Vault to get token: %w", err)
}
if secret == nil {
return nil, "", nil, fmt.Errorf("failed to get token from Vault: %w", ErrSecret)
}
// Get the Vault token from the response.
if token, err = secret.TokenID(); err != nil {
return nil, "", nil, fmt.Errorf("failed to get token from Vault response: %w", err)
}
// Set the token for the client as the one it just received.
client.SetToken(token)
return client, token, secret, nil
}
There's a standard AuthMethod interface in api package.
type AuthMethod interface {
Login(ctx context.Context, client *Client) (*Secret, error)
}
One may implement this interface with any login method. Here's an example of basic username/password login
type CredentialsAuthMethod struct {
username string
password string
}
func (m *CredentialsAuthMethod) Login(ctx context.Context, client *api.Client) (*api.Secret, error) {
options := map[string]any{
"password": m.password,
}
path := fmt.Sprintf("auth/userpass/login/%s", m.username)
secret, err := client.Logical().WriteWithContext(ctx, path, options)
if err != nil {
return nil, errorx.EnhanceStackTrace(err, "failed to authorize in vault with credentials")
}
return secret, nil
}
Once new AuthMethod implemented, it can be used to get a token for the client:
client, err := api.NewClient(&api.Config{Address: "https://vault.local"})
if err != nil {
return nil, errorx.EnhanceStackTrace(err, "failed to initialize vault client")
}
secret, err := client.Auth().Login(context.Background(), &CredentialsAuthMethod{
username: c.username,
password: c.password,
})
if err != nil {
return nil, errorx.EnhanceStackTrace(err, "failed to login in vault")
}
token := secret.Auth.ClientToken
client.SetToken(token)

Resources