blob: 90d38750c0df7411af5fe498b4b77285f933b3c3 [file] [log] [blame]
package blesser
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"veyron/services/identity"
"veyron/services/identity/googleoauth"
"veyron2"
"veyron2/ipc"
"veyron2/vdl/vdlutil"
"veyron2/vlog"
)
type googleOAuth struct {
rt veyron2.Runtime
authcodeClient struct{ ID, Secret string }
accessTokenClient struct{ ID string }
duration time.Duration
domain string
}
// GoogleParams represents all the parameters provided to NewGoogleOAuthBlesserServer
type GoogleParams struct {
// The Veyron runtime to use
R veyron2.Runtime
// The OAuth client ID and secret for clients of the BlessUsingAuthorizationCode RPC
AuthorizationCodeClient struct {
ID, Secret string
}
// The OAuth client ID for the chrome-extension that will make BlessUsingAccessToken RPCs.
AccessTokenClient struct {
ID string
}
// The duration for which blessings will be valid.
BlessingDuration time.Duration
// If non-empty, only email addresses from this domain will be blessed.
DomainRestriction string
}
// NewGoogleOAuthBlesserServer provides an identity.OAuthBlesserService that uses authorization
// codes to obtain the username of a client and provide blessings with that name.
//
// For more details, see documentation on Google OAuth 2.0 flows:
// https://developers.google.com/accounts/docs/OAuth2
//
// Blessings generated by this server expire after duration. If domain is non-empty, then blessings
// are generated only for email addresses from that domain.
func NewGoogleOAuthBlesserServer(p GoogleParams) interface{} {
b := &googleOAuth{
rt: p.R,
duration: p.BlessingDuration,
domain: p.DomainRestriction,
}
b.authcodeClient.ID = p.AuthorizationCodeClient.ID
b.authcodeClient.Secret = p.AuthorizationCodeClient.Secret
b.accessTokenClient.ID = p.AccessTokenClient.ID
return identity.NewServerOAuthBlesser(b)
}
func (b *googleOAuth) BlessUsingAuthorizationCode(ctx ipc.ServerContext, authcode, redirectURL string) (vdlutil.Any, error) {
if len(b.authcodeClient.ID) == 0 {
return nil, fmt.Errorf("server not configured for blessing based on authorization codes")
}
config := googleoauth.NewOAuthConfig(b.authcodeClient.ID, b.authcodeClient.Secret, redirectURL)
name, err := googleoauth.ExchangeAuthCodeForEmail(config, authcode)
if err != nil {
return nil, err
}
return b.bless(ctx, name)
}
func (b *googleOAuth) BlessUsingAccessToken(ctx ipc.ServerContext, accesstoken string) (vdlutil.Any, error) {
if len(b.accessTokenClient.ID) == 0 {
return nil, fmt.Errorf("server not configured for blessing based on access tokens")
}
// URL from: https://developers.google.com/accounts/docs/OAuth2UserAgent#validatetoken
tokeninfo, err := http.Get("https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=" + accesstoken)
if err != nil {
return nil, fmt.Errorf("unable to use token: %v", err)
}
if tokeninfo.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unable to verify access token: %v", tokeninfo.StatusCode)
}
// tokeninfo contains a JSON-encoded struct
var token struct {
IssuedTo string `json:"issued_to"`
Audience string `json:"audience"`
UserID string `json:"user_id"`
Scope string `json:"scope"`
ExpiresIn int64 `json:"expires_in"`
Email string `json:"email"`
VerifiedEmail bool `json:"verified_email"`
AccessType string `json:"access_type"`
}
if err := json.NewDecoder(tokeninfo.Body).Decode(&token); err != nil {
return "", fmt.Errorf("invalid JSON response from Google's tokeninfo API: %v", err)
}
if token.Audience != b.accessTokenClient.ID {
vlog.Infof("Got access token [%+v], wanted client id %v", token, b.accessTokenClient.ID)
return "", fmt.Errorf("token not meant for this purpose, confused deputy? https://developers.google.com/accounts/docs/OAuth2UserAgent#validatetoken")
}
if !token.VerifiedEmail {
return nil, fmt.Errorf("email not verified")
}
return b.bless(ctx, token.Email)
}
func (b *googleOAuth) bless(ctx ipc.ServerContext, name string) (vdlutil.Any, error) {
if len(b.domain) > 0 && !strings.HasSuffix(name, "@"+b.domain) {
return nil, fmt.Errorf("blessings for %q are not allowed", name)
}
self := b.rt.Identity()
var err error
// Use the blessing that was used to authenticate with the client to bless it.
if self, err = self.Derive(ctx.LocalID()); err != nil {
return nil, err
}
// TODO(ashankar,ataly): Use the same set of caveats as is used by the HTTP handler.
// For example, a third-party revocation caveat?
return self.Bless(ctx.RemoteID(), name, b.duration, nil)
}