From f15b27aadf5ebad06ae633a7dbd4eaf78c8ce6ca Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Fri, 26 Aug 2022 21:42:41 +0200 Subject: [PATCH] Remove own copy of oauth2 implementation (#1127) at some point (~7years ago) the oauth2 implementation was copied into the code-base and never touched. We only use it for gitlab the rest is already back using std. This migrates to the std oauth2 implementation --- server/remote/gitlab/gitlab.go | 71 +++--- shared/oauth2/oauth2.go | 451 --------------------------------- 2 files changed, 32 insertions(+), 490 deletions(-) delete mode 100644 shared/oauth2/oauth2.go diff --git a/server/remote/gitlab/gitlab.go b/server/remote/gitlab/gitlab.go index 62187abe1..2b14f65cb 100644 --- a/server/remote/gitlab/gitlab.go +++ b/server/remote/gitlab/gitlab.go @@ -18,7 +18,7 @@ import ( "context" "crypto/tls" "fmt" - "io/ioutil" + "io" "net/http" "net/url" "strings" @@ -26,13 +26,13 @@ import ( "github.com/rs/zerolog/log" "github.com/xanzy/go-gitlab" + "golang.org/x/oauth2" "github.com/woodpecker-ci/woodpecker/server" "github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/remote" "github.com/woodpecker-ci/woodpecker/server/remote/common" "github.com/woodpecker-ci/woodpecker/server/store" - "github.com/woodpecker-ci/woodpecker/shared/oauth2" "github.com/woodpecker-ci/woodpecker/shared/utils" ) @@ -75,21 +75,28 @@ func (g *Gitlab) Name() string { return "gitlab" } -func (g *Gitlab) oauth2Config() *oauth2.Config { +func (g *Gitlab) oauth2Config(ctx context.Context) (*oauth2.Config, context.Context) { return &oauth2.Config{ - ClientID: g.ClientID, - ClientSecret: g.ClientSecret, - Scope: defaultScope, - AuthURL: fmt.Sprintf("%s/oauth/authorize", g.URL), - TokenURL: fmt.Sprintf("%s/oauth/token", g.URL), - RedirectURL: fmt.Sprintf("%s/authorize", server.Config.Server.OAuthHost), - } + ClientID: g.ClientID, + ClientSecret: g.ClientSecret, + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("%s/oauth/authorize", g.URL), + TokenURL: fmt.Sprintf("%s/oauth/token", g.URL), + }, + Scopes: []string{defaultScope}, + RedirectURL: fmt.Sprintf("%s/authorize", server.Config.Server.OAuthHost), + }, + + context.WithValue(ctx, oauth2.HTTPClient, &http.Client{Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: g.SkipVerify}, + Proxy: http.ProxyFromEnvironment, + }}) } // Login authenticates the session and returns the // remote user details. func (g *Gitlab) Login(ctx context.Context, res http.ResponseWriter, req *http.Request) (*model.User, error) { - config := g.oauth2Config() + config, oauth2Ctx := g.oauth2Config(ctx) // get the OAuth errors if err := req.FormValue("error"); err != "" { @@ -103,19 +110,11 @@ func (g *Gitlab) Login(ctx context.Context, res http.ResponseWriter, req *http.R // get the OAuth code code := req.FormValue("code") if len(code) == 0 { - authCodeURL, err := config.AuthCodeURL("woodpecker") - if err != nil { - return nil, fmt.Errorf("authCodeURL error: %v", err) - } - http.Redirect(res, req, authCodeURL, http.StatusSeeOther) + http.Redirect(res, req, config.AuthCodeURL("woodpecker"), http.StatusSeeOther) return nil, nil } - trans := &oauth2.Transport{Config: config, Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: g.SkipVerify}, - Proxy: http.ProxyFromEnvironment, - }} - token, err := trans.Exchange(code) + token, err := config.Exchange(oauth2Ctx, code) if err != nil { return nil, fmt.Errorf("Error exchanging token. %s", err) } @@ -147,29 +146,23 @@ func (g *Gitlab) Login(ctx context.Context, res http.ResponseWriter, req *http.R // Refresh refreshes the Gitlab oauth2 access token. If the token is // refreshed the user is updated and a true value is returned. func (g *Gitlab) Refresh(ctx context.Context, user *model.User) (bool, error) { - config := g.oauth2Config() + config, oauth2Ctx := g.oauth2Config(ctx) config.RedirectURL = "" - trans := &oauth2.Transport{ - Config: config, - Token: &oauth2.Token{ - AccessToken: user.Token, - RefreshToken: user.Secret, - Expiry: time.Unix(user.Expiry, 0), - }, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: g.SkipVerify}, - Proxy: http.ProxyFromEnvironment, - }, - } + source := config.TokenSource(oauth2Ctx, &oauth2.Token{ + AccessToken: user.Token, + RefreshToken: user.Secret, + Expiry: time.Unix(user.Expiry, 0), + }) - if err := trans.Refresh(); err != nil { + token, err := source.Token() + if err != nil || len(token.AccessToken) == 0 { return false, err } - user.Token = trans.Token.AccessToken - user.Secret = trans.Token.RefreshToken - user.Expiry = trans.Token.Expiry.UTC().Unix() + user.Token = token.AccessToken + user.Secret = token.RefreshToken + user.Expiry = token.Expiry.UTC().Unix() return true, nil } @@ -556,7 +549,7 @@ func (g *Gitlab) Branches(ctx context.Context, user *model.User, repo *model.Rep // and returns the required data in a standard format. func (g *Gitlab) Hook(ctx context.Context, req *http.Request) (*model.Repo, *model.Build, error) { defer req.Body.Close() - payload, err := ioutil.ReadAll(req.Body) + payload, err := io.ReadAll(req.Body) if err != nil { return nil, nil, err } diff --git a/shared/oauth2/oauth2.go b/shared/oauth2/oauth2.go deleted file mode 100644 index 5e5ec2dd8..000000000 --- a/shared/oauth2/oauth2.go +++ /dev/null @@ -1,451 +0,0 @@ -// Copyright 2011 The goauth2 Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package oauth supports making OAuth2-authenticated HTTP requests. -// -// Example usage: -// -// // Specify your configuration. (typically as a global variable) -// var config = &oauth.Config{ -// ClientId: YOUR_CLIENT_ID, -// ClientSecret: YOUR_CLIENT_SECRET, -// Scope: "https://www.googleapis.com/auth/buzz", -// AuthURL: "https://accounts.google.com/o/oauth2/auth", -// TokenURL: "https://accounts.google.com/o/oauth2/token", -// RedirectURL: "http://you.example.org/handler", -// } -// -// // A landing page redirects to the OAuth provider to get the auth code. -// func landing(w http.ResponseWriter, r *http.Request) { -// http.Redirect(w, r, config.AuthCodeURL("foo"), http.StatusFound) -// } -// -// // The user will be redirected back to this handler, that takes the -// // "code" query parameter and Exchanges it for an access token. -// func handler(w http.ResponseWriter, r *http.Request) { -// t := &oauth.Transport{Config: config} -// t.Exchange(r.FormValue("code")) -// // The Transport now has a valid Token. Create an *http.Client -// // with which we can make authenticated API requests. -// c := t.Client() -// c.Post(...) -// // ... -// // btw, r.FormValue("state") == "foo" -// } -// -package oauth2 - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "mime" - "net/http" - "net/url" - "os" - "strconv" - "strings" - "sync" - "time" -) - -// OAuthError is the error type returned by many operations. -// -// In retrospect it should not exist. Don't depend on it. -type OAuthError struct { - prefix string - msg string -} - -func (oe OAuthError) Error() string { - return "OAuthError: " + oe.prefix + ": " + oe.msg -} - -// Cache specifies the methods that implement a Token cache. -type Cache interface { - Token() (*Token, error) - PutToken(*Token) error -} - -// CacheFile implements Cache. Its value is the name of the file in which -// the Token is stored in JSON format. -type CacheFile string - -func (f CacheFile) Token() (*Token, error) { - file, err := os.Open(string(f)) - if err != nil { - return nil, OAuthError{"CacheFile.Token", err.Error()} - } - defer file.Close() - tok := &Token{} - if err := json.NewDecoder(file).Decode(tok); err != nil { - return nil, OAuthError{"CacheFile.Token", err.Error()} - } - return tok, nil -} - -func (f CacheFile) PutToken(tok *Token) error { - file, err := os.OpenFile(string(f), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) - if err != nil { - return OAuthError{"CacheFile.PutToken", err.Error()} - } - if err := json.NewEncoder(file).Encode(tok); err != nil { - file.Close() - return OAuthError{"CacheFile.PutToken", err.Error()} - } - if err := file.Close(); err != nil { - return OAuthError{"CacheFile.PutToken", err.Error()} - } - return nil -} - -// Config is the configuration of an OAuth consumer. -type Config struct { - // ClientID is the OAuth client identifier used when communicating with - // the configured OAuth provider. - ClientID string - - // ClientSecret is the OAuth client secret used when communicating with - // the configured OAuth provider. - ClientSecret string - - // Scope identifies the level of access being requested. Multiple scope - // values should be provided as a space-delimited string. - Scope string - - // AuthURL is the URL the user will be directed to in order to grant - // access. - AuthURL string - - // TokenURL is the URL used to retrieve OAuth tokens. - TokenURL string - - // RedirectURL is the URL to which the user will be returned after - // granting (or denying) access. - RedirectURL string - - // TokenCache allows tokens to be cached for subsequent requests. - TokenCache Cache - - // AccessType is an OAuth extension that gets sent as the - // "access_type" field in the URL from AuthCodeURL. - // See https://developers.google.com/accounts/docs/OAuth2WebServer. - // It may be "online" (the default) or "offline". - // If your application needs to refresh access tokens when the - // user is not present at the browser, then use offline. This - // will result in your application obtaining a refresh token - // the first time your application exchanges an authorization - // code for a user. - AccessType string - - // ApprovalPrompt indicates whether the user should be - // re-prompted for consent. If set to "auto" (default) the - // user will be prompted only if they haven't previously - // granted consent and the code can only be exchanged for an - // access token. - // If set to "force" the user will always be prompted, and the - // code can be exchanged for a refresh token. - ApprovalPrompt string -} - -// Token contains an end-user's tokens. -// This is the data you must store to persist authentication. -type Token struct { - AccessToken string - RefreshToken string - Expiry time.Time // If zero the token has no (known) expiry time. - - // Extra optionally contains extra metadata from the server - // when updating a token. The only current key that may be - // populated is "id_token". It may be nil and will be - // initialized as needed. - Extra map[string]string -} - -// Expired reports whether the token has expired or is invalid. -func (t *Token) Expired() bool { - if t.AccessToken == "" { - return true - } - if t.Expiry.IsZero() { - return false - } - return t.Expiry.Before(time.Now()) -} - -// Transport implements http.RoundTripper. When configured with a valid -// Config and Token it can be used to make authenticated HTTP requests. -// -// t := &oauth.Transport{config} -// t.Exchange(code) -// // t now contains a valid Token -// r, _, err := t.Client().Get("http://example.org/url/requiring/auth") -// -// It will automatically refresh the Token if it can, -// updating the supplied Token in place. -type Transport struct { - *Config - *Token - - // mu guards modifying the token. - mu sync.Mutex - - // Transport is the HTTP transport to use when making requests. - // It will default to http.DefaultTransport if nil. - // (It should never be an oauth.Transport.) - Transport http.RoundTripper -} - -// Client returns an *http.Client that makes OAuth-authenticated requests. -func (t *Transport) Client() *http.Client { - return &http.Client{Transport: t} -} - -func (t *Transport) transport() http.RoundTripper { - if t.Transport != nil { - return t.Transport - } - return http.DefaultTransport -} - -// AuthCodeURL returns a URL that the end-user should be redirected to, -// so that they may obtain an authorization code. -func (c *Config) AuthCodeURL(state string) (string, error) { - _url, err := url.Parse(c.AuthURL) - if err != nil { - return "", fmt.Errorf("AuthURL malformed: %v", err) - } - if err := _url.Query().Get("error"); err != "" { - return "", fmt.Errorf("AuthURL contains error: %v", err) - } - q := url.Values{ - "response_type": {"code"}, - "client_id": {c.ClientID}, - "state": condVal(state), - "scope": condVal(c.Scope), - "redirect_uri": condVal(c.RedirectURL), - "access_type": condVal(c.AccessType), - "approval_prompt": condVal(c.ApprovalPrompt), - }.Encode() - if _url.RawQuery == "" { - _url.RawQuery = q - } else { - _url.RawQuery += "&" + q - } - return _url.String(), nil -} - -func condVal(v string) []string { - if v == "" { - return nil - } - return []string{v} -} - -// Exchange takes a code and gets access Token from the remote server. -func (t *Transport) Exchange(code string) (*Token, error) { - if t.Config == nil { - return nil, OAuthError{"Exchange", "no Config supplied"} - } - - // If the transport or the cache already has a token, it is - // passed to `updateToken` to preserve existing refresh token. - tok := t.Token - if tok == nil && t.TokenCache != nil { - tok, _ = t.TokenCache.Token() - } - if tok == nil { - tok = new(Token) - } - err := t.updateToken(tok, url.Values{ - "grant_type": {"authorization_code"}, - "redirect_uri": {t.RedirectURL}, - "scope": {t.Scope}, - "code": {code}, - }) - if err != nil { - return nil, err - } - t.Token = tok - if t.TokenCache != nil { - return tok, t.TokenCache.PutToken(tok) - } - return tok, nil -} - -// RoundTrip executes a single HTTP transaction using the Transport's -// Token as authorization headers. -// -// This method will attempt to renew the Token if it has expired and may return -// an error related to that Token renewal before attempting the client request. -// If the Token cannot be renewed a non-nil os.Error value will be returned. -// If the Token is invalid callers should expect HTTP-level errors, -// as indicated by the Response's StatusCode. -func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { - accessToken, err := t.getAccessToken() - if err != nil { - return nil, err - } - // To set the Authorization header, we must make a copy of the Request - // so that we don't modify the Request we were given. - // This is required by the specification of http.RoundTripper. - req = cloneRequest(req) - req.Header.Set("Authorization", "Bearer "+accessToken) - - // Make the HTTP request. - return t.transport().RoundTrip(req) -} - -func (t *Transport) getAccessToken() (string, error) { - t.mu.Lock() - defer t.mu.Unlock() - - if t.Token == nil { - if t.Config == nil { - return "", OAuthError{"RoundTrip", "no Config supplied"} - } - if t.TokenCache == nil { - return "", OAuthError{"RoundTrip", "no Token supplied"} - } - var err error - t.Token, err = t.TokenCache.Token() - if err != nil { - return "", err - } - } - - // Refresh the Token if it has expired. - if t.Expired() { - if err := t.Refresh(); err != nil { - return "", err - } - } - if t.AccessToken == "" { - return "", errors.New("no access token obtained from refresh") - } - return t.AccessToken, nil -} - -// cloneRequest returns a clone of the provided *http.Request. -// The clone is a shallow copy of the struct and its Header map. -func cloneRequest(r *http.Request) *http.Request { - // shallow copy of the struct - r2 := new(http.Request) - *r2 = *r - // deep copy of the Header - r2.Header = make(http.Header) - for k, s := range r.Header { - r2.Header[k] = s - } - return r2 -} - -// Refresh renews the Transport's AccessToken using its RefreshToken. -func (t *Transport) Refresh() error { - if t.Token == nil { - return OAuthError{"Refresh", "no existing Token"} - } - if t.RefreshToken == "" { - return OAuthError{"Refresh", "Token expired; no Refresh Token"} - } - if t.Config == nil { - return OAuthError{"Refresh", "no Config supplied"} - } - - err := t.updateToken(t.Token, url.Values{ - "grant_type": {"refresh_token"}, - "refresh_token": {t.RefreshToken}, - }) - if err != nil { - return err - } - if t.TokenCache != nil { - return t.TokenCache.PutToken(t.Token) - } - return nil -} - -// AuthenticateClient gets an access Token using the client_credentials grant -// type. -func (t *Transport) AuthenticateClient() error { - if t.Config == nil { - return OAuthError{"Exchange", "no Config supplied"} - } - if t.Token == nil { - t.Token = &Token{} - } - return t.updateToken(t.Token, url.Values{"grant_type": {"client_credentials"}}) -} - -// updateToken mutates both tok and v. -func (t *Transport) updateToken(tok *Token, v url.Values) error { - v.Set("client_id", t.ClientID) - v.Set("client_secret", t.ClientSecret) - client := &http.Client{Transport: t.transport()} - req, err := http.NewRequest("POST", t.TokenURL, strings.NewReader(v.Encode())) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.SetBasicAuth(t.ClientID, t.ClientSecret) - r, err := client.Do(req) - if err != nil { - return err - } - defer r.Body.Close() - if r.StatusCode != 200 { - return OAuthError{"updateToken", "Unexpected HTTP status " + r.Status} - } - var b struct { - Access string `json:"access_token"` - Refresh string `json:"refresh_token"` - ExpiresIn int64 `json:"expires_in"` // seconds - ID string `json:"id_token"` - } - - body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20)) - if err != nil { - return err - } - - content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) - switch content { - case "application/x-www-form-urlencoded", "text/plain": - vals, err := url.ParseQuery(string(body)) - if err != nil { - return err - } - - b.Access = vals.Get("access_token") - b.Refresh = vals.Get("refresh_token") - b.ExpiresIn, _ = strconv.ParseInt(vals.Get("expires_in"), 10, 64) - b.ID = vals.Get("id_token") - default: - if err = json.Unmarshal(body, &b); err != nil { - return fmt.Errorf("got bad response from server: %q", body) - } - } - if b.Access == "" { - return errors.New("received empty access token from authorization server") - } - tok.AccessToken = b.Access - // Don't overwrite `RefreshToken` with an empty value - if b.Refresh != "" { - tok.RefreshToken = b.Refresh - } - if b.ExpiresIn == 0 { - tok.Expiry = time.Time{} - } else { - tok.Expiry = time.Now().Add(time.Duration(b.ExpiresIn) * time.Second) - } - if b.ID != "" { - if tok.Extra == nil { - tok.Extra = make(map[string]string) - } - tok.Extra["id_token"] = b.ID - } - return nil -}