blob: 7017320fabde8f989813fb39e726d2b452060845 [file] [log] [blame]
// This implementation of Authenticator uses tokeninfo API to validate
// bearer token.
//
// It is intended to be used only on dev server.
package endpoints
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"golang.org/x/net/context"
"google.golang.org/appengine/log"
"google.golang.org/appengine/user"
)
const tokeninfoEndpointURL = "https://www.googleapis.com/oauth2/v2/tokeninfo"
type tokeninfo struct {
IssuedTo string `json:"issued_to"`
Audience string `json:"audience"`
UserID string `json:"user_id"`
Scope string `json:"scope"`
ExpiresIn int `json:"expires_in"`
Email string `json:"email"`
VerifiedEmail bool `json:"verified_email"`
AccessType string `json:"access_type"`
// ErrorDescription is populated when an error occurs. Usually, the response
// either contains only ErrorDescription or the fields above
ErrorDescription string `json:"error_description"`
}
// fetchTokeninfo retrieves token info from tokeninfoEndpointURL (tokeninfo API)
func fetchTokeninfo(c context.Context, token string) (*tokeninfo, error) {
url := tokeninfoEndpointURL + "?access_token=" + token
log.Debugf(c, "Fetching token info from %q", url)
resp, err := newHTTPClient(c).Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
log.Debugf(c, "Tokeninfo replied with %s", resp.Status)
ti := &tokeninfo{}
if err = json.NewDecoder(resp.Body).Decode(ti); err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
errMsg := fmt.Sprintf("Error fetching tokeninfo (status %d)", resp.StatusCode)
if ti.ErrorDescription != "" {
errMsg += ": " + ti.ErrorDescription
}
return nil, errors.New(errMsg)
}
switch {
case ti.ExpiresIn <= 0:
return nil, errors.New("Token is expired")
case !ti.VerifiedEmail:
return nil, fmt.Errorf("Unverified email %q", ti.Email)
case ti.Email == "":
return nil, fmt.Errorf("Invalid email address")
}
return ti, err
}
// scopedTokeninfo validates fetched token by matching tokeninfo.Scope
// with scope arg.
func scopedTokeninfo(c context.Context, scope string) (*tokeninfo, error) {
token := parseToken(HTTPRequest(c))
if token == "" {
return nil, errors.New("No token found")
}
ti, err := fetchTokeninfo(c, token)
if err != nil {
return nil, err
}
for _, s := range strings.Split(ti.Scope, " ") {
if s == scope {
return ti, nil
}
}
return nil, fmt.Errorf("No scope matches: expected one of %q, got %q",
ti.Scope, scope)
}
// tokeninfoAuthenticator is an Authenticator that uses tokeninfo API
// to validate bearer token.
type tokeninfoAuthenticator struct{}
// CurrentOAuthClientID returns a clientID associated with the scope.
func (tokeninfoAuthenticator) CurrentOAuthClientID(c context.Context, scope string) (string, error) {
ti, err := scopedTokeninfo(c, scope)
if err != nil {
return "", err
}
return ti.IssuedTo, nil
}
// CurrentOAuthUser returns a user associated with the request in context.
func (tokeninfoAuthenticator) CurrentOAuthUser(c context.Context, scope string) (*user.User, error) {
ti, err := scopedTokeninfo(c, scope)
if err != nil {
return nil, err
}
return &user.User{
ID: ti.UserID,
Email: ti.Email,
ClientID: ti.IssuedTo,
}, nil
}
// tokeninfoAuthenticatorFactory creates a new tokeninfoAuthenticator from r.
// To be used as auth.go/AuthenticatorFactory.
func tokeninfoAuthenticatorFactory() Authenticator {
return tokeninfoAuthenticator{}
}