Merge "importer: let the implementations build and parse the callback URL"

This commit is contained in:
mpl 2014-05-16 22:55:07 +00:00 committed by Gerrit Code Review
commit 50afc86843
6 changed files with 147 additions and 13 deletions

View File

@ -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)
}

View File

@ -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) {

View File

@ -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)

View File

@ -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 }

95
pkg/importer/oauth.go Normal file
View File

@ -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
}

View File

@ -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 }