blob: e7607c56670bdd219f7c8d335f573d2c69ed66d5 [file] [log] [blame]
// 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))
}