Search code examples
webgoogle-cloud-platformgoogle-iapidentity-aware-proxygoogle-cloud-identity-aware-proxy

How to access already authenticated user from web application behind Google Identity Aware Proxy?


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?


Solution

  • 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
    }