blob: 0bb36cf65d8ddd0a5da0d3fcaeef18e0d10ea54d [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package googleoauth
import (
"context"
"encoding/json"
"net/http"
"net/url"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/retry/transient"
"google.golang.org/api/googleapi"
"golang.org/x/net/context/ctxhttp"
)
const (
// TokeninfoEndpoint is Google's token info endpoint.
TokeninfoEndpoint = "https://oauth2.googleapis.com/tokeninfo"
)
// ErrBadToken is returned by GetTokenInfo if the passed token is invalid.
var ErrBadToken = errors.New("bad token")
// TokenInfoParams are parameters for GetTokenInfo call.
type TokenInfoParams struct {
AccessToken string // an access token to check
IDToken string // an ID token to check (overrides AccessToken)
Client *http.Client // non-authenticating client to use for the call
Endpoint string // an endpoint to use instead of the default one
}
// TokenInfo is information about an access or ID tokens.
//
// Of primary importance are 'email', 'email_verified', 'scope' and 'aud'
// fields. If the caller using token info endpoint to validate tokens, it MUST
// check correctness of these fields.
type TokenInfo struct {
Azp string `json:"azp"`
Aud string `json:"aud"`
Sub string `json:"sub"`
Scope string `json:"scope"`
Exp int64 `json:"exp,string"`
ExpiresIn int64 `json:"expires_in,string"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified,string"`
AccessType string `json:"access_type"`
}
// GetTokenInfo queries token info endpoint and returns information about
// the token if it is recognized.
//
// See https://developers.google.com/identity/sign-in/android/backend-auth#calling-the-tokeninfo-endpoint.
//
// On invalid token (as indicated by 4** HTTP response) returns ErrBadToken. On
// other HTTP-level errors (e.g. HTTP 500) returns transient-wrapped
// *googleapi.Error. On network-level errors returns them in a transient
// wrapper.
func GetTokenInfo(ctx context.Context, params TokenInfoParams) (*TokenInfo, error) {
if params.Client == nil {
params.Client = http.DefaultClient
}
if params.Endpoint == "" {
params.Endpoint = TokeninfoEndpoint
}
// Note: we must not log full URL of this call, it contains sensitive info.
v := url.Values{}
if params.IDToken != "" {
v.Add("id_token", params.IDToken)
} else {
v.Add("access_token", params.AccessToken)
}
resp, err := ctxhttp.Get(ctx, params.Client, params.Endpoint+"?"+v.Encode())
if err != nil {
return nil, transient.Tag.Apply(err)
}
defer googleapi.CloseBody(resp)
if err := googleapi.CheckResponse(resp); err != nil {
if apiErr, ok := err.(*googleapi.Error); ok && apiErr.Code < 500 {
return nil, ErrBadToken
}
return nil, transient.Tag.Apply(err)
}
info := &TokenInfo{}
if err := json.NewDecoder(resp.Body).Decode(info); err != nil {
// This should never happen. If it does, the token endpoint has gone mad,
// and maybe it will recover soon. So mark the error as transient.
logging.WithError(err).Errorf(ctx, "Bad token info endpoint response")
return nil, transient.Tag.Apply(err)
}
return info, nil
}