veyron/services/identity, veyron/tools/identity: Identity tool can request blessing with
caveats.
* New oauth flow to keep the blessing process secure from malicious identity tools.
* The oauth flow can be seen https://docs.google.com/a/google.com/document/d/1SRoc2cKE9iE1fWR7aSmMoccZoi4ZE8BQL7sr1LDNVkk/edit?usp=sharing.
Change-Id: I534f216953a1825cce899ffbfd82768db49b4108
diff --git a/services/identity/blesser/macaroon.go b/services/identity/blesser/macaroon.go
new file mode 100644
index 0000000..3cee53a
--- /dev/null
+++ b/services/identity/blesser/macaroon.go
@@ -0,0 +1,65 @@
+package blesser
+
+import (
+ "bytes"
+ "fmt"
+ "time"
+
+ "veyron.io/veyron/veyron/services/identity"
+ "veyron.io/veyron/veyron/services/identity/util"
+
+ "veyron.io/veyron/veyron2"
+ "veyron.io/veyron/veyron2/ipc"
+ "veyron.io/veyron/veyron2/security"
+ "veyron.io/veyron/veyron2/vdl/vdlutil"
+ "veyron.io/veyron/veyron2/vom"
+)
+
+type macaroonBlesser struct {
+ rt veyron2.Runtime
+ key []byte
+}
+
+// BlessingMacaroon contains the data that is encoded into the macaroon for creating blessings.
+type BlessingMacaroon struct {
+ Creation time.Time
+ Caveats []security.Caveat
+ Name string
+}
+
+// NewMacaroonBlesserServer provides an identity.MacaroonBlesser Service that uses an
+// bless macaroon.
+//
+// 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 NewMacaroonBlesserServer(r veyron2.Runtime, key []byte) interface{} {
+ return identity.NewServerMacaroonBlesser(&macaroonBlesser{
+ rt: r,
+ key: key,
+ })
+}
+
+func (b *macaroonBlesser) Bless(ctx ipc.ServerContext, macaroon string) (vdlutil.Any, error) {
+ inputs, err := util.Macaroon(macaroon).Decode(b.key)
+ if err != nil {
+ return nil, err
+ }
+ m := BlessingMacaroon{}
+ if err := vom.NewDecoder(bytes.NewBuffer(inputs)).Decode(&m); err != nil {
+ return nil, err
+ }
+ if time.Now().After(m.Creation.Add(time.Minute * 5)) {
+ return nil, fmt.Errorf("bless failed: macaroon has expired")
+ }
+ return b.bless(ctx, m.Name, m.Caveats)
+}
+
+func (b *macaroonBlesser) bless(ctx ipc.ServerContext, name string, caveats []security.Caveat) (vdlutil.Any, error) {
+ 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
+ }
+ return self.Bless(ctx.RemoteID(), name, time.Hour*24*365, caveats)
+}
diff --git a/services/identity/blesser/oauth.go b/services/identity/blesser/oauth.go
index bc7e346..9463692 100644
--- a/services/identity/blesser/oauth.go
+++ b/services/identity/blesser/oauth.go
@@ -8,7 +8,6 @@
"time"
"veyron.io/veyron/veyron/services/identity"
- "veyron.io/veyron/veyron/services/identity/googleoauth"
"veyron.io/veyron/veyron/services/identity/revocation"
"veyron.io/veyron/veyron2"
@@ -32,10 +31,6 @@
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 IDs for the clients of the BlessUsingAccessToken RPCs.
AccessTokenClients []struct {
ID string
@@ -59,29 +54,14 @@
// 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{
+ return identity.NewServerOAuthBlesser(&googleOAuth{
rt: p.R,
duration: p.BlessingDuration,
domain: p.DomainRestriction,
dischargerLocation: p.DischargerLocation,
revocationManager: p.RevocationManager,
- }
- b.authcodeClient.ID = p.AuthorizationCodeClient.ID
- b.authcodeClient.Secret = p.AuthorizationCodeClient.Secret
- b.accessTokenClients = p.AccessTokenClients
- 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)
+ accessTokenClients: p.AccessTokenClients,
+ })
}
func (b *googleOAuth) BlessUsingAccessToken(ctx ipc.ServerContext, accesstoken string) (vdlutil.Any, error) {
@@ -145,5 +125,5 @@
}
}
- return revocation.Bless(self, ctx.RemoteID(), name, b.duration, revocationCaveat)
+ return revocation.Bless(self, ctx.RemoteID(), name, b.duration, nil, revocationCaveat)
}
diff --git a/services/identity/googleoauth/handler.go b/services/identity/googleoauth/handler.go
index 0edf142..53fa571 100644
--- a/services/identity/googleoauth/handler.go
+++ b/services/identity/googleoauth/handler.go
@@ -1,6 +1,20 @@
-// Package googleoauth implements an http.Handler that uses OAuth 2.0 to
-// authenticate with Google and then renders a page that displays all the
-// blessings that were provided for that Google user.
+// Package googleoauth implements an http.Handler that has two main purposes
+// listed below:
+
+// (1) Uses OAuth 2.0 to authenticate with Google and then renders a page that
+// displays all the blessings that were provided for that Google user.
+// The client calls the /listblessings route which redirects to listblessingscallback which
+// renders the list.
+// (2) Performs the oauth flow for seeking a blessing using the identity tool
+// located at veyron/tools/identity.
+// The seek blessing flow works as follows:
+// (a) Client (identity tool) hits the /seekblessings route.
+// (b) /seekblessings performs google oauth with a redirect to /seekblessingscallback.
+// (c) Client specifies desired caveats in the form that /seekblessingscallback displays.
+// (d) Submission of the form sends caveat information to /sendmacaroon.
+// (e) /sendmacaroon sends a macaroon with blessing information to client
+// (via a redirect to an HTTP server run by the tool).
+// (f) Client invokes bless rpc with macaroon.
//
// The GoogleIDToken is currently validated by sending an HTTP request to
// googleapis.com. This adds a round-trip and service may be denied by
@@ -11,11 +25,14 @@
package googleoauth
import (
+ "bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
+ "net"
"net/http"
+ "net/url"
"path"
"strings"
"time"
@@ -23,18 +40,33 @@
"code.google.com/p/goauth2/oauth"
"veyron.io/veyron/veyron/services/identity/auditor"
+ "veyron.io/veyron/veyron/services/identity/blesser"
"veyron.io/veyron/veyron/services/identity/revocation"
"veyron.io/veyron/veyron/services/identity/util"
+ "veyron.io/veyron/veyron2"
"veyron.io/veyron/veyron2/security"
"veyron.io/veyron/veyron2/vlog"
+ "veyron.io/veyron/veyron2/vom"
)
const (
clientIDCookie = "VeyronHTTPIdentityClientID"
revocationCookie = "VeyronHTTPIdentityRevocationID"
+
+ ListBlessingsRoute = "listblessings"
+ listBlessingsCallbackRoute = "listblessingscallback"
+ revokeRoute = "revoke"
+ SeekBlessingsRoute = "seekblessings"
+ addCaveatsRoute = "addcaveat"
+ sendMacaroonRoute = "sendmacaroon"
)
type HandlerArgs struct {
+ // The Veyron runtime to use
+ R veyron2.Runtime
+ // The Key that is used for creating and verifying macaroons.
+ // This needs to be common between the handler and the MacaroonBlesser service.
+ MacaroonKey []byte
// URL at which the hander is installed.
// e.g. http://host:port/google/
// This is where the handler is installed and where redirect requests
@@ -47,16 +79,37 @@
// (auditor.ReadAuditLog).
Auditor string
// The RevocationManager is used to revoke blessings granted with a revocation caveat.
+ // If this is empty then revocation caveats will not be added to blessings, and instead
+ // Expiry Caveats of duration BlessingDuration will be added.
RevocationManager *revocation.RevocationManager
+ // The object name of the discharger service.
+ DischargerLocation string
+ // MacaroonBlessingService is the object name to which macaroons create by this HTTP
+ // handler can be exchanged for a blessing.
+ MacaroonBlessingService string
+ // If non-empty, only email addressses from this domain will be blessed.
+ DomainRestriction string
+ // BlessingDuration is the duration that blessings granted will be valid for
+ // if RevocationManager is nil.
+ BlessingDuration time.Duration
}
-func (a *HandlerArgs) redirectURL() string {
- ret := a.Addr
- if !strings.HasSuffix(ret, "/") {
- ret += "/"
+func (a *HandlerArgs) oauthConfig(redirectSuffix string) *oauth.Config {
+ return &oauth.Config{
+ ClientId: a.ClientID,
+ ClientSecret: a.ClientSecret,
+ RedirectURL: redirectURL(a.Addr, redirectSuffix),
+ Scope: "email",
+ AuthURL: "https://accounts.google.com/o/oauth2/auth",
+ TokenURL: "https://accounts.google.com/o/oauth2/token",
}
- ret += "oauth2callback"
- return ret
+}
+
+func redirectURL(baseURL, suffix string) string {
+ if !strings.HasSuffix(baseURL, "/") {
+ baseURL += "/"
+ }
+ return baseURL + suffix
}
// URL used to verify google tokens.
@@ -68,68 +121,57 @@
// and can be used to use OAuth 2.0 to authenticate with Google, mint a new
// identity and bless it with the Google email address.
func NewHandler(args HandlerArgs) (http.Handler, error) {
- config := NewOAuthConfig(args.ClientID, args.ClientSecret, args.redirectURL())
csrfCop, err := util.NewCSRFCop()
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("NewHandler failed to create csrfCop: %v", err)
}
return &handler{
- config: config,
- csrfCop: csrfCop,
- auditor: args.Auditor,
- revocationManager: args.RevocationManager,
+ args: args,
+ csrfCop: csrfCop,
}, nil
}
-// NewOAuthConfig returns the oauth.Config required for obtaining just the email address from Google using OAuth 2.0.
-func NewOAuthConfig(clientID, clientSecret, redirectURL string) *oauth.Config {
- return &oauth.Config{
- ClientId: clientID,
- ClientSecret: clientSecret,
- RedirectURL: redirectURL,
- Scope: "email",
- AuthURL: "https://accounts.google.com/o/oauth2/auth",
- TokenURL: "https://accounts.google.com/o/oauth2/token",
- }
-}
-
type handler struct {
- config *oauth.Config
- csrfCop *util.CSRFCop
- auditor string
- revocationManager *revocation.RevocationManager
+ args HandlerArgs
+ csrfCop *util.CSRFCop
}
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch path.Base(r.URL.Path) {
- case "auth":
- h.auth(w, r)
- case "oauth2callback":
- h.callback(w, r)
- case "revoke":
+ case ListBlessingsRoute:
+ h.listBlessings(w, r)
+ case listBlessingsCallbackRoute:
+ h.listBlessingsCallback(w, r)
+ case revokeRoute:
h.revoke(w, r)
+ case SeekBlessingsRoute:
+ h.seekBlessings(w, r)
+ case addCaveatsRoute:
+ h.addCaveats(w, r)
+ case sendMacaroonRoute:
+ h.sendMacaroon(w, r)
default:
util.HTTPBadRequest(w, r, nil)
}
}
-func (h *handler) auth(w http.ResponseWriter, r *http.Request) {
+func (h *handler) listBlessings(w http.ResponseWriter, r *http.Request) {
csrf, err := h.csrfCop.NewToken(w, r, clientIDCookie, nil)
if err != nil {
vlog.Infof("Failed to create CSRF token[%v] for request %#v", err, r)
- util.HTTPBadRequest(w, r, fmt.Errorf("Suspected automated request: %v", err))
+ util.HTTPServerError(w, fmt.Errorf("failed to create new token: %v", err))
return
}
- http.Redirect(w, r, h.config.AuthCodeURL(csrf), http.StatusFound)
+ http.Redirect(w, r, h.args.oauthConfig(listBlessingsCallbackRoute).AuthCodeURL(csrf), http.StatusFound)
}
-func (h *handler) callback(w http.ResponseWriter, r *http.Request) {
+func (h *handler) listBlessingsCallback(w http.ResponseWriter, r *http.Request) {
if err := h.csrfCop.ValidateToken(r.FormValue("state"), r, clientIDCookie, nil); err != nil {
vlog.Infof("Invalid CSRF token: %v in request: %#v", err, r)
util.HTTPBadRequest(w, r, fmt.Errorf("Suspected request forgery: %v", err))
return
}
- email, err := ExchangeAuthCodeForEmail(h.config, r.FormValue("code"))
+ email, err := exchangeAuthCodeForEmail(h.args.oauthConfig(listBlessingsCallbackRoute), r.FormValue("code"))
if err != nil {
util.HTTPBadRequest(w, r, err)
return
@@ -143,13 +185,14 @@
Token string
}
tmplargs := struct {
- Log chan tmplentry
- Email, Token string
+ Log chan tmplentry
+ Email, RevokeRoute string
}{
- Log: make(chan tmplentry),
- Email: email,
+ Log: make(chan tmplentry),
+ Email: email,
+ RevokeRoute: revokeRoute,
}
- if entrych, err := auditor.ReadAuditLog(h.auditor, email); err != nil {
+ if entrych, err := auditor.ReadAuditLog(h.args.Auditor, email); err != nil {
vlog.Errorf("Unable to read audit log: %v", err)
util.HTTPServerError(w, fmt.Errorf("unable to read audit log"))
return
@@ -172,13 +215,13 @@
continue
}
if blessEntry.RevocationCaveat != nil {
- if revocationTime := h.revocationManager.GetRevocationTime(blessEntry.RevocationCaveat.ID()); revocationTime != nil {
+ if revocationTime := h.args.RevocationManager.GetRevocationTime(blessEntry.RevocationCaveat.ID()); revocationTime != nil {
tmplentry.RevocationTime = *revocationTime
} else {
caveatID := base64.URLEncoding.EncodeToString([]byte(blessEntry.RevocationCaveat.ID()))
if tmplentry.Token, err = h.csrfCop.NewToken(w, r, revocationCookie, caveatID); err != nil {
vlog.Infof("Failed to create CSRF token[%v] for request %#v", err, r)
- util.HTTPBadRequest(w, r, fmt.Errorf("Suspected automated request: %v", err))
+ util.HTTPServerError(w, fmt.Errorf("failed to create new token: %v", err))
return
}
}
@@ -192,10 +235,10 @@
// header cannot be changed once the body is written to, this needs to be called first.
if _, err = h.csrfCop.MaybeSetCookie(w, r, revocationCookie); err != nil {
vlog.Infof("Failed to set CSRF cookie[%v] for request %#v", err, r)
- util.HTTPBadRequest(w, r, fmt.Errorf("Suspected automated request: %v", err))
+ util.HTTPServerError(w, err)
return
}
- if err := tmpl.Execute(w, tmplargs); err != nil {
+ if err := tmplViewBlessings.Execute(w, tmplargs); err != nil {
vlog.Errorf("Unable to execute audit page template: %v", err)
util.HTTPServerError(w, err)
}
@@ -207,7 +250,7 @@
success = `{"success": "true"}`
failure = `{"success": "false"}`
)
- if h.revocationManager == nil {
+ if h.args.RevocationManager == nil {
vlog.Infof("no provided revocation manager")
w.Write([]byte(failure))
return
@@ -234,7 +277,7 @@
w.Write([]byte(failure))
return
}
- if err := h.revocationManager.Revoke(caveatID); err != nil {
+ if err := h.args.RevocationManager.Revoke(caveatID); err != nil {
vlog.Infof("Revocation failed: %s", err)
w.Write([]byte(failure))
return
@@ -256,10 +299,221 @@
return string(caveatID), nil
}
-// ExchangeAuthCodeForEmail exchanges the authorization code (which must
+type seekBlessingsMacaroon struct {
+ RedirectURL, State string
+}
+
+func validLoopbackURL(u string) (*url.URL, error) {
+ netURL, err := url.Parse(u)
+ if err != nil {
+ return nil, fmt.Errorf("invalid url: %v", err)
+ }
+ // Remove the port from the netURL.Host.
+ host, _, err := net.SplitHostPort(netURL.Host)
+ // Check if its localhost or loopback ip
+ if host == "localhost" {
+ return netURL, nil
+ }
+ urlIP := net.ParseIP(host)
+ if urlIP.IsLoopback() {
+ return netURL, nil
+ }
+ return nil, fmt.Errorf("invalid loopback url")
+}
+
+func (h *handler) seekBlessings(w http.ResponseWriter, r *http.Request) {
+ if _, err := validLoopbackURL(r.FormValue("redirect_url")); err != nil {
+ vlog.Infof("seekBlessings failed: invalid redirect_url: %v", err)
+ util.HTTPBadRequest(w, r, fmt.Errorf("invalid redirect_url: %v", err))
+ return
+ }
+ outputMacaroon, err := h.csrfCop.NewToken(w, r, clientIDCookie, seekBlessingsMacaroon{
+ RedirectURL: r.FormValue("redirect_url"),
+ State: r.FormValue("state"),
+ })
+ if err != nil {
+ vlog.Infof("Failed to create CSRF token[%v] for request %#v", err, r)
+ util.HTTPServerError(w, fmt.Errorf("failed to create new token: %v", err))
+ return
+ }
+ http.Redirect(w, r, h.args.oauthConfig(addCaveatsRoute).AuthCodeURL(outputMacaroon), http.StatusFound)
+}
+
+type addCaveatsMacaroon struct {
+ ToolRedirectURL, ToolState, Email string
+}
+
+func (h *handler) addCaveats(w http.ResponseWriter, r *http.Request) {
+ var inputMacaroon seekBlessingsMacaroon
+ if err := h.csrfCop.ValidateToken(r.FormValue("state"), r, clientIDCookie, &inputMacaroon); err != nil {
+ util.HTTPBadRequest(w, r, fmt.Errorf("Suspected request forgery: %v", err))
+ return
+ }
+ email, err := exchangeAuthCodeForEmail(h.args.oauthConfig(addCaveatsRoute), r.FormValue("code"))
+ if err != nil {
+ util.HTTPBadRequest(w, r, err)
+ return
+ }
+ if len(h.args.DomainRestriction) > 0 && !strings.HasSuffix(email, "@"+h.args.DomainRestriction) {
+ util.HTTPBadRequest(w, r, fmt.Errorf("blessings for name %q are not allowed due to domain restriction", email))
+ return
+ }
+ outputMacaroon, err := h.csrfCop.NewToken(w, r, clientIDCookie, addCaveatsMacaroon{
+ ToolRedirectURL: inputMacaroon.RedirectURL,
+ ToolState: inputMacaroon.State,
+ Email: email,
+ })
+ if err != nil {
+ vlog.Infof("Failed to create caveatForm token[%v] for request %#v", err, r)
+ util.HTTPServerError(w, fmt.Errorf("failed to create new token: %v", err))
+ return
+ }
+ tmplargs := struct {
+ CaveatMap map[string]caveatInfo
+ Macaroon, MacaroonRoute string
+ }{caveatMap, outputMacaroon, sendMacaroonRoute}
+ w.Header().Set("Context-Type", "text/html")
+ if err := tmplSelectCaveats.Execute(w, tmplargs); err != nil {
+ vlog.Errorf("Unable to execute bless page template: %v", err)
+ util.HTTPServerError(w, err)
+ }
+}
+
+func (h *handler) sendMacaroon(w http.ResponseWriter, r *http.Request) {
+ var inputMacaroon addCaveatsMacaroon
+ if err := h.csrfCop.ValidateToken(r.FormValue("macaroon"), r, clientIDCookie, &inputMacaroon); err != nil {
+ util.HTTPBadRequest(w, r, fmt.Errorf("Suspected request forgery: %v", err))
+ return
+ }
+ caveats, err := h.caveats(r)
+ if err != nil {
+ util.HTTPBadRequest(w, r, fmt.Errorf("failed to extract caveats: ", err))
+ return
+ }
+ buf := &bytes.Buffer{}
+ m := blesser.BlessingMacaroon{
+ Creation: time.Now(),
+ Caveats: caveats,
+ Name: inputMacaroon.Email,
+ }
+ if err := vom.NewEncoder(buf).Encode(m); err != nil {
+ util.HTTPServerError(w, fmt.Errorf("failed to encode BlessingsMacaroon: ", err))
+ return
+ }
+ // Construct the url to send back to the tool.
+ baseURL, err := validLoopbackURL(inputMacaroon.ToolRedirectURL)
+ if err != nil {
+ util.HTTPBadRequest(w, r, fmt.Errorf("invalid ToolRedirectURL: ", err))
+ return
+ }
+ params := url.Values{}
+ params.Add("macaroon", string(util.NewMacaroon(h.args.MacaroonKey, buf.Bytes())))
+ params.Add("state", inputMacaroon.ToolState)
+ params.Add("object_name", h.args.MacaroonBlessingService)
+ baseURL.RawQuery = params.Encode()
+ http.Redirect(w, r, baseURL.String(), http.StatusFound)
+}
+
+func (h *handler) caveats(r *http.Request) ([]security.Caveat, error) {
+ if err := r.ParseForm(); err != nil {
+ return nil, err
+ }
+ var caveats []security.Caveat
+ var caveat security.Caveat
+ var err error
+
+ userProvidedExpiryCaveat := false
+
+ for i, cavName := range r.Form["caveat"] {
+ if cavName == "none" {
+ continue
+ }
+ if cavName == "ExpiryCaveat" {
+ userProvidedExpiryCaveat = true
+ }
+ args := strings.Split(r.Form[cavName][i], ",")
+ cavInfo, ok := caveatMap[cavName]
+ if !ok {
+ return nil, fmt.Errorf("unable to create caveat %s: caveat does not exist", cavName)
+ }
+ caveat, err := cavInfo.New(args...)
+ if err != nil {
+ return nil, fmt.Errorf("unable to create caveat %s(%v): cavInfo.New failed: %v", cavName, args, err)
+ }
+ caveats = append(caveats, caveat)
+ }
+
+ // TODO(suharshs): have a checkbox in the form that says "include revocation caveat".
+ if h.args.RevocationManager != nil {
+ revocationCaveat, err := h.args.RevocationManager.NewCaveat(h.args.R.Identity().PublicID(), h.args.DischargerLocation)
+ if err != nil {
+ return nil, err
+ }
+ caveat, err = security.NewCaveat(revocationCaveat)
+ } else if !userProvidedExpiryCaveat {
+ caveat, err = security.ExpiryCaveat(time.Now().Add(h.args.BlessingDuration))
+ }
+ if err != nil {
+ return nil, err
+ }
+ // revocationCaveat need to be prepended for extraction in ReadBlessAuditEntry.
+ caveats = append([]security.Caveat{caveat}, caveats...)
+
+ return caveats, nil
+}
+
+type caveatInfo struct {
+ New func(args ...string) (security.Caveat, error)
+ Placeholder string
+}
+
+// caveatMap is a map from Caveat name to caveat information.
+// To add to this map append
+// key = "CaveatName"
+// New = func that returns instantiation of specific caveat wrapped in security.Caveat.
+// Placeholder = the placeholder text for the html input element.
+var caveatMap = map[string]caveatInfo{
+ "ExpiryCaveat": {
+ New: func(args ...string) (security.Caveat, error) {
+ if len(args) != 1 {
+ return security.Caveat{}, fmt.Errorf("must pass exactly one duration string.")
+ }
+ dur, err := time.ParseDuration(args[0])
+ if err != nil {
+ return security.Caveat{}, fmt.Errorf("parse duration failed: %v", err)
+ }
+ return security.ExpiryCaveat(time.Now().Add(dur))
+ },
+ Placeholder: "i.e. 2h45m. Valid time units are ns, us (or µs), ms, s, m, h.",
+ },
+ "MethodCaveat": {
+ New: func(args ...string) (security.Caveat, error) {
+ if len(args) < 1 {
+ return security.Caveat{}, fmt.Errorf("must pass at least one method")
+ }
+ return security.MethodCaveat(args[0], args[1:]...)
+ },
+ Placeholder: "Comma-separated method names.",
+ },
+ "PeerBlessingsCaveat": {
+ New: func(args ...string) (security.Caveat, error) {
+ if len(args) < 1 {
+ return security.Caveat{}, fmt.Errorf("must pass at least one blessing pattern")
+ }
+ var patterns []security.BlessingPattern
+ for _, arg := range args {
+ patterns = append(patterns, security.BlessingPattern(arg))
+ }
+ return security.PeerBlessingsCaveat(patterns[0], patterns[1:]...)
+ },
+ Placeholder: "Comma-separated blessing patterns.",
+ },
+}
+
+// exchangeAuthCodeForEmail exchanges the authorization code (which must
// have been obtained with scope=email) for an OAuth token and then uses Google's
// tokeninfo API to extract the email address from that token.
-func ExchangeAuthCodeForEmail(config *oauth.Config, authcode string) (string, error) {
+func exchangeAuthCodeForEmail(config *oauth.Config, authcode string) (string, error) {
t, err := (&oauth.Transport{Config: config}).Exchange(authcode)
if err != nil {
return "", fmt.Errorf("failed to exchange authorization code for token: %v", err)
diff --git a/services/identity/googleoauth/template.go b/services/identity/googleoauth/template.go
index a0507e1..d203858 100644
--- a/services/identity/googleoauth/template.go
+++ b/services/identity/googleoauth/template.go
@@ -2,7 +2,7 @@
import "html/template"
-var tmpl = template.Must(template.New("auditor").Parse(`<!doctype html>
+var tmplViewBlessings = template.Must(template.New("auditor").Parse(`<!doctype html>
<html>
<head>
<meta charset="UTF-8">
@@ -39,7 +39,7 @@
$(".revoke").click(function() {
var revokeButton = $(this);
$.ajax({
- url: "/google/revoke",
+ url: "/google/{{.RevokeRoute}}",
type: "POST",
data: JSON.stringify({
"Token": revokeButton.val()
@@ -108,3 +108,73 @@
</div>
</body>
</html>`))
+
+var tmplSelectCaveats = template.Must(template.New("bless").Parse(`<!doctype html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Veyron Identity Derivation</title>
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
+<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
+<script>
+ // TODO(suharshs): Move this and other JS/CSS to an assets directory in identity server.
+ $(document).ready(function() {
+ $('.caveatInput').hide(); // Hide all the inputs at start.
+
+ // When a caveat selector changes show the corresponding input box.
+ $('body').on('change', '.caveats', function (){
+ // Grab the div encapsulating the select and the corresponding inputs.
+ var caveatSelector = $(this).parents(".caveatRow");
+ // Hide the visible inputs and show the selected one.
+ caveatSelector.find('.caveatInput').hide();
+ caveatSelector.find('#'+$(this).val()).show();
+ });
+
+ // Upon clicking the '+' button a new caveat selector should appear.
+ $('body').on('click', '.addCaveat', function() {
+ var selector = $(this).parents(".caveatRow");
+ var newSelector = selector.clone();
+ // Hide all inputs since nothing is selected in this clone.
+ newSelector.find('.caveatInput').hide();
+ selector.after(newSelector);
+ // Change the '+' button to a '-' button.
+ $(this).replaceWith('<button type="button" class="btn btn-danger btn-sm removeCaveat">-</button>')
+ });
+
+ // Upon clicking the '-' button caveats should be removed.
+ $('body').on('click', '.removeCaveat', function() {
+ $(this).parents(".caveatRow").remove();
+ });
+ });
+</script>
+</head>
+<body class="container">
+<form class="form-signin" method="POST" name="input" action="/google/{{.MacaroonRoute}}">
+<h2 class="form-signin-heading">Select Caveats</h2>
+<input type="text" class="hidden" name="macaroon" value="{{.Macaroon}}">
+<div class="caveatRow row">
+<br/>
+ <div class="col-md-4">
+ <select name="caveat" class="form-control caveats">
+ <option value="none" selected="selected">Select a caveat.</option>
+ {{ $caveatMap := .CaveatMap }}
+ {{range $key, $value := $caveatMap}}
+ <option name="{{$key}}" value="{{$key}}">{{$key}}</option>
+ {{end}}
+ </select>
+ </div>
+ <div class="col-md-7">
+ {{range $key, $entry := $caveatMap}}
+ <input type="text" id="{{$key}}" class="form-control caveatInput" name="{{$key}}" placeholder="{{$entry.Placeholder}}">
+ {{end}}
+ </div>
+ <div class="col-md-1">
+ <button type="button" class="btn btn-info btn-sm addCaveat">+</button>
+ </div>
+</div>
+<br/>
+<button class="btn btn-lg btn-primary btn-block" type="submit">Bless</button>
+</form>
+</body>
+</html>`))
diff --git a/services/identity/handlers/bless.go b/services/identity/handlers/bless.go
deleted file mode 100644
index 468df38..0000000
--- a/services/identity/handlers/bless.go
+++ /dev/null
@@ -1,241 +0,0 @@
-package handlers
-
-import (
- "fmt"
- "html/template"
- "net/http"
- "strings"
- "time"
-
- "veyron.io/veyron/veyron/services/identity/util"
- "veyron.io/veyron/veyron2/security"
- "veyron.io/veyron/veyron2/vlog"
-)
-
-// Bless is an http.HandlerFunc that renders/processes a form that takes as
-// input a base64-vom-encoded security.PrivateID (blessor) and a
-// security.PublicID (blessee) and returns a base64-vom-encoded
-// security.PublicID obtained by blessor.Bless(blessee, ...).
-func Bless(w http.ResponseWriter, r *http.Request) {
- if r.Method == "GET" {
- renderForm(w, r)
- return
- }
- if r.Method != "POST" {
- util.HTTPBadRequest(w, r, fmt.Errorf("only GET or POST requests accepted"))
- return
- }
- if err := r.ParseForm(); err != nil {
- util.HTTPBadRequest(w, r, fmt.Errorf("failed to parse form: %v", err))
- return
- }
- duration, err := time.ParseDuration(defaultIfEmpty(r.FormValue("duration"), "24h"))
- if err != nil {
- util.HTTPBadRequest(w, r, fmt.Errorf("failed to parse duration: %v", err))
- return
- }
- blessor, err := decodeBlessor(r)
- if err != nil {
- util.HTTPBadRequest(w, r, err)
- return
- }
- blessee, err := decodeBlessee(r)
- if err != nil {
- util.HTTPBadRequest(w, r, err)
- return
- }
- name := r.FormValue("name")
- caveats, err := caveats(r)
- if err != nil {
- util.HTTPBadRequest(w, r, fmt.Errorf("failed to created caveats: ", err))
- }
- blessed, err := blessor.Bless(blessee, name, duration, caveats)
- if err != nil {
- util.HTTPBadRequest(w, r, fmt.Errorf("%q.Bless(%q, %q) failed: %v", blessor, blessee, name, err))
- return
- }
- util.HTTPSend(w, blessed)
-}
-
-func caveats(r *http.Request) ([]security.Caveat, error) {
- if err := r.ParseForm(); err != nil {
- return nil, err
- }
- var caveats []security.Caveat
- for i, cavName := range r.Form["caveat"] {
- if cavName == "none" {
- continue
- }
- args := strings.Split(r.Form[cavName][i], ",")
- cavInfo, ok := caveatMap[cavName]
- if !ok {
- return nil, fmt.Errorf("unable to create caveat %s: caveat does not exist", cavName)
- }
- caveat, err := cavInfo.New(args...)
- if err != nil {
- return nil, fmt.Errorf("unable to create caveat %s(%v): cavInfo.New failed: %v", cavName, args, err)
- }
- caveats = append(caveats, caveat)
- }
- return caveats, nil
-}
-
-func decodeBlessor(r *http.Request) (security.PrivateID, error) {
- var blessor security.PrivateID
- b64 := r.FormValue("blessor")
- if err := util.Base64VomDecode(b64, &blessor); err != nil {
- return nil, fmt.Errorf("Base64VomDecode of blessor into %T failed: %v", blessor, err)
- }
- return blessor, nil
-}
-
-func decodeBlessee(r *http.Request) (security.PublicID, error) {
- var pub security.PublicID
- b64 := r.FormValue("blessee")
- if err := util.Base64VomDecode(b64, &pub); err == nil {
- return pub, nil
- }
- var priv security.PrivateID
- err := util.Base64VomDecode(b64, &priv)
- if err == nil {
- return priv.PublicID(), nil
- }
- return nil, fmt.Errorf("Base64VomDecode of blessee into %T or %T failed: %v", pub, priv, err)
-}
-
-func renderForm(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Context-Type", "text/html")
- if err := tmpl.Execute(w, caveatMap); err != nil {
- vlog.Errorf("Unable to execute bless page template: %v", err)
- util.HTTPServerError(w, err)
- }
-}
-
-var tmpl = template.Must(template.New("bless").Parse(`<!doctype html>
-<html>
-<head>
-<meta charset="UTF-8">
-<title>Veyron Identity Derivation</title>
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
-<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
-<script>
- // TODO(suharshs): Move this and other JS/CSS to an assets directory in identity server.
- $(document).ready(function() {
- $('.caveatInput').hide(); // Hide all the inputs at start.
-
- // When a caveat selector changes show the corresponding input box.
- $('body').on('change', '.caveats', function (){
- // Grab the div encapsulating the select and the corresponding inputs.
- var caveatSelector = $(this).parents(".caveatRow");
- // Hide the visible inputs and show the selected one.
- caveatSelector.find('.caveatInput').hide();
- caveatSelector.find('#'+$(this).val()).show();
- });
-
- // Upon clicking the '+' button a new caveat selector should appear.
- $('body').on('click', '.addCaveat', function() {
- var selector = $(this).parents(".caveatRow");
- var newSelector = selector.clone();
- // Hide all inputs since nothing is selected in this clone.
- newSelector.find('.caveatInput').hide();
- selector.after(newSelector);
- // Change the '+' button to a '-' button.
- $(this).replaceWith('<button type="button" class="btn btn-danger btn-sm removeCaveat">-</button>')
- });
-
- // Upon clicking the '-' button caveats should be removed.
- $('body').on('click', '.removeCaveat', function() {
- $(this).parents(".caveatRow").remove();
- });
- });
-</script>
-</head>
-<body class="container">
-<form class="form-signin" method="POST" name="input" action="/bless/">
-<h2 class="form-signin-heading">Blessings</h2>
-<input type="text" class="form-control" name="blessor" placeholder="Base64VOM encoded PrivateID of blessor">
-<br/>
-<input type="text" class="form-control" name="blessee" placeholder="Base64VOM encoded PublicID/PrivateID of blessee">
-<br/>
-<input type="text" class="form-control" name="name" placeholder="Name">
-<br/>
-<input type="text" class="form-control" name="duration" placeholder="Duration. Defaults to 24h">
-<div class="caveatRow row">
- <br/>
- <div class="col-md-4">
- <select name="caveat" class="form-control caveats">
- <option value="none" selected="selected">Select a caveat.</option>
- {{ $caveatMap := . }}
- {{range $key, $value := $caveatMap}}
- <option name="{{$key}}" value="{{$key}}">{{$key}}</option>
- {{end}}
- </select>
- </div>
- <div class="col-md-7">
- {{range $key, $entry := $caveatMap}}
- <input type="text" id="{{$key}}" class="form-control caveatInput" name="{{$key}}" placeholder="{{$entry.Placeholder}}">
- {{end}}
- </div>
- <div class="col-md-1">
- <button type="button" class="btn btn-info btn-sm addCaveat">+</button>
- </div>
-</div>
-<br/>
-<button class="btn btn-lg btn-primary btn-block" type="submit">Bless</button>
-</form>
-</body>
-</html>`))
-
-func defaultIfEmpty(str, def string) string {
- if len(str) == 0 {
- return def
- }
- return str
-}
-
-// caveatMap is a map from Caveat name to caveat information.
-// To add to this map append
-// key = "CaveatName"
-// New = func that returns instantiation of specific caveat wrapped in security.Caveat.
-// Placeholder = the placeholder text for the html input element.
-var caveatMap = map[string]struct {
- New func(args ...string) (security.Caveat, error)
- Placeholder string
-}{
- "ExpiryCaveat": {
- New: func(args ...string) (security.Caveat, error) {
- if len(args) != 1 {
- return security.Caveat{}, fmt.Errorf("must pass exactly one duration string.")
- }
- dur, err := time.ParseDuration(args[0])
- if err != nil {
- return security.Caveat{}, fmt.Errorf("parse duration failed: %v", err)
- }
- return security.ExpiryCaveat(time.Now().Add(dur))
- },
- Placeholder: "i.e. 2h45m. Valid time units are ns, us (or µs), ms, s, m, h.",
- },
- "MethodCaveat": {
- New: func(args ...string) (security.Caveat, error) {
- if len(args) < 1 {
- return security.Caveat{}, fmt.Errorf("must pass at least one method")
- }
- return security.MethodCaveat(args[0], args[1:]...)
- },
- Placeholder: "Comma-separated method names.",
- },
- "PeerBlessingsCaveat": {
- New: func(args ...string) (security.Caveat, error) {
- if len(args) < 1 {
- return security.Caveat{}, fmt.Errorf("must pass at least one blessing pattern")
- }
- var patterns []security.BlessingPattern
- for _, arg := range args {
- patterns = append(patterns, security.BlessingPattern(arg))
- }
- return security.PeerBlessingsCaveat(patterns[0], patterns[1:]...)
- },
- Placeholder: "Comma-separated blessing patterns.",
- },
-}
diff --git a/services/identity/handlers/handlers_test.go b/services/identity/handlers/handlers_test.go
index bf0d49a..a19ee91 100644
--- a/services/identity/handlers/handlers_test.go
+++ b/services/identity/handlers/handlers_test.go
@@ -1,14 +1,11 @@
package handlers
import (
- "fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
- "net/url"
"reflect"
"testing"
- "time"
"veyron.io/veyron/veyron/services/identity/util"
@@ -59,115 +56,6 @@
}
}
-func TestBless(t *testing.T) {
- r, err := rt.New()
- if err != nil {
- t.Fatal(err)
- }
- defer r.Cleanup()
-
- ts := httptest.NewServer(http.HandlerFunc(Bless))
- defer ts.Close()
-
- // GET requests should succeed (render the form)
- if resp, err := http.Get(ts.URL); err != nil || resp.StatusCode != http.StatusOK {
- t.Errorf("Got (%+v, %v) want (200, nil)", resp, err)
- }
-
- blessor, err := r.NewIdentity("god")
- if err != nil {
- t.Fatal(err)
- }
- blessee, err := r.NewIdentity("person")
- if err != nil {
- t.Fatal(err)
- }
-
- bless := func(blesser security.PrivateID, blessee security.PublicID, name string) security.PublicID {
- blessedID, err := blesser.Bless(blessee, name, 24*time.Hour, nil)
- if err != nil {
- t.Fatalf("%q.Bless(%q, %q, ...) failed: %v", blesser, blessee, name, err)
- }
- return blessedID
- }
-
- tests := []struct {
- Blessor, Blessee interface{}
- BlessingName string
- ExpectedBlessedID security.PublicID
- }{
- { // No field specified, bad request
- Blessor: nil,
- Blessee: nil,
- },
- { // No blessee specified, bad request
- Blessor: blessor,
- Blessee: nil,
- },
- { // No blessor specified, bad request
- Blessor: nil,
- Blessee: blessee,
- },
- { // No name specified, bad request
- Blessor: blessor,
- Blessee: blessee,
- },
- { // Blessor is a security.PublicID, bad request
- Blessor: blessor.PublicID(),
- Blessee: blessee,
- BlessingName: "batman",
- },
- { // Everything specified, blessee is a security.PrivateID. Should succeed
- Blessor: blessor,
- Blessee: blessee,
- BlessingName: "batman",
- ExpectedBlessedID: bless(blessor, blessee.PublicID(), "batman"),
- },
- { // Everything specified, blessee is a security.PublicID. Should succeed
- Blessor: blessor,
- Blessee: blessee.PublicID(),
- BlessingName: "batman",
- ExpectedBlessedID: bless(blessor, blessee.PublicID(), "batman"),
- },
- }
- for _, test := range tests {
- debug := fmt.Sprintf("%q.Bless(%q, %q, ...)", test.Blessor, test.Blessee, test.BlessingName)
- v := url.Values{}
- if test.Blessor != nil {
- v.Set("blessor", b64vomencode(test.Blessor))
- } else {
- v.Set("blessor", "")
- }
- if test.Blessee != nil {
- v.Set("blessee", b64vomencode(test.Blessee))
- } else {
- v.Set("blessee", "")
- }
- v.Set("name", test.BlessingName)
- res, err := http.PostForm(ts.URL, v)
- if test.ExpectedBlessedID == nil {
- if res.StatusCode != http.StatusBadRequest {
- t.Errorf("%v: Got (%v=%v) want 400", debug, res.StatusCode, res.Status)
- }
- continue
- }
- id, err := parseResponse(res, nil)
- if err != nil {
- t.Errorf("%v error: %v", debug, err)
- continue
- }
- pub, ok := id.(security.PublicID)
- if !ok {
- t.Errorf("%v returned %T, want security.PublicID", debug, id)
- continue
- }
- if got, want := fmt.Sprintf("%s", pub), fmt.Sprintf("%s", test.ExpectedBlessedID); got != want {
- t.Errorf("%v returned an identity %q want %q", debug, got, want)
- continue
- }
- }
-}
-
func parseResponse(r *http.Response, err error) (interface{}, error) {
if err != nil {
return nil, err
@@ -182,11 +70,3 @@
}
return parsed, nil
}
-
-func b64vomencode(obj interface{}) string {
- str, err := util.Base64VomEncode(obj)
- if err != nil {
- panic(err)
- }
- return str
-}
diff --git a/services/identity/identity.vdl b/services/identity/identity.vdl
index 5dd2891..bec7d91 100644
--- a/services/identity/identity.vdl
+++ b/services/identity/identity.vdl
@@ -1,8 +1,7 @@
// Package identity defines services for identity providers in the veyron ecosystem.
package identity
-
-// OAuthBlesser exchanges OAuth authorization codes OR access tokens for
+// OAuthBlesser exchanges OAuth access tokens for
// an email address from an OAuth-based identity provider and uses the email
// address obtained to bless the client.
//
@@ -10,28 +9,23 @@
// though the Google implementation also has informative documentation at
// https://developers.google.com/accounts/docs/OAuth2
//
-// WARNING: There is no binding between the channel over which the
-// authorization code or access token was obtained (typically https)
-// and the channel used to make the RPC (a veyron virtual circuit).
-// Thus, if Mallory possesses the authorization code or access token
-// associated with Alice's account, she may be able to obtain a blessing
-// with Alice's name on it.
+// WARNING: There is no binding between the channel over which the access token
+// was obtained (typically https) and the channel used to make the RPC (a
+// veyron virtual circuit).
+// Thus, if Mallory possesses the access token associated with Alice's account,
+// she may be able to obtain a blessing with Alice's name on it.
//
-// TODO(ashankar,toddw): Once the "OneOf" type becomes available in VDL,
-// then the "any" should be replaced by:
-// OneOf<wire.ChainPublicID, []wire.ChainPublicID>
-// where wire is from:
-// import "veyron.io/veyron/veyron2/security/wire"
+// TODO(ashankar): Update this to use the new security model:
+// (blessing security.WireBlessing, error)
type OAuthBlesser interface {
- // BlessUsingAuthorizationCode exchanges the provided authorization code
- // for an access token and then uses that access token to obtain an
- // email address.
- //
- // The redirect URL used to obtain the authorization code must also
- // be provided.
- BlessUsingAuthorizationCode(authcode, redirecturl string) (blessing any, err error)
-
// BlessUsingAccessToken uses the provided access token to obtain the email
// address and returns a blessing.
BlessUsingAccessToken(token string) (blessing any, err error)
}
+
+// MacaroonBlesser returns a blessing given the provided macaroon string.
+type MacaroonBlesser interface {
+ // Bless uses the provided macaroon (which contains email and caveats)
+ // to return a blessing for the client.
+ Bless(macaroon string) (blessing any, err error)
+}
diff --git a/services/identity/identity.vdl.go b/services/identity/identity.vdl.go
index 93ed657..d47d17f 100644
--- a/services/identity/identity.vdl.go
+++ b/services/identity/identity.vdl.go
@@ -18,7 +18,7 @@
// It corrects a bug where _gen_wiretype is unused in VDL pacakges where only bootstrap types are used on interfaces.
const _ = _gen_wiretype.TypeIDInvalid
-// OAuthBlesser exchanges OAuth authorization codes OR access tokens for
+// OAuthBlesser exchanges OAuth access tokens for
// an email address from an OAuth-based identity provider and uses the email
// address obtained to bless the client.
//
@@ -42,13 +42,6 @@
// OAuthBlesser_ExcludingUniversal is the interface without internal framework-added methods
// to enable embedding without method collisions. Not to be used directly by clients.
type OAuthBlesser_ExcludingUniversal interface {
- // BlessUsingAuthorizationCode exchanges the provided authorization code
- // for an access token and then uses that access token to obtain an
- // email address.
- //
- // The redirect URL used to obtain the authorization code must also
- // be provided.
- BlessUsingAuthorizationCode(ctx _gen_context.T, authcode string, redirecturl string, opts ..._gen_ipc.CallOpt) (reply _gen_vdlutil.Any, err error)
// BlessUsingAccessToken uses the provided access token to obtain the email
// address and returns a blessing.
BlessUsingAccessToken(ctx _gen_context.T, token string, opts ..._gen_ipc.CallOpt) (reply _gen_vdlutil.Any, err error)
@@ -61,13 +54,6 @@
// OAuthBlesserService is the interface the server implements.
type OAuthBlesserService interface {
- // BlessUsingAuthorizationCode exchanges the provided authorization code
- // for an access token and then uses that access token to obtain an
- // email address.
- //
- // The redirect URL used to obtain the authorization code must also
- // be provided.
- BlessUsingAuthorizationCode(context _gen_ipc.ServerContext, authcode string, redirecturl string) (reply _gen_vdlutil.Any, err error)
// BlessUsingAccessToken uses the provided access token to obtain the email
// address and returns a blessing.
BlessUsingAccessToken(context _gen_ipc.ServerContext, token string) (reply _gen_vdlutil.Any, err error)
@@ -120,17 +106,6 @@
return _gen_veyron2.RuntimeFromContext(ctx).Client()
}
-func (__gen_c *clientStubOAuthBlesser) BlessUsingAuthorizationCode(ctx _gen_context.T, authcode string, redirecturl string, opts ..._gen_ipc.CallOpt) (reply _gen_vdlutil.Any, err error) {
- var call _gen_ipc.Call
- if call, err = __gen_c.client(ctx).StartCall(ctx, __gen_c.name, "BlessUsingAuthorizationCode", []interface{}{authcode, redirecturl}, opts...); err != nil {
- return
- }
- if ierr := call.Finish(&reply, &err); ierr != nil {
- err = ierr
- }
- return
-}
-
func (__gen_c *clientStubOAuthBlesser) BlessUsingAccessToken(ctx _gen_context.T, token string, opts ..._gen_ipc.CallOpt) (reply _gen_vdlutil.Any, err error) {
var call _gen_ipc.Call
if call, err = __gen_c.client(ctx).StartCall(ctx, __gen_c.name, "BlessUsingAccessToken", []interface{}{token}, opts...); err != nil {
@@ -187,8 +162,6 @@
// Note: This exhibits some weird behavior like returning a nil error if the method isn't found.
// This will change when it is replaced with Signature().
switch method {
- case "BlessUsingAuthorizationCode":
- return []interface{}{}, nil
case "BlessUsingAccessToken":
return []interface{}{}, nil
default:
@@ -207,16 +180,6 @@
{Name: "err", Type: 66},
},
}
- result.Methods["BlessUsingAuthorizationCode"] = _gen_ipc.MethodSignature{
- InArgs: []_gen_ipc.MethodArgument{
- {Name: "authcode", Type: 3},
- {Name: "redirecturl", Type: 3},
- },
- OutArgs: []_gen_ipc.MethodArgument{
- {Name: "blessing", Type: 65},
- {Name: "err", Type: 66},
- },
- }
result.TypeDefs = []_gen_vdlutil.Any{
_gen_wiretype.NamedPrimitiveType{Type: 0x1, Name: "anydata", Tags: []string(nil)}, _gen_wiretype.NamedPrimitiveType{Type: 0x1, Name: "error", Tags: []string(nil)}}
@@ -242,12 +205,180 @@
return
}
-func (__gen_s *ServerStubOAuthBlesser) BlessUsingAuthorizationCode(call _gen_ipc.ServerCall, authcode string, redirecturl string) (reply _gen_vdlutil.Any, err error) {
- reply, err = __gen_s.service.BlessUsingAuthorizationCode(call, authcode, redirecturl)
+func (__gen_s *ServerStubOAuthBlesser) BlessUsingAccessToken(call _gen_ipc.ServerCall, token string) (reply _gen_vdlutil.Any, err error) {
+ reply, err = __gen_s.service.BlessUsingAccessToken(call, token)
return
}
-func (__gen_s *ServerStubOAuthBlesser) BlessUsingAccessToken(call _gen_ipc.ServerCall, token string) (reply _gen_vdlutil.Any, err error) {
- reply, err = __gen_s.service.BlessUsingAccessToken(call, token)
+// MacaroonBlesser returns a blessing given the provided macaroon string.
+// MacaroonBlesser is the interface the client binds and uses.
+// MacaroonBlesser_ExcludingUniversal is the interface without internal framework-added methods
+// to enable embedding without method collisions. Not to be used directly by clients.
+type MacaroonBlesser_ExcludingUniversal interface {
+ // Bless uses the provided macaroon (which contains email and caveats)
+ // to return a blessing for the client.
+ Bless(ctx _gen_context.T, macaroon string, opts ..._gen_ipc.CallOpt) (reply _gen_vdlutil.Any, err error)
+}
+type MacaroonBlesser interface {
+ _gen_ipc.UniversalServiceMethods
+ MacaroonBlesser_ExcludingUniversal
+}
+
+// MacaroonBlesserService is the interface the server implements.
+type MacaroonBlesserService interface {
+
+ // Bless uses the provided macaroon (which contains email and caveats)
+ // to return a blessing for the client.
+ Bless(context _gen_ipc.ServerContext, macaroon string) (reply _gen_vdlutil.Any, err error)
+}
+
+// BindMacaroonBlesser returns the client stub implementing the MacaroonBlesser
+// interface.
+//
+// If no _gen_ipc.Client is specified, the default _gen_ipc.Client in the
+// global Runtime is used.
+func BindMacaroonBlesser(name string, opts ..._gen_ipc.BindOpt) (MacaroonBlesser, error) {
+ var client _gen_ipc.Client
+ switch len(opts) {
+ case 0:
+ // Do nothing.
+ case 1:
+ if clientOpt, ok := opts[0].(_gen_ipc.Client); opts[0] == nil || ok {
+ client = clientOpt
+ } else {
+ return nil, _gen_vdlutil.ErrUnrecognizedOption
+ }
+ default:
+ return nil, _gen_vdlutil.ErrTooManyOptionsToBind
+ }
+ stub := &clientStubMacaroonBlesser{defaultClient: client, name: name}
+
+ return stub, nil
+}
+
+// NewServerMacaroonBlesser creates a new server stub.
+//
+// It takes a regular server implementing the MacaroonBlesserService
+// interface, and returns a new server stub.
+func NewServerMacaroonBlesser(server MacaroonBlesserService) interface{} {
+ return &ServerStubMacaroonBlesser{
+ service: server,
+ }
+}
+
+// clientStubMacaroonBlesser implements MacaroonBlesser.
+type clientStubMacaroonBlesser struct {
+ defaultClient _gen_ipc.Client
+ name string
+}
+
+func (__gen_c *clientStubMacaroonBlesser) client(ctx _gen_context.T) _gen_ipc.Client {
+ if __gen_c.defaultClient != nil {
+ return __gen_c.defaultClient
+ }
+ return _gen_veyron2.RuntimeFromContext(ctx).Client()
+}
+
+func (__gen_c *clientStubMacaroonBlesser) Bless(ctx _gen_context.T, macaroon string, opts ..._gen_ipc.CallOpt) (reply _gen_vdlutil.Any, err error) {
+ var call _gen_ipc.Call
+ if call, err = __gen_c.client(ctx).StartCall(ctx, __gen_c.name, "Bless", []interface{}{macaroon}, opts...); err != nil {
+ return
+ }
+ if ierr := call.Finish(&reply, &err); ierr != nil {
+ err = ierr
+ }
+ return
+}
+
+func (__gen_c *clientStubMacaroonBlesser) UnresolveStep(ctx _gen_context.T, opts ..._gen_ipc.CallOpt) (reply []string, err error) {
+ var call _gen_ipc.Call
+ if call, err = __gen_c.client(ctx).StartCall(ctx, __gen_c.name, "UnresolveStep", nil, opts...); err != nil {
+ return
+ }
+ if ierr := call.Finish(&reply, &err); ierr != nil {
+ err = ierr
+ }
+ return
+}
+
+func (__gen_c *clientStubMacaroonBlesser) Signature(ctx _gen_context.T, opts ..._gen_ipc.CallOpt) (reply _gen_ipc.ServiceSignature, err error) {
+ var call _gen_ipc.Call
+ if call, err = __gen_c.client(ctx).StartCall(ctx, __gen_c.name, "Signature", nil, opts...); err != nil {
+ return
+ }
+ if ierr := call.Finish(&reply, &err); ierr != nil {
+ err = ierr
+ }
+ return
+}
+
+func (__gen_c *clientStubMacaroonBlesser) GetMethodTags(ctx _gen_context.T, method string, opts ..._gen_ipc.CallOpt) (reply []interface{}, err error) {
+ var call _gen_ipc.Call
+ if call, err = __gen_c.client(ctx).StartCall(ctx, __gen_c.name, "GetMethodTags", []interface{}{method}, opts...); err != nil {
+ return
+ }
+ if ierr := call.Finish(&reply, &err); ierr != nil {
+ err = ierr
+ }
+ return
+}
+
+// ServerStubMacaroonBlesser wraps a server that implements
+// MacaroonBlesserService and provides an object that satisfies
+// the requirements of veyron2/ipc.ReflectInvoker.
+type ServerStubMacaroonBlesser struct {
+ service MacaroonBlesserService
+}
+
+func (__gen_s *ServerStubMacaroonBlesser) GetMethodTags(call _gen_ipc.ServerCall, method string) ([]interface{}, error) {
+ // TODO(bprosnitz) GetMethodTags() will be replaces with Signature().
+ // Note: This exhibits some weird behavior like returning a nil error if the method isn't found.
+ // This will change when it is replaced with Signature().
+ switch method {
+ case "Bless":
+ return []interface{}{}, nil
+ default:
+ return nil, nil
+ }
+}
+
+func (__gen_s *ServerStubMacaroonBlesser) Signature(call _gen_ipc.ServerCall) (_gen_ipc.ServiceSignature, error) {
+ result := _gen_ipc.ServiceSignature{Methods: make(map[string]_gen_ipc.MethodSignature)}
+ result.Methods["Bless"] = _gen_ipc.MethodSignature{
+ InArgs: []_gen_ipc.MethodArgument{
+ {Name: "macaroon", Type: 3},
+ },
+ OutArgs: []_gen_ipc.MethodArgument{
+ {Name: "blessing", Type: 65},
+ {Name: "err", Type: 66},
+ },
+ }
+
+ result.TypeDefs = []_gen_vdlutil.Any{
+ _gen_wiretype.NamedPrimitiveType{Type: 0x1, Name: "anydata", Tags: []string(nil)}, _gen_wiretype.NamedPrimitiveType{Type: 0x1, Name: "error", Tags: []string(nil)}}
+
+ return result, nil
+}
+
+func (__gen_s *ServerStubMacaroonBlesser) UnresolveStep(call _gen_ipc.ServerCall) (reply []string, err error) {
+ if unresolver, ok := __gen_s.service.(_gen_ipc.Unresolver); ok {
+ return unresolver.UnresolveStep(call)
+ }
+ if call.Server() == nil {
+ return
+ }
+ var published []string
+ if published, err = call.Server().Published(); err != nil || published == nil {
+ return
+ }
+ reply = make([]string, len(published))
+ for i, p := range published {
+ reply[i] = _gen_naming.Join(p, call.Name())
+ }
+ return
+}
+
+func (__gen_s *ServerStubMacaroonBlesser) Bless(call _gen_ipc.ServerCall, macaroon string) (reply _gen_vdlutil.Any, err error) {
+ reply, err = __gen_s.service.Bless(call, macaroon)
return
}
diff --git a/services/identity/identityd/main.go b/services/identity/identityd/main.go
index 2218046..24ec2d9 100644
--- a/services/identity/identityd/main.go
+++ b/services/identity/identityd/main.go
@@ -2,6 +2,7 @@
package main
import (
+ "crypto/rand"
"flag"
"fmt"
"html/template"
@@ -28,6 +29,7 @@
"veyron.io/veyron/veyron/services/identity/revocation"
services "veyron.io/veyron/veyron/services/security"
"veyron.io/veyron/veyron/services/security/discharger"
+ "veyron.io/veyron/veyron2/verror"
)
var (
@@ -43,17 +45,22 @@
auditfilter = flag.String("audit_filter", "", "If non-empty, instead of starting the server the audit log will be dumped to STDOUT (with the filter set to the value of this flag. '/' can be used to dump all events).")
// Configuration for various Google OAuth-based clients.
- googleConfigWeb = flag.String("google_config_web", "", "Path to JSON-encoded OAuth client configuration for the web application that renders the audit log for blessings provided by this provider.")
- googleConfigInstalled = flag.String("google_config_installed", "", "Path to the JSON-encoded OAuth client configuration for installed client applications that obtain blessings (via the OAuthBlesser.BlessUsingAuthorizationCode RPC) from this server (like the 'identity' command like tool and its 'seekblessing' command.")
- googleConfigChrome = flag.String("google_config_chrome", "", "Path to the JSON-encoded OAuth client configuration for Chrome browser applications that obtain blessings from this server (via the OAuthBlesser.BlessUsingAccessToken RPC) from this server.")
- googleConfigAndroid = flag.String("google_config_android", "", "Path to the JSON-encoded OAuth client configuration for Android applications that obtain blessings from this server (via the OAuthBlesser.BlessUsingAccessToken RPC) from this server.")
- googleDomain = flag.String("google_domain", "", "An optional domain name. When set, only email addresses from this domain are allowed to authenticate via Google OAuth")
+ googleConfigWeb = flag.String("google_config_web", "", "Path to JSON-encoded OAuth client configuration for the web application that renders the audit log for blessings provided by this provider.")
+ googleConfigChrome = flag.String("google_config_chrome", "", "Path to the JSON-encoded OAuth client configuration for Chrome browser applications that obtain blessings from this server (via the OAuthBlesser.BlessUsingAccessToken RPC) from this server.")
+ googleConfigAndroid = flag.String("google_config_android", "", "Path to the JSON-encoded OAuth client configuration for Android applications that obtain blessings from this server (via the OAuthBlesser.BlessUsingAccessToken RPC) from this server.")
+ googleDomain = flag.String("google_domain", "", "An optional domain name. When set, only email addresses from this domain are allowed to authenticate via Google OAuth")
// Revoker/Discharger configuration
// TODO(ashankar,ataly,suharshs): Re-enable by default once the move to the new security API is complete?
revocationDir = flag.String("revocation_dir", "" /*filepath.Join(os.TempDir(), "revocation_dir")*/, "Path where the revocation manager will store caveat and revocation information.")
)
+const (
+ googleService = "google"
+ macaroonService = "macaroon"
+ dischargerService = "discharger"
+)
+
func main() {
flag.Usage = usage
r := rt.Init(providerIdentity())
@@ -75,24 +82,30 @@
if enableRandomHandler() {
http.Handle("/random/", handlers.Random{r}) // mint identities with a random name
}
- http.HandleFunc("/bless/", handlers.Bless) // use a provided PrivateID to bless a provided PublicID
-
+ macaroonKey := make([]byte, 32)
+ if _, err := rand.Read(macaroonKey); err != nil {
+ vlog.Fatalf("macaroonKey generation failed: %v", err)
+ }
// Google OAuth
- ipcServer, ipcServerEP, err := setupGoogleBlessingDischargingServer(r, revocationManager)
+ ipcServer, published, err := setupServices(r, revocationManager, macaroonKey)
if err != nil {
vlog.Fatalf("Failed to setup veyron services for blessing: %v", err)
}
if ipcServer != nil {
defer ipcServer.Stop()
}
- if clientID, clientSecret, ok := getOAuthClientIDAndSecret(*googleConfigWeb); ok && len(*auditprefix) > 0 {
+ if clientID, clientSecret, ok := getOAuthClientIDAndSecret(*googleConfigWeb); ok {
n := "/google/"
h, err := googleoauth.NewHandler(googleoauth.HandlerArgs{
- Addr: fmt.Sprintf("%s%s", httpaddress(), n),
- ClientID: clientID,
- ClientSecret: clientSecret,
- Auditor: *auditprefix,
- RevocationManager: revocationManager,
+ R: r,
+ MacaroonKey: macaroonKey,
+ Addr: fmt.Sprintf("%s%s", httpaddress(), n),
+ ClientID: clientID,
+ ClientSecret: clientSecret,
+ Auditor: *auditprefix,
+ RevocationManager: revocationManager,
+ BlessingDuration: time.Duration(*minExpiryDays) * 24 * time.Hour,
+ MacaroonBlessingService: naming.JoinAddressName(published[0], macaroonService),
})
if err != nil {
vlog.Fatalf("Failed to create googleoauth handler: %v", err)
@@ -100,24 +113,21 @@
http.Handle(n, h)
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- var servers []string
- if ipcServer != nil {
- servers, _ = ipcServer.Published()
- }
- if len(servers) == 0 {
- // No addresses published, publish the endpoint instead (which may not be usable everywhere, but oh-well).
- servers = append(servers, ipcServerEP.String())
- }
args := struct {
Self security.PublicID
- GoogleWeb, RandomWeb bool
+ RandomWeb bool
GoogleServers, DischargeServers []string
+ ListBlessingsRoute string
}{
Self: rt.R().Identity().PublicID(),
- GoogleWeb: len(*googleConfigWeb) > 0,
RandomWeb: enableRandomHandler(),
- GoogleServers: appendSuffixTo(servers, "google"),
- DischargeServers: appendSuffixTo(servers, "discharger"),
+ DischargeServers: appendSuffixTo(published, dischargerService),
+ }
+ if len(*googleConfigChrome) > 0 || len(*googleConfigAndroid) > 0 {
+ args.GoogleServers = appendSuffixTo(published, googleService)
+ }
+ if len(*auditprefix) > 0 && len(*googleConfigWeb) > 0 {
+ args.ListBlessingsRoute = googleoauth.ListBlessingsRoute
}
if err := tmpl.Execute(w, args); err != nil {
vlog.Info("Failed to render template:", err)
@@ -138,52 +148,50 @@
// newDispatcher returns a dispatcher for both the blessing and the discharging service.
// their suffix. ReflectInvoker is used to invoke methods.
-func newDispatcher(params blesser.GoogleParams) ipc.Dispatcher {
- blessingService := ipc.ReflectInvoker(blesser.NewGoogleOAuthBlesserServer(params))
- dischargerService := ipc.ReflectInvoker(services.NewServerDischarger(discharger.NewDischarger(params.R.Identity())))
- allowEveryoneACLAuth := vsecurity.NewACLAuthorizer(security.ACL{In: map[security.BlessingPattern]security.LabelSet{
- security.AllPrincipals: security.AllLabels,
- }})
- return &dispatcher{blessingService, dischargerService, allowEveryoneACLAuth}
+func newDispatcher(googleParams blesser.GoogleParams, macaroonKey []byte) ipc.Dispatcher {
+ d := &dispatcher{
+ invokers: map[string]ipc.Invoker{
+ macaroonService: ipc.ReflectInvoker(blesser.NewMacaroonBlesserServer(googleParams.R, macaroonKey)),
+ dischargerService: ipc.ReflectInvoker(services.NewServerDischarger(discharger.NewDischarger(googleParams.R.Identity()))),
+ },
+ auth: vsecurity.NewACLAuthorizer(security.ACL{In: map[security.BlessingPattern]security.LabelSet{
+ security.AllPrincipals: security.AllLabels,
+ }}),
+ }
+ if len(*googleConfigChrome) > 0 || len(*googleConfigAndroid) > 0 {
+ d.invokers[googleService] = ipc.ReflectInvoker(blesser.NewGoogleOAuthBlesserServer(googleParams))
+ }
+ return d
}
type dispatcher struct {
- blessingInvoker, dischargerInvoker ipc.Invoker
- auth security.Authorizer
+ invokers map[string]ipc.Invoker
+ auth security.Authorizer
}
func (d dispatcher) Lookup(suffix, method string) (ipc.Invoker, security.Authorizer, error) {
- switch suffix {
- case "google":
- return d.blessingInvoker, d.auth, nil
- case "discharger":
- return d.dischargerInvoker, d.auth, nil
- default:
- return nil, nil, fmt.Errorf("suffix does not exist")
+ if invoker := d.invokers[suffix]; invoker != nil {
+ return invoker, d.auth, nil
}
+ return nil, nil, verror.NoExistf("%q is not a valid suffix at this server", suffix)
}
-// Starts the blessing service and the discharging service on the same port.
-func setupGoogleBlessingDischargingServer(r veyron2.Runtime, revocationManager *revocation.RevocationManager) (ipc.Server, naming.Endpoint, error) {
+// Starts the blessing services and the discharging service on the same port.
+func setupServices(r veyron2.Runtime, revocationManager *revocation.RevocationManager, macaroonKey []byte) (ipc.Server, []string, error) {
var enable bool
- params := blesser.GoogleParams{
+ googleParams := blesser.GoogleParams{
R: r,
BlessingDuration: time.Duration(*minExpiryDays) * 24 * time.Hour,
DomainRestriction: *googleDomain,
RevocationManager: revocationManager,
}
- if clientID, clientSecret, ok := getOAuthClientIDAndSecret(*googleConfigInstalled); ok {
- enable = true
- params.AuthorizationCodeClient.ID = clientID
- params.AuthorizationCodeClient.Secret = clientSecret
- }
if clientID, ok := getOAuthClientID(*googleConfigChrome); ok {
enable = true
- params.AccessTokenClients = append(params.AccessTokenClients, struct{ ID string }{clientID})
+ googleParams.AccessTokenClients = append(googleParams.AccessTokenClients, struct{ ID string }{clientID})
}
if clientID, ok := getOAuthClientID(*googleConfigAndroid); ok {
enable = true
- params.AccessTokenClients = append(params.AccessTokenClients, struct{ ID string }{clientID})
+ googleParams.AccessTokenClients = append(googleParams.AccessTokenClients, struct{ ID string }{clientID})
}
if !enable {
return nil, nil, nil
@@ -196,19 +204,26 @@
if err != nil {
return nil, nil, fmt.Errorf("server.Listen(%q, %q) failed: %v", "tcp", *address, err)
}
- params.DischargerLocation = naming.JoinAddressName(ep.String(), "discharger")
- dispatcher := newDispatcher(params)
+ googleParams.DischargerLocation = naming.JoinAddressName(ep.String(), dischargerService)
+
+ dispatcher := newDispatcher(googleParams, macaroonKey)
objectname := fmt.Sprintf("identity/%s", r.Identity().PublicID().Names()[0])
if err := server.Serve(objectname, dispatcher); err != nil {
return nil, nil, fmt.Errorf("failed to start Veyron services: %v", err)
}
vlog.Infof("Google blessing and discharger services enabled at endpoint %v and name %q", ep, objectname)
- return server, ep, nil
+
+ published, _ := server.Published()
+ if len(published) == 0 {
+ // No addresses published, publish the endpoint instead (which may not be usable everywhere, but oh-well).
+ published = append(published, ep.String())
+ }
+ return server, published, nil
}
func enableTLS() bool { return len(*tlsconfig) > 0 }
func enableRandomHandler() bool {
- return len(*googleConfigInstalled)+len(*googleConfigWeb)+len(*googleConfigChrome)+len(*googleConfigAndroid) == 0
+ return len(*googleConfigWeb)+len(*googleConfigChrome)+len(*googleConfigAndroid) == 0
}
func getOAuthClientID(config string) (clientID string, ok bool) {
fname := config
@@ -362,14 +377,13 @@
{{if .DischargeServers}}
<li>RevocationCaveat Discharges are provided via Veyron RPCs to: <tt>{{range .DischargeServers}}{{.}}{{end}}</tt></li>
{{end}}
-{{if .GoogleWeb}}
-<li>You can <a class="btn btn-xs btn-primary" href="/google/auth">enumerate</a> blessings provided with your
+{{if .ListBlessingsRoute}}
+<li>You can <a class="btn btn-xs btn-primary" href="/google/{{.ListBlessingsRoute}}">enumerate</a> blessings provided with your
email address as the name.</li>
{{end}}
{{if .RandomWeb}}
<li>You can obtain a randomly assigned PrivateID <a class="btn btn-sm btn-primary" href="/random/">here</a></li>
{{end}}
-<li>You can offload cryptographic operations <a class="btn btn-xs btn-primary" href="/bless/">for blessing</a> to this HTTP server</li>
</ul>
</div>
diff --git a/services/identity/revocation/bless.go b/services/identity/revocation/bless.go
index 473bcd4..03f8df3 100644
--- a/services/identity/revocation/bless.go
+++ b/services/identity/revocation/bless.go
@@ -11,9 +11,7 @@
)
// Bless creates a blessing on behalf of the identity server.
-func Bless(server security.PrivateID, blessee security.PublicID, email string, duration time.Duration, revocationCaveat security.ThirdPartyCaveat) (security.PublicID, error) {
- // TODO(suharshs): Pass caveats to here when macaroon new oauth flow is complete.
- var caveats []security.Caveat
+func Bless(server security.PrivateID, blessee security.PublicID, email string, duration time.Duration, caveats []security.Caveat, revocationCaveat security.ThirdPartyCaveat) (security.PublicID, error) {
if revocationCaveat != nil {
caveat, err := security.NewCaveat(revocationCaveat)
if err != nil {
diff --git a/services/identity/revocation/bless_test.go b/services/identity/revocation/bless_test.go
index 6e20ad4..f26cf7a 100644
--- a/services/identity/revocation/bless_test.go
+++ b/services/identity/revocation/bless_test.go
@@ -55,7 +55,7 @@
t.Fatalf("discharger.NewRevocationCaveat failed: %v", err)
}
- correct_blessed, err := Bless(self, self.PublicID(), "test", time.Second, cav)
+ correct_blessed, err := Bless(self, self.PublicID(), "test", time.Second, nil, cav)
if err != nil {
t.Fatalf("Bless: failed with caveats: %v", err)
}
@@ -76,7 +76,7 @@
}
// Test no caveat
- correct_blessed, err = Bless(self, self.PublicID(), "test", time.Second, nil)
+ correct_blessed, err = Bless(self, self.PublicID(), "test", time.Second, nil, nil)
if err != nil {
t.Fatalf("Bless: failed with no caveats: %v", err)
}
diff --git a/services/identity/util/csrf.go b/services/identity/util/csrf.go
index e1bc218..5d3ad5f 100644
--- a/services/identity/util/csrf.go
+++ b/services/identity/util/csrf.go
@@ -23,6 +23,12 @@
key []byte
}
+func (c *CSRFCop) keyForCookie(cookie []byte) []byte {
+ hm := hmac.New(sha256.New, c.key)
+ hm.Write(cookie)
+ return hm.Sum(nil)
+}
+
func NewCSRFCop() (*CSRFCop, error) {
key := make([]byte, keyLength)
if _, err := rand.Read(key); err != nil {
@@ -45,11 +51,7 @@
return "", err
}
}
- m, err := c.createMacaroon(buf.Bytes(), cookieValue)
- if err != nil {
- return "", err
- }
- return b64encode(append(m.Data, m.HMAC...)), nil
+ return string(NewMacaroon(c.keyForCookie(cookieValue), buf.Bytes())), nil
}
// ValidateToken checks the validity of the provided CSRF token for the
@@ -65,67 +67,18 @@
if err != nil {
return fmt.Errorf("invalid cookie")
}
- decToken, err := b64decode(token)
+ encodedInput, err := Macaroon(token).Decode(c.keyForCookie(cookieValue))
if err != nil {
- return fmt.Errorf("invalid token: %v", err)
- }
- m := macaroon{
- Data: decToken[:len(decToken)-sha256.Size],
- HMAC: decToken[len(decToken)-sha256.Size:],
+ return err
}
if decoded != nil {
- if err := vom.NewDecoder(bytes.NewBuffer(m.Data)).Decode(decoded); err != nil {
+ if err := vom.NewDecoder(bytes.NewBuffer(encodedInput)).Decode(decoded); err != nil {
return fmt.Errorf("invalid token data: %v", err)
}
}
- return c.verifyMacaroon(m, cookieValue)
-}
-
-// macaroon encapsulates an arbitrary slice of data with an HMAC for integrity protection.
-// Term borrowed from http://research.google.com/pubs/pub41892.html.
-type macaroon struct {
- Data, HMAC []byte
-}
-
-func (c *CSRFCop) createMacaroon(input, hiddenInput []byte) (*macaroon, error) {
- m := &macaroon{Data: input}
- var err error
- if m.HMAC, err = c.hmac(m.Data, hiddenInput); err != nil {
- return nil, err
- }
- return m, nil
-}
-
-func (c *CSRFCop) verifyMacaroon(m macaroon, hiddenInput []byte) error {
- hm, err := c.hmac(m.Data, hiddenInput)
- if err != nil {
- return fmt.Errorf("invalid macaroon: %v", err)
- }
- if !hmac.Equal(m.HMAC, hm) {
- return fmt.Errorf("invalid macaroon, HMAC does not match")
- }
return nil
}
-func (c *CSRFCop) hmac(input, hiddenInput []byte) ([]byte, error) {
- hm := hmac.New(sha256.New, c.key)
- var err error
- // We hash inputs and hiddenInputs to make each a fixed length to avoid
- // ambiguity with simple concatenation of bytes.
- w := func(data []byte) error {
- tmp := sha256.Sum256(data)
- _, err = hm.Write(tmp[:])
- return err
- }
- if err := w(input); err != nil {
- return nil, err
- }
- if err := w(hiddenInput); err != nil {
- return nil, err
- }
- return hm.Sum(nil), nil
-}
-
func (*CSRFCop) MaybeSetCookie(w http.ResponseWriter, req *http.Request, cookieName string) ([]byte, error) {
cookie, err := req.Cookie(cookieName)
switch err {
diff --git a/services/identity/util/csrf_test.go b/services/identity/util/csrf_test.go
index 06e7f17..14ac43c 100644
--- a/services/identity/util/csrf_test.go
+++ b/services/identity/util/csrf_test.go
@@ -16,7 +16,7 @@
r := newRequest()
c, err := NewCSRFCop()
if err != nil {
- t.Fatalf("NewCSRFCop() failed: %v", err)
+ t.Fatalf("NewCSRFCop failed: %v", err)
}
w := httptest.NewRecorder()
tok, err := c.NewToken(w, r, cookieName, nil)
@@ -51,7 +51,7 @@
r := newRequest()
c, err := NewCSRFCop()
if err != nil {
- t.Fatalf("NewCSRFCop() failed: %v", err)
+ t.Fatalf("NewCSRFCop failed: %v", err)
}
w := httptest.NewRecorder()
r.AddCookie(&http.Cookie{Name: cookieName, Value: "u776AC7hf794pTtGVlO50w=="})
@@ -76,7 +76,7 @@
r := newRequest()
c, err := NewCSRFCop()
if err != nil {
- t.Fatalf("NewCSRFCop() failed: %v", err)
+ t.Fatalf("NewCSRFCop failed: %v", err)
}
w := httptest.NewRecorder()
r.AddCookie(&http.Cookie{Name: cookieName, Value: "u776AC7hf794pTtGVlO50w=="})
diff --git a/services/identity/util/macaroon.go b/services/identity/util/macaroon.go
new file mode 100644
index 0000000..f924393
--- /dev/null
+++ b/services/identity/util/macaroon.go
@@ -0,0 +1,39 @@
+package util
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/sha256"
+ "fmt"
+)
+
+// Macaroon encapsulates an arbitrary slice of data with an HMAC for integrity protection.
+// Term borrowed from http://research.google.com/pubs/pub41892.html.
+type Macaroon string
+
+// NewMacaroon creates an opaque token that encodes "data".
+//
+// Input can be extracted from the returned token only if the key provided to NewMacaroon is known.
+func NewMacaroon(key, data []byte) Macaroon {
+ return Macaroon(b64encode(append(data, computeHMAC(key, data)...)))
+}
+
+// Decode returns the input if the macaroon is decodable with the provided key.
+func (m Macaroon) Decode(key []byte) (input []byte, err error) {
+ decoded, err := b64decode(string(m))
+ if err != nil {
+ return nil, err
+ }
+ data := decoded[:len(decoded)-sha256.Size]
+ decodedHMAC := decoded[len(decoded)-sha256.Size:]
+ if !bytes.Equal(decodedHMAC, computeHMAC(key, data)) {
+ return nil, fmt.Errorf("invalid macaroon, HMAC does not match")
+ }
+ return data, nil
+}
+
+func computeHMAC(key, data []byte) []byte {
+ hm := hmac.New(sha256.New, key)
+ hm.Write(data)
+ return hm.Sum(nil)
+}
diff --git a/services/identity/util/macaroon_test.go b/services/identity/util/macaroon_test.go
new file mode 100644
index 0000000..eef90df
--- /dev/null
+++ b/services/identity/util/macaroon_test.go
@@ -0,0 +1,40 @@
+package util
+
+import (
+ "bytes"
+ "crypto/rand"
+ "testing"
+)
+
+func TestMacaroon(t *testing.T) {
+ key := randBytes(t)
+ incorrectKey := randBytes(t)
+ input := randBytes(t)
+
+ m := NewMacaroon(key, input)
+
+ // Test incorrect key.
+ decoded, err := m.Decode(incorrectKey)
+ if err == nil {
+ t.Errorf("m.Decode should have failed")
+ }
+ if decoded != nil {
+ t.Errorf("decoded value should be nil when decode fails")
+ }
+
+ // Test correct key.
+ if decoded, err = m.Decode(key); err != nil {
+ t.Errorf("m.Decode should have succeeded")
+ }
+ if !bytes.Equal(decoded, input) {
+ t.Errorf("decoded value should equal input")
+ }
+}
+
+func randBytes(t *testing.T) []byte {
+ b := make([]byte, 16)
+ if _, err := rand.Read(b); err != nil {
+ t.Fatalf("bytes creation failed: %v", err)
+ }
+ return b
+}
diff --git a/services/wsprd/wspr/wspr_test.go b/services/wsprd/wspr/wspr_test.go
index 83ab796..8981d74 100644
--- a/services/wsprd/wspr/wspr_test.go
+++ b/services/wsprd/wspr/wspr_test.go
@@ -36,11 +36,6 @@
}
// This is never used. Only needed for mock.
-func (m *mockBlesserService) BlessUsingAuthorizationCode(c context.T, authCode, redirect string, co ...ipc.CallOpt) (vdlutil.Any, error) {
- return m.id.PublicID(), nil
-}
-
-// This is never used. Only needed for mock.
func (m *mockBlesserService) GetMethodTags(c context.T, s string, co ...ipc.CallOpt) ([]interface{}, error) {
return nil, nil
}
diff --git a/tools/identity/bless.go b/tools/identity/bless.go
new file mode 100644
index 0000000..05d5546
--- /dev/null
+++ b/tools/identity/bless.go
@@ -0,0 +1,134 @@
+package main
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "fmt"
+ "html/template"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "os/exec"
+ "strings"
+
+ "veyron.io/veyron/veyron/services/identity/googleoauth"
+ "veyron.io/veyron/veyron2/vlog"
+)
+
+func getMacaroonForBlessRPC(blessServerURL string, blessedChan <-chan string) (<-chan string, error) {
+ // Setup a HTTP server to recieve a blessing macaroon from the identity server.
+ // Steps:
+ // 1. Generate a state token to be included in the HTTP request
+ // (though, arguably, the random port assigment for the HTTP server is enough
+ // for XSRF protection)
+ // 2. Setup a HTTP server which will receive the final blessing macaroon from the id server.
+ // 3. Print out the link (to start the auth flow) for the user to click.
+ // 4. Return the macaroon and the rpc object name(where to make the MacaroonBlesser.Bless RPC call)
+ // in the "result" channel.
+ var stateBuf [32]byte
+ if _, err := rand.Read(stateBuf[:]); err != nil {
+ return nil, fmt.Errorf("failed to generate state token for OAuth: %v", err)
+ }
+ state := base64.URLEncoding.EncodeToString(stateBuf[:])
+
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ return nil, fmt.Errorf("failed to setup authorization code interception server: %v", err)
+ }
+ result := make(chan string)
+
+ redirectURL := fmt.Sprintf("http://%s/macaroon", ln.Addr())
+ http.HandleFunc("/macaroon", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ tmplArgs := struct {
+ Blessing, ErrShort, ErrLong string
+ }{}
+ defer func() {
+ if len(tmplArgs.ErrShort) > 0 {
+ w.WriteHeader(http.StatusBadRequest)
+ }
+ if err := tmpl.Execute(w, tmplArgs); err != nil {
+ vlog.Info("Failed to render template:", err)
+ }
+ }()
+
+ toolState := r.FormValue("state")
+ if toolState != state {
+ tmplArgs.ErrShort = "Unexpected request"
+ tmplArgs.ErrLong = "Mismatched state parameter. Possible cross-site-request-forging?"
+ return
+ }
+ result <- r.FormValue("macaroon")
+ result <- r.FormValue("object_name")
+ defer close(result)
+ blessed, ok := <-blessedChan
+ if !ok {
+ tmplArgs.ErrShort = "No blessing received"
+ tmplArgs.ErrLong = "Unable to obtain blessing from the Veyron service"
+ return
+ }
+ tmplArgs.Blessing = blessed
+ ln.Close()
+ })
+ go http.Serve(ln, nil)
+
+ // Print the link to start the flow.
+ url, err := seekBlessingURL(blessServerURL, redirectURL, state)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create seekBlessingURL: %s", err)
+ }
+ fmt.Fprintln(os.Stderr, "Please visit the following URL to complete the blessing creation:")
+ fmt.Fprintln(os.Stderr, url)
+ // Make an attempt to start the browser as a convenience.
+ // If it fails, doesn't matter - the client can see the URL printed above.
+ // Use exec.Command().Start instead of exec.Command().Run since there is no
+ // need to wait for the command to return (and indeed on some window managers,
+ // the command will not exit until the browser is closed).
+ exec.Command(openCommand, url).Start()
+ return result, nil
+}
+
+func seekBlessingURL(blessServerURL, redirectURL, state string) (string, error) {
+ baseURL, err := url.Parse(joinURL(blessServerURL, googleoauth.SeekBlessingsRoute))
+ if err != nil {
+ return "", fmt.Errorf("failed to parse url: %v", err)
+ }
+ params := url.Values{}
+ params.Add("redirect_url", redirectURL)
+ params.Add("state", state)
+ baseURL.RawQuery = params.Encode()
+ return baseURL.String(), nil
+}
+
+func joinURL(baseURL, suffix string) string {
+ if !strings.HasSuffix(baseURL, "/") {
+ baseURL += "/"
+ }
+ return baseURL + suffix
+}
+
+var tmpl = template.Must(template.New("name").Parse(`<!doctype html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Veyron Identity: Google</title>
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
+{{if .Blessing}}
+<!--Attempt to close the window. Though this script does not work on many browser configurations-->
+<script type="text/javascript">window.close();</script>
+{{end}}
+</head>
+<body>
+<div class="container">
+{{if .ErrShort}}
+<h1><span class="label label-danger">error</span>{{.ErrShort}}</h1>
+<div class="well">{{.ErrLong}}</div>
+{{else}}
+<h3>Received blessing: <tt>{{.Blessing}}</tt></h3>
+<div class="well">If the name is prefixed with "unknown/", ignore that. You can close this window, the command line tool has retrieved the blessing</div>
+{{end}}
+</div>
+</body>
+</html>`))
diff --git a/tools/identity/googleoauth.go b/tools/identity/googleoauth.go
deleted file mode 100644
index df07829..0000000
--- a/tools/identity/googleoauth.go
+++ /dev/null
@@ -1,119 +0,0 @@
-package main
-
-import (
- "crypto/rand"
- "encoding/base64"
- "fmt"
- "html/template"
- "net"
- "net/http"
- "os"
- "os/exec"
-
- "veyron.io/veyron/veyron/services/identity/googleoauth"
- "veyron.io/veyron/veyron2/vlog"
-)
-
-func getOAuthAuthorizationCodeFromGoogle(clientID string, blessing <-chan string) (<-chan string, error) {
- // Setup an HTTP server so that OAuth authorization codes can be intercepted.
- // Steps:
- // 1. Generate a state token to be included in the HTTP request
- // (though, arguably, the random port assignment for the HTTP server is
- // enough for XSRF protecetion)
- // 2. Setup an HTTP server which will intercept redirect links from the OAuth
- // flow.
- // 3. Print out the link for the user to click
- // 4. Return the authorization code obtained from the redirect to the "result"
- // channel.
- var stateBuf [32]byte
- if _, err := rand.Read(stateBuf[:]); err != nil {
- return nil, fmt.Errorf("failed to generate state token for OAuth: %v", err)
- }
- state := base64.URLEncoding.EncodeToString(stateBuf[:])
-
- ln, err := net.Listen("tcp", "127.0.0.1:0")
- if err != nil {
- return nil, fmt.Errorf("failed to setup OAuth authorization code interception: %v", err)
- }
- redirectURL := fmt.Sprintf("http://%s", ln.Addr())
- result := make(chan string, 1)
- result <- redirectURL
- http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/html")
- tmplArgs := struct {
- Blessing, ErrShort, ErrLong string
- }{}
- defer func() {
- if len(tmplArgs.ErrShort) > 0 {
- w.WriteHeader(http.StatusBadRequest)
- }
- if err := tmpl.Execute(w, tmplArgs); err != nil {
- vlog.Info("Failed to render template:", err)
- }
- }()
- if urlstate := r.FormValue("state"); urlstate != state {
- tmplArgs.ErrShort = "Unexpected request"
- tmplArgs.ErrLong = "Mismatched state parameter. Possible cross-site-request-forging?"
- return
- }
- code := r.FormValue("code")
- if len(code) == 0 {
- tmplArgs.ErrShort = "No authorization code received"
- tmplArgs.ErrLong = "Expected Google to issue an authorization code using 'code' as a URL parameter in the redirect"
- return
- }
- ln.Close() // No need for the HTTP server anymore
- result <- code
- blessed, ok := <-blessing
- defer close(result)
- if !ok {
- tmplArgs.ErrShort = "No blessing received"
- tmplArgs.ErrLong = "Unable to obtain blessing from the Veyron service"
- return
- }
- tmplArgs.Blessing = blessed
- return
- })
- go http.Serve(ln, nil)
-
- // Print out the link to start the OAuth flow (to STDERR so that STDOUT output contains
- // only the final blessing) and try to open it in the browser as well.
- //
- // TODO(ashankar): Detect devices with limited I/O and then decide to use the device flow
- // instead? See: https://developers.google.com/accounts/docs/OAuth2#device
- url := googleoauth.NewOAuthConfig(clientID, "", redirectURL).AuthCodeURL(state)
- fmt.Fprintln(os.Stderr, "Please visit the following URL to authenticate with Google:")
- fmt.Fprintln(os.Stderr, url)
- // Make an attempt to start the browser as a convenience.
- // If it fails, doesn't matter - the client can see the URL printed above.
- // Use exec.Command().Start instead of exec.Command().Run since there is no
- // need to wait for the command to return (and indeed on some window managers,
- // the command will not exit until the browser is closed).
- exec.Command(openCommand, url).Start()
- return result, nil
-}
-
-var tmpl = template.Must(template.New("name").Parse(`<!doctype html>
-<html>
-<head>
-<meta charset="UTF-8">
-<title>Veyron Identity: Google</title>
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
-{{if .Blessing}}
-<!--Attempt to close the window. Though this script does not work on many browser configurations-->
-<script type="text/javascript">window.close();</script>
-{{end}}
-</head>
-<body>
-<div class="container">
-{{if .ErrShort}}
-<h1><span class="label label-danger">error</span>{{.ErrShort}}</h1>
-<div class="well">{{.ErrLong}}</div>
-{{else}}
-<h3>Received blessing: <tt>{{.Blessing}}</tt></h3>
-<div class="well">If the name is prefixed with "unknown/", ignore that. You can close this window, the command line tool has retrieved the blessing</div>
-{{end}}
-</div>
-</body>
-</html>`))
diff --git a/tools/identity/main.go b/tools/identity/main.go
index 35b5379..3519345 100644
--- a/tools/identity/main.go
+++ b/tools/identity/main.go
@@ -316,12 +316,12 @@
blessedChan := make(chan string)
defer close(blessedChan)
- authcodeChan, err := getOAuthAuthorizationCodeFromGoogle(flagSeekBlessingOAuthClientID, blessedChan)
+ macaroonChan, err := getMacaroonForBlessRPC(flagSeekBlessingFrom, blessedChan)
if err != nil {
return fmt.Errorf("failed to get authorization code from Google: %v", err)
}
- redirectURL := <-authcodeChan
- authcode := <-authcodeChan
+ macaroon := <-macaroonChan
+ service := <-macaroonChan
ctx, cancel := r.NewContext().WithTimeout(time.Minute)
defer cancel()
@@ -330,13 +330,12 @@
const maxWait = 20 * time.Second
var reply vdlutil.Any
for {
- blesser, err := identity.BindOAuthBlesser(flagSeekBlessingFrom, r.Client())
+ blesser, err := identity.BindMacaroonBlesser(service, r.Client())
if err == nil {
-
- reply, err = blesser.BlessUsingAuthorizationCode(ctx, authcode, redirectURL)
+ reply, err = blesser.Bless(ctx, macaroon)
}
if err != nil {
- vlog.Infof("Failed to get blessing from %q: %v, will try again in %v", flagSeekBlessingFrom, err, wait)
+ vlog.Infof("Failed to get blessing from %q: %v, will try again in %v", service, err, wait)
time.Sleep(wait)
if wait = wait + 2*time.Second; wait > maxWait {
wait = maxWait
@@ -348,7 +347,7 @@
return fmt.Errorf("received %T, want security.PublicID", reply)
}
if id, err = id.Derive(blessed); err != nil {
- return fmt.Errorf("received incompatible blessing from %q: %v", flagSeekBlessingFrom, err)
+ return fmt.Errorf("received incompatible blessing from %q: %v", service, err)
}
output, err := util.Base64VomEncode(id)
if err != nil {
@@ -356,8 +355,8 @@
}
fmt.Println(output)
blessedChan <- fmt.Sprint(blessed)
- // Wait for getOAuthAuthenticationCodeFromGoogle to clean up:
- <-authcodeChan
+ // Wait for getTokenForBlessRPC to clean up:
+ <-macaroonChan
return nil
}
},
@@ -369,8 +368,7 @@
cmdBless.Flags.StringVar(&flagBlessWith, "with", "", "Path to file containing identity to bless with (or - for STDIN)")
cmdBless.Flags.DurationVar(&flagBlessFor, "for", 365*24*time.Hour, "Expiry time of blessing (defaults to 1 year)")
cmdSeekBlessing.Flags.StringVar(&flagSeekBlessingFor, "for", "", "Path to file containing identity to bless (or - for STDIN)")
- cmdSeekBlessing.Flags.StringVar(&flagSeekBlessingOAuthClientID, "clientid", "761523829214-4ms7bae18ef47j6590u9ncs19ffuo7b3.apps.googleusercontent.com", "OAuth client ID used to make OAuth request for an authorization code")
- cmdSeekBlessing.Flags.StringVar(&flagSeekBlessingFrom, "from", "/proxy.envyor.com:8101/identity/veyron-test/google", "Object name of Veyron service running the identity.OAuthBlesser service to seek blessings from")
+ cmdSeekBlessing.Flags.StringVar(&flagSeekBlessingFrom, "from", "http://proxy.envyor.com:8125/google", "URL to use to begin the seek blessings process")
(&cmdline.Command{
Name: "identity",