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