| package blesser |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "net/http" |
| "time" |
| |
| "veyron.io/veyron/veyron/services/identity" |
| "veyron.io/veyron/veyron/services/identity/revocation" |
| |
| "veyron.io/veyron/veyron2/ipc" |
| "veyron.io/veyron/veyron2/security" |
| "veyron.io/veyron/veyron2/vlog" |
| ) |
| |
| type googleOAuth struct { |
| authcodeClient struct{ ID, Secret string } |
| accessTokenClients []AccessTokenClient |
| duration time.Duration |
| domain string |
| dischargerLocation string |
| revocationManager *revocation.RevocationManager |
| } |
| |
| // AccessTokenClient represents a client of the BlessUsingAccessToken RPCs. |
| type AccessTokenClient struct { |
| // Descriptive name of the client. |
| Name string |
| // OAuth Client ID. |
| ClientID string |
| } |
| |
| // GoogleParams represents all the parameters provided to NewGoogleOAuthBlesserServer |
| type GoogleParams struct { |
| // The OAuth client IDs and names for the clients of the BlessUsingAccessToken RPCs. |
| AccessTokenClients []AccessTokenClient |
| // If non-empty, only email addresses from this domain will be blessed. |
| DomainRestriction string |
| // The object name of the discharger service. If this is empty then revocation caveats will not be granted. |
| DischargerLocation string |
| // The revocation manager that generates caveats and manages revocation. |
| RevocationManager *revocation.RevocationManager |
| // The duration for which blessings will be valid. (Used iff RevocationManager is nil). |
| BlessingDuration time.Duration |
| } |
| |
| // 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{} { |
| return identity.OAuthBlesserServer(&googleOAuth{ |
| duration: p.BlessingDuration, |
| domain: p.DomainRestriction, |
| dischargerLocation: p.DischargerLocation, |
| revocationManager: p.RevocationManager, |
| accessTokenClients: p.AccessTokenClients, |
| }) |
| } |
| |
| func (b *googleOAuth) BlessUsingAccessToken(ctx ipc.ServerContext, accesstoken string) (security.WireBlessings, string, error) { |
| var noblessings security.WireBlessings |
| if len(b.accessTokenClients) == 0 { |
| return noblessings, "", 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 noblessings, "", fmt.Errorf("unable to use token: %v", err) |
| } |
| if tokeninfo.StatusCode != http.StatusOK { |
| return noblessings, "", 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 noblessings, "", fmt.Errorf("invalid JSON response from Google's tokeninfo API: %v", err) |
| } |
| var client AccessTokenClient |
| audienceMatch := false |
| for _, c := range b.accessTokenClients { |
| if token.Audience == c.ClientID { |
| client = c |
| audienceMatch = true |
| break |
| } |
| } |
| if !audienceMatch { |
| vlog.Infof("Got access token [%+v], wanted one of client ids %v", token, b.accessTokenClients) |
| return noblessings, "", fmt.Errorf("token not meant for this purpose, confused deputy? https://developers.google.com/accounts/docs/OAuth2UserAgent#validatetoken") |
| } |
| if !token.VerifiedEmail { |
| return noblessings, "", fmt.Errorf("email not verified") |
| } |
| // Append client.Name to the blessing (e.g., "android", "chrome"). Since blessings issued by |
| // this process do not have many caveats on them and typically have a large expiry duration, |
| // we append this suffix so that servers can explicitly distinguish these clients while |
| // specifying authorization policies (say, via ACLs). |
| return b.bless(ctx, token.Email+security.ChainSeparator+client.Name) |
| } |
| |
| func (b *googleOAuth) bless(ctx ipc.ServerContext, extension string) (security.WireBlessings, string, error) { |
| var noblessings security.WireBlessings |
| self := ctx.LocalPrincipal() |
| if self == nil { |
| return noblessings, "", fmt.Errorf("server error: no authentication happened") |
| } |
| var caveat security.Caveat |
| var err error |
| if b.revocationManager != nil { |
| caveat, err = b.revocationManager.NewCaveat(self.PublicKey(), b.dischargerLocation) |
| } else { |
| caveat, err = security.ExpiryCaveat(time.Now().Add(b.duration)) |
| } |
| if err != nil { |
| return noblessings, "", err |
| } |
| blessing, err := self.Bless(ctx.RemoteBlessings().PublicKey(), ctx.LocalBlessings(), extension, caveat) |
| if err != nil { |
| return noblessings, "", err |
| } |
| return security.MarshalBlessings(blessing), extension, nil |
| } |