| // 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 oauth implements an http.Handler that has two main purposes |
| // listed below: |
| // |
| // (1) Uses OAuth to authenticate 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 principal tool |
| // located at v.io/x/ref/cmd/principal. |
| // The seek blessing flow works as follows: |
| // (a) Client (principal tool) hits the /seekblessings route. |
| // (b) /seekblessings performs 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. |
| |
| package oauth |
| |
| import ( |
| "encoding/base64" |
| "encoding/json" |
| "fmt" |
| "io/ioutil" |
| "net" |
| "net/http" |
| "net/url" |
| "path" |
| "strings" |
| "time" |
| |
| "v.io/v23" |
| "v.io/v23/context" |
| "v.io/v23/security" |
| "v.io/v23/vom" |
| "v.io/x/ref/services/identity/internal/auditor" |
| "v.io/x/ref/services/identity/internal/caveats" |
| "v.io/x/ref/services/identity/internal/revocation" |
| "v.io/x/ref/services/identity/internal/templates" |
| "v.io/x/ref/services/identity/internal/util" |
| ) |
| |
| const ( |
| clientIDCookie = "VeyronHTTPIdentityClientID" |
| |
| ListBlessingsRoute = "listblessings" |
| listBlessingsCallbackRoute = "listblessingscallback" |
| revokeRoute = "revoke" |
| SeekBlessingsRoute = "seekblessings" |
| addCaveatsRoute = "addcaveats" |
| sendMacaroonRoute = "sendmacaroon" |
| ) |
| |
| type HandlerArgs struct { |
| // The principal to use. |
| Principal security.Principal |
| // 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/ |
| Addr string |
| // BlessingLogReder is needed for reading audit logs. |
| BlessingLogReader auditor.BlessingLogReader |
| // The RevocationManager is used to revoke blessings granted with a revocation caveat. |
| // If nil, then revocation caveats cannot be added to blessings and an expiration caveat |
| // will be used instead. |
| RevocationManager revocation.RevocationManager |
| // The object name of the discharger service. |
| DischargerLocation string |
| // MacaroonBlessingService is a function that returns the object names to which macaroons |
| // created by this HTTP handler can be exchanged for a blessing. |
| MacaroonBlessingService func() []string |
| // OAuthProvider is used to authenticate and get a blessee email. |
| OAuthProvider OAuthProvider |
| // CaveatSelector is used to obtain caveats from the user when seeking a blessing. |
| CaveatSelector caveats.CaveatSelector |
| // AssetsPrefix is the host where web assets for rendering the list blessings template are stored. |
| AssetsPrefix string |
| // DischargeServers is the list of published disharges services. |
| DischargeServers []string |
| } |
| |
| // BlessingMacaroon contains the data that is encoded into the macaroon for creating blessings. |
| type BlessingMacaroon struct { |
| Creation time.Time |
| Caveats []security.Caveat |
| Name string |
| PublicKey []byte // Marshaled public key of the principal tool. |
| } |
| |
| func redirectURL(baseURL, suffix string) string { |
| if !strings.HasSuffix(baseURL, "/") { |
| baseURL += "/" |
| } |
| return baseURL + suffix |
| } |
| |
| // NewHandler returns an http.Handler that expects to be rooted at args.Addr |
| // and can be used to authenticate with args.OAuthProvider, mint a new |
| // identity and bless it with the OAuthProvider email address. |
| func NewHandler(ctx *context.T, args HandlerArgs) http.Handler { |
| csrfCop := util.NewCSRFCop(ctx) |
| return &handler{ |
| args: args, |
| csrfCop: csrfCop, |
| ctx: ctx, |
| } |
| } |
| |
| type handler struct { |
| args HandlerArgs |
| csrfCop *util.CSRFCop |
| ctx *context.T |
| } |
| |
| func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| switch path.Base(r.URL.Path) { |
| case ListBlessingsRoute: |
| h.listBlessings(h.ctx, w, r) |
| case listBlessingsCallbackRoute: |
| h.listBlessingsCallback(h.ctx, w, r) |
| case revokeRoute: |
| h.revoke(h.ctx, w, r) |
| case SeekBlessingsRoute: |
| h.seekBlessings(h.ctx, w, r) |
| case addCaveatsRoute: |
| h.addCaveats(h.ctx, w, r) |
| case sendMacaroonRoute: |
| h.sendMacaroon(h.ctx, w, r) |
| default: |
| util.HTTPBadRequest(w, r, nil) |
| } |
| } |
| |
| func (h *handler) listBlessings(ctx *context.T, w http.ResponseWriter, r *http.Request) { |
| csrf, err := h.csrfCop.NewToken(w, r, clientIDCookie, nil) |
| if err != nil { |
| ctx.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.OAuthProvider.AuthURL(redirectURL(h.args.Addr, listBlessingsCallbackRoute), csrf, ReuseApproval), http.StatusFound) |
| } |
| |
| func (h *handler) listBlessingsCallback(ctx *context.T, w http.ResponseWriter, r *http.Request) { |
| if err := h.csrfCop.ValidateToken(r.FormValue("state"), r, clientIDCookie, nil); err != nil { |
| ctx.Infof("Invalid CSRF token: %v in request: %#v", err, r) |
| util.HTTPBadRequest(w, r, fmt.Errorf("Suspected request forgery: %v", err)) |
| return |
| } |
| email, err := h.args.OAuthProvider.ExchangeAuthCodeForEmail(r.FormValue("code"), redirectURL(h.args.Addr, listBlessingsCallbackRoute)) |
| if err != nil { |
| util.HTTPBadRequest(w, r, err) |
| return |
| } |
| |
| type tmplentry struct { |
| Timestamp time.Time |
| Caveats []string |
| RevocationTime time.Time |
| Blessed security.Blessings |
| Token string |
| Error error |
| } |
| self, _ := h.args.Principal.BlessingStore().Default() |
| tmplargs := struct { |
| Log chan tmplentry |
| Email, RevokeRoute, AssetsPrefix string |
| Self security.Blessings |
| DischargeServers []string |
| }{ |
| Log: make(chan tmplentry), |
| Email: email, |
| RevokeRoute: revokeRoute, |
| AssetsPrefix: h.args.AssetsPrefix, |
| Self: self, |
| DischargeServers: h.args.DischargeServers, |
| } |
| entrych := h.args.BlessingLogReader.Read(ctx, email) |
| |
| w.Header().Set("Context-Type", "text/html") |
| // This MaybeSetCookie call is needed to ensure that a cookie is created. Since the |
| // header cannot be changed once the body is written to, this needs to be called first. |
| if _, err = h.csrfCop.MaybeSetCookie(w, r, clientIDCookie); err != nil { |
| ctx.Infof("Failed to set CSRF cookie[%v] for request %#v", err, r) |
| util.HTTPServerError(w, err) |
| return |
| } |
| go func(ch chan tmplentry) { |
| defer close(ch) |
| for entry := range entrych { |
| tmplEntry := tmplentry{ |
| Error: entry.DecodeError, |
| Timestamp: entry.Timestamp, |
| Blessed: entry.Blessings, |
| } |
| if len(entry.Caveats) > 0 { |
| if tmplEntry.Caveats, err = prettyPrintCaveats(entry.Caveats); err != nil { |
| ctx.Errorf("Failed to pretty print caveats: %v", err) |
| tmplEntry.Error = fmt.Errorf("failed to pretty print caveats: %v", err) |
| } |
| } |
| if len(entry.RevocationCaveatID) > 0 && h.args.RevocationManager != nil { |
| if revocationTime := h.args.RevocationManager.GetRevocationTime(entry.RevocationCaveatID); revocationTime != nil { |
| tmplEntry.RevocationTime = *revocationTime |
| } else { |
| caveatID := base64.URLEncoding.EncodeToString([]byte(entry.RevocationCaveatID)) |
| if tmplEntry.Token, err = h.csrfCop.NewToken(w, r, clientIDCookie, caveatID); err != nil { |
| ctx.Errorf("Failed to create CSRF token[%v] for request %#v", err, r) |
| tmplEntry.Error = fmt.Errorf("server error: unable to create revocation token") |
| } |
| } |
| } |
| ch <- tmplEntry |
| } |
| }(tmplargs.Log) |
| if err := templates.ListBlessings.Execute(w, tmplargs); err != nil { |
| ctx.Errorf("Unable to execute audit page template: %v", err) |
| util.HTTPServerError(w, err) |
| } |
| } |
| |
| // prettyPrintCaveats returns a user friendly string for vanadium standard caveat. |
| // Unrecognized caveats will fall back to the Caveat's String() method. |
| func prettyPrintCaveats(cavs []security.Caveat) ([]string, error) { |
| s := make([]string, len(cavs)) |
| for i, cav := range cavs { |
| if cav.Id == security.PublicKeyThirdPartyCaveat.Id { |
| c := cav.ThirdPartyDetails() |
| s[i] = fmt.Sprintf("ThirdPartyCaveat: Requires discharge from %v (ID=%q)", c.Location(), c.ID()) |
| continue |
| } |
| |
| var param interface{} |
| if err := vom.Decode(cav.ParamVom, ¶m); err != nil { |
| return nil, err |
| } |
| switch cav.Id { |
| case security.ExpiryCaveat.Id: |
| s[i] = fmt.Sprintf("Expires at %v", param) |
| case security.MethodCaveat.Id: |
| s[i] = fmt.Sprintf("Restricted to methods %v", param) |
| case security.PeerBlessingsCaveat.Id: |
| s[i] = fmt.Sprintf("Restricted to peers with blessings %v", param) |
| default: |
| s[i] = cav.String() |
| } |
| } |
| return s, nil |
| } |
| |
| func (h *handler) revoke(ctx *context.T, w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| const ( |
| success = `{"success": "true"}` |
| failure = `{"success": "false"}` |
| ) |
| if h.args.RevocationManager == nil { |
| ctx.Infof("no provided revocation manager") |
| w.Write([]byte(failure)) |
| return |
| } |
| |
| content, err := ioutil.ReadAll(r.Body) |
| if err != nil { |
| ctx.Infof("Failed to parse request: %s", err) |
| w.Write([]byte(failure)) |
| return |
| } |
| var requestParams struct { |
| Token string |
| } |
| if err := json.Unmarshal(content, &requestParams); err != nil { |
| ctx.Infof("json.Unmarshal failed : %s", err) |
| w.Write([]byte(failure)) |
| return |
| } |
| |
| var caveatID string |
| if caveatID, err = h.validateRevocationToken(ctx, requestParams.Token, r); err != nil { |
| ctx.Infof("failed to validate token for caveat: %s", err) |
| w.Write([]byte(failure)) |
| return |
| } |
| if err := h.args.RevocationManager.Revoke(caveatID); err != nil { |
| ctx.Infof("Revocation failed: %s", err) |
| w.Write([]byte(failure)) |
| return |
| } |
| |
| w.Write([]byte(success)) |
| return |
| } |
| |
| func (h *handler) validateRevocationToken(ctx *context.T, Token string, r *http.Request) (string, error) { |
| var encCaveatID string |
| if err := h.csrfCop.ValidateToken(Token, r, clientIDCookie, &encCaveatID); err != nil { |
| return "", fmt.Errorf("invalid CSRF token: %v in request: %#v", err, r) |
| } |
| caveatID, err := base64.URLEncoding.DecodeString(encCaveatID) |
| if err != nil { |
| return "", fmt.Errorf("decode caveatID failed: %v", err) |
| } |
| return string(caveatID), nil |
| } |
| |
| type seekBlessingsMacaroon struct { |
| RedirectURL, State string |
| PublicKey []byte // Marshaled public key of the principal tool. |
| } |
| |
| 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(ctx *context.T, w http.ResponseWriter, r *http.Request) { |
| redirect := r.FormValue("redirect_url") |
| if _, err := validLoopbackURL(redirect); err != nil { |
| ctx.Infof("seekBlessings failed: invalid redirect_url: %v", err) |
| util.HTTPBadRequest(w, r, fmt.Errorf("invalid redirect_url: %v", err)) |
| return |
| } |
| pubKeyBytes, err := base64.URLEncoding.DecodeString(r.FormValue("public_key")) |
| if err != nil { |
| ctx.Infof("seekBlessings failed: invalid public_key: %v", err) |
| util.HTTPBadRequest(w, r, fmt.Errorf("invalid public_key: %v", err)) |
| return |
| } |
| outputMacaroon, err := h.csrfCop.NewToken(w, r, clientIDCookie, seekBlessingsMacaroon{ |
| RedirectURL: redirect, |
| State: r.FormValue("state"), |
| PublicKey: pubKeyBytes, |
| }) |
| if err != nil { |
| ctx.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.OAuthProvider.AuthURL(redirectURL(h.args.Addr, addCaveatsRoute), outputMacaroon, ExplicitApproval), http.StatusFound) |
| } |
| |
| type addCaveatsMacaroon struct { |
| ToolRedirectURL, ToolState, Email string |
| ToolPublicKey []byte // Marshaled public key of the principal tool. |
| } |
| |
| func (h *handler) addCaveats(ctx *context.T, 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 := h.args.OAuthProvider.ExchangeAuthCodeForEmail(r.FormValue("code"), redirectURL(h.args.Addr, addCaveatsRoute)) |
| if err != nil { |
| util.HTTPBadRequest(w, r, err) |
| return |
| } |
| outputMacaroon, err := h.csrfCop.NewToken(w, r, clientIDCookie, addCaveatsMacaroon{ |
| ToolRedirectURL: inputMacaroon.RedirectURL, |
| ToolState: inputMacaroon.State, |
| ToolPublicKey: inputMacaroon.PublicKey, |
| Email: email, |
| }) |
| if err != nil { |
| ctx.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 |
| } |
| localBlessings := security.DefaultBlessingPatterns(h.args.Principal) |
| if len(localBlessings) == 0 { |
| ctx.Infof("server principal has no blessings: %v", h.args.Principal) |
| util.HTTPServerError(w, fmt.Errorf("failed to get server blessings")) |
| return |
| } |
| fullBlessingName := strings.Join([]string{string(localBlessings[0]), email}, security.ChainSeparator) |
| if err := h.args.CaveatSelector.Render(fullBlessingName, outputMacaroon, redirectURL(h.args.Addr, sendMacaroonRoute), w, r); err != nil { |
| ctx.Errorf("Unable to invoke render caveat selector: %v", err) |
| util.HTTPServerError(w, err) |
| } |
| } |
| |
| func (h *handler) sendMacaroon(ctx *context.T, w http.ResponseWriter, r *http.Request) { |
| var inputMacaroon addCaveatsMacaroon |
| caveatInfos, macaroonString, blessingExtension, err := h.args.CaveatSelector.ParseSelections(r) |
| cancelled := err == caveats.ErrSeekblessingsCancelled |
| if !cancelled && err != nil { |
| util.HTTPBadRequest(w, r, fmt.Errorf("failed to parse blessing information: %v", err)) |
| return |
| } |
| if err := h.csrfCop.ValidateToken(macaroonString, r, clientIDCookie, &inputMacaroon); err != nil { |
| util.HTTPBadRequest(w, r, fmt.Errorf("suspected request forgery: %v", 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: %v", err)) |
| return |
| } |
| // Now that we have a valid tool redirect url, we can send the errors to the tool. |
| if cancelled { |
| h.sendErrorToTool(ctx, w, r, inputMacaroon.ToolState, baseURL, caveats.ErrSeekblessingsCancelled) |
| } |
| caveats, err := h.caveats(ctx, caveatInfos) |
| if err != nil { |
| h.sendErrorToTool(ctx, w, r, inputMacaroon.ToolState, baseURL, fmt.Errorf("failed to create caveats: %v", err)) |
| return |
| } |
| parts := []string{inputMacaroon.Email} |
| if len(blessingExtension) > 0 { |
| parts = append(parts, blessingExtension) |
| } |
| if len(caveats) == 0 { |
| h.sendErrorToTool(ctx, w, r, inputMacaroon.ToolState, baseURL, fmt.Errorf("server disallows attempts to bless with no caveats")) |
| return |
| } |
| m := BlessingMacaroon{ |
| Creation: time.Now(), |
| Caveats: caveats, |
| Name: strings.Join(parts, security.ChainSeparator), |
| PublicKey: inputMacaroon.ToolPublicKey, |
| } |
| macBytes, err := vom.Encode(m) |
| if err != nil { |
| h.sendErrorToTool(ctx, w, r, inputMacaroon.ToolState, baseURL, fmt.Errorf("failed to encode BlessingsMacaroon: %v", err)) |
| return |
| } |
| marshalKey, err := h.args.Principal.PublicKey().MarshalBinary() |
| if err != nil { |
| h.sendErrorToTool(ctx, w, r, inputMacaroon.ToolState, baseURL, fmt.Errorf("failed to marshal public key: %v", err)) |
| return |
| } |
| encKey := base64.URLEncoding.EncodeToString(marshalKey) |
| objectNames := h.args.MacaroonBlessingService() |
| if len(objectNames) == 0 { |
| h.sendErrorToTool(ctx, w, r, inputMacaroon.ToolState, baseURL, fmt.Errorf("failed to get local server endpoints")) |
| return |
| } |
| params := url.Values{} |
| mac, err := util.NewMacaroon(v23.GetPrincipal(ctx), macBytes) |
| if err != nil { |
| h.sendErrorToTool(ctx, w, r, inputMacaroon.ToolState, baseURL, err) |
| return |
| } |
| params.Add("macaroon", string(mac)) |
| params.Add("state", inputMacaroon.ToolState) |
| for _, s := range objectNames { |
| params.Add("object_name", s) |
| } |
| params.Add("root_key", encKey) |
| baseURL.RawQuery = params.Encode() |
| http.Redirect(w, r, baseURL.String(), http.StatusFound) |
| } |
| |
| func (h *handler) sendErrorToTool(ctx *context.T, w http.ResponseWriter, r *http.Request, toolState string, baseURL *url.URL, err error) { |
| errEnc := base64.URLEncoding.EncodeToString([]byte(err.Error())) |
| params := url.Values{} |
| params.Add("error", errEnc) |
| params.Add("state", toolState) |
| baseURL.RawQuery = params.Encode() |
| http.Redirect(w, r, baseURL.String(), http.StatusFound) |
| } |
| |
| func (h *handler) caveats(ctx *context.T, caveatInfos []caveats.CaveatInfo) (cavs []security.Caveat, err error) { |
| caveatFactories := caveats.NewCaveatFactory() |
| for _, caveatInfo := range caveatInfos { |
| if caveatInfo.Type == "Revocation" { |
| caveatInfo.Args = []interface{}{h.args.RevocationManager, h.args.Principal.PublicKey(), h.args.DischargerLocation} |
| } |
| cav, err := caveatFactories.New(caveatInfo) |
| if err != nil { |
| return nil, err |
| } |
| cavs = append(cavs, cav) |
| } |
| return |
| } |