add gitea and stash login providers

pull/1/head
Brad Rydzewski 6 years ago
parent 75c3d8669d
commit c5678d82b0

@ -12,9 +12,11 @@ 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"
"github.com/drone/go-login/login/stash"
)
var (
@ -22,6 +24,8 @@ var (
providerURL = flag.String("provider-url", "", "")
clientID = flag.String("client-id", "", "")
clientSecret = flag.String("client-secret", "", "")
consumerKey = flag.String("consumer-key", "", "")
consumerRsa = flag.String("consumer-private-key", "", "")
redirectURL = flag.String("redirect-url", "http://localhost:8080/login", "")
address = flag.String("address", ":8080", "")
help = flag.Bool("help", false, "")
@ -38,12 +42,18 @@ func main() {
var auther http.Handler
switch *provider {
case "gogs", "gitea":
case "gogs":
auther = gogs.New(
http.HandlerFunc(details),
gogs.WithAddress(*providerURL),
gogs.WithLoginRedirect("/login/form"),
)
case "gitea":
auther = gitea.New(
http.HandlerFunc(details),
gitea.WithAddress(*providerURL),
gitea.WithLoginRedirect("/login/form"),
)
case "gitlab":
auther = gitlab.New(
http.HandlerFunc(details),
@ -66,6 +76,14 @@ func main() {
bitbucket.WithClientSecret(*clientSecret),
bitbucket.WithRedirectURL(*redirectURL),
)
case "stash":
auther = stash.New(
http.HandlerFunc(details),
stash.WithConsumerKey(*consumerKey),
stash.WithPrivateKeyFile(*consumerRsa),
stash.WithCallbackURL(*redirectURL),
stash.WithAddress(*providerURL),
)
}
// handles the authorization flow and displays the
@ -87,7 +105,7 @@ func details(w http.ResponseWriter, r *http.Request) {
return
}
token := login.TokenFrom(ctx)
fmt.Fprintf(w, success, token.Access)
fmt.Fprintf(w, success, token.Access, token.Refresh)
}
// display the login form.
@ -109,7 +127,9 @@ var loginForm = `
var success = `
<html>
<body>
<h1>Token</h1>
<h1>Access Token</h1>
<h2>%s</h2>
<h1>Refresh / Secret Token</h1>
<h2>%s</h2>
</body>
</html>
@ -127,11 +147,13 @@ var failure = `
func usage() {
fmt.Println(`Usage: go run main.go [OPTION]...
--provider oauth provider (github, gitlab, gogs, gitea, bitbucket)
--provider-url oauth provider url (gitea, gogs only)
--client-id oauth client id
--client-secret oauth client secret
--redirect-url oauth redirect url
--address http server address (:8080)
--help display this help and exit`)
--provider provider (github, gitlab, gogs, gitea, bitbucket)
--provider-url provider url (gitea, gogs, stash only)
--client-id oauth2 client id
--client-secret oauth2 client secret
--consumer-key oauth1 consumer key
--consumer-private-key oauth1 consumer rsa private key file
--redirect-url oauth redirect url
--address http server address (:8080)
--help display this help and exit`)
}

@ -0,0 +1,133 @@
// 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 (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/drone/go-login/login"
)
type token struct {
Name string `json:"name"`
Sha1 string `json:"sha1,omitempty"`
}
type handler struct {
next http.Handler
label string
login string
server string
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")
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))
}
func (h *handler) createFindToken(user, pass string) (*token, error) {
tokens, err := h.findTokens(user, pass)
if err != nil {
return nil, err
}
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
}
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),
)
}
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
}
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),
)
}
out := []*token{}
err = json.NewDecoder(res.Body).Decode(&out)
return out, err
}

@ -0,0 +1,158 @@
// 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 (
"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"),
},
}
for _, test := range tests {
gock.Flush()
if test.tokens != nil {
gock.New("https://gitea.io").
Get("/api/v1/users/janedoe/token").
MatchHeader("Authorization", test.auth).
Reply(200).
JSON(test.tokens)
} else {
gock.New("https://gitea.io").
Get("/api/v1/users/janedoe/token").
Reply(404)
}
if test.token != nil {
gock.New("https://gitea.io").
Post("/api/v1/users/janedoe/token").
MatchHeader("Authorization", test.auth).
Reply(200).
JSON(test.token)
} else {
gock.New("https://gitea.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://gitea.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 TestLoginRedirect(t *testing.T) {
h := New(
http.NotFoundHandler(),
WithLoginRedirect("/login/form"),
WithAddress("https://gitea.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)
}
}

@ -0,0 +1,62 @@
// 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
}
}

@ -0,0 +1,39 @@
// 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)
}
}

@ -0,0 +1,268 @@
// Copyright (c) 2015 Dalton Hubble. All rights reserved.
// Copyrights licensed under the MIT License.
package oauth1
import (
"bytes"
"crypto/rand"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
)
const (
authorizationHeaderParam = "Authorization"
authorizationPrefix = "OAuth " // trailing space is intentional
oauthConsumerKeyParam = "oauth_consumer_key"
oauthNonceParam = "oauth_nonce"
oauthSignatureParam = "oauth_signature"
oauthSignatureMethodParam = "oauth_signature_method"
oauthTimestampParam = "oauth_timestamp"
oauthTokenParam = "oauth_token"
oauthVersionParam = "oauth_version"
oauthCallbackParam = "oauth_callback"
oauthVerifierParam = "oauth_verifier"
defaultOauthVersion = "1.0"
contentType = "Content-Type"
formContentType = "application/x-www-form-urlencoded"
)
// clock provides a interface for current time providers. A Clock can be used
// in place of calling time.Now() directly.
type clock interface {
Now() time.Time
}
// A noncer provides random nonce strings.
type noncer interface {
Nonce() string
}
// auther adds an "OAuth" Authorization header field to requests.
type auther struct {
config *Config
clock clock
noncer noncer
}
func newAuther(config *Config) *auther {
return &auther{
config: config,
}
}
// setRequestTokenAuthHeader adds the OAuth1 header for the request token
// request (temporary credential) according to RFC 5849 2.1.
func (a *auther) setRequestTokenAuthHeader(req *http.Request) error {
oauthParams := a.commonOAuthParams()
oauthParams[oauthCallbackParam] = a.config.CallbackURL
params, err := collectParameters(req, oauthParams)
if err != nil {
return err
}
signatureBase := signatureBase(req, params)
signature, err := a.signer().Sign("", signatureBase)
if err != nil {
return err
}
oauthParams[oauthSignatureParam] = signature
req.Header.Set(authorizationHeaderParam, authHeaderValue(oauthParams))
return nil
}
// setAccessTokenAuthHeader sets the OAuth1 header for the access token request
// (token credential) according to RFC 5849 2.3.
func (a *auther) setAccessTokenAuthHeader(req *http.Request, requestToken, requestSecret, verifier string) error {
oauthParams := a.commonOAuthParams()
oauthParams[oauthTokenParam] = requestToken
oauthParams[oauthVerifierParam] = verifier
params, err := collectParameters(req, oauthParams)
if err != nil {
return err
}
signatureBase := signatureBase(req, params)
signature, err := a.signer().Sign(requestSecret, signatureBase)
if err != nil {
return err
}
oauthParams[oauthSignatureParam] = signature
req.Header.Set(authorizationHeaderParam, authHeaderValue(oauthParams))
return nil
}
// // setRequestAuthHeader sets the OAuth1 header for making authenticated
// // requests with an AccessToken (token credential) according to RFC 5849 3.1.
// func (a *auther) setRequestAuthHeader(req *http.Request, accessToken *token) error {
// oauthParams := a.commonOAuthParams()
// oauthParams[oauthTokenParam] = accessToken.Token
// params, err := collectParameters(req, oauthParams)
// if err != nil {
// return err
// }
// signatureBase := signatureBase(req, params)
// signature, err := a.signer().Sign(accessToken.TokenSecret, signatureBase)
// if err != nil {
// return err
// }
// oauthParams[oauthSignatureParam] = signature
// req.Header.Set(authorizationHeaderParam, authHeaderValue(oauthParams))
// return nil
// }
// commonOAuthParams returns a map of the common OAuth1 protocol parameters,
// excluding the oauth_signature parameter.
func (a *auther) commonOAuthParams() map[string]string {
return map[string]string{
oauthConsumerKeyParam: a.config.ConsumerKey,
oauthSignatureMethodParam: a.signer().Name(),
oauthTimestampParam: strconv.FormatInt(a.epoch(), 10),
oauthNonceParam: a.nonce(),
oauthVersionParam: defaultOauthVersion,
}
}
// Returns a base64 encoded random 32 byte string.
func (a *auther) nonce() string {
if a.noncer != nil {
return a.noncer.Nonce()
}
b := make([]byte, 32)
rand.Read(b)
return base64.StdEncoding.EncodeToString(b)
}
// Returns the Unix epoch seconds.
func (a *auther) epoch() int64 {
if a.clock != nil {
return a.clock.Now().Unix()
}
return time.Now().Unix()
}
// Returns the Config's Signer or the default Signer.
func (a *auther) signer() Signer {
if a.config.Signer != nil {
return a.config.Signer
}
return &HMACSigner{ConsumerSecret: a.config.ConsumerSecret}
}
// authHeaderValue formats OAuth parameters according to RFC 5849 3.5.1. OAuth
// params are percent encoded, sorted by key (for testability), and joined by
// "=" into pairs. Pairs are joined with a ", " comma separator into a header
// string.
// The given OAuth params should include the "oauth_signature" key.
func authHeaderValue(oauthParams map[string]string) string {
pairs := sortParameters(encodeParameters(oauthParams), `%s="%s"`)
return authorizationPrefix + strings.Join(pairs, ", ")
}
// encodeParameters percent encodes parameter keys and values according to
// RFC5849 3.6 and RFC3986 2.1 and returns a new map.
func encodeParameters(params map[string]string) map[string]string {
encoded := map[string]string{}
for key, value := range params {
encoded[percentEncode(key)] = percentEncode(value)
}
return encoded
}
// sortParameters sorts parameters by key and returns a slice of key/value
// pairs formatted with the given format string (e.g. "%s=%s").
func sortParameters(params map[string]string, format string) []string {
// sort by key
keys := make([]string, len(params))
i := 0
for key := range params {
keys[i] = key
i++
}
sort.Strings(keys)
// parameter join
pairs := make([]string, len(params))
for i, key := range keys {
pairs[i] = fmt.Sprintf(format, key, params[key])
}
return pairs
}
// collectParameters collects request parameters from the request query, OAuth
// parameters (which should exclude oauth_signature), and the request body
// provided the body is single part, form encoded, and the form content type
// header is set. The returned map of collected parameter keys and values
// follow RFC 5849 3.4.1.3, except duplicate parameters are not supported.
func collectParameters(req *http.Request, oauthParams map[string]string) (map[string]string, error) {
// add oauth, query, and body parameters into params
params := map[string]string{}
for key, value := range req.URL.Query() {
// most backends do not accept duplicate query keys
params[key] = value[0]
}
if req.Body != nil && req.Header.Get(contentType) == formContentType {
// reads data to a []byte, draining req.Body
b, err := ioutil.ReadAll(req.Body)
if err != nil {
return nil, err
}
values, err := url.ParseQuery(string(b))
if err != nil {
return nil, err
}
for key, value := range values {
// not supporting params with duplicate keys
params[key] = value[0]
}
// reinitialize Body with ReadCloser over the []byte
req.Body = ioutil.NopCloser(bytes.NewReader(b))
}
for key, value := range oauthParams {
params[key] = value
}
return params, nil
}
// signatureBase combines the uppercase request method, percent encoded base
// string URI, and normalizes the request parameters int a parameter string.
// Returns the OAuth1 signature base string according to RFC5849 3.4.1.
func signatureBase(req *http.Request, params map[string]string) string {
method := strings.ToUpper(req.Method)
baseURL := baseURI(req)
parameterString := normalizedParameterString(params)
// signature base string constructed accoding to 3.4.1.1
baseParts := []string{method, percentEncode(baseURL), percentEncode(parameterString)}
return strings.Join(baseParts, "&")
}
// baseURI returns the base string URI of a request according to RFC 5849
// 3.4.1.2. The scheme and host are lowercased, the port is dropped if it
// is 80 or 443, and the path minus query parameters is included.
func baseURI(req *http.Request) string {
scheme := strings.ToLower(req.URL.Scheme)
host := strings.ToLower(req.URL.Host)
if hostPort := strings.Split(host, ":"); len(hostPort) == 2 && (hostPort[1] == "80" || hostPort[1] == "443") {
host = hostPort[0]
}
// TODO: use req.URL.EscapedPath() once Go 1.5 is more generally adopted
// For now, hacky workaround accomplishes the same internal escaping mode
// escape(u.Path, encodePath) for proper compliance with the OAuth1 spec.
path := req.URL.Path
if path != "" {
path = strings.Split(req.URL.RequestURI(), "?")[0]
}
return fmt.Sprintf("%v://%v%v", scheme, host, path)
}
// parameterString normalizes collected OAuth parameters (which should exclude
// oauth_signature) into a parameter string as defined in RFC 5894 3.4.1.3.2.
// The parameters are encoded, sorted by key, keys and values joined with "&",
// and pairs joined with "=" (e.g. foo=bar&q=gopher).
func normalizedParameterString(params map[string]string) string {
return strings.Join(sortParameters(encodeParameters(params), "%s=%s"), "&")
}

@ -0,0 +1,4 @@
// Copyright (c) 2015 Dalton Hubble. All rights reserved.
// Copyrights licensed under the MIT License.
package oauth1

@ -0,0 +1,160 @@
// 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 oauth1
import (
"errors"
"io"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
)
// token stores the authorization credentials used to
// access protected resources.
type token struct {
Token string
TokenSecret string
}
// Config stores the application configuration.
type Config struct {
// HTTP client used to communicate with the authorization
// server. If nil, DefaultClient is used.
Client *http.Client
// A Signer signs messages to create signed OAuth1 Requests.
// If nil, the HMAC signing algorithm is used.
Signer Signer
// A value used by the Consumer to identify itself
// to the Service Provider.
ConsumerKey string
// A secret used by the Consumer to establish
// ownership of the Consumer Key.
ConsumerSecret string
// An absolute URL to which the Service Provider will redirect
// the User back when the Obtaining User Authorization step
// is completed.
//
// If the Consumer is unable to receive callbacks or a callback
// URL has been established via other means, the parameter
// value MUST be set to oob (case sensitive), to indicate
// an out-of-band configuration.
CallbackURL string
// The URL used to obtain an unauthorized
// Request Token.
RequestTokenURL string
// The URL used to obtain User authorization
// for Consumer access.
AccessTokenURL string
// The URL used to exchange the User-authorized
// Request Token for an Access Token.
AuthorizationURL string
}
// authorizeRedirect returns a client authorization
// redirect endpoint.
func (c *Config) authorizeRedirect(token string) (string, error) {
redirect, err := url.Parse(c.AuthorizationURL)
if err != nil {
return "", err
}
params := make(url.Values)
params.Add("oauth_token", token)
redirect.RawQuery = params.Encode()
return redirect.String(), nil
}
// requestToken gets a request token from the server.
func (c *Config) requestToken() (*token, error) {
endpoint, err := url.Parse(c.RequestTokenURL)
if err != nil {
return nil, err
}
req := &http.Request{
URL: endpoint,
Method: "POST",
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{},
}
err = newAuther(c).setRequestTokenAuthHeader(req)
if err != nil {
return nil, err
}
res, err := c.client().Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode > 300 {
// TODO(bradrydzewski) unmarshal the oauth1 error.
return nil, errors.New("Invalid Response")
}
return parseToken(res.Body)
}
// authorizeToken returns a client authorization
// redirect endpoint.
func (c *Config) authorizeToken(token, verifier string) (*token, error) {
endpoint, err := url.Parse(c.AccessTokenURL)
if err != nil {
return nil, err
}
req := &http.Request{
URL: endpoint,
Method: "POST",
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{},
}
err = newAuther(c).setAccessTokenAuthHeader(req, token, "", verifier)
if err != nil {
return nil, err
}
res, err := c.client().Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode > 300 {
x, _ := httputil.DumpResponse(res, true)
println(string(x))
// TODO(bradrydzewski) unmarshal the oauth1 error.
return nil, errors.New("Invalid Response")
}
return parseToken(res.Body)
}
func (c *Config) client() *http.Client {
client := c.Client
if client == nil {
client = http.DefaultClient
}
return client
}
func parseToken(r io.Reader) (*token, error) {
b, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
v, err := url.ParseQuery(string(b))
if err != nil {
return nil, err
}
return &token{
Token: v.Get("oauth_token"),
TokenSecret: v.Get("oauth_token_secret"),
}, nil
}

@ -0,0 +1,5 @@
// 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 oauth1

@ -0,0 +1,41 @@
// Copyright (c) 2015 Dalton Hubble. All rights reserved.
// Copyrights licensed under the MIT License.
package oauth1
import (
"bytes"
"fmt"
)
// percentEncode percent encodes a string according
// to RFC 3986 2.1.
func percentEncode(input string) string {
var buf bytes.Buffer
for _, b := range []byte(input) {
// if in unreserved set
if shouldEscape(b) {
buf.Write([]byte(fmt.Sprintf("%%%02X", b)))
} else {
// do not escape, write byte as-is
buf.WriteByte(b)
}
}
return buf.String()
}
// shouldEscape returns false if the byte is an unreserved
// character that should not be escaped and true otherwise,
// according to RFC 3986 2.1.
func shouldEscape(c byte) bool {
// RFC3986 2.3 unreserved characters
if 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' {
return false
}
switch c {
case '-', '.', '_', '~':
return false
}
// all other bytes must be escaped
return true
}

@ -0,0 +1,4 @@
// Copyright (c) 2015 Dalton Hubble. All rights reserved.
// Copyrights licensed under the MIT License.
package oauth1

@ -0,0 +1,65 @@
// 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 oauth1
import (
"net/http"
"github.com/drone/go-login/login"
)
// Handler returns a Handler that runs h at the completion
// of the oauth2 authorization flow.
func Handler(h http.Handler, c *Config) http.Handler {
return &handler{next: h, conf: c}
}
type handler struct {
conf *Config
next http.Handler
}
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
verifier := r.FormValue("oauth_verifier")
if verifier == "" {
token, err := h.conf.requestToken()
if err != nil {
ctx = login.WithError(ctx, err)
h.next.ServeHTTP(w, r.WithContext(ctx))
return
}
redirectTo, err := h.conf.authorizeRedirect(token.Token)
if err != nil {
ctx = login.WithError(ctx, err)
h.next.ServeHTTP(w, r.WithContext(ctx))
return
}
http.Redirect(w, r, redirectTo, 302)
return
}
token := r.FormValue("oauth_token")
// requests the access_token from the authorization server.
// If an error is encountered, write the error to the
// context and prceed with the next http.Handler in the chain.
accessToken, err := h.conf.authorizeToken(token, verifier)
if err != nil {
ctx = login.WithError(ctx, err)
h.next.ServeHTTP(w, r.WithContext(ctx))
return
}
// converts the oauth2 token type to the internal Token
// type and attaches to the context.
ctx = login.WithToken(ctx, &login.Token{
Access: accessToken.Token,
Refresh: accessToken.TokenSecret,
})
h.next.ServeHTTP(w, r.WithContext(ctx))
}

@ -0,0 +1,65 @@
// Copyright (c) 2015 Dalton Hubble. All rights reserved.
// Copyrights licensed under the MIT License.
package oauth1
import (
"crypto"
"crypto/hmac"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"encoding/base64"
"strings"
)
// A Signer signs messages to create signed OAuth1 Requests.
type Signer interface {
// Name returns the name of the signing method.
Name() string
// Sign signs the message using the given secret key.
Sign(key string, message string) (string, error)
}
// HMACSigner signs messages with an HMAC SHA1 digest, using the concatenated
// consumer secret and token secret as the key.
type HMACSigner struct {
ConsumerSecret string
}
// Name returns the HMAC-SHA1 method.
func (s *HMACSigner) Name() string {
return "HMAC-SHA1"
}
// Sign creates a concatenated consumer and token secret key and calculates
// the HMAC digest of the message. Returns the base64 encoded digest bytes.
func (s *HMACSigner) Sign(tokenSecret, message string) (string, error) {
signingKey := strings.Join([]string{s.ConsumerSecret, tokenSecret}, "&")
mac := hmac.New(sha1.New, []byte(signingKey))
mac.Write([]byte(message))
signatureBytes := mac.Sum(nil)
return base64.StdEncoding.EncodeToString(signatureBytes), nil
}
// RSASigner RSA PKCS1-v1_5 signs SHA1 digests of messages using the given
// RSA private key.
type RSASigner struct {
PrivateKey *rsa.PrivateKey
}
// Name returns the RSA-SHA1 method.
func (s *RSASigner) Name() string {
return "RSA-SHA1"
}
// Sign uses RSA PKCS1-v1_5 to sign a SHA1 digest of the given message. The
// tokenSecret is not used with this signing scheme.
func (s *RSASigner) Sign(tokenSecret, message string) (string, error) {
digest := sha1.Sum([]byte(message))
signature, err := rsa.SignPKCS1v15(rand.Reader, s.PrivateKey, crypto.SHA1, digest[:])
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(signature), nil
}

@ -0,0 +1,4 @@
// Copyright (c) 2015 Dalton Hubble. All rights reserved.
// Copyrights licensed under the MIT License.
package oauth1

@ -0,0 +1,125 @@
// 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
import (
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/drone/go-login/login/internal/oauth1"
)
const (
requestTokenURL = "%s/plugins/servlet/oauth/request-token"
authorizeTokenURL = "%s/plugins/servlet/oauth/authorize"
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)
// WithClient configures the authorization handler with a
// custom http.Client.
func WithClient(client *http.Client) Option {
return func(o *Options) {
o.client = client
}
}
// WithConsumerKey configures the authorization handler with
// the oauth_consumer_key.
func WithConsumerKey(consumerKey string) Option {
return func(o *Options) {
o.consumerKey = consumerKey
}
}
// WithConsumerSecret configures the authorization handler
// with the oauth_consumer_secret.
func WithConsumerSecret(consumerSecret string) Option {
return func(o *Options) {
o.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, "/")
}
}
// WithPrivateKeyFile configures the authorization handler
// with the oauth private rsa key for signing requests.
func WithPrivateKeyFile(path string) Option {
d, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
return WithPrivateKey(d)
}
// WithPrivateKey configures the authorization handler
// with the oauth private rsa key for signing requests.
func WithPrivateKey(data []byte) Option {
return func(o *Options) {
p, _ := pem.Decode(data)
k, err := x509.ParsePKCS1PrivateKey(p.Bytes)
if err != nil {
panic(err)
}
o.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)
}
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),
})
}
Loading…
Cancel
Save