go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/gcloud/googleoauth/info.go (about)

     1  // Copyright 2017 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package googleoauth
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"net/http"
    21  	"net/url"
    22  
    23  	"go.chromium.org/luci/common/errors"
    24  	"go.chromium.org/luci/common/logging"
    25  	"go.chromium.org/luci/common/retry/transient"
    26  	"google.golang.org/api/googleapi"
    27  
    28  	"golang.org/x/net/context/ctxhttp"
    29  )
    30  
    31  const (
    32  	// TokeninfoEndpoint is Google's token info endpoint.
    33  	TokeninfoEndpoint = "https://oauth2.googleapis.com/tokeninfo"
    34  )
    35  
    36  // ErrBadToken is returned by GetTokenInfo if the passed token is invalid.
    37  var ErrBadToken = errors.New("bad token")
    38  
    39  // TokenInfoParams are parameters for GetTokenInfo call.
    40  type TokenInfoParams struct {
    41  	AccessToken string // an access token to check
    42  	IDToken     string // an ID token to check (overrides AccessToken)
    43  
    44  	Client   *http.Client // non-authenticating client to use for the call
    45  	Endpoint string       // an endpoint to use instead of the default one
    46  }
    47  
    48  // TokenInfo is information about an access or ID tokens.
    49  //
    50  // Of primary importance are 'email', 'email_verified', 'scope' and 'aud'
    51  // fields. If the caller using token info endpoint to validate tokens, it MUST
    52  // check correctness of these fields.
    53  type TokenInfo struct {
    54  	Azp           string `json:"azp"`
    55  	Aud           string `json:"aud"`
    56  	Sub           string `json:"sub"`
    57  	Scope         string `json:"scope"`
    58  	Exp           int64  `json:"exp,string"`
    59  	ExpiresIn     int64  `json:"expires_in,string"`
    60  	Email         string `json:"email"`
    61  	EmailVerified bool   `json:"email_verified,string"`
    62  	AccessType    string `json:"access_type"`
    63  }
    64  
    65  // GetTokenInfo queries token info endpoint and returns information about
    66  // the token if it is recognized.
    67  //
    68  // See https://developers.google.com/identity/sign-in/android/backend-auth#calling-the-tokeninfo-endpoint.
    69  //
    70  // On invalid token (as indicated by 4** HTTP response) returns ErrBadToken. On
    71  // other HTTP-level errors (e.g. HTTP 500) returns transient-wrapped
    72  // *googleapi.Error. On network-level errors returns them in a transient
    73  // wrapper.
    74  func GetTokenInfo(ctx context.Context, params TokenInfoParams) (*TokenInfo, error) {
    75  	if params.Client == nil {
    76  		params.Client = http.DefaultClient
    77  	}
    78  	if params.Endpoint == "" {
    79  		params.Endpoint = TokeninfoEndpoint
    80  	}
    81  
    82  	// Note: we must not log full URL of this call, it contains sensitive info.
    83  	v := url.Values{}
    84  	if params.IDToken != "" {
    85  		v.Add("id_token", params.IDToken)
    86  	} else {
    87  		v.Add("access_token", params.AccessToken)
    88  	}
    89  	resp, err := ctxhttp.Get(ctx, params.Client, params.Endpoint+"?"+v.Encode())
    90  	if err != nil {
    91  		return nil, transient.Tag.Apply(err)
    92  	}
    93  	defer googleapi.CloseBody(resp)
    94  	if err := googleapi.CheckResponse(resp); err != nil {
    95  		if apiErr, ok := err.(*googleapi.Error); ok && apiErr.Code < 500 {
    96  			return nil, ErrBadToken
    97  		}
    98  		return nil, transient.Tag.Apply(err)
    99  	}
   100  
   101  	info := &TokenInfo{}
   102  	if err := json.NewDecoder(resp.Body).Decode(info); err != nil {
   103  		// This should never happen. If it does, the token endpoint has gone mad,
   104  		// and maybe it will recover soon. So mark the error as transient.
   105  		logging.WithError(err).Errorf(ctx, "Bad token info endpoint response")
   106  		return nil, transient.Tag.Apply(err)
   107  	}
   108  
   109  	return info, nil
   110  }