| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package handlers |
| |
| import ( |
| "encoding/base64" |
| "fmt" |
| "net/http" |
| "strings" |
| "time" |
| |
| "v.io/v23" |
| "v.io/v23/context" |
| "v.io/v23/security" |
| "v.io/v23/vom" |
| "v.io/x/ref/services/identity/internal/blesser" |
| "v.io/x/ref/services/identity/internal/util" |
| ) |
| |
| const ( |
| PublicKeyFormKey = "public_key" |
| AccessTokenFormKey = "token" |
| CaveatsFormKey = "caveats" |
| ) |
| |
| type accessTokenBlesser struct { |
| ctx *context.T |
| params blesser.OAuthBlesserParams |
| } |
| |
| // NewOAuthBlessingHandler returns an http.Handler that uses OAuth2 access tokens |
| // to obtain the username of the requestor and reponds with blessings for that username. |
| // |
| // The blessings are namespaced under the ClientID for the access token. In particular, |
| // the name of the granted blessing is of the form <idp>/<clientID>/<email> where <idp> |
| // is the name of the default blessings used by the identity provider. |
| // |
| // Blessings generated by this service carry a third-party revocation caveat if a |
| // RevocationManager is specified by the params or they carry an ExpiryCaveat that |
| // expires after the duration specified by the params. |
| // |
| // The handler expects the following parameters in the form data sent during a |
| // request |
| // - "public_key": Base64 DER encoded PKIX representation of the client's public key |
| // - "caveats": Base64 VOM encoded list of caveats |
| // - "token": OAuth2 access token |
| // |
| // WARNINGS: |
| // - There is no binding between the channel over which the access token |
| // was obtained and the channel used to make this request. |
| // - There is no "proof of possession of private key" required by the server. |
| // Thus, if Mallory (attacker) possesses the access token associated with Alice's |
| // account (victim), she may be able to obtain a blessing with Alice's name on it |
| // for any public key of her choice. |
| func NewOAuthBlessingHandler(ctx *context.T, params blesser.OAuthBlesserParams) http.Handler { |
| return &accessTokenBlesser{ctx, params} |
| } |
| |
| func (a *accessTokenBlesser) blessingCaveats(r *http.Request, p security.Principal) ([]security.Caveat, error) { |
| caveatsVom, err := base64.URLEncoding.DecodeString(r.FormValue(CaveatsFormKey)) |
| if err != nil { |
| return nil, fmt.Errorf("failed to base64 decode caveats: %v", err) |
| } |
| |
| var caveats []security.Caveat |
| if err := vom.Decode(caveatsVom, &caveats); err != nil { |
| return nil, fmt.Errorf("failed to VOM decode caveats: %v", err) |
| } |
| |
| // TODO(suharshs, ataly): Should we ensure that we have at least a |
| // revocation or expiry caveat? |
| if len(caveats) == 0 { |
| var ( |
| cav security.Caveat |
| err error |
| ) |
| if a.params.RevocationManager != nil { |
| cav, err = a.params.RevocationManager.NewCaveat(p.PublicKey(), a.params.DischargerLocation) |
| } else { |
| cav, err = security.NewExpiryCaveat(time.Now().Add(a.params.BlessingDuration)) |
| } |
| if err != nil { |
| return nil, fmt.Errorf("failed to construct caveats: %v", err) |
| } |
| caveats = append(caveats, cav) |
| } |
| return caveats, nil |
| |
| } |
| |
| func (a *accessTokenBlesser) remotePublicKey(r *http.Request) (security.PublicKey, error) { |
| publicKeyVom, err := base64.URLEncoding.DecodeString(r.FormValue(PublicKeyFormKey)) |
| if err != nil { |
| return nil, fmt.Errorf("failed to base64 decode public key: %v", err) |
| } |
| return security.UnmarshalPublicKey(publicKeyVom) |
| } |
| |
| func (a *accessTokenBlesser) blessingExtension(r *http.Request) (string, error) { |
| email, clientID, err := a.params.OAuthProvider.GetEmailAndClientID(r.FormValue(AccessTokenFormKey)) |
| if err != nil { |
| return "", err |
| } |
| // We use <clientID>/<email> as the extension in order to namespace the blessing under |
| // the <clientID>. This has the downside that the blessing cannot be used to act on |
| // behalf of the user, i.e., services access controlled to blessings matching"<idp>/<email>" |
| // would not authorize this blessing. |
| // |
| // The alternative is to use the extension <email>/<clientID> however this is risky as it |
| // may provide too much authority to the app, especially since we don't have a set of default |
| // caveats to apply to the blessing. |
| // |
| // TODO(ataly, ashankar): Think about changing to the extension <email>/<clientID>. |
| return strings.Join([]string{clientID, email}, security.ChainSeparator), nil |
| } |
| |
| func (a *accessTokenBlesser) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| remoteKey, err := a.remotePublicKey(r) |
| if err != nil { |
| a.ctx.Info("Failed to decode public key [%v] for request %#v", err, r) |
| util.HTTPServerError(w, err) |
| return |
| } |
| |
| p := v23.GetPrincipal(a.ctx) |
| with := p.BlessingStore().Default() |
| |
| caveats, err := a.blessingCaveats(r, p) |
| if err != nil { |
| a.ctx.Info("Failed to constuct caveats for blessing [%v] for request %#v", err, r) |
| util.HTTPServerError(w, err) |
| return |
| } |
| |
| extension, err := a.blessingExtension(r) |
| if err != nil { |
| a.ctx.Info("Failed to process access token [%v] for request %#v", err, r) |
| util.HTTPServerError(w, err) |
| return |
| } |
| |
| blessings, err := p.Bless(remoteKey, with, extension, caveats[0], caveats[1:]...) |
| if err != nil { |
| a.ctx.Info("Failed to Bless [%v] for request %#v", err, r) |
| util.HTTPServerError(w, fmt.Errorf("Bless failed: %v", err)) |
| return |
| } |
| |
| blessingsVom, err := vom.Encode(blessings) |
| if err != nil { |
| a.ctx.Info("Failed to VOM encode blessings [%v] for request %#v", err, r) |
| util.HTTPServerError(w, fmt.Errorf("failed to VOM encode blessings: %v", err)) |
| return |
| } |
| blessingsVomB64 := base64.URLEncoding.EncodeToString(blessingsVom) |
| w.Header().Set("Content-Type", "application/text") |
| w.Write([]byte(blessingsVomB64)) |
| } |