add gitea and stash login providers
parent
75c3d8669d
commit
c5678d82b0
@ -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…
Reference in New Issue