From 68ff8a20db645338d2358aee2d4905696de9c766 Mon Sep 17 00:00:00 2001 From: mpl Date: Thu, 15 May 2014 01:09:20 +0200 Subject: [PATCH] importer: let the implementations build and parse the callback URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Credits to Tamás Gulácsi for finding out about the state parameter. Change-Id: I139eb4f74e607278c9aee93fd40c25ae081d47e3 --- pkg/importer/dummy/dummy.go | 14 ++++ pkg/importer/foursquare/foursquare.go | 19 ++++-- pkg/importer/importer.go | 24 +++++-- pkg/importer/noop.go | 4 +- pkg/importer/oauth.go | 95 +++++++++++++++++++++++++++ pkg/importer/twitter/twitter.go | 4 +- 6 files changed, 147 insertions(+), 13 deletions(-) create mode 100644 pkg/importer/oauth.go diff --git a/pkg/importer/dummy/dummy.go b/pkg/importer/dummy/dummy.go index 3d76e1b1c..1cb49c26b 100644 --- a/pkg/importer/dummy/dummy.go +++ b/pkg/importer/dummy/dummy.go @@ -100,6 +100,7 @@ func (*imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.Set // to an importer, or when an account is being re-logged into to // refresh its access token. // You typically start the OAuth redirect flow here. + // The importer.OAuth2.RedirectURL and importer.OAuth2.RedirectState helpers can be used for OAuth2. http.Redirect(w, r, ctx.CallbackURL(), http.StatusFound) return nil } @@ -173,3 +174,16 @@ func (im *imp) Run(ctx *importer.RunContext) (err error) { func (im *imp) ServeHTTP(w http.ResponseWriter, r *http.Request) { httputil.BadRequestError(w, "Unexpected path: %s", r.URL.Path) } + +func (im *imp) CallbackRequestAccount(r *http.Request) (blob.Ref, error) { + // We do not actually use OAuth, but this method works for us anyway. + // Even if your importer implementation does not use OAuth, you can + // probably just embed importer.OAuth1 in your implementation type. + // If OAuth2, embedding importer.OAuth2 should work. + return importer.OAuth1{}.CallbackRequestAccount(r) +} + +func (im *imp) CallbackURLParameters(acctRef blob.Ref) string { + // See comment in CallbackRequestAccount. + return importer.OAuth1{}.CallbackURLParameters(acctRef) +} diff --git a/pkg/importer/foursquare/foursquare.go b/pkg/importer/foursquare/foursquare.go index 858114210..59b281d04 100644 --- a/pkg/importer/foursquare/foursquare.go +++ b/pkg/importer/foursquare/foursquare.go @@ -70,6 +70,8 @@ var _ importer.ImporterSetupHTMLer = (*imp)(nil) type imp struct { mu sync.Mutex // guards following imageFileRef map[string]blob.Ref // url to file schema blob + + importer.OAuth2 // for CallbackRequestAccount and CallbackURLParameters } func (im *imp) NeedsAPIKey() bool { return true } @@ -432,6 +434,7 @@ func doGet(ctx *context.Context, url string, form url.Values) (*http.Response, e return res, nil } +// auth returns a new oauth.Config func auth(ctx *importer.SetupContext) (*oauth.Config, error) { clientId, secret, err := ctx.Credentials() if err != nil { @@ -444,18 +447,20 @@ func auth(ctx *importer.SetupContext) (*oauth.Config, error) { TokenURL: tokenURL, RedirectURL: ctx.CallbackURL(), }, nil - } -// possibly common methods for accessing oauth2 sites - func (im *imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) error { oauthConfig, err := auth(ctx) - if err == nil { - state := "no_clue_what_this_is" // TODO: ask adg to document this. or send him a CL. - http.Redirect(w, r, oauthConfig.AuthCodeURL(state), 302) + if err != nil { + return err } - return err + oauthConfig.RedirectURL = im.RedirectURL(im, ctx) + state, err := im.RedirectState(im, ctx) + if err != nil { + return err + } + http.Redirect(w, r, oauthConfig.AuthCodeURL(state), http.StatusFound) + return nil } func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) { diff --git a/pkg/importer/importer.go b/pkg/importer/importer.go index c376a77ae..91f4a1077 100644 --- a/pkg/importer/importer.go +++ b/pkg/importer/importer.go @@ -76,6 +76,17 @@ type Importer interface { ServeSetup(w http.ResponseWriter, r *http.Request, ctx *SetupContext) error ServeCallback(w http.ResponseWriter, r *http.Request, ctx *SetupContext) + + // CallbackRequestAccount extracts the blobref of the importer account from + // the callback URL parameters of r. For example, it will be encoded as: + // For Twitter (OAuth1), in its own URL parameter: "acct=sha1-f2b0b7da718b97ce8c31591d8ed4645c777f3ef4" + // For Picasa: (OAuth2), in the OAuth2 "state" parameter: "state=acct:sha1-97911b1a5887eb5862d1c81666ba839fc1363ea1" + CallbackRequestAccount(r *http.Request) (acctRef blob.Ref, err error) + + // CallbackURLParameters uses the input importer account blobRef to build + // and return the URL parameters string (including the prefixed "?"), that + // will be appended to the callback URL. + CallbackURLParameters(acctRef blob.Ref) string } // ImporterSetupHTMLer is an optional interface that may be implemented by @@ -177,7 +188,8 @@ func (sc *SetupContext) Credentials() (clientID, clientSecret string, err error) } func (sc *SetupContext) CallbackURL() string { - return sc.Host.ImporterBaseURL() + sc.ia.im.name + "/callback?acct=" + sc.AccountNode.PermanodeRef().String() + return sc.Host.ImporterBaseURL() + sc.ia.im.name + "/callback" + + sc.ia.im.impl.CallbackURLParameters(sc.AccountNode.PermanodeRef()) } // AccountURL returns the URL to an account of an importer @@ -354,9 +366,13 @@ func (h *Host) serveImporterAcctCallback(w http.ResponseWriter, r *http.Request, http.Error(w, "invalid method", 400) return } - acctRef, ok := blob.Parse(r.FormValue("acct")) - if !ok { - http.Error(w, "missing 'acct' blobref param", 400) + acctRef, err := imp.impl.CallbackRequestAccount(r) + if err != nil { + httputil.ServeError(w, r, err) + return + } + if !acctRef.Valid() { + httputil.ServeError(w, r, errors.New("No valid blobref returned from CallbackRequestAccount(r)")) return } ia, err := imp.account(acctRef) diff --git a/pkg/importer/noop.go b/pkg/importer/noop.go index f16627b0f..fa0b9d6eb 100644 --- a/pkg/importer/noop.go +++ b/pkg/importer/noop.go @@ -24,7 +24,9 @@ import ( var TODOImporter Importer = todoImp{} -type todoImp struct{} +type todoImp struct { + OAuth1 // for CallbackRequestAccount and CallbackURLParameters +} func (todoImp) NeedsAPIKey() bool { return false } diff --git a/pkg/importer/oauth.go b/pkg/importer/oauth.go new file mode 100644 index 000000000..00f5fed8b --- /dev/null +++ b/pkg/importer/oauth.go @@ -0,0 +1,95 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package importer + +import ( + "errors" + "fmt" + "log" + "net/http" + "net/url" + "strings" + + "camlistore.org/pkg/blob" +) + +// OAuth1 provides methods that the importer implementations can use to +// help with OAuth authentication. +type OAuth1 struct{} + +func (OAuth1) CallbackRequestAccount(r *http.Request) (blob.Ref, error) { + acctRef, ok := blob.Parse(r.FormValue("acct")) + if !ok { + return blob.Ref{}, errors.New("missing 'acct=' blobref param") + } + return acctRef, nil +} + +func (OAuth1) CallbackURLParameters(acctRef blob.Ref) string { + return "?acct=" + acctRef.String() +} + +// OAuth2 provides methods that the importer implementations can use to +// help with OAuth2 authentication. +type OAuth2 struct{} + +func (OAuth2) CallbackRequestAccount(r *http.Request) (blob.Ref, error) { + state := r.FormValue("state") + if state == "" { + return blob.Ref{}, errors.New("missing 'state' parameter") + } + if !strings.HasPrefix(state, "acct:") { + return blob.Ref{}, errors.New("wrong 'state' parameter value, missing 'acct:' prefix.") + } + acctRef, ok := blob.Parse(strings.TrimPrefix(state, "acct:")) + if !ok { + return blob.Ref{}, errors.New("invalid account blobref in 'state' parameter") + } + return acctRef, nil +} + +func (OAuth2) CallbackURLParameters(acctRef blob.Ref) string { + return "?state=acct:" + acctRef.String() +} + +// RedirectURL returns the redirect URI that imp should set in an oauth.Config +// for the authorization phase of OAuth2 authentication. +func (OAuth2) RedirectURL(imp Importer, ctx *SetupContext) string { + // We strip our callback URL of its query component, because the Redirect URI + // we send during authorization has to match exactly the registered redirect + // URI(s). This query component should be stored in the "state" paremeter instead. + // See http://tools.ietf.org/html/rfc6749#section-3.1.2.2 + fullCallback := ctx.CallbackURL() + queryPart := imp.CallbackURLParameters(ctx.AccountNode.PermanodeRef()) + log.Printf("WARNING: callback URL %q has no query component", fullCallback) + return strings.TrimSuffix(fullCallback, queryPart) +} + +// RedirectState returns the "state" query parameter that should be used for the authorization +// phase of OAuth2 authentication. This parameter contains the query component of the redirection +// URI. See http://tools.ietf.org/html/rfc6749#section-3.1.2.2 +func (OAuth2) RedirectState(imp Importer, ctx *SetupContext) (state string, err error) { + m, err := url.ParseQuery(strings.TrimPrefix(imp.CallbackURLParameters(ctx.AccountNode.PermanodeRef()), "?")) + if err != nil { + return "", fmt.Errorf("could not parse callback parameters string as a query: %q", imp.CallbackURLParameters(ctx.AccountNode.PermanodeRef())) + } + state = m.Get("state") + if state == "" { + return "", errors.New("\"state\" not found in callback parameters") + } + return state, nil +} diff --git a/pkg/importer/twitter/twitter.go b/pkg/importer/twitter/twitter.go index 245d0d60b..2a40a22c8 100644 --- a/pkg/importer/twitter/twitter.go +++ b/pkg/importer/twitter/twitter.go @@ -61,7 +61,9 @@ func init() { var _ importer.ImporterSetupHTMLer = (*imp)(nil) -type imp struct{} +type imp struct { + importer.OAuth1 // for CallbackRequestAccount and CallbackURLParameters +} func (im *imp) NeedsAPIKey() bool { return true }