0

I have a web application which sits behind Google's Identity Aware Proxy (IAP). IAP authenticates the user before forwarding to my web application. How can I access the already authenticated user from my web application?

In Getting the user's identity it states there are X-Goog-Authenticated-User-Email and X-Goog-Authenticated-User-Id headers. However, I don't see those in the response headers:

accept-ranges: bytes
alt-svc: clear
content-length: 14961
content-type: text/html; charset=utf-8
date: Thu, 01 Apr 2021 15:21:01 GMT
last-modified: Wed, 31 Mar 2021 19:34:58 GMT
via: 1.1 google

I do see a few cookies:

GCP_IAAP_AUTH_TOKEN_xxx
GCP_IAP_UID
GCP_IAP_XSRF_NONCE_xxx

For example, I want to be able to show their name and avatar photo in my web app to show that they are authenticated and logged in. I know that info is available via Google's OAuth2 struct, but how can I get that from IAP?

Edward J. Stembler
  • 1,556
  • 3
  • 22
  • 44
  • The header `X-Goog-Authenticated-User-Email` is sent from IAP to your backend as request headers and not to the client as response headers. The client receives the authentication cookies. – John Hanley Apr 01 '21 at 23:45
  • @JohnHanley I had read that somewhere too. I was in the process of adding a log message to scrutinize the value. Somewhat tedious since this cannot be tested on the local development machine and must be deployed to production where IAP resides. I'll test it out though. Thanks! – Edward J. Stembler Apr 02 '21 at 12:55
  • 1
    Which service are you using behind IAP? One trick I do with a new service is to create an HTML table from the headers and environment variables and then send that back as a response. This allows me to see all the headers sent to a service such as Cloud Run or App Engine. – John Hanley Apr 02 '21 at 14:43
  • I can see them now after deploying a simple /headers route which loops through them and writes to the ResponseWriter. `X-Goog-Authenticated-User-Id`, `X-Goog-Authenticated-User-Email` and `X-Goog-Iap-Jwt-Assertion`. Thanks @JohnHanley! – Edward J. Stembler Apr 05 '21 at 18:41

1 Answers1

0

I was able to get this working after @JohnHanley mentioned that the headers only show up when running behind IAP. You cannot see them during local development.

I could see them after deploying a simple, temporary, /headers route which loops through them and writes to the ResponseWriter. X-Goog-Authenticated-User-Id, X-Goog-Authenticated-User-Email and X-Goog-Iap-Jwt-Assertion.

import (
    "fmt"
    "net/http"

    "github.com/rs/zerolog/log"
)

func headersHandler(w http.ResponseWriter, r *http.Request) {
    log.Info().Msg("Entering headersHandler")

    fmt.Fprintf(w, "Request Headers\n\n")
    log.Debug().Msg("Request Headers:")
    for name, values := range r.Header {
        log.Debug().Interface(name, values).Send()
        fmt.Fprintf(w, "%s = %s\n", name, values)
    }
}

This was a temporary route. Once I could confirm the headers, I deleted it.

Additionally, I had to enable Google's People API for the ProjectId where my web application was being hosted.

Afterwards, I did a test using the Go package for google.golang.org/api/people/v1 and found that the convention of using the currently authenticated user via people/me didn't work in my case since it returns the service account being used. Instead, I had to programmatically fill in the user id people/userid. Then it worked.

For my use-case, I created a /user route to return a subset of the user information, i.e. name, email, photo url.

Struct:

type GoogleUser struct {
    Name     string `json:"name"`
    Email    string `json:"email"`
    PhotoUrl string `json:"photo_url"`
}

Handler:

func userHandler(w http.ResponseWriter, r *http.Request) {
    log.Info().Msg("Entering userHandler")

    var err error

    // Make sure this is a valid API request
    // Request header Content-Type: application/json must be present
    if !ValidAPIRequest(r) {
        err = writeJSONError(w, ResponseStatusNotFound("Not found"))
        if err != nil {
            log.Error().Msg(err.Error())
        }
        return
    }

    // Extract user id from header
    var userId string = r.Header.Get("X-Goog-Authenticated-User-Id")
    if userId != "" {
        userId = strings.ReplaceAll(userId, "accounts.google.com:", "")
    }

    // Extract user email from header
    var userEmail string = r.Header.Get("X-Goog-Authenticated-User-Email")
    if userEmail != "" {
        userEmail = strings.ReplaceAll(userEmail, "accounts.google.com:", "")
    }

    // Get the currently authenticated Google user
    googleUser, err := GetCurrentGoogleUser(userId, userEmail)
    if err != nil {
        log.Error().Msg(err.Error())
        err = writeJSONError(w, ResponseStatusInternalError(err.Error()))
        if err != nil {
            log.Error().Msg(err.Error())
        }
        return
    }

    // Write the JSON response
    err = writeJSONGoogleUser(w, http.StatusOK, &googleUser)
    if err != nil {
        log.Error().Msg(err.Error())
    }
}

Google People API:

func GetCurrentGoogleUser(userId string, userEmail string) (GoogleUser, error) {
    // Pre-conditions
    if userId == "" {
        return GoogleUser{}, errors.New("userId is blank")
    }
    if userEmail == "" {
        return GoogleUser{}, errors.New("userEmail is blank")
    }

    log.Debug().
        Str("userId", userId).
        Str("userEmail", userEmail).
        Send()

    ctx := context.Background()

    // Instantiate a new People service 
    peopleService, err := people.NewService(ctx, option.WithAPIKey(GoogleAPIKey))
    if err != nil {
        return GoogleUser{}, err
    }

    // Define the resource name using the user id
    var resourceName string = fmt.Sprintf("people/%s", userId)
    
    // Get the user profile 
profile, err := peopleService.People.Get(resourceName).PersonFields("names,photos").Do()
    if err != nil {
        return GoogleUser{}, err
    }

    log.Debug().
        Interface("profile", profile).
        Send()

    return GoogleUser{Name: profile.Names[0].DisplayName, Email: userEmail, PhotoUrl: profile.Photos[0].Url}, nil
}
Edward J. Stembler
  • 1,556
  • 3
  • 22
  • 44