diff --git a/example/main.go b/example/main.go index 1a2751e..2dbba2d 100644 --- a/example/main.go +++ b/example/main.go @@ -12,7 +12,6 @@ import ( "github.com/drone/go-login/login" "github.com/drone/go-login/login/bitbucket" - "github.com/drone/go-login/login/gitea" "github.com/drone/go-login/login/github" "github.com/drone/go-login/login/gitlab" "github.com/drone/go-login/login/gogs" @@ -42,54 +41,51 @@ func main() { var auther http.Handler switch *provider { - case "gogs": - auther = gogs.New( - http.HandlerFunc(details), - gogs.WithAddress(*providerURL), + case "gogs", "gitea": + auther = gogs.New(*providerURL, gogs.WithLoginRedirect("/login/form"), - ) - case "gitea": - auther = gitea.New( + ).Authorize( http.HandlerFunc(details), - gitea.WithAddress(*providerURL), - gitea.WithLoginRedirect("/login/form"), ) case "gitlab": auther = gitlab.New( - http.HandlerFunc(details), gitlab.WithClientID(*clientID), gitlab.WithClientSecret(*clientSecret), gitlab.WithRedirectURL(*redirectURL), gitlab.WithScope("read_user", "api"), + ).Authorize( + http.HandlerFunc(details), ) case "github": auther = github.New( - http.HandlerFunc(details), github.WithClientID(*clientID), github.WithClientSecret(*clientSecret), github.WithScope("repo", "user", "read:org"), + ).Authorize( + http.HandlerFunc(details), ) case "bitbucket": auther = bitbucket.New( - http.HandlerFunc(details), bitbucket.WithClientID(*clientID), bitbucket.WithClientSecret(*clientSecret), bitbucket.WithRedirectURL(*redirectURL), + ).Authorize( + http.HandlerFunc(details), ) case "stash": - auther = stash.New( - http.HandlerFunc(details), + auther = stash.New(*providerURL, stash.WithConsumerKey(*consumerKey), stash.WithPrivateKeyFile(*consumerRsa), stash.WithCallbackURL(*redirectURL), - stash.WithAddress(*providerURL), + ).Authorize( + http.HandlerFunc(details), ) } // handles the authorization flow and displays the // authorization results at completion. - http.Handle("/login", auther) http.Handle("/login/form", http.HandlerFunc(form)) + http.Handle("/login", auther) // redirects the user to the login handler. http.Handle("/", http.RedirectHandler("/login", http.StatusSeeOther)) diff --git a/login/bitbucket/bitbucket.go b/login/bitbucket/bitbucket.go index 56a44da..243664e 100644 --- a/login/bitbucket/bitbucket.go +++ b/login/bitbucket/bitbucket.go @@ -7,72 +7,84 @@ package bitbucket import ( "net/http" + "github.com/drone/go-login/login" "github.com/drone/go-login/login/internal/oauth2" ) -// Options provides the Bitbucket authentication options. -type Options struct { - redirectURL string - clientID string - clientSecret string - client *http.Client -} - -func createOptions() *Options { - return &Options{ - client: http.DefaultClient, - } -} +const ( + accessTokenURL = "https://bitbucket.org/site/oauth2/access_token" + authorizationURL = "https://bitbucket.org/site/oauth2/authorize" +) // Option configures an authorization handler option. -type Option func(o *Options) +type Option func(a *Authorizer) // WithClient configures the authorization handler with a // custom http.Client. func WithClient(client *http.Client) Option { - return func(o *Options) { - o.client = client + return func(a *Authorizer) { + a.client = client } } // WithClientID configures the authorization handler with // the client_id. func WithClientID(clientID string) Option { - return func(o *Options) { - o.clientID = clientID + return func(a *Authorizer) { + a.clientID = clientID } } // WithClientSecret configures the authorization handler // with the client_secret. func WithClientSecret(clientSecret string) Option { - return func(o *Options) { - o.clientSecret = clientSecret + return func(a *Authorizer) { + a.clientSecret = clientSecret } } // WithRedirectURL configures the authorization handler // with the redirect_url func WithRedirectURL(redirectURL string) Option { - return func(o *Options) { - o.redirectURL = redirectURL + return func(a *Authorizer) { + a.redirectURL = redirectURL + } +} + +// Authorizer configures a Bitbucket auth provider. +type Authorizer struct { + redirectURL string + clientID string + clientSecret string + client *http.Client +} + +func newDefault() *Authorizer { + return &Authorizer{ + client: http.DefaultClient, } } -// New returns a http.Handler that runs h at the completion -// of the Bitbucket authorization flow. The Bitbucket -// authorization is passed to h in the http.Request context. -func New(h http.Handler, opt ...Option) http.Handler { - opts := createOptions() - for _, fn := range opt { - fn(opts) +// New returns a Bitbucket authorization provider. +func New(opts ...Option) login.Authorizer { + auther := newDefault() + for _, opt := range opts { + opt(auther) } + return auther +} + +// Authorize returns a http.Handler that runs h at the +// completion of the GitHub authorization flow. The GitHub +// authorization details are available to h in the +// http.Request context. +func (a *Authorizer) Authorize(h http.Handler) http.Handler { return oauth2.Handler(h, &oauth2.Config{ - Client: opts.client, - ClientID: opts.clientID, - ClientSecret: opts.clientSecret, - RedirectURL: opts.redirectURL, - AccessTokenURL: "https://bitbucket.org/site/oauth2/access_token", - AuthorizationURL: "https://bitbucket.org/site/oauth2/authorize", + Client: a.client, + ClientID: a.clientID, + ClientSecret: a.clientSecret, + RedirectURL: a.redirectURL, + AccessTokenURL: accessTokenURL, + AuthorizationURL: authorizationURL, }) } diff --git a/login/bitbucket/bitbucket_test.go b/login/bitbucket/bitbucket_test.go index da3d1fc..c0e0d61 100644 --- a/login/bitbucket/bitbucket_test.go +++ b/login/bitbucket/bitbucket_test.go @@ -11,40 +11,40 @@ import ( func TestWithClient(t *testing.T) { c := &http.Client{} - o := &Options{} - WithClient(c)(o) - if got, want := o.client, c; got != want { + a := &Authorizer{} + WithClient(c)(a) + if got, want := a.client, c; got != want { t.Errorf("Expect custom client") } } func TestWithClientID(t *testing.T) { - opts := &Options{} - WithClientID("3da54155991")(opts) - if got, want := opts.clientID, "3da54155991"; got != want { + a := &Authorizer{} + WithClientID("3da54155991")(a) + if got, want := a.clientID, "3da54155991"; got != want { t.Errorf("Expect custom client_id") } } func TestWithClientSecret(t *testing.T) { - opts := &Options{} - WithClientSecret("5012f6c60b2")(opts) - if got, want := opts.clientSecret, "5012f6c60b2"; got != want { + a := &Authorizer{} + WithClientSecret("5012f6c60b2")(a) + if got, want := a.clientSecret, "5012f6c60b2"; got != want { t.Errorf("Expect custom client_secret") } } func TestWithRedirectURL(t *testing.T) { - opts := &Options{} - WithRedirectURL("http://company.com/login")(opts) - if got, want := opts.redirectURL, "http://company.com/login"; got != want { + a := &Authorizer{} + WithRedirectURL("http://company.com/login")(a) + if got, want := a.redirectURL, "http://company.com/login"; got != want { t.Errorf("Expect custom redirect_uri") } } -func TestDefaultOptions(t *testing.T) { - opts := createOptions() - if got, want := opts.client, http.DefaultClient; got != want { +func TestDefaultAuthorizer(t *testing.T) { + a := newDefault() + if got, want := a.client, http.DefaultClient; got != want { t.Errorf("Expect default client is http.DefaultClient") } } diff --git a/login/gitea/option.go b/login/gitea/option.go deleted file mode 100644 index cfbe05e..0000000 --- a/login/gitea/option.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2017 Drone.IO Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package gitea - -import ( - "net/http" - "strings" -) - -// Options provides the Gogs authentication options. -type Options struct { - label string - login string - server string - client *http.Client -} - -func defaultOptions() *Options { - return &Options{ - label: "default", - client: http.DefaultClient, - } -} - -// Option configures an authorization handler option. -type Option func(o *Options) - -// WithAddress configures the authorization handler with -// the progived Gogs server address. -func WithAddress(server string) Option { - return func(o *Options) { - o.server = strings.TrimPrefix(server, "/") - } -} - -// WithClient configures the authorization handler with a -// custom http.Client. -func WithClient(client *http.Client) Option { - return func(o *Options) { - o.client = client - } -} - -// WithTokenName configures the authorization handler to -// use the specificed token name when finding and creating -// authorization tokens. -func WithTokenName(name string) Option { - return func(o *Options) { - o.label = name - } -} - -// WithLoginRedirect configures the authorization handler -// to redirect the http.Request to the login form when the -// username or password are missing from the Form data. -func WithLoginRedirect(path string) Option { - return func(o *Options) { - o.login = path - } -} diff --git a/login/gitea/option_test.go b/login/gitea/option_test.go deleted file mode 100644 index 0d0caa4..0000000 --- a/login/gitea/option_test.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2017 Drone.IO Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package gitea - -import ( - "net/http" - "testing" -) - -func TestWithAddress(t *testing.T) { - h := New(nil, WithAddress("https://try.gitea.io")) - if got, want := h.(*handler).server, "https://try.gitea.io"; got != want { - t.Errorf("Expect server address %q, got %q", want, got) - } -} - -func TestWithClient(t *testing.T) { - c := &http.Client{} - h := New(nil, WithClient(c)) - if got, want := h.(*handler).client, c; got != want { - t.Errorf("Expect custom client") - } -} - -func TestWithTokenName(t *testing.T) { - h := New(nil, WithTokenName("some_token")) - if got, want := h.(*handler).label, "some_token"; got != want { - t.Errorf("Expect token name url %q, got %q", want, got) - } -} - -func TestWithLoginRedirect(t *testing.T) { - h := New(nil, WithLoginRedirect("/path/to/login")) - if got, want := h.(*handler).login, "/path/to/login"; got != want { - t.Errorf("Expect login redirect url %q, got %q", want, got) - } -} diff --git a/login/github/github.go b/login/github/github.go index c0a7eca..82f36b7 100644 --- a/login/github/github.go +++ b/login/github/github.go @@ -8,11 +8,13 @@ import ( "net/http" "strings" + "github.com/drone/go-login/login" "github.com/drone/go-login/login/internal/oauth2" ) -// Options provides the GitHub authentication options. -type Options struct { +// Authorizer configures a GitHub authorization +// provider. +type Authorizer struct { scope []string clientID string clientSecret string @@ -20,20 +22,20 @@ type Options struct { client *http.Client } -func defaultOptions() *Options { - return &Options{ +func newDefault() *Authorizer { + return &Authorizer{ server: "https://github.com", client: http.DefaultClient, } } // Option configures an authorization handler option. -type Option func(o *Options) +type Option func(a *Authorizer) // WithClient configures the authorization handler with a // custom http.Client. func WithClient(client *http.Client) Option { - return func(o *Options) { + return func(o *Authorizer) { o.client = client } } @@ -41,7 +43,7 @@ func WithClient(client *http.Client) Option { // WithClientID configures the authorization handler with // the client_id. func WithClientID(clientID string) Option { - return func(o *Options) { + return func(o *Authorizer) { o.clientID = clientID } } @@ -49,7 +51,7 @@ func WithClientID(clientID string) Option { // WithClientSecret configures the authorization handler // with the client_secret. func WithClientSecret(clientSecret string) Option { - return func(o *Options) { + return func(o *Authorizer) { o.clientSecret = clientSecret } } @@ -57,7 +59,7 @@ func WithClientSecret(clientSecret string) Option { // WithScope configures the authorization handler with the // these scopes. func WithScope(scope ...string) Option { - return func(o *Options) { + return func(o *Authorizer) { o.scope = scope } } @@ -65,26 +67,32 @@ func WithScope(scope ...string) Option { // WithAddress configures the authorization handler with // a GitHub enterprise server address. func WithAddress(server string) Option { - return func(o *Options) { + return func(o *Authorizer) { o.server = strings.TrimSuffix(server, "/") } } -// New returns a http.Handler that runs h at the completion -// of the GitHub authorization flow. The GitHub authorization -// is availabe to h in the http.Request context. -func New(h http.Handler, opt ...Option) http.Handler { - opts := defaultOptions() - for _, fn := range opt { - fn(opts) +// New returns a GitHub authorization provider. +func New(opts ...Option) login.Authorizer { + v := newDefault() + for _, opt := range opts { + opt(v) } + return v +} + +// Authorize returns a http.Handler that runs h at the +// completion of the GitHub authorization flow. The GitHub +// authorization details are available to h in the +// http.Request context. +func (a *Authorizer) Authorize(h http.Handler) http.Handler { return oauth2.Handler(h, &oauth2.Config{ BasicAuthOff: true, - Client: opts.client, - ClientID: opts.clientID, - ClientSecret: opts.clientSecret, - AccessTokenURL: opts.server + "/login/oauth/access_token", - AuthorizationURL: opts.server + "/login/oauth/authorize", - Scope: opts.scope, + Client: a.client, + ClientID: a.clientID, + ClientSecret: a.clientSecret, + AccessTokenURL: a.server + "/login/oauth/access_token", + AuthorizationURL: a.server + "/login/oauth/authorize", + Scope: a.scope, }) } diff --git a/login/github/github_test.go b/login/github/github_test.go index c2ecbfb..7b34308 100644 --- a/login/github/github_test.go +++ b/login/github/github_test.go @@ -13,54 +13,54 @@ import ( func TestWithClient(t *testing.T) { c := &http.Client{} - o := &Options{} - WithClient(c)(o) - if got, want := o.client, c; got != want { + v := &Authorizer{} + WithClient(c)(v) + if got, want := v.client, c; got != want { t.Errorf("Expect custom client option applied") } } func TestWithClientID(t *testing.T) { - opts := &Options{} - WithClientID("3da54155991")(opts) - if got, want := opts.clientID, "3da54155991"; got != want { + v := &Authorizer{} + WithClientID("3da54155991")(v) + if got, want := v.clientID, "3da54155991"; got != want { t.Errorf("Expect client_id option applied") } } func TestWithClientSecret(t *testing.T) { - opts := &Options{} - WithClientSecret("5012f6c60b2")(opts) - if got, want := opts.clientSecret, "5012f6c60b2"; got != want { + v := &Authorizer{} + WithClientSecret("5012f6c60b2")(v) + if got, want := v.clientSecret, "5012f6c60b2"; got != want { t.Errorf("Expect client_secret option applied") } } func TestWithAddress(t *testing.T) { - opts := &Options{} - WithAddress("https://company.github.com/")(opts) - if strings.HasSuffix(opts.server, "/") { + v := &Authorizer{} + WithAddress("https://company.github.com/")(v) + if strings.HasSuffix(v.server, "/") { t.Errorf("Expect trailing slash removed from server address") } - if got, want := opts.server, "https://company.github.com"; got != want { + if got, want := v.server, "https://company.github.com"; got != want { t.Errorf("Expect server address option applied") } } func TestWithScope(t *testing.T) { - opts := &Options{} - WithScope("user", "repo")(opts) - if got, want := opts.scope, []string{"user", "repo"}; !reflect.DeepEqual(got, want) { + v := &Authorizer{} + WithScope("user", "repo")(v) + if got, want := v.scope, []string{"user", "repo"}; !reflect.DeepEqual(got, want) { t.Errorf("Expect scope option applied") } } -func TestDefaultOptions(t *testing.T) { - opts := defaultOptions() - if got, want := opts.client, http.DefaultClient; got != want { +func TestDefaultAuthorizer(t *testing.T) { + v := newDefault() + if got, want := v.client, http.DefaultClient; got != want { t.Errorf("Expect default client is http.DefaultClient") } - if got, want := opts.server, "https://github.com"; got != want { + if got, want := v.server, "https://github.com"; got != want { t.Errorf("Expect default server is https://github.com") } } diff --git a/login/gitlab/gitlab.go b/login/gitlab/gitlab.go index dc6b593..171c8e0 100644 --- a/login/gitlab/gitlab.go +++ b/login/gitlab/gitlab.go @@ -8,11 +8,12 @@ import ( "net/http" "strings" + "github.com/drone/go-login/login" "github.com/drone/go-login/login/internal/oauth2" ) -// Options provides the GitLab authentication options. -type Options struct { +// Authorizer configures the GitLab auth provider. +type Authorizer struct { scope []string clientID string clientSecret string @@ -21,80 +22,88 @@ type Options struct { client *http.Client } -func createOptions() *Options { - return &Options{ +func newDefault() *Authorizer { + return &Authorizer{ server: "https://gitlab.com", client: http.DefaultClient, } } // Option configures an authorization handler option. -type Option func(o *Options) +type Option func(a *Authorizer) // WithClient configures the authorization handler with a // custom http.Client. func WithClient(client *http.Client) Option { - return func(o *Options) { - o.client = client + return func(a *Authorizer) { + a.client = client } } // WithClientID configures the authorization handler with // the client_id. func WithClientID(clientID string) Option { - return func(o *Options) { - o.clientID = clientID + return func(a *Authorizer) { + a.clientID = clientID } } // WithClientSecret configures the authorization handler // with the client_secret. func WithClientSecret(clientSecret string) Option { - return func(o *Options) { - o.clientSecret = clientSecret + return func(a *Authorizer) { + a.clientSecret = clientSecret } } // WithRedirectURL configures the authorization handler // with the redirect_url func WithRedirectURL(redirectURL string) Option { - return func(o *Options) { - o.redirectURL = redirectURL + return func(a *Authorizer) { + a.redirectURL = redirectURL } } // WithScope configures the authorization handler with the // these scopes. func WithScope(scope ...string) Option { - return func(o *Options) { - o.scope = scope + return func(a *Authorizer) { + a.scope = scope } } // WithAddress configures the authorization handler with // the the self-hosted GitLab server address. func WithAddress(server string) Option { - return func(o *Options) { - o.server = strings.TrimSuffix(server, "/") + return func(a *Authorizer) { + if server != "" { + a.server = strings.TrimSuffix(server, "/") + } } } -// New returns a http.Handler that runs h at the completion -// of the GitLab authorization flow. The GitLab authorization -// is availabe to h in the http.Request context. -func New(h http.Handler, opt ...Option) http.Handler { - opts := createOptions() - for _, fn := range opt { - fn(opts) +// New returns a GitLab authorization provider. +func New(opts ...Option) login.Authorizer { + auther := newDefault() + for _, opt := range opts { + opt(auther) } + return auther +} + +// Authorize returns a http.Handler that runs h at the +// completion of the GitLab authorization flow. The GitLab +// authorization details are available to h in the +// http.Request context. +func (a *Authorizer) Authorize(h http.Handler) http.Handler { return oauth2.Handler(h, &oauth2.Config{ BasicAuthOff: true, - Client: opts.client, - ClientID: opts.clientID, - ClientSecret: opts.clientSecret, - RedirectURL: opts.redirectURL, - AccessTokenURL: opts.server + "/oauth/token", - AuthorizationURL: opts.server + "/oauth/authorize", - Scope: opts.scope, + Client: a.client, + ClientID: a.clientID, + ClientSecret: a.clientSecret, + RedirectURL: a.redirectURL, + AccessTokenURL: a.server + "/oauth/token", + AuthorizationURL: a.server + "/oauth/authorize", + Scope: a.scope, }) } diff --git a/login/gitlab/gitlab_test.go b/login/gitlab/gitlab_test.go index d279899..c4d26e8 100644 --- a/login/gitlab/gitlab_test.go +++ b/login/gitlab/gitlab_test.go @@ -13,62 +13,62 @@ import ( func TestWithClient(t *testing.T) { c := &http.Client{} - o := &Options{} - WithClient(c)(o) - if got, want := o.client, c; got != want { + a := &Authorizer{} + WithClient(c)(a) + if got, want := a.client, c; got != want { t.Errorf("Expect custom client option applied") } } func TestWithClientID(t *testing.T) { - opts := &Options{} - WithClientID("3da54155991")(opts) - if got, want := opts.clientID, "3da54155991"; got != want { + a := &Authorizer{} + WithClientID("3da54155991")(a) + if got, want := a.clientID, "3da54155991"; got != want { t.Errorf("Expect client_id option applied") } } func TestWithClientSecret(t *testing.T) { - opts := &Options{} - WithClientSecret("5012f6c60b2")(opts) - if got, want := opts.clientSecret, "5012f6c60b2"; got != want { + a := &Authorizer{} + WithClientSecret("5012f6c60b2")(a) + if got, want := a.clientSecret, "5012f6c60b2"; got != want { t.Errorf("Expect client_secret option applied") } } func TestWithRedirectURL(t *testing.T) { - opts := &Options{} - WithRedirectURL("http://company.com/login")(opts) - if got, want := opts.redirectURL, "http://company.com/login"; got != want { + a := &Authorizer{} + WithRedirectURL("http://company.com/login")(a) + if got, want := a.redirectURL, "http://company.com/login"; got != want { t.Errorf("Expect redirect_uri option applied") } } func TestWithAddress(t *testing.T) { - opts := &Options{} - WithAddress("https://company.gitlab.com/")(opts) - if strings.HasSuffix(opts.server, "/") { + a := &Authorizer{} + WithAddress("https://company.gitlab.com/")(a) + if strings.HasSuffix(a.server, "/") { t.Errorf("Expect trailing slash removed from server address") } - if got, want := opts.server, "https://company.gitlab.com"; got != want { + if got, want := a.server, "https://company.gitlab.com"; got != want { t.Errorf("Expect server address option applied") } } func TestWithScope(t *testing.T) { - opts := &Options{} - WithScope("read_user", "api")(opts) - if got, want := opts.scope, []string{"read_user", "api"}; !reflect.DeepEqual(got, want) { + a := &Authorizer{} + WithScope("read_user", "api")(a) + if got, want := a.scope, []string{"read_user", "api"}; !reflect.DeepEqual(got, want) { t.Errorf("Expect scope option applied") } } -func TestDefaultOptions(t *testing.T) { - opts := createOptions() - if got, want := opts.client, http.DefaultClient; got != want { +func TestDefaultAuthorizer(t *testing.T) { + a := newDefault() + if got, want := a.client, http.DefaultClient; got != want { t.Errorf("Expect default client is http.DefaultClient") } - if got, want := opts.server, "https://gitlab.com"; got != want { + if got, want := a.server, "https://gitlab.com"; got != want { t.Errorf("Expect default server is https://gitlab.com") } } diff --git a/login/gogs/gogs.go b/login/gogs/gogs.go index ba02224..c16da57 100644 --- a/login/gogs/gogs.go +++ b/login/gogs/gogs.go @@ -5,129 +5,76 @@ package gogs import ( - "bytes" - "encoding/json" - "errors" - "fmt" "net/http" + "strings" "github.com/drone/go-login/login" ) -type token struct { - Name string `json:"name"` - Sha1 string `json:"sha1,omitempty"` -} - -type handler struct { - next http.Handler +// Authorizer configures the Gogs auth provider. +type Authorizer struct { label string login string server string client *http.Client } -// New returns a Handler that runs h at the completion of -// the gogs authorization flow. The gogs authorization -// is availabe to h in the http.Request context. -func New(h http.Handler, opt ...Option) http.Handler { - opts := defaultOptions() - for _, fn := range opt { - fn(opts) - } - return &handler{ - next: h, - label: opts.label, - login: opts.login, - server: opts.server, - client: opts.client, +func newDefault() *Authorizer { + return &Authorizer{ + label: "default", + client: http.DefaultClient, } } -func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - user := r.FormValue("username") - pass := r.FormValue("password") - if (user == "" || pass == "") && h.login != "" { - http.Redirect(w, r, h.login, 303) - return - } - token, err := h.createFindToken(user, pass) - if err != nil { - ctx = login.WithError(ctx, err) - } else { - ctx = login.WithToken(ctx, &login.Token{ - Access: token.Sha1, - }) - } - h.next.ServeHTTP(w, r.WithContext(ctx)) -} +// Option configures an authorization handler option. +type Option func(a *Authorizer) -func (h *handler) createFindToken(user, pass string) (*token, error) { - tokens, err := h.findTokens(user, pass) - if err != nil { - return nil, err +// WithClient configures the authorization handler with a +// custom http.Client. +func WithClient(client *http.Client) Option { + return func(a *Authorizer) { + a.client = client } - for _, token := range tokens { - if token.Name == h.label { - return token, nil - } - } - return h.createToken(user, pass) } -func (h *handler) createToken(user, pass string) (*token, error) { - path := fmt.Sprintf("%s/api/v1/users/%s/tokens", h.server, user) - - buf := new(bytes.Buffer) - json.NewEncoder(buf).Encode(&token{ - Name: h.label, - }) - - req, err := http.NewRequest("POST", path, buf) - if err != nil { - return nil, err +// WithTokenName configures the authorization handler to +// use the specificed token name when finding and creating +// authorization tokens. +func WithTokenName(name string) Option { + return func(a *Authorizer) { + a.label = name } - req.Header.Set("Content-Type", "application/json") - req.SetBasicAuth(user, pass) +} - res, err := h.client.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode > 299 { - return nil, errors.New( - http.StatusText(res.StatusCode), - ) +// WithLoginRedirect configures the authorization handler +// to redirect the http.Request to the login form when the +// username or password are missing from the Form data. +func WithLoginRedirect(path string) Option { + return func(a *Authorizer) { + a.login = path } - - out := new(token) - err = json.NewDecoder(res.Body).Decode(out) - return out, err } -func (h *handler) findTokens(user, pass string) ([]*token, error) { - path := fmt.Sprintf("%s/api/v1/users/%s/tokens", h.server, user) - req, err := http.NewRequest("GET", path, nil) - if err != nil { - return nil, err +// New returns a Gogs authorization provider. +func New(address string, opts ...Option) login.Authorizer { + auther := newDefault() + auther.server = strings.TrimSuffix(address, "/") + for _, opt := range opts { + opt(auther) } - req.Header.Set("Content-Type", "application/json") - req.SetBasicAuth(user, pass) + return auther +} - res, err := h.client.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode > 299 { - return nil, errors.New( - http.StatusText(res.StatusCode), - ) +// Authorize returns a http.Handler that runs h at the +// completion of the GitLab authorization flow. The GitLab +// authorization details are available to h in the +// http.Request context. +func (a *Authorizer) Authorize(h http.Handler) http.Handler { + return &handler{ + next: h, + label: a.label, + login: a.login, + server: a.server, + client: a.client, } - - out := []*token{} - err = json.NewDecoder(res.Body).Decode(&out) - return out, err } diff --git a/login/gogs/gogs_test.go b/login/gogs/gogs_test.go index 2cbb0e7..3c1b9c3 100644 --- a/login/gogs/gogs_test.go +++ b/login/gogs/gogs_test.go @@ -5,154 +5,28 @@ package gogs import ( - "context" - "errors" "net/http" - "net/http/httptest" - "net/url" - "strings" "testing" - - "github.com/drone/go-login/login" - "github.com/h2non/gock" ) -func TestLogin(t *testing.T) { - defer gock.Off() - - tests := []struct { - user string - pass string - path string - auth string - tokens []*token - token *token - err error - }{ - // Success, match found. - { - user: "janedoe", - pass: "password", - path: "/api/v1/users/janedoe/token", - auth: "Basic amFuZWRvZTpwYXNzd29yZA==", - token: &token{Name: "default", Sha1: "3da541559"}, - tokens: []*token{{Name: "default", Sha1: "3da541559"}}, - }, - // Success, match not found, token created. - { - user: "janedoe", - pass: "password", - path: "/api/v1/users/janedoe/token", - auth: "Basic amFuZWRvZTpwYXNzd29yZA==", - token: &token{Name: "default", Sha1: "918a808c2"}, - tokens: []*token{}, - }, - // Failure, error getting token list. - { - user: "janedoe", - pass: "password", - path: "/api/v1/users/janedoe/token", - auth: "Basic amFuZWRvZTpwYXNzd29yZA==", - tokens: nil, - token: nil, - err: errors.New("Not Found"), - }, - // Failure, match not found, error creating token. - { - user: "janedoe", - pass: "password", - path: "/api/v1/users/janedoe/token", - auth: "Basic amFuZWRvZTpwYXNzd29yZA==", - tokens: []*token{{Name: "some-random-token-name", Sha1: "918a808c2"}}, - token: nil, - err: errors.New("Not Found"), - }, +func TestWithClient(t *testing.T) { + c := &http.Client{} + h := New("https://try.gogs.io", WithClient(c)) + if got, want := h.(*Authorizer).client, c; got != want { + t.Errorf("Expect custom client") } +} - for _, test := range tests { - gock.Flush() - - if test.tokens != nil { - gock.New("https://gogs.io"). - Get("/api/v1/users/janedoe/token"). - MatchHeader("Authorization", test.auth). - Reply(200). - JSON(test.tokens) - } else { - gock.New("https://gogs.io"). - Get("/api/v1/users/janedoe/token"). - Reply(404) - } - - if test.token != nil { - gock.New("https://gogs.io"). - Post("/api/v1/users/janedoe/token"). - MatchHeader("Authorization", test.auth). - Reply(200). - JSON(test.token) - } else { - gock.New("https://gogs.io"). - Post("/api/v1/users/janedoe/token"). - Reply(404) - } - - var ctx context.Context - fn := func(w http.ResponseWriter, r *http.Request) { - ctx = r.Context() - } - - h := New( - http.HandlerFunc(fn), - WithAddress("https://gogs.io"), - ) - - data := url.Values{ - "username": {test.user}, - "password": {test.pass}, - }.Encode() - - res := httptest.NewRecorder() - req := httptest.NewRequest("POST", "/", strings.NewReader(data)) - req.Header.Set( - "Content-Type", "application/x-www-form-urlencoded", - ) - - h.ServeHTTP(res, req) - - tok := login.TokenFrom(ctx) - err := login.ErrorFrom(ctx) - - if test.err != nil { - if err == nil { - t.Errorf("Want error") - } else if got, want := err.Error(), test.err.Error(); got != want { - t.Errorf("Want error %q, got %q", want, got) - } - } else { - if tok == nil { - t.Errorf("Want user token, got nil") - } else if got, want := tok.Access, test.token.Sha1; got != want { - t.Errorf("Want access token %s, got %s", want, got) - } - } +func TestWithTokenName(t *testing.T) { + h := New("https://try.gogs.io", WithTokenName("some_token")) + if got, want := h.(*Authorizer).label, "some_token"; got != want { + t.Errorf("Expect token name url %q, got %q", want, got) } } -func TestLoginRedirect(t *testing.T) { - h := New( - http.NotFoundHandler(), - WithLoginRedirect("/login/form"), - WithAddress("https://gogs.io"), - ).(*handler) - - r := httptest.NewRequest("POST", "/login", nil) - w := httptest.NewRecorder() - h.ServeHTTP(w, r) - - if want, got := w.Code, 303; want != got { - t.Errorf("Want status code %d, got %d", want, got) - } - if want, got := w.Header().Get("Location"), "/login/form"; want != got { - t.Errorf("Want redirect location %s, got %s", want, got) +func TestWithLoginRedirect(t *testing.T) { + h := New("https://try.gogs.io", WithLoginRedirect("/path/to/login")) + if got, want := h.(*Authorizer).login, "/path/to/login"; got != want { + t.Errorf("Expect login redirect url %q, got %q", want, got) } } diff --git a/login/gitea/gitea.go b/login/gogs/handler.go similarity index 85% rename from login/gitea/gitea.go rename to login/gogs/handler.go index 2e2c93f..ad2cc85 100644 --- a/login/gitea/gitea.go +++ b/login/gogs/handler.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package gitea +package gogs import ( "bytes" @@ -27,23 +27,6 @@ type handler struct { client *http.Client } -// New returns a Handler that runs h at the completion of -// the gitea authorization flow. The gitea authorization -// is availabe to h in the http.Request context. -func New(h http.Handler, opt ...Option) http.Handler { - opts := defaultOptions() - for _, fn := range opt { - fn(opts) - } - return &handler{ - next: h, - label: opts.label, - login: opts.login, - server: opts.server, - client: opts.client, - } -} - func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() user := r.FormValue("username") diff --git a/login/gitea/gitea_test.go b/login/gogs/handler_test.go similarity index 93% rename from login/gitea/gitea_test.go rename to login/gogs/handler_test.go index 0aa654d..5b43f85 100644 --- a/login/gitea/gitea_test.go +++ b/login/gogs/handler_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package gitea +package gogs import ( "context" @@ -73,25 +73,25 @@ func TestLogin(t *testing.T) { gock.Flush() if test.tokens != nil { - gock.New("https://gitea.io"). + gock.New("https://gogs.io"). Get("/api/v1/users/janedoe/token"). MatchHeader("Authorization", test.auth). Reply(200). JSON(test.tokens) } else { - gock.New("https://gitea.io"). + gock.New("https://gogs.io"). Get("/api/v1/users/janedoe/token"). Reply(404) } if test.token != nil { - gock.New("https://gitea.io"). + gock.New("https://gogs.io"). Post("/api/v1/users/janedoe/token"). MatchHeader("Authorization", test.auth). Reply(200). JSON(test.token) } else { - gock.New("https://gitea.io"). + gock.New("https://gogs.io"). Post("/api/v1/users/janedoe/token"). Reply(404) } @@ -101,9 +101,8 @@ func TestLogin(t *testing.T) { ctx = r.Context() } - h := New( + h := New("https://gogs.io").Authorize( http.HandlerFunc(fn), - WithAddress("https://gitea.io"), ) data := url.Values{ @@ -139,11 +138,11 @@ func TestLogin(t *testing.T) { } func TestLoginRedirect(t *testing.T) { - h := New( - http.NotFoundHandler(), + h := New("https://gogs.io", WithLoginRedirect("/login/form"), - WithAddress("https://gitea.io"), - ).(*handler) + ).Authorize( + http.NotFoundHandler(), + ) r := httptest.NewRequest("POST", "/login", nil) w := httptest.NewRecorder() diff --git a/login/gogs/option.go b/login/gogs/option.go deleted file mode 100644 index 41388f0..0000000 --- a/login/gogs/option.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2017 Drone.IO Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package gogs - -import ( - "net/http" - "strings" -) - -// Options provides the Gogs authentication options. -type Options struct { - label string - login string - server string - client *http.Client -} - -func defaultOptions() *Options { - return &Options{ - label: "default", - client: http.DefaultClient, - } -} - -// Option configures an authorization handler option. -type Option func(o *Options) - -// WithAddress configures the authorization handler with -// the progived Gogs server address. -func WithAddress(server string) Option { - return func(o *Options) { - o.server = strings.TrimPrefix(server, "/") - } -} - -// WithClient configures the authorization handler with a -// custom http.Client. -func WithClient(client *http.Client) Option { - return func(o *Options) { - o.client = client - } -} - -// WithTokenName configures the authorization handler to -// use the specificed token name when finding and creating -// authorization tokens. -func WithTokenName(name string) Option { - return func(o *Options) { - o.label = name - } -} - -// WithLoginRedirect configures the authorization handler -// to redirect the http.Request to the login form when the -// username or password are missing from the Form data. -func WithLoginRedirect(path string) Option { - return func(o *Options) { - o.login = path - } -} diff --git a/login/gogs/option_test.go b/login/gogs/option_test.go deleted file mode 100644 index cec1470..0000000 --- a/login/gogs/option_test.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2017 Drone.IO Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package gogs - -import ( - "net/http" - "testing" -) - -func TestWithAddress(t *testing.T) { - h := New(nil, WithAddress("https://try.gogs.io")) - if got, want := h.(*handler).server, "https://try.gogs.io"; got != want { - t.Errorf("Expect server address %q, got %q", want, got) - } -} - -func TestWithClient(t *testing.T) { - c := &http.Client{} - h := New(nil, WithClient(c)) - if got, want := h.(*handler).client, c; got != want { - t.Errorf("Expect custom client") - } -} - -func TestWithTokenName(t *testing.T) { - h := New(nil, WithTokenName("some_token")) - if got, want := h.(*handler).label, "some_token"; got != want { - t.Errorf("Expect token name url %q, got %q", want, got) - } -} - -func TestWithLoginRedirect(t *testing.T) { - h := New(nil, WithLoginRedirect("/path/to/login")) - if got, want := h.(*handler).login, "/path/to/login"; got != want { - t.Errorf("Expect login redirect url %q, got %q", want, got) - } -} diff --git a/login/login.go b/login/login.go index ecaf85e..08344fb 100644 --- a/login/login.go +++ b/login/login.go @@ -6,9 +6,27 @@ package login import ( "context" + "net/http" "time" ) +// Authorizer returns a http.Handler that runs h at the +// completion of the authorization flow. The authorization +// results are available to h in the http.Request context. +type Authorizer interface { + Authorize(h http.Handler) http.Handler +} + +// Middleware defines a Login middleware. The middleware +// wraps the http.Handler and intercepts the http.Request +// to perform authentication. The http.Handler is invoked +// when authentication is complete, with authentication +// details (oauth token, etc) passed to the handler via +// the http.Request context. +type Middleware interface { + Wrap(h http.Handler) http.Handler +} + // Token represents an authorization token. type Token struct { Access string diff --git a/login/stash/stash.go b/login/stash/stash.go index 9959cf8..0e384c0 100644 --- a/login/stash/stash.go +++ b/login/stash/stash.go @@ -12,6 +12,7 @@ import ( "net/http" "strings" + "github.com/drone/go-login/login" "github.com/drone/go-login/login/internal/oauth1" ) @@ -21,63 +22,38 @@ const ( accessTokenURL = "%s/plugins/servlet/oauth/access-token" ) -// Options provides the Bitbucket Server (Stash) -// authentication options. -type Options struct { - callbackURL string - address string - consumerKey string - consumerSecret string - signer oauth1.Signer - client *http.Client -} - -func createOptions() *Options { - return &Options{ - client: http.DefaultClient, - } -} - // Option configures an authorization handler option. -type Option func(o *Options) +type Option func(a *Authorizer) // WithClient configures the authorization handler with a // custom http.Client. func WithClient(client *http.Client) Option { - return func(o *Options) { - o.client = client + return func(a *Authorizer) { + a.client = client } } // WithConsumerKey configures the authorization handler with // the oauth_consumer_key. func WithConsumerKey(consumerKey string) Option { - return func(o *Options) { - o.consumerKey = consumerKey + return func(a *Authorizer) { + a.consumerKey = consumerKey } } // WithConsumerSecret configures the authorization handler // with the oauth_consumer_secret. func WithConsumerSecret(consumerSecret string) Option { - return func(o *Options) { - o.consumerSecret = consumerSecret + return func(a *Authorizer) { + a.consumerSecret = consumerSecret } } // WithCallbackURL configures the authorization handler // with the oauth_callback_url func WithCallbackURL(callbackURL string) Option { - return func(o *Options) { - o.callbackURL = callbackURL - } -} - -// WithAddress configures the authorization handler with -// the Bitbucket Server address. -func WithAddress(address string) Option { - return func(o *Options) { - o.address = strings.TrimPrefix(address, "/") + return func(a *Authorizer) { + a.callbackURL = callbackURL } } @@ -94,32 +70,56 @@ func WithPrivateKeyFile(path string) Option { // WithPrivateKey configures the authorization handler // with the oauth private rsa key for signing requests. func WithPrivateKey(data []byte) Option { - return func(o *Options) { + return func(a *Authorizer) { p, _ := pem.Decode(data) k, err := x509.ParsePKCS1PrivateKey(p.Bytes) if err != nil { panic(err) } - o.signer = &oauth1.RSASigner{PrivateKey: k} + a.signer = &oauth1.RSASigner{PrivateKey: k} } } -// New returns a http.Handler that runs h at the completion -// of the Bitbucket authorization flow. The Bitbucket -// authorization is passed to h in the http.Request context. -func New(h http.Handler, opt ...Option) http.Handler { - opts := createOptions() - for _, fn := range opt { - fn(opts) +// Authorizer configures the Bitbucket Server (Stash) +// authorization provider. +type Authorizer struct { + callbackURL string + address string + consumerKey string + consumerSecret string + signer oauth1.Signer + client *http.Client +} + +func newDefault() *Authorizer { + return &Authorizer{ + client: http.DefaultClient, } +} + +// New returns a Bitbucket Server authorization provider. +func New(address string, opts ...Option) login.Authorizer { + auther := newDefault() + auther.address = strings.TrimPrefix(address, "/") + for _, opt := range opts { + opt(auther) + } + return auther +} + +// Authorize returns a http.Handler that runs h at the +// completion of the GitHub authorization flow. The GitHub +// authorization details are available to h in the +// http.Request context. +func (a *Authorizer) Authorize(h http.Handler) http.Handler { return oauth1.Handler(h, &oauth1.Config{ - Signer: opts.signer, - Client: opts.client, - ConsumerKey: opts.consumerKey, - ConsumerSecret: opts.consumerSecret, - CallbackURL: opts.callbackURL, - AccessTokenURL: fmt.Sprintf(accessTokenURL, opts.address), - AuthorizationURL: fmt.Sprintf(authorizeTokenURL, opts.address), - RequestTokenURL: fmt.Sprintf(requestTokenURL, opts.address), + Signer: a.signer, + Client: a.client, + ConsumerKey: a.consumerKey, + ConsumerSecret: a.consumerSecret, + CallbackURL: a.callbackURL, + AccessTokenURL: fmt.Sprintf(accessTokenURL, a.address), + AuthorizationURL: fmt.Sprintf(authorizeTokenURL, a.address), + RequestTokenURL: fmt.Sprintf(requestTokenURL, a.address), }) } diff --git a/login/stash/stash_test.go b/login/stash/stash_test.go new file mode 100644 index 0000000..2956ec6 --- /dev/null +++ b/login/stash/stash_test.go @@ -0,0 +1,5 @@ +// Copyright 2018 Drone.IO Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package stash