mirror of https://github.com/k3s-io/k3s
Merge pull request #8835 from jlowdermilk/release-notes
Optionally use a github api token when compiling relase-notespull/6/head
commit
8d1481ab4a
|
@ -475,7 +475,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "golang.org/x/oauth2",
|
"ImportPath": "golang.org/x/oauth2",
|
||||||
"Rev": "2e66694fea36dc820636630792a55cdc6987e05b"
|
"Rev": "b5adcc2dcdf009d0391547edc6ecbaff889f5bb9"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "google.golang.org/appengine",
|
"ImportPath": "google.golang.org/appengine",
|
||||||
|
|
|
@ -8,7 +8,7 @@ install:
|
||||||
- export GOPATH="$HOME/gopath"
|
- export GOPATH="$HOME/gopath"
|
||||||
- mkdir -p "$GOPATH/src/golang.org/x"
|
- mkdir -p "$GOPATH/src/golang.org/x"
|
||||||
- mv "$TRAVIS_BUILD_DIR" "$GOPATH/src/golang.org/x/oauth2"
|
- mv "$TRAVIS_BUILD_DIR" "$GOPATH/src/golang.org/x/oauth2"
|
||||||
- go get -v -t -d -tags='appengine appenginevm' golang.org/x/oauth2/...
|
- go get -v -t -d golang.org/x/oauth2/...
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- go test -v -tags='appengine appenginevm' golang.org/x/oauth2/...
|
- go test -v golang.org/x/oauth2/...
|
||||||
|
|
|
@ -1,25 +1,31 @@
|
||||||
# Contributing
|
# Contributing to Go
|
||||||
|
|
||||||
We don't use GitHub pull requests but use Gerrit for code reviews,
|
Go is an open source project.
|
||||||
similar to the Go project.
|
|
||||||
|
|
||||||
1. Sign one of the contributor license agreements below.
|
It is the work of hundreds of contributors. We appreciate your help!
|
||||||
2. `go get golang.org/x/review/git-codereview` to install the code reviewing tool.
|
|
||||||
3. Get the package by running `go get -d golang.org/x/oauth2`.
|
|
||||||
Make changes and create a change by running `git codereview change <name>`, provide a command message, and use `git codereview mail` to create a Gerrit CL.
|
|
||||||
Keep amending to the change and mail as your recieve feedback.
|
|
||||||
|
|
||||||
For more information about the workflow, see Go's [Contribution Guidelines](https://golang.org/doc/contribute.html).
|
|
||||||
|
|
||||||
Before we can accept any pull requests
|
## Filing issues
|
||||||
we have to jump through a couple of legal hurdles,
|
|
||||||
primarily a Contributor License Agreement (CLA):
|
|
||||||
|
|
||||||
- **If you are an individual writing original source code**
|
When [filing an issue](https://github.com/golang/oauth2/issues), make sure to answer these five questions:
|
||||||
and you're sure you own the intellectual property,
|
|
||||||
then you'll need to sign an [individual CLA](http://code.google.com/legal/individual-cla-v1.0.html).
|
1. What version of Go are you using (`go version`)?
|
||||||
- **If you work for a company that wants to allow you to contribute your work**,
|
2. What operating system and processor architecture are you using?
|
||||||
then you'll need to sign a [corporate CLA](http://code.google.com/legal/corporate-cla-v1.0.html).
|
3. What did you do?
|
||||||
|
4. What did you expect to see?
|
||||||
|
5. What did you see instead?
|
||||||
|
|
||||||
|
General questions should go to the [golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead of the issue tracker.
|
||||||
|
The gophers there will answer or ask you to file an issue if you've tripped over a bug.
|
||||||
|
|
||||||
|
## Contributing code
|
||||||
|
|
||||||
|
Please read the [Contribution Guidelines](https://golang.org/doc/contribute.html)
|
||||||
|
before sending patches.
|
||||||
|
|
||||||
|
**We do not accept GitHub pull requests**
|
||||||
|
(we use [Gerrit](https://code.google.com/p/gerrit/) instead for code review).
|
||||||
|
|
||||||
|
Unless otherwise noted, the Go source files are distributed under
|
||||||
|
the BSD-style license found in the LICENSE file.
|
||||||
|
|
||||||
You can sign these electronically (just scroll to the bottom).
|
|
||||||
After that, we'll be able to accept your pull requests.
|
|
||||||
|
|
|
@ -16,3 +16,49 @@ See godoc for further documentation and examples.
|
||||||
* [godoc.org/golang.org/x/oauth2/google](http://godoc.org/golang.org/x/oauth2/google)
|
* [godoc.org/golang.org/x/oauth2/google](http://godoc.org/golang.org/x/oauth2/google)
|
||||||
|
|
||||||
|
|
||||||
|
## App Engine
|
||||||
|
|
||||||
|
In change 96e89be (March 2015) we removed the `oauth2.Context2` type in favor
|
||||||
|
of the [`context.Context`](https://golang.org/x/net/context#Context) type from
|
||||||
|
the `golang.org/x/net/context` package
|
||||||
|
|
||||||
|
This means its no longer possible to use the "Classic App Engine"
|
||||||
|
`appengine.Context` type with the `oauth2` package. (You're using
|
||||||
|
Classic App Engine if you import the package `"appengine"`.)
|
||||||
|
|
||||||
|
To work around this, you may use the new `"google.golang.org/appengine"`
|
||||||
|
package. This package has almost the same API as the `"appengine"` package,
|
||||||
|
but it can be fetched with `go get` and used on "Managed VMs" and well as
|
||||||
|
Classic App Engine.
|
||||||
|
|
||||||
|
See the [new `appengine` package's readme](https://github.com/golang/appengine#updating-a-go-app-engine-app)
|
||||||
|
for information on updating your app.
|
||||||
|
|
||||||
|
If you don't want to update your entire app to use the new App Engine packages,
|
||||||
|
you may use both sets of packages in parallel, using only the new packages
|
||||||
|
with the `oauth2` package.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/google"
|
||||||
|
newappengine "google.golang.org/appengine"
|
||||||
|
newurlfetch "google.golang.org/appengine/urlfetch"
|
||||||
|
|
||||||
|
"appengine"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var c appengine.Context = appengine.NewContext(r)
|
||||||
|
c.Infof("Logging a message with the old package")
|
||||||
|
|
||||||
|
var ctx context.Context = newappengine.NewContext(r)
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: &oauth2.Transport{
|
||||||
|
Source: google.AppEngineTokenSource(ctx, "scope"),
|
||||||
|
Base: &newurlfetch.Transport{Context: ctx},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
client.Get("...")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,38 +2,24 @@
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// +build appengine,!appenginevm
|
// +build appengine appenginevm
|
||||||
|
|
||||||
// App Engine hooks.
|
// App Engine hooks.
|
||||||
|
|
||||||
package oauth2
|
package oauth2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"appengine"
|
"golang.org/x/net/context"
|
||||||
"appengine/urlfetch"
|
"golang.org/x/oauth2/internal"
|
||||||
|
"google.golang.org/appengine/urlfetch"
|
||||||
)
|
)
|
||||||
|
|
||||||
var warnOnce sync.Once
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
registerContextClientFunc(contextClientAppEngine)
|
internal.RegisterContextClientFunc(contextClientAppEngine)
|
||||||
}
|
}
|
||||||
|
|
||||||
func contextClientAppEngine(ctx Context) (*http.Client, error) {
|
func contextClientAppEngine(ctx context.Context) (*http.Client, error) {
|
||||||
if actx, ok := ctx.(appengine.Context); ok {
|
return urlfetch.Client(ctx), nil
|
||||||
return urlfetch.Client(actx), nil
|
|
||||||
}
|
|
||||||
// The user did it wrong. We'll log once (and hope they see it
|
|
||||||
// in dev_appserver), but stil return (nil, nil) in case some
|
|
||||||
// other contextClientFunc hook finds a way to proceed.
|
|
||||||
warnOnce.Do(gaeDoingItWrongHelp)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func gaeDoingItWrongHelp() {
|
|
||||||
log.Printf("WARNING: you attempted to use the oauth2 package without passing a valid appengine.Context or *http.Request as the oauth2.Context. App Engine requires that all service RPCs (including urlfetch) be associated with an *http.Request/appengine.Context.")
|
|
||||||
}
|
}
|
||||||
|
|
112
Godeps/_workspace/src/golang.org/x/oauth2/clientcredentials/clientcredentials.go
generated
vendored
Normal file
112
Godeps/_workspace/src/golang.org/x/oauth2/clientcredentials/clientcredentials.go
generated
vendored
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
// Copyright 2014 The oauth2 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 clientcredentials implements the OAuth2.0 "client credentials" token flow,
|
||||||
|
// also known as the "two-legged OAuth 2.0".
|
||||||
|
//
|
||||||
|
// This should be used when the client is acting on its own behalf or when the client
|
||||||
|
// is the resource owner. It may also be used when requesting access to protected
|
||||||
|
// resources based on an authorization previously arranged with the authorization
|
||||||
|
// server.
|
||||||
|
//
|
||||||
|
// See http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.4
|
||||||
|
package clientcredentials
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// tokenFromInternal maps an *internal.Token struct into
|
||||||
|
// an *oauth2.Token struct.
|
||||||
|
func tokenFromInternal(t *internal.Token) *oauth2.Token {
|
||||||
|
if t == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tk := &oauth2.Token{
|
||||||
|
AccessToken: t.AccessToken,
|
||||||
|
TokenType: t.TokenType,
|
||||||
|
RefreshToken: t.RefreshToken,
|
||||||
|
Expiry: t.Expiry,
|
||||||
|
}
|
||||||
|
return tk.WithExtra(t.Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrieveToken takes a *Config and uses that to retrieve an *internal.Token.
|
||||||
|
// This token is then mapped from *internal.Token into an *oauth2.Token which is
|
||||||
|
// returned along with an error.
|
||||||
|
func retrieveToken(ctx context.Context, c *Config, v url.Values) (*oauth2.Token, error) {
|
||||||
|
tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.TokenURL, v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tokenFromInternal(tk), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client Credentials Config describes a 2-legged OAuth2 flow, with both the
|
||||||
|
// client application information and the server's endpoint URLs.
|
||||||
|
type Config struct {
|
||||||
|
// ClientID is the application's ID.
|
||||||
|
ClientID string
|
||||||
|
|
||||||
|
// ClientSecret is the application's secret.
|
||||||
|
ClientSecret string
|
||||||
|
|
||||||
|
// TokenURL is the resource server's token endpoint
|
||||||
|
// URL. This is a constant specific to each server.
|
||||||
|
TokenURL string
|
||||||
|
|
||||||
|
// Scope specifies optional requested permissions.
|
||||||
|
Scopes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token uses client credentials to retreive a token.
|
||||||
|
// The HTTP client to use is derived from the context.
|
||||||
|
// If nil, http.DefaultClient is used.
|
||||||
|
func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) {
|
||||||
|
return retrieveToken(ctx, c, url.Values{
|
||||||
|
"grant_type": {"client_credentials"},
|
||||||
|
"scope": internal.CondVal(strings.Join(c.Scopes, " ")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client returns an HTTP client using the provided token.
|
||||||
|
// The token will auto-refresh as necessary. The underlying
|
||||||
|
// HTTP transport will be obtained using the provided context.
|
||||||
|
// The returned client and its Transport should not be modified.
|
||||||
|
func (c *Config) Client(ctx context.Context) *http.Client {
|
||||||
|
return oauth2.NewClient(ctx, c.TokenSource(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenSource returns a TokenSource that returns t until t expires,
|
||||||
|
// automatically refreshing it as necessary using the provided context and the
|
||||||
|
// client ID and client secret.
|
||||||
|
//
|
||||||
|
// Most users will use Config.Client instead.
|
||||||
|
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
|
||||||
|
source := &tokenSource{
|
||||||
|
ctx: ctx,
|
||||||
|
conf: c,
|
||||||
|
}
|
||||||
|
return oauth2.ReuseTokenSource(nil, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
type tokenSource struct {
|
||||||
|
ctx context.Context
|
||||||
|
conf *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token refreshes the token by using a new client credentials request.
|
||||||
|
// tokens received this way do not include a refresh token
|
||||||
|
func (c *tokenSource) Token() (*oauth2.Token, error) {
|
||||||
|
return retrieveToken(c.ctx, c.conf, url.Values{
|
||||||
|
"grant_type": {"client_credentials"},
|
||||||
|
"scope": internal.CondVal(strings.Join(c.conf.Scopes, " ")),
|
||||||
|
})
|
||||||
|
}
|
96
Godeps/_workspace/src/golang.org/x/oauth2/clientcredentials/clientcredentials_test.go
generated
vendored
Normal file
96
Godeps/_workspace/src/golang.org/x/oauth2/clientcredentials/clientcredentials_test.go
generated
vendored
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
// Copyright 2014 The oauth2 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 clientcredentials
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newConf(url string) *Config {
|
||||||
|
return &Config{
|
||||||
|
ClientID: "CLIENT_ID",
|
||||||
|
ClientSecret: "CLIENT_SECRET",
|
||||||
|
Scopes: []string{"scope1", "scope2"},
|
||||||
|
TokenURL: url + "/token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockTransport struct {
|
||||||
|
rt func(req *http.Request) (resp *http.Response, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||||
|
return t.rt(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenRequest(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.String() != "/token" {
|
||||||
|
t.Errorf("authenticate client request URL = %q; want %q", r.URL, "/token")
|
||||||
|
}
|
||||||
|
headerAuth := r.Header.Get("Authorization")
|
||||||
|
if headerAuth != "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ=" {
|
||||||
|
t.Errorf("Unexpected authorization header, %v is found.", headerAuth)
|
||||||
|
}
|
||||||
|
if got, want := r.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; got != want {
|
||||||
|
t.Errorf("Content-Type header = %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
r.Body.Close()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed reading request body: %s.", err)
|
||||||
|
}
|
||||||
|
if string(body) != "client_id=CLIENT_ID&grant_type=client_credentials&scope=scope1+scope2" {
|
||||||
|
t.Errorf("payload = %q; want %q", string(body), "client_id=CLIENT_ID&grant_type=client_credentials&scope=scope1+scope2")
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w.Write([]byte("access_token=90d64460d14870c08c81352a05dedd3465940a7c&token_type=bearer"))
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
conf := newConf(ts.URL)
|
||||||
|
tok, err := conf.Token(oauth2.NoContext)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if !tok.Valid() {
|
||||||
|
t.Fatalf("token invalid. got: %#v", tok)
|
||||||
|
}
|
||||||
|
if tok.AccessToken != "90d64460d14870c08c81352a05dedd3465940a7c" {
|
||||||
|
t.Errorf("Access token = %q; want %q", tok.AccessToken, "90d64460d14870c08c81352a05dedd3465940a7c")
|
||||||
|
}
|
||||||
|
if tok.TokenType != "bearer" {
|
||||||
|
t.Errorf("token type = %q; want %q", tok.TokenType, "bearer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenRefreshRequest(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.String() == "/somethingelse" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.URL.String() != "/token" {
|
||||||
|
t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL)
|
||||||
|
}
|
||||||
|
headerContentType := r.Header.Get("Content-Type")
|
||||||
|
if headerContentType != "application/x-www-form-urlencoded" {
|
||||||
|
t.Errorf("Unexpected Content-Type header, %v is found.", headerContentType)
|
||||||
|
}
|
||||||
|
body, _ := ioutil.ReadAll(r.Body)
|
||||||
|
if string(body) != "client_id=CLIENT_ID&grant_type=client_credentials&scope=scope1+scope2" {
|
||||||
|
t.Errorf("Unexpected refresh token payload, %v is found.", string(body))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
conf := newConf(ts.URL)
|
||||||
|
c := conf.Client(oauth2.NoContext)
|
||||||
|
c.Get(ts.URL + "/somethingelse")
|
||||||
|
}
|
|
@ -7,15 +7,10 @@ package oauth2_test
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"testing"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(jbd): Remove after Go 1.4.
|
|
||||||
// Related to https://codereview.appspot.com/107320046
|
|
||||||
func TestA(t *testing.T) {}
|
|
||||||
|
|
||||||
func ExampleConfig() {
|
func ExampleConfig() {
|
||||||
conf := &oauth2.Config{
|
conf := &oauth2.Config{
|
||||||
ClientID: "YOUR_CLIENT_ID",
|
ClientID: "YOUR_CLIENT_ID",
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
// Copyright 2015 The oauth2 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 facebook provides constants for using OAuth2 to access Facebook.
|
||||||
|
package facebook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Endpoint is Facebook's OAuth 2.0 endpoint.
|
||||||
|
var Endpoint = oauth2.Endpoint{
|
||||||
|
AuthURL: "https://www.facebook.com/dialog/oauth",
|
||||||
|
TokenURL: "https://graph.facebook.com/oauth/access_token",
|
||||||
|
}
|
|
@ -2,36 +2,82 @@
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// +build appengine,!appenginevm
|
|
||||||
|
|
||||||
package google
|
package google
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"appengine"
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Set at init time by appengine_hook.go. If nil, we're not on App Engine.
|
||||||
|
var appengineTokenFunc func(c context.Context, scopes ...string) (token string, expiry time.Time, err error)
|
||||||
|
|
||||||
// AppEngineTokenSource returns a token source that fetches tokens
|
// AppEngineTokenSource returns a token source that fetches tokens
|
||||||
// issued to the current App Engine application's service account.
|
// issued to the current App Engine application's service account.
|
||||||
// If you are implementing a 3-legged OAuth 2.0 flow on App Engine
|
// If you are implementing a 3-legged OAuth 2.0 flow on App Engine
|
||||||
// that involves user accounts, see oauth2.Config instead.
|
// that involves user accounts, see oauth2.Config instead.
|
||||||
//
|
//
|
||||||
// You are required to provide a valid appengine.Context as context.
|
// The provided context must have come from appengine.NewContext.
|
||||||
func AppEngineTokenSource(ctx appengine.Context, scope ...string) oauth2.TokenSource {
|
func AppEngineTokenSource(ctx context.Context, scope ...string) oauth2.TokenSource {
|
||||||
|
if appengineTokenFunc == nil {
|
||||||
|
panic("google: AppEngineTokenSource can only be used on App Engine.")
|
||||||
|
}
|
||||||
|
scopes := append([]string{}, scope...)
|
||||||
|
sort.Strings(scopes)
|
||||||
return &appEngineTokenSource{
|
return &appEngineTokenSource{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
scopes: scope,
|
scopes: scopes,
|
||||||
fetcherFunc: aeFetcherFunc,
|
key: strings.Join(scopes, " "),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var aeFetcherFunc = func(ctx oauth2.Context, scope ...string) (string, time.Time, error) {
|
// aeTokens helps the fetched tokens to be reused until their expiration.
|
||||||
c, ok := ctx.(appengine.Context)
|
var (
|
||||||
if !ok {
|
aeTokensMu sync.Mutex
|
||||||
return "", time.Time{}, errInvalidContext
|
aeTokens = make(map[string]*tokenLock) // key is space-separated scopes
|
||||||
}
|
)
|
||||||
return appengine.AccessToken(c, scope...)
|
|
||||||
|
type tokenLock struct {
|
||||||
|
mu sync.Mutex // guards t; held while fetching or updating t
|
||||||
|
t *oauth2.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
type appEngineTokenSource struct {
|
||||||
|
ctx context.Context
|
||||||
|
scopes []string
|
||||||
|
key string // to aeTokens map; space-separated scopes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *appEngineTokenSource) Token() (*oauth2.Token, error) {
|
||||||
|
if appengineTokenFunc == nil {
|
||||||
|
panic("google: AppEngineTokenSource can only be used on App Engine.")
|
||||||
|
}
|
||||||
|
|
||||||
|
aeTokensMu.Lock()
|
||||||
|
tok, ok := aeTokens[ts.key]
|
||||||
|
if !ok {
|
||||||
|
tok = &tokenLock{}
|
||||||
|
aeTokens[ts.key] = tok
|
||||||
|
}
|
||||||
|
aeTokensMu.Unlock()
|
||||||
|
|
||||||
|
tok.mu.Lock()
|
||||||
|
defer tok.mu.Unlock()
|
||||||
|
if tok.t.Valid() {
|
||||||
|
return tok.t, nil
|
||||||
|
}
|
||||||
|
access, exp, err := appengineTokenFunc(ts.ctx, ts.scopes...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tok.t = &oauth2.Token{
|
||||||
|
AccessToken: access,
|
||||||
|
Expiry: exp,
|
||||||
|
}
|
||||||
|
return tok.t, nil
|
||||||
}
|
}
|
||||||
|
|
13
Godeps/_workspace/src/golang.org/x/oauth2/google/appengine_hook.go
generated
vendored
Normal file
13
Godeps/_workspace/src/golang.org/x/oauth2/google/appengine_hook.go
generated
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright 2015 The oauth2 Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build appengine appenginevm
|
||||||
|
|
||||||
|
package google
|
||||||
|
|
||||||
|
import "google.golang.org/appengine"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
appengineTokenFunc = appengine.AccessToken
|
||||||
|
}
|
|
@ -1,36 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build appenginevm !appengine
|
|
||||||
|
|
||||||
package google
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"google.golang.org/appengine"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AppEngineTokenSource returns a token source that fetches tokens
|
|
||||||
// issued to the current App Engine application's service account.
|
|
||||||
// If you are implementing a 3-legged OAuth 2.0 flow on App Engine
|
|
||||||
// that involves user accounts, see oauth2.Config instead.
|
|
||||||
//
|
|
||||||
// You are required to provide a valid appengine.Context as context.
|
|
||||||
func AppEngineTokenSource(ctx appengine.Context, scope ...string) oauth2.TokenSource {
|
|
||||||
return &appEngineTokenSource{
|
|
||||||
ctx: ctx,
|
|
||||||
scopes: scope,
|
|
||||||
fetcherFunc: aeVMFetcherFunc,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var aeVMFetcherFunc = func(ctx oauth2.Context, scope ...string) (string, time.Time, error) {
|
|
||||||
c, ok := ctx.(appengine.Context)
|
|
||||||
if !ok {
|
|
||||||
return "", time.Time{}, errInvalidContext
|
|
||||||
}
|
|
||||||
return appengine.AccessToken(c, scope...)
|
|
||||||
}
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
// Copyright 2015 The oauth2 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 google
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/jwt"
|
||||||
|
"google.golang.org/cloud/compute/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultClient returns an HTTP Client that uses the
|
||||||
|
// DefaultTokenSource to obtain authentication credentials.
|
||||||
|
//
|
||||||
|
// This client should be used when developing services
|
||||||
|
// that run on Google App Engine or Google Compute Engine
|
||||||
|
// and use "Application Default Credentials."
|
||||||
|
//
|
||||||
|
// For more details, see:
|
||||||
|
// https://developers.google.com/accounts/docs/application-default-credentials
|
||||||
|
//
|
||||||
|
func DefaultClient(ctx context.Context, scope ...string) (*http.Client, error) {
|
||||||
|
ts, err := DefaultTokenSource(ctx, scope...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return oauth2.NewClient(ctx, ts), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultTokenSource is a token source that uses
|
||||||
|
// "Application Default Credentials".
|
||||||
|
//
|
||||||
|
// It looks for credentials in the following places,
|
||||||
|
// preferring the first location found:
|
||||||
|
//
|
||||||
|
// 1. A JSON file whose path is specified by the
|
||||||
|
// GOOGLE_APPLICATION_CREDENTIALS environment variable.
|
||||||
|
// 2. A JSON file in a location known to the gcloud command-line tool.
|
||||||
|
// On Windows, this is %APPDATA%/gcloud/application_default_credentials.json.
|
||||||
|
// On other systems, $HOME/.config/gcloud/application_default_credentials.json.
|
||||||
|
// 3. On Google App Engine it uses the appengine.AccessToken function.
|
||||||
|
// 4. On Google Compute Engine, it fetches credentials from the metadata server.
|
||||||
|
// (In this final case any provided scopes are ignored.)
|
||||||
|
//
|
||||||
|
// For more details, see:
|
||||||
|
// https://developers.google.com/accounts/docs/application-default-credentials
|
||||||
|
//
|
||||||
|
func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSource, error) {
|
||||||
|
// First, try the environment variable.
|
||||||
|
const envVar = "GOOGLE_APPLICATION_CREDENTIALS"
|
||||||
|
if filename := os.Getenv(envVar); filename != "" {
|
||||||
|
ts, err := tokenSourceFromFile(ctx, filename, scope)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("google: error getting credentials using %v environment variable: %v", envVar, err)
|
||||||
|
}
|
||||||
|
return ts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second, try a well-known file.
|
||||||
|
filename := wellKnownFile()
|
||||||
|
_, err := os.Stat(filename)
|
||||||
|
if err == nil {
|
||||||
|
ts, err2 := tokenSourceFromFile(ctx, filename, scope)
|
||||||
|
if err2 == nil {
|
||||||
|
return ts, nil
|
||||||
|
}
|
||||||
|
err = err2
|
||||||
|
} else if os.IsNotExist(err) {
|
||||||
|
err = nil // ignore this error
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("google: error getting credentials using well-known file (%v): %v", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third, if we're on Google App Engine use those credentials.
|
||||||
|
if appengineTokenFunc != nil {
|
||||||
|
return AppEngineTokenSource(ctx, scope...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fourth, if we're on Google Compute Engine use the metadata server.
|
||||||
|
if metadata.OnGCE() {
|
||||||
|
return ComputeTokenSource(""), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// None are found; return helpful error.
|
||||||
|
const url = "https://developers.google.com/accounts/docs/application-default-credentials"
|
||||||
|
return nil, fmt.Errorf("google: could not find default credentials. See %v for more information.", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wellKnownFile() string {
|
||||||
|
const f = "application_default_credentials.json"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return filepath.Join(os.Getenv("APPDATA"), "gcloud", f)
|
||||||
|
}
|
||||||
|
return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenSourceFromFile(ctx context.Context, filename string, scopes []string) (oauth2.TokenSource, error) {
|
||||||
|
b, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var d struct {
|
||||||
|
// Common fields
|
||||||
|
Type string
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
|
||||||
|
// User Credential fields
|
||||||
|
ClientSecret string `json:"client_secret"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
|
||||||
|
// Service Account fields
|
||||||
|
ClientEmail string `json:"client_email"`
|
||||||
|
PrivateKeyID string `json:"private_key_id"`
|
||||||
|
PrivateKey string `json:"private_key"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(b, &d); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch d.Type {
|
||||||
|
case "authorized_user":
|
||||||
|
cfg := &oauth2.Config{
|
||||||
|
ClientID: d.ClientID,
|
||||||
|
ClientSecret: d.ClientSecret,
|
||||||
|
Scopes: append([]string{}, scopes...), // copy
|
||||||
|
Endpoint: Endpoint,
|
||||||
|
}
|
||||||
|
tok := &oauth2.Token{RefreshToken: d.RefreshToken}
|
||||||
|
return cfg.TokenSource(ctx, tok), nil
|
||||||
|
case "service_account":
|
||||||
|
cfg := &jwt.Config{
|
||||||
|
Email: d.ClientEmail,
|
||||||
|
PrivateKey: []byte(d.PrivateKey),
|
||||||
|
Scopes: append([]string{}, scopes...), // copy
|
||||||
|
TokenURL: JWTTokenURL,
|
||||||
|
}
|
||||||
|
return cfg.TokenSource(ctx), nil
|
||||||
|
case "":
|
||||||
|
return nil, errors.New("missing 'type' field in credentials")
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown credential type: %q", d.Type)
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,6 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/oauth2/google"
|
"golang.org/x/oauth2/google"
|
||||||
|
@ -20,9 +19,14 @@ import (
|
||||||
"google.golang.org/appengine/urlfetch"
|
"google.golang.org/appengine/urlfetch"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Remove after Go 1.4.
|
func ExampleDefaultClient() {
|
||||||
// Related to https://codereview.appspot.com/107320046
|
client, err := google.DefaultClient(oauth2.NoContext,
|
||||||
func TestA(t *testing.T) {}
|
"https://www.googleapis.com/auth/devstorage.full_control")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
client.Get("...")
|
||||||
|
}
|
||||||
|
|
||||||
func Example_webServer() {
|
func Example_webServer() {
|
||||||
// Your credentials should be obtained from the Google
|
// Your credentials should be obtained from the Google
|
||||||
|
@ -74,6 +78,19 @@ func ExampleJWTConfigFromJSON() {
|
||||||
client.Get("...")
|
client.Get("...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExampleSDKConfig() {
|
||||||
|
// The credentials will be obtained from the first account that
|
||||||
|
// has been authorized with `gcloud auth login`.
|
||||||
|
conf, err := google.NewSDKConfig("")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
// Initiate an http.Client. The following GET request will be
|
||||||
|
// authorized and authenticated on the behalf of the SDK user.
|
||||||
|
client := conf.Client(oauth2.NoContext)
|
||||||
|
client.Get("...")
|
||||||
|
}
|
||||||
|
|
||||||
func Example_serviceAccount() {
|
func Example_serviceAccount() {
|
||||||
// Your credentials should be obtained from the Google
|
// Your credentials should be obtained from the Google
|
||||||
// Developer Console (https://console.developers.google.com).
|
// Developer Console (https://console.developers.google.com).
|
||||||
|
|
|
@ -2,15 +2,16 @@
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// Package google provides support for making
|
// Package google provides support for making OAuth2 authorized and
|
||||||
// OAuth2 authorized and authenticated HTTP requests
|
// authenticated HTTP requests to Google APIs.
|
||||||
// to Google APIs. It supports Web server, client-side,
|
// It supports the Web server flow, client-side credentials, service accounts,
|
||||||
// service accounts, Google Compute Engine service accounts,
|
// Google Compute Engine service accounts, and Google App Engine service
|
||||||
// and Google App Engine service accounts authorization
|
// accounts.
|
||||||
// and authentications flows:
|
|
||||||
//
|
//
|
||||||
// For more information, please read
|
// For more information, please read
|
||||||
// https://developers.google.com/accounts/docs/OAuth2.
|
// https://developers.google.com/accounts/docs/OAuth2
|
||||||
|
// and
|
||||||
|
// https://developers.google.com/accounts/docs/application-default-credentials.
|
||||||
package google
|
package google
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -25,9 +26,6 @@ import (
|
||||||
"google.golang.org/cloud/compute/metadata"
|
"google.golang.org/cloud/compute/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(bradfitz,jbd): import "google.golang.org/cloud/compute/metadata" instead of
|
|
||||||
// the metaClient and metadata.google.internal stuff below.
|
|
||||||
|
|
||||||
// Endpoint is Google's OAuth 2.0 endpoint.
|
// Endpoint is Google's OAuth 2.0 endpoint.
|
||||||
var Endpoint = oauth2.Endpoint{
|
var Endpoint = oauth2.Endpoint{
|
||||||
AuthURL: "https://accounts.google.com/o/oauth2/auth",
|
AuthURL: "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
@ -37,6 +35,50 @@ var Endpoint = oauth2.Endpoint{
|
||||||
// JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow.
|
// JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow.
|
||||||
const JWTTokenURL = "https://accounts.google.com/o/oauth2/token"
|
const JWTTokenURL = "https://accounts.google.com/o/oauth2/token"
|
||||||
|
|
||||||
|
// ConfigFromJSON uses a Google Developers Console client_credentials.json
|
||||||
|
// file to construct a config.
|
||||||
|
// client_credentials.json can be downloadable from https://console.developers.google.com,
|
||||||
|
// under "APIs & Auth" > "Credentials". Download the Web application credentials in the
|
||||||
|
// JSON format and provide the contents of the file as jsonKey.
|
||||||
|
func ConfigFromJSON(jsonKey []byte, scope ...string) (*oauth2.Config, error) {
|
||||||
|
type cred struct {
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
ClientSecret string `json:"client_secret"`
|
||||||
|
RedirectURIs []string `json:"redirect_uris"`
|
||||||
|
AuthURI string `json:"auth_uri"`
|
||||||
|
TokenURI string `json:"token_uri"`
|
||||||
|
}
|
||||||
|
var j struct {
|
||||||
|
Web *cred `json:"web"`
|
||||||
|
Installed *cred `json:"installed"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(jsonKey, &j); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var c *cred
|
||||||
|
switch {
|
||||||
|
case j.Web != nil:
|
||||||
|
c = j.Web
|
||||||
|
case j.Installed != nil:
|
||||||
|
c = j.Installed
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("oauth2/google: no credentials found")
|
||||||
|
}
|
||||||
|
if len(c.RedirectURIs) < 1 {
|
||||||
|
return nil, errors.New("oauth2/google: missing redirect URL in the client_credentials.json")
|
||||||
|
}
|
||||||
|
return &oauth2.Config{
|
||||||
|
ClientID: c.ClientID,
|
||||||
|
ClientSecret: c.ClientSecret,
|
||||||
|
RedirectURL: c.RedirectURIs[0],
|
||||||
|
Scopes: scope,
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: c.AuthURI,
|
||||||
|
TokenURL: c.TokenURI,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// JWTConfigFromJSON uses a Google Developers service account JSON key file to read
|
// JWTConfigFromJSON uses a Google Developers service account JSON key file to read
|
||||||
// the credentials that authorize and authenticate the requests.
|
// the credentials that authorize and authenticate the requests.
|
||||||
// Create a service account on "Credentials" page under "APIs & Auth" for your
|
// Create a service account on "Credentials" page under "APIs & Auth" for your
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright 2015 The oauth2 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 google
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var webJSONKey = []byte(`
|
||||||
|
{
|
||||||
|
"web": {
|
||||||
|
"auth_uri": "https://google.com/o/oauth2/auth",
|
||||||
|
"client_secret": "3Oknc4jS_wA2r9i",
|
||||||
|
"token_uri": "https://google.com/o/oauth2/token",
|
||||||
|
"client_email": "222-nprqovg5k43uum874cs9osjt2koe97g8@developer.gserviceaccount.com",
|
||||||
|
"redirect_uris": ["https://www.example.com/oauth2callback"],
|
||||||
|
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/222-nprqovg5k43uum874cs9osjt2koe97g8@developer.gserviceaccount.com",
|
||||||
|
"client_id": "222-nprqovg5k43uum874cs9osjt2koe97g8.apps.googleusercontent.com",
|
||||||
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
|
"javascript_origins": ["https://www.example.com"]
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
var installedJSONKey = []byte(`{
|
||||||
|
"installed": {
|
||||||
|
"client_id": "222-installed.apps.googleusercontent.com",
|
||||||
|
"redirect_uris": ["https://www.example.com/oauth2callback"]
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
|
||||||
|
func TestConfigFromJSON(t *testing.T) {
|
||||||
|
conf, err := ConfigFromJSON(webJSONKey, "scope1", "scope2")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if got, want := conf.ClientID, "222-nprqovg5k43uum874cs9osjt2koe97g8.apps.googleusercontent.com"; got != want {
|
||||||
|
t.Errorf("ClientID = %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := conf.ClientSecret, "3Oknc4jS_wA2r9i"; got != want {
|
||||||
|
t.Errorf("ClientSecret = %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := conf.RedirectURL, "https://www.example.com/oauth2callback"; got != want {
|
||||||
|
t.Errorf("RedictURL = %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := strings.Join(conf.Scopes, ","), "scope1,scope2"; got != want {
|
||||||
|
t.Errorf("Scopes = %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := conf.Endpoint.AuthURL, "https://google.com/o/oauth2/auth"; got != want {
|
||||||
|
t.Errorf("AuthURL = %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := conf.Endpoint.TokenURL, "https://google.com/o/oauth2/token"; got != want {
|
||||||
|
t.Errorf("TokenURL = %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigFromJSON_Installed(t *testing.T) {
|
||||||
|
conf, err := ConfigFromJSON(installedJSONKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if got, want := conf.ClientID, "222-installed.apps.googleusercontent.com"; got != want {
|
||||||
|
t.Errorf("ClientID = %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
// Copyright 2015 The oauth2 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 google
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sdkCredentials struct {
|
||||||
|
Data []struct {
|
||||||
|
Credential struct {
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
ClientSecret string `json:"client_secret"`
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
TokenExpiry *time.Time `json:"token_expiry"`
|
||||||
|
} `json:"credential"`
|
||||||
|
Key struct {
|
||||||
|
Account string `json:"account"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
} `json:"key"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An SDKConfig provides access to tokens from an account already
|
||||||
|
// authorized via the Google Cloud SDK.
|
||||||
|
type SDKConfig struct {
|
||||||
|
conf oauth2.Config
|
||||||
|
initialToken *oauth2.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSDKConfig creates an SDKConfig for the given Google Cloud SDK
|
||||||
|
// account. If account is empty, the account currently active in
|
||||||
|
// Google Cloud SDK properties is used.
|
||||||
|
// Google Cloud SDK credentials must be created by running `gcloud auth`
|
||||||
|
// before using this function.
|
||||||
|
// The Google Cloud SDK is available at https://cloud.google.com/sdk/.
|
||||||
|
func NewSDKConfig(account string) (*SDKConfig, error) {
|
||||||
|
configPath, err := sdkConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: error getting SDK config path: %v", err)
|
||||||
|
}
|
||||||
|
credentialsPath := filepath.Join(configPath, "credentials")
|
||||||
|
f, err := os.Open(credentialsPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: failed to load SDK credentials: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var c sdkCredentials
|
||||||
|
if err := json.NewDecoder(f).Decode(&c); err != nil {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: failed to decode SDK credentials from %q: %v", credentialsPath, err)
|
||||||
|
}
|
||||||
|
if len(c.Data) == 0 {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: no credentials found in %q, run `gcloud auth login` to create one", credentialsPath)
|
||||||
|
}
|
||||||
|
if account == "" {
|
||||||
|
propertiesPath := filepath.Join(configPath, "properties")
|
||||||
|
f, err := os.Open(propertiesPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: failed to load SDK properties: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
ini, err := internal.ParseINI(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: failed to parse SDK properties %q: %v", propertiesPath, err)
|
||||||
|
}
|
||||||
|
core, ok := ini["core"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: failed to find [core] section in %v", ini)
|
||||||
|
}
|
||||||
|
active, ok := core["account"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: failed to find %q attribute in %v", "account", core)
|
||||||
|
}
|
||||||
|
account = active
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range c.Data {
|
||||||
|
if account == "" || d.Key.Account == account {
|
||||||
|
if d.Credential.AccessToken == "" && d.Credential.RefreshToken == "" {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: no token available for account %q", account)
|
||||||
|
}
|
||||||
|
var expiry time.Time
|
||||||
|
if d.Credential.TokenExpiry != nil {
|
||||||
|
expiry = *d.Credential.TokenExpiry
|
||||||
|
}
|
||||||
|
return &SDKConfig{
|
||||||
|
conf: oauth2.Config{
|
||||||
|
ClientID: d.Credential.ClientID,
|
||||||
|
ClientSecret: d.Credential.ClientSecret,
|
||||||
|
Scopes: strings.Split(d.Key.Scope, " "),
|
||||||
|
Endpoint: Endpoint,
|
||||||
|
RedirectURL: "oob",
|
||||||
|
},
|
||||||
|
initialToken: &oauth2.Token{
|
||||||
|
AccessToken: d.Credential.AccessToken,
|
||||||
|
RefreshToken: d.Credential.RefreshToken,
|
||||||
|
Expiry: expiry,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("oauth2/google: no such credentials for account %q", account)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client returns an HTTP client using Google Cloud SDK credentials to
|
||||||
|
// authorize requests. The token will auto-refresh as necessary. The
|
||||||
|
// underlying http.RoundTripper will be obtained using the provided
|
||||||
|
// context. The returned client and its Transport should not be
|
||||||
|
// modified.
|
||||||
|
func (c *SDKConfig) Client(ctx context.Context) *http.Client {
|
||||||
|
return &http.Client{
|
||||||
|
Transport: &oauth2.Transport{
|
||||||
|
Source: c.TokenSource(ctx),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenSource returns an oauth2.TokenSource that retrieve tokens from
|
||||||
|
// Google Cloud SDK credentials using the provided context.
|
||||||
|
// It will returns the current access token stored in the credentials,
|
||||||
|
// and refresh it when it expires, but it won't update the credentials
|
||||||
|
// with the new access token.
|
||||||
|
func (c *SDKConfig) TokenSource(ctx context.Context) oauth2.TokenSource {
|
||||||
|
return c.conf.TokenSource(ctx, c.initialToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scopes are the OAuth 2.0 scopes the current account is authorized for.
|
||||||
|
func (c *SDKConfig) Scopes() []string {
|
||||||
|
return c.conf.Scopes
|
||||||
|
}
|
||||||
|
|
||||||
|
// sdkConfigPath tries to guess where the gcloud config is located.
|
||||||
|
// It can be overridden during tests.
|
||||||
|
var sdkConfigPath = func() (string, error) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return filepath.Join(os.Getenv("APPDATA"), "gcloud"), nil
|
||||||
|
}
|
||||||
|
homeDir := guessUnixHomeDir()
|
||||||
|
if homeDir == "" {
|
||||||
|
return "", errors.New("unable to get current user home directory: os/user lookup failed; $HOME is empty")
|
||||||
|
}
|
||||||
|
return filepath.Join(homeDir, ".config", "gcloud"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func guessUnixHomeDir() string {
|
||||||
|
usr, err := user.Current()
|
||||||
|
if err == nil {
|
||||||
|
return usr.HomeDir
|
||||||
|
}
|
||||||
|
return os.Getenv("HOME")
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright 2015 The oauth2 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 google
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestSDKConfig(t *testing.T) {
|
||||||
|
sdkConfigPath = func() (string, error) {
|
||||||
|
return "testdata/gcloud", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
account string
|
||||||
|
accessToken string
|
||||||
|
err bool
|
||||||
|
}{
|
||||||
|
{"", "bar_access_token", false},
|
||||||
|
{"foo@example.com", "foo_access_token", false},
|
||||||
|
{"bar@example.com", "bar_access_token", false},
|
||||||
|
{"baz@serviceaccount.example.com", "", true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
c, err := NewSDKConfig(tt.account)
|
||||||
|
if got, want := err != nil, tt.err; got != want {
|
||||||
|
if !tt.err {
|
||||||
|
t.Errorf("expected no error, got error: %v", tt.err, err)
|
||||||
|
} else {
|
||||||
|
t.Errorf("expected error, got none")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tok := c.initialToken
|
||||||
|
if tok == nil {
|
||||||
|
t.Errorf("expected token %q, got: nil", tt.accessToken)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if tok.AccessToken != tt.accessToken {
|
||||||
|
t.Errorf("expected token %q, got: %q", tt.accessToken, tok.AccessToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,71 +0,0 @@
|
||||||
// Copyright 2014 The oauth2 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 google
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
aeTokensMu sync.Mutex // guards aeTokens and appEngineTokenSource.key
|
|
||||||
|
|
||||||
// aeTokens helps the fetched tokens to be reused until their expiration.
|
|
||||||
aeTokens = make(map[string]*tokenLock) // key is '\0'-separated scopes
|
|
||||||
)
|
|
||||||
|
|
||||||
var errInvalidContext = errors.New("oauth2: a valid appengine.Context is required")
|
|
||||||
|
|
||||||
type tokenLock struct {
|
|
||||||
mu sync.Mutex // guards t; held while updating t
|
|
||||||
t *oauth2.Token
|
|
||||||
}
|
|
||||||
|
|
||||||
type appEngineTokenSource struct {
|
|
||||||
ctx oauth2.Context
|
|
||||||
|
|
||||||
// fetcherFunc makes the actual RPC to fetch a new access
|
|
||||||
// token with an expiry time. Provider of this function is
|
|
||||||
// responsible to assert that the given context is valid.
|
|
||||||
fetcherFunc func(ctx oauth2.Context, scope ...string) (accessToken string, expiry time.Time, err error)
|
|
||||||
|
|
||||||
// scopes and key are guarded by the package-level mutex aeTokensMu
|
|
||||||
scopes []string
|
|
||||||
key string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *appEngineTokenSource) Token() (*oauth2.Token, error) {
|
|
||||||
aeTokensMu.Lock()
|
|
||||||
if ts.key == "" {
|
|
||||||
sort.Sort(sort.StringSlice(ts.scopes))
|
|
||||||
ts.key = strings.Join(ts.scopes, string(0))
|
|
||||||
}
|
|
||||||
tok, ok := aeTokens[ts.key]
|
|
||||||
if !ok {
|
|
||||||
tok = &tokenLock{}
|
|
||||||
aeTokens[ts.key] = tok
|
|
||||||
}
|
|
||||||
aeTokensMu.Unlock()
|
|
||||||
|
|
||||||
tok.mu.Lock()
|
|
||||||
defer tok.mu.Unlock()
|
|
||||||
if tok.t.Valid() {
|
|
||||||
return tok.t, nil
|
|
||||||
}
|
|
||||||
access, exp, err := ts.fetcherFunc(ts.ctx, ts.scopes...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tok.t = &oauth2.Token{
|
|
||||||
AccessToken: access,
|
|
||||||
Expiry: exp,
|
|
||||||
}
|
|
||||||
return tok.t, nil
|
|
||||||
}
|
|
122
Godeps/_workspace/src/golang.org/x/oauth2/google/testdata/gcloud/credentials
generated
vendored
Normal file
122
Godeps/_workspace/src/golang.org/x/oauth2/google/testdata/gcloud/credentials
generated
vendored
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"credential": {
|
||||||
|
"_class": "OAuth2Credentials",
|
||||||
|
"_module": "oauth2client.client",
|
||||||
|
"access_token": "foo_access_token",
|
||||||
|
"client_id": "foo_client_id",
|
||||||
|
"client_secret": "foo_client_secret",
|
||||||
|
"id_token": {
|
||||||
|
"at_hash": "foo_at_hash",
|
||||||
|
"aud": "foo_aud",
|
||||||
|
"azp": "foo_azp",
|
||||||
|
"cid": "foo_cid",
|
||||||
|
"email": "foo@example.com",
|
||||||
|
"email_verified": true,
|
||||||
|
"exp": 1420573614,
|
||||||
|
"iat": 1420569714,
|
||||||
|
"id": "1337",
|
||||||
|
"iss": "accounts.google.com",
|
||||||
|
"sub": "1337",
|
||||||
|
"token_hash": "foo_token_hash",
|
||||||
|
"verified_email": true
|
||||||
|
},
|
||||||
|
"invalid": false,
|
||||||
|
"refresh_token": "foo_refresh_token",
|
||||||
|
"revoke_uri": "https://accounts.google.com/o/oauth2/revoke",
|
||||||
|
"token_expiry": "2015-01-09T00:51:51Z",
|
||||||
|
"token_response": {
|
||||||
|
"access_token": "foo_access_token",
|
||||||
|
"expires_in": 3600,
|
||||||
|
"id_token": "foo_id_token",
|
||||||
|
"token_type": "Bearer"
|
||||||
|
},
|
||||||
|
"token_uri": "https://accounts.google.com/o/oauth2/token",
|
||||||
|
"user_agent": "Cloud SDK Command Line Tool"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"account": "foo@example.com",
|
||||||
|
"clientId": "foo_client_id",
|
||||||
|
"scope": "https://www.googleapis.com/auth/appengine.admin https://www.googleapis.com/auth/bigquery https://www.googleapis.com/auth/compute https://www.googleapis.com/auth/devstorage.full_control https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/ndev.cloudman https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/sqlservice.admin https://www.googleapis.com/auth/prediction https://www.googleapis.com/auth/projecthosting",
|
||||||
|
"type": "google-cloud-sdk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"credential": {
|
||||||
|
"_class": "OAuth2Credentials",
|
||||||
|
"_module": "oauth2client.client",
|
||||||
|
"access_token": "bar_access_token",
|
||||||
|
"client_id": "bar_client_id",
|
||||||
|
"client_secret": "bar_client_secret",
|
||||||
|
"id_token": {
|
||||||
|
"at_hash": "bar_at_hash",
|
||||||
|
"aud": "bar_aud",
|
||||||
|
"azp": "bar_azp",
|
||||||
|
"cid": "bar_cid",
|
||||||
|
"email": "bar@example.com",
|
||||||
|
"email_verified": true,
|
||||||
|
"exp": 1420573614,
|
||||||
|
"iat": 1420569714,
|
||||||
|
"id": "1337",
|
||||||
|
"iss": "accounts.google.com",
|
||||||
|
"sub": "1337",
|
||||||
|
"token_hash": "bar_token_hash",
|
||||||
|
"verified_email": true
|
||||||
|
},
|
||||||
|
"invalid": false,
|
||||||
|
"refresh_token": "bar_refresh_token",
|
||||||
|
"revoke_uri": "https://accounts.google.com/o/oauth2/revoke",
|
||||||
|
"token_expiry": "2015-01-09T00:51:51Z",
|
||||||
|
"token_response": {
|
||||||
|
"access_token": "bar_access_token",
|
||||||
|
"expires_in": 3600,
|
||||||
|
"id_token": "bar_id_token",
|
||||||
|
"token_type": "Bearer"
|
||||||
|
},
|
||||||
|
"token_uri": "https://accounts.google.com/o/oauth2/token",
|
||||||
|
"user_agent": "Cloud SDK Command Line Tool"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"account": "bar@example.com",
|
||||||
|
"clientId": "bar_client_id",
|
||||||
|
"scope": "https://www.googleapis.com/auth/appengine.admin https://www.googleapis.com/auth/bigquery https://www.googleapis.com/auth/compute https://www.googleapis.com/auth/devstorage.full_control https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/ndev.cloudman https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/sqlservice.admin https://www.googleapis.com/auth/prediction https://www.googleapis.com/auth/projecthosting",
|
||||||
|
"type": "google-cloud-sdk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"credential": {
|
||||||
|
"_class": "ServiceAccountCredentials",
|
||||||
|
"_kwargs": {},
|
||||||
|
"_module": "oauth2client.client",
|
||||||
|
"_private_key_id": "00000000000000000000000000000000",
|
||||||
|
"_private_key_pkcs8_text": "-----BEGIN RSA PRIVATE KEY-----\nMIICWwIBAAKBgQCt3fpiynPSaUhWSIKMGV331zudwJ6GkGmvQtwsoK2S2LbvnSwU\nNxgj4fp08kIDR5p26wF4+t/HrKydMwzftXBfZ9UmLVJgRdSswmS5SmChCrfDS5OE\nvFFcN5+6w1w8/Nu657PF/dse8T0bV95YrqyoR0Osy8WHrUOMSIIbC3hRuwIDAQAB\nAoGAJrGE/KFjn0sQ7yrZ6sXmdLawrM3mObo/2uI9T60+k7SpGbBX0/Pi6nFrJMWZ\nTVONG7P3Mu5aCPzzuVRYJB0j8aldSfzABTY3HKoWCczqw1OztJiEseXGiYz4QOyr\nYU3qDyEpdhS6q6wcoLKGH+hqRmz6pcSEsc8XzOOu7s4xW8kCQQDkc75HjhbarCnd\nJJGMe3U76+6UGmdK67ltZj6k6xoB5WbTNChY9TAyI2JC+ppYV89zv3ssj4L+02u3\nHIHFGxsHAkEAwtU1qYb1tScpchPobnYUFiVKJ7KA8EZaHVaJJODW/cghTCV7BxcJ\nbgVvlmk4lFKn3lPKAgWw7PdQsBTVBUcCrQJATPwoIirizrv3u5soJUQxZIkENAqV\nxmybZx9uetrzP7JTrVbFRf0SScMcyN90hdLJiQL8+i4+gaszgFht7sNMnwJAAbfj\nq0UXcauQwALQ7/h2oONfTg5S+MuGC/AxcXPSMZbMRGGoPh3D5YaCv27aIuS/ukQ+\n6dmm/9AGlCb64fsIWQJAPaokbjIifo+LwC5gyK73Mc4t8nAOSZDenzd/2f6TCq76\nS1dcnKiPxaED7W/y6LJiuBT2rbZiQ2L93NJpFZD/UA==\n-----END RSA PRIVATE KEY-----\n",
|
||||||
|
"_revoke_uri": "https://accounts.google.com/o/oauth2/revoke",
|
||||||
|
"_scopes": "https://www.googleapis.com/auth/appengine.admin https://www.googleapis.com/auth/bigquery https://www.googleapis.com/auth/compute https://www.googleapis.com/auth/devstorage.full_control https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/ndev.cloudman https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/sqlservice.admin https://www.googleapis.com/auth/prediction https://www.googleapis.com/auth/projecthosting",
|
||||||
|
"_service_account_email": "baz@serviceaccount.example.com",
|
||||||
|
"_service_account_id": "baz.serviceaccount.example.com",
|
||||||
|
"_token_uri": "https://accounts.google.com/o/oauth2/token",
|
||||||
|
"_user_agent": "Cloud SDK Command Line Tool",
|
||||||
|
"access_token": null,
|
||||||
|
"assertion_type": null,
|
||||||
|
"client_id": null,
|
||||||
|
"client_secret": null,
|
||||||
|
"id_token": null,
|
||||||
|
"invalid": false,
|
||||||
|
"refresh_token": null,
|
||||||
|
"revoke_uri": "https://accounts.google.com/o/oauth2/revoke",
|
||||||
|
"service_account_name": "baz@serviceaccount.example.com",
|
||||||
|
"token_expiry": null,
|
||||||
|
"token_response": null,
|
||||||
|
"user_agent": "Cloud SDK Command Line Tool"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"account": "baz@serviceaccount.example.com",
|
||||||
|
"clientId": "baz_client_id",
|
||||||
|
"scope": "https://www.googleapis.com/auth/appengine.admin https://www.googleapis.com/auth/bigquery https://www.googleapis.com/auth/compute https://www.googleapis.com/auth/devstorage.full_control https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/ndev.cloudman https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/sqlservice.admin https://www.googleapis.com/auth/prediction https://www.googleapis.com/auth/projecthosting",
|
||||||
|
"type": "google-cloud-sdk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"file_version": 1
|
||||||
|
}
|
2
Godeps/_workspace/src/golang.org/x/oauth2/google/testdata/gcloud/properties
generated
vendored
Normal file
2
Godeps/_workspace/src/golang.org/x/oauth2/google/testdata/gcloud/properties
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[core]
|
||||||
|
account = bar@example.com
|
|
@ -6,10 +6,14 @@
|
||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseKey converts the binary contents of a private key file
|
// ParseKey converts the binary contents of a private key file
|
||||||
|
@ -26,12 +30,47 @@ func ParseKey(key []byte) (*rsa.PrivateKey, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
parsedKey, err = x509.ParsePKCS1PrivateKey(key)
|
parsedKey, err = x509.ParsePKCS1PrivateKey(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("private key should be a PEM or plain PKSC1 or PKCS8; parse error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parsed, ok := parsedKey.(*rsa.PrivateKey)
|
parsed, ok := parsedKey.(*rsa.PrivateKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("oauth2: private key is invalid")
|
return nil, errors.New("private key is invalid")
|
||||||
}
|
}
|
||||||
return parsed, nil
|
return parsed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseINI(ini io.Reader) (map[string]map[string]string, error) {
|
||||||
|
result := map[string]map[string]string{
|
||||||
|
"": map[string]string{}, // root section
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(ini)
|
||||||
|
currentSection := ""
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if strings.HasPrefix(line, ";") {
|
||||||
|
// comment.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||||
|
currentSection = strings.TrimSpace(line[1 : len(line)-1])
|
||||||
|
result[currentSection] = map[string]string{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) == 2 && parts[0] != "" {
|
||||||
|
result[currentSection][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("error scanning ini: %v", err)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CondVal(v string) []string {
|
||||||
|
if v == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{v}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
// Copyright 2014 The oauth2 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 internal contains support packages for oauth2 package.
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseINI(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ini string
|
||||||
|
want map[string]map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
`root = toor
|
||||||
|
[foo]
|
||||||
|
bar = hop
|
||||||
|
ini = nin
|
||||||
|
`,
|
||||||
|
map[string]map[string]string{
|
||||||
|
"": map[string]string{"root": "toor"},
|
||||||
|
"foo": map[string]string{"bar": "hop", "ini": "nin"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[empty]
|
||||||
|
[section]
|
||||||
|
empty=
|
||||||
|
`,
|
||||||
|
map[string]map[string]string{
|
||||||
|
"": map[string]string{},
|
||||||
|
"empty": map[string]string{},
|
||||||
|
"section": map[string]string{"empty": ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`ignore
|
||||||
|
[invalid
|
||||||
|
=stuff
|
||||||
|
;comment=true
|
||||||
|
`,
|
||||||
|
map[string]map[string]string{
|
||||||
|
"": map[string]string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
result, err := ParseINI(strings.NewReader(tt.ini))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ParseINI(%q) error %v, want: no error", tt.ini, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(result, tt.want) {
|
||||||
|
t.Errorf("ParseINI(%q) = %#v, want: %#v", tt.ini, result, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,213 @@
|
||||||
|
// Copyright 2014 The oauth2 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 internal contains support packages for oauth2 package.
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Token represents the crendentials used to authorize
|
||||||
|
// the requests to access protected resources on the OAuth 2.0
|
||||||
|
// provider's backend.
|
||||||
|
//
|
||||||
|
// This type is a mirror of oauth2.Token and exists to break
|
||||||
|
// an otherwise-circular dependency. Other internal packages
|
||||||
|
// should convert this Token into an oauth2.Token before use.
|
||||||
|
type Token struct {
|
||||||
|
// AccessToken is the token that authorizes and authenticates
|
||||||
|
// the requests.
|
||||||
|
AccessToken string
|
||||||
|
|
||||||
|
// TokenType is the type of token.
|
||||||
|
// The Type method returns either this or "Bearer", the default.
|
||||||
|
TokenType string
|
||||||
|
|
||||||
|
// RefreshToken is a token that's used by the application
|
||||||
|
// (as opposed to the user) to refresh the access token
|
||||||
|
// if it expires.
|
||||||
|
RefreshToken string
|
||||||
|
|
||||||
|
// Expiry is the optional expiration time of the access token.
|
||||||
|
//
|
||||||
|
// If zero, TokenSource implementations will reuse the same
|
||||||
|
// token forever and RefreshToken or equivalent
|
||||||
|
// mechanisms for that TokenSource will not be used.
|
||||||
|
Expiry time.Time
|
||||||
|
|
||||||
|
// Raw optionally contains extra metadata from the server
|
||||||
|
// when updating a token.
|
||||||
|
Raw interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenJSON is the struct representing the HTTP response from OAuth2
|
||||||
|
// providers returning a token in JSON form.
|
||||||
|
type tokenJSON struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
|
||||||
|
Expires expirationTime `json:"expires"` // broken Facebook spelling of expires_in
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *tokenJSON) expiry() (t time.Time) {
|
||||||
|
if v := e.ExpiresIn; v != 0 {
|
||||||
|
return time.Now().Add(time.Duration(v) * time.Second)
|
||||||
|
}
|
||||||
|
if v := e.Expires; v != 0 {
|
||||||
|
return time.Now().Add(time.Duration(v) * time.Second)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type expirationTime int32
|
||||||
|
|
||||||
|
func (e *expirationTime) UnmarshalJSON(b []byte) error {
|
||||||
|
var n json.Number
|
||||||
|
err := json.Unmarshal(b, &n)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i, err := n.Int64()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*e = expirationTime(i)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var brokenAuthHeaderProviders = []string{
|
||||||
|
"https://accounts.google.com/",
|
||||||
|
"https://www.googleapis.com/",
|
||||||
|
"https://github.com/",
|
||||||
|
"https://api.instagram.com/",
|
||||||
|
"https://www.douban.com/",
|
||||||
|
"https://api.dropbox.com/",
|
||||||
|
"https://api.soundcloud.com/",
|
||||||
|
"https://www.linkedin.com/",
|
||||||
|
"https://api.twitch.tv/",
|
||||||
|
"https://oauth.vk.com/",
|
||||||
|
"https://api.odnoklassniki.ru/",
|
||||||
|
"https://connect.stripe.com/",
|
||||||
|
"https://api.pushbullet.com/",
|
||||||
|
"https://oauth.sandbox.trainingpeaks.com/",
|
||||||
|
"https://oauth.trainingpeaks.com/",
|
||||||
|
"https://www.strava.com/oauth/",
|
||||||
|
"https://app.box.com/",
|
||||||
|
"https://test-sandbox.auth.corp.google.com",
|
||||||
|
"https://user.gini.net/",
|
||||||
|
}
|
||||||
|
|
||||||
|
// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL
|
||||||
|
// implements the OAuth2 spec correctly
|
||||||
|
// See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
|
||||||
|
// In summary:
|
||||||
|
// - Reddit only accepts client secret in the Authorization header
|
||||||
|
// - Dropbox accepts either it in URL param or Auth header, but not both.
|
||||||
|
// - Google only accepts URL param (not spec compliant?), not Auth header
|
||||||
|
// - Stripe only accepts client secret in Auth header with Bearer method, not Basic
|
||||||
|
func providerAuthHeaderWorks(tokenURL string) bool {
|
||||||
|
for _, s := range brokenAuthHeaderProviders {
|
||||||
|
if strings.HasPrefix(tokenURL, s) {
|
||||||
|
// Some sites fail to implement the OAuth2 spec fully.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume the provider implements the spec properly
|
||||||
|
// otherwise. We can add more exceptions as they're
|
||||||
|
// discovered. We will _not_ be adding configurable hooks
|
||||||
|
// to this package to let users select server bugs.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func RetrieveToken(ctx context.Context, ClientID, ClientSecret, TokenURL string, v url.Values) (*Token, error) {
|
||||||
|
hc, err := ContextClient(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
v.Set("client_id", ClientID)
|
||||||
|
bustedAuth := !providerAuthHeaderWorks(TokenURL)
|
||||||
|
if bustedAuth && ClientSecret != "" {
|
||||||
|
v.Set("client_secret", ClientSecret)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", TokenURL, strings.NewReader(v.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
if !bustedAuth {
|
||||||
|
req.SetBasicAuth(ClientID, ClientSecret)
|
||||||
|
}
|
||||||
|
r, err := hc.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
||||||
|
}
|
||||||
|
if code := r.StatusCode; code < 200 || code > 299 {
|
||||||
|
return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", r.Status, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
var token *Token
|
||||||
|
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 nil, err
|
||||||
|
}
|
||||||
|
token = &Token{
|
||||||
|
AccessToken: vals.Get("access_token"),
|
||||||
|
TokenType: vals.Get("token_type"),
|
||||||
|
RefreshToken: vals.Get("refresh_token"),
|
||||||
|
Raw: vals,
|
||||||
|
}
|
||||||
|
e := vals.Get("expires_in")
|
||||||
|
if e == "" {
|
||||||
|
// TODO(jbd): Facebook's OAuth2 implementation is broken and
|
||||||
|
// returns expires_in field in expires. Remove the fallback to expires,
|
||||||
|
// when Facebook fixes their implementation.
|
||||||
|
e = vals.Get("expires")
|
||||||
|
}
|
||||||
|
expires, _ := strconv.Atoi(e)
|
||||||
|
if expires != 0 {
|
||||||
|
token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
var tj tokenJSON
|
||||||
|
if err = json.Unmarshal(body, &tj); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
token = &Token{
|
||||||
|
AccessToken: tj.AccessToken,
|
||||||
|
TokenType: tj.TokenType,
|
||||||
|
RefreshToken: tj.RefreshToken,
|
||||||
|
Expiry: tj.expiry(),
|
||||||
|
Raw: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
json.Unmarshal(body, &token.Raw) // no error checks for optional fields
|
||||||
|
}
|
||||||
|
// Don't overwrite `RefreshToken` with an empty value
|
||||||
|
// if this was a token refreshing request.
|
||||||
|
if token.RefreshToken == "" {
|
||||||
|
token.RefreshToken = v.Get("refresh_token")
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
// Copyright 2014 The oauth2 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 internal contains support packages for oauth2 package.
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_providerAuthHeaderWorks(t *testing.T) {
|
||||||
|
for _, p := range brokenAuthHeaderProviders {
|
||||||
|
if providerAuthHeaderWorks(p) {
|
||||||
|
t.Errorf("URL: %s not found in list", p)
|
||||||
|
}
|
||||||
|
p := fmt.Sprintf("%ssomesuffix", p)
|
||||||
|
if providerAuthHeaderWorks(p) {
|
||||||
|
t.Errorf("URL: %s not found in list", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p := "https://api.not-in-the-list-example.com/"
|
||||||
|
if !providerAuthHeaderWorks(p) {
|
||||||
|
t.Errorf("URL: %s found in list", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright 2014 The oauth2 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 internal contains support packages for oauth2 package.
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPClient is the context key to use with golang.org/x/net/context's
|
||||||
|
// WithValue function to associate an *http.Client value with a context.
|
||||||
|
var HTTPClient ContextKey
|
||||||
|
|
||||||
|
// ContextKey is just an empty struct. It exists so HTTPClient can be
|
||||||
|
// an immutable public variable with a unique type. It's immutable
|
||||||
|
// because nobody else can create a ContextKey, being unexported.
|
||||||
|
type ContextKey struct{}
|
||||||
|
|
||||||
|
// ContextClientFunc is a func which tries to return an *http.Client
|
||||||
|
// given a Context value. If it returns an error, the search stops
|
||||||
|
// with that error. If it returns (nil, nil), the search continues
|
||||||
|
// down the list of registered funcs.
|
||||||
|
type ContextClientFunc func(context.Context) (*http.Client, error)
|
||||||
|
|
||||||
|
var contextClientFuncs []ContextClientFunc
|
||||||
|
|
||||||
|
func RegisterContextClientFunc(fn ContextClientFunc) {
|
||||||
|
contextClientFuncs = append(contextClientFuncs, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ContextClient(ctx context.Context) (*http.Client, error) {
|
||||||
|
for _, fn := range contextClientFuncs {
|
||||||
|
c, err := fn(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if c != nil {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hc, ok := ctx.Value(HTTPClient).(*http.Client); ok {
|
||||||
|
return hc, nil
|
||||||
|
}
|
||||||
|
return http.DefaultClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ContextTransport(ctx context.Context) http.RoundTripper {
|
||||||
|
hc, err := ContextClient(ctx)
|
||||||
|
// This is a rare error case (somebody using nil on App Engine).
|
||||||
|
if err != nil {
|
||||||
|
return ErrorTransport{err}
|
||||||
|
}
|
||||||
|
return hc.Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorTransport returns the specified error on RoundTrip.
|
||||||
|
// This RoundTripper should be used in rare error cases where
|
||||||
|
// error handling can be postponed to response handling time.
|
||||||
|
type ErrorTransport struct{ Err error }
|
||||||
|
|
||||||
|
func (t ErrorTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||||
|
return nil, t.Err
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/oauth2/internal"
|
"golang.org/x/oauth2/internal"
|
||||||
"golang.org/x/oauth2/jws"
|
"golang.org/x/oauth2/jws"
|
||||||
|
@ -57,7 +58,7 @@ type Config struct {
|
||||||
|
|
||||||
// TokenSource returns a JWT TokenSource using the configuration
|
// TokenSource returns a JWT TokenSource using the configuration
|
||||||
// in c and the HTTP client from the provided context.
|
// in c and the HTTP client from the provided context.
|
||||||
func (c *Config) TokenSource(ctx oauth2.Context) oauth2.TokenSource {
|
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
|
||||||
return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c})
|
return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,14 +67,14 @@ func (c *Config) TokenSource(ctx oauth2.Context) oauth2.TokenSource {
|
||||||
// obtained from c.
|
// obtained from c.
|
||||||
//
|
//
|
||||||
// The returned client and its Transport should not be modified.
|
// The returned client and its Transport should not be modified.
|
||||||
func (c *Config) Client(ctx oauth2.Context) *http.Client {
|
func (c *Config) Client(ctx context.Context) *http.Client {
|
||||||
return oauth2.NewClient(ctx, c.TokenSource(ctx))
|
return oauth2.NewClient(ctx, c.TokenSource(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
// jwtSource is a source that always does a signed JWT request for a token.
|
// jwtSource is a source that always does a signed JWT request for a token.
|
||||||
// It should typically be wrapped with a reuseTokenSource.
|
// It should typically be wrapped with a reuseTokenSource.
|
||||||
type jwtSource struct {
|
type jwtSource struct {
|
||||||
ctx oauth2.Context
|
ctx context.Context
|
||||||
conf *Config
|
conf *Config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
// Copyright 2015 The oauth2 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 linkedin provides constants for using OAuth2 to access LinkedIn.
|
||||||
|
package linkedin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Endpoint is LinkedIn's OAuth 2.0 endpoint.
|
||||||
|
var Endpoint = oauth2.Endpoint{
|
||||||
|
AuthURL: "https://www.linkedin.com/uas/oauth2/authorization",
|
||||||
|
TokenURL: "https://www.linkedin.com/uas/oauth2/accessToken",
|
||||||
|
}
|
|
@ -9,30 +9,19 @@ package oauth2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"mime"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
"golang.org/x/oauth2/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Context can be an golang.org/x/net.Context, or an App Engine Context.
|
// NoContext is the default context you should supply if not using
|
||||||
// If you don't care and aren't running on App Engine, you may use NoContext.
|
// your own context.Context (see https://golang.org/x/net/context).
|
||||||
type Context interface{}
|
var NoContext = context.TODO()
|
||||||
|
|
||||||
// NoContext is the default context. If you're not running this code
|
|
||||||
// on App Engine or not using golang.org/x/net.Context to provide a custom
|
|
||||||
// HTTP client, you should use NoContext.
|
|
||||||
var NoContext Context = nil
|
|
||||||
|
|
||||||
// Config describes a typical 3-legged OAuth2 flow, with both the
|
// Config describes a typical 3-legged OAuth2 flow, with both the
|
||||||
// client application information and the server's endpoint URLs.
|
// client application information and the server's endpoint URLs.
|
||||||
|
@ -78,28 +67,34 @@ var (
|
||||||
// "access_type" field that gets sent in the URL returned by
|
// "access_type" field that gets sent in the URL returned by
|
||||||
// AuthCodeURL.
|
// AuthCodeURL.
|
||||||
//
|
//
|
||||||
// Online (the default if neither is specified) is the default.
|
// Online is the default if neither is specified. If your
|
||||||
// If your application needs to refresh access tokens when the
|
// application needs to refresh access tokens when the user
|
||||||
// user is not present at the browser, then use offline. This
|
// is not present at the browser, then use offline. This will
|
||||||
// will result in your application obtaining a refresh token
|
// result in your application obtaining a refresh token the
|
||||||
// the first time your application exchanges an authorization
|
// first time your application exchanges an authorization
|
||||||
// code for a user.
|
// code for a user.
|
||||||
AccessTypeOnline AuthCodeOption = setParam{"access_type", "online"}
|
AccessTypeOnline AuthCodeOption = SetAuthURLParam("access_type", "online")
|
||||||
AccessTypeOffline AuthCodeOption = setParam{"access_type", "offline"}
|
AccessTypeOffline AuthCodeOption = SetAuthURLParam("access_type", "offline")
|
||||||
|
|
||||||
// ApprovalForce forces the users to view the consent dialog
|
// ApprovalForce forces the users to view the consent dialog
|
||||||
// and confirm the permissions request at the URL returned
|
// and confirm the permissions request at the URL returned
|
||||||
// from AuthCodeURL, even if they've already done so.
|
// from AuthCodeURL, even if they've already done so.
|
||||||
ApprovalForce AuthCodeOption = setParam{"approval_prompt", "force"}
|
ApprovalForce AuthCodeOption = SetAuthURLParam("approval_prompt", "force")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// An AuthCodeOption is passed to Config.AuthCodeURL.
|
||||||
|
type AuthCodeOption interface {
|
||||||
|
setValue(url.Values)
|
||||||
|
}
|
||||||
|
|
||||||
type setParam struct{ k, v string }
|
type setParam struct{ k, v string }
|
||||||
|
|
||||||
func (p setParam) setValue(m url.Values) { m.Set(p.k, p.v) }
|
func (p setParam) setValue(m url.Values) { m.Set(p.k, p.v) }
|
||||||
|
|
||||||
// An AuthCodeOption is passed to Config.AuthCodeURL.
|
// SetAuthURLParam builds an AuthCodeOption which passes key/value parameters
|
||||||
type AuthCodeOption interface {
|
// to a provider's authorization endpoint.
|
||||||
setValue(url.Values)
|
func SetAuthURLParam(key, value string) AuthCodeOption {
|
||||||
|
return setParam{key, value}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthCodeURL returns a URL to OAuth 2.0 provider's consent page
|
// AuthCodeURL returns a URL to OAuth 2.0 provider's consent page
|
||||||
|
@ -118,9 +113,9 @@ func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
|
||||||
v := url.Values{
|
v := url.Values{
|
||||||
"response_type": {"code"},
|
"response_type": {"code"},
|
||||||
"client_id": {c.ClientID},
|
"client_id": {c.ClientID},
|
||||||
"redirect_uri": condVal(c.RedirectURL),
|
"redirect_uri": internal.CondVal(c.RedirectURL),
|
||||||
"scope": condVal(strings.Join(c.Scopes, " ")),
|
"scope": internal.CondVal(strings.Join(c.Scopes, " ")),
|
||||||
"state": condVal(state),
|
"state": internal.CondVal(state),
|
||||||
}
|
}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt.setValue(v)
|
opt.setValue(v)
|
||||||
|
@ -134,118 +129,106 @@ func (c *Config) AuthCodeURL(state string, opts ...AuthCodeOption) string {
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PasswordCredentialsToken converts a resource owner username and password
|
||||||
|
// pair into a token.
|
||||||
|
//
|
||||||
|
// Per the RFC, this grant type should only be used "when there is a high
|
||||||
|
// degree of trust between the resource owner and the client (e.g., the client
|
||||||
|
// is part of the device operating system or a highly privileged application),
|
||||||
|
// and when other authorization grant types are not available."
|
||||||
|
// See https://tools.ietf.org/html/rfc6749#section-4.3 for more info.
|
||||||
|
//
|
||||||
|
// The HTTP client to use is derived from the context.
|
||||||
|
// If nil, http.DefaultClient is used.
|
||||||
|
func (c *Config) PasswordCredentialsToken(ctx context.Context, username, password string) (*Token, error) {
|
||||||
|
return retrieveToken(ctx, c, url.Values{
|
||||||
|
"grant_type": {"password"},
|
||||||
|
"username": {username},
|
||||||
|
"password": {password},
|
||||||
|
"scope": internal.CondVal(strings.Join(c.Scopes, " ")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Exchange converts an authorization code into a token.
|
// Exchange converts an authorization code into a token.
|
||||||
//
|
//
|
||||||
// It is used after a resource provider redirects the user back
|
// It is used after a resource provider redirects the user back
|
||||||
// to the Redirect URI (the URL obtained from AuthCodeURL).
|
// to the Redirect URI (the URL obtained from AuthCodeURL).
|
||||||
//
|
//
|
||||||
// The HTTP client to use is derived from the context. If nil,
|
// The HTTP client to use is derived from the context.
|
||||||
// http.DefaultClient is used. See the Context type's documentation.
|
// If a client is not provided via the context, http.DefaultClient is used.
|
||||||
//
|
//
|
||||||
// The code will be in the *http.Request.FormValue("code"). Before
|
// The code will be in the *http.Request.FormValue("code"). Before
|
||||||
// calling Exchange, be sure to validate FormValue("state").
|
// calling Exchange, be sure to validate FormValue("state").
|
||||||
func (c *Config) Exchange(ctx Context, code string) (*Token, error) {
|
func (c *Config) Exchange(ctx context.Context, code string) (*Token, error) {
|
||||||
return retrieveToken(ctx, c, url.Values{
|
return retrieveToken(ctx, c, url.Values{
|
||||||
"grant_type": {"authorization_code"},
|
"grant_type": {"authorization_code"},
|
||||||
"code": {code},
|
"code": {code},
|
||||||
"redirect_uri": condVal(c.RedirectURL),
|
"redirect_uri": internal.CondVal(c.RedirectURL),
|
||||||
"scope": condVal(strings.Join(c.Scopes, " ")),
|
"scope": internal.CondVal(strings.Join(c.Scopes, " ")),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// contextClientFunc is a func which tries to return an *http.Client
|
|
||||||
// given a Context value. If it returns an error, the search stops
|
|
||||||
// with that error. If it returns (nil, nil), the search continues
|
|
||||||
// down the list of registered funcs.
|
|
||||||
type contextClientFunc func(Context) (*http.Client, error)
|
|
||||||
|
|
||||||
var contextClientFuncs []contextClientFunc
|
|
||||||
|
|
||||||
func registerContextClientFunc(fn contextClientFunc) {
|
|
||||||
contextClientFuncs = append(contextClientFuncs, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func contextClient(ctx Context) (*http.Client, error) {
|
|
||||||
for _, fn := range contextClientFuncs {
|
|
||||||
c, err := fn(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if c != nil {
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if xc, ok := ctx.(context.Context); ok {
|
|
||||||
if hc, ok := xc.Value(HTTPClient).(*http.Client); ok {
|
|
||||||
return hc, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return http.DefaultClient, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func contextTransport(ctx Context) http.RoundTripper {
|
|
||||||
hc, err := contextClient(ctx)
|
|
||||||
if err != nil {
|
|
||||||
// This is a rare error case (somebody using nil on App Engine),
|
|
||||||
// so I'd rather not everybody do an error check on this Client
|
|
||||||
// method. They can get the error that they're doing it wrong
|
|
||||||
// later, at client.Get/PostForm time.
|
|
||||||
return errorTransport{err}
|
|
||||||
}
|
|
||||||
return hc.Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client returns an HTTP client using the provided token.
|
// Client returns an HTTP client using the provided token.
|
||||||
// The token will auto-refresh as necessary. The underlying
|
// The token will auto-refresh as necessary. The underlying
|
||||||
// HTTP transport will be obtained using the provided context.
|
// HTTP transport will be obtained using the provided context.
|
||||||
// The returned client and its Transport should not be modified.
|
// The returned client and its Transport should not be modified.
|
||||||
func (c *Config) Client(ctx Context, t *Token) *http.Client {
|
func (c *Config) Client(ctx context.Context, t *Token) *http.Client {
|
||||||
return NewClient(ctx, c.TokenSource(ctx, t))
|
return NewClient(ctx, c.TokenSource(ctx, t))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenSource returns a TokenSource that returns t until t expires,
|
// TokenSource returns a TokenSource that returns t until t expires,
|
||||||
// automatically refreshing it as necessary using the provided context.
|
// automatically refreshing it as necessary using the provided context.
|
||||||
// See the the Context documentation.
|
|
||||||
//
|
//
|
||||||
// Most users will use Config.Client instead.
|
// Most users will use Config.Client instead.
|
||||||
func (c *Config) TokenSource(ctx Context, t *Token) TokenSource {
|
func (c *Config) TokenSource(ctx context.Context, t *Token) TokenSource {
|
||||||
nwn := &reuseTokenSource{t: t}
|
tkr := &tokenRefresher{
|
||||||
nwn.new = tokenRefresher{
|
ctx: ctx,
|
||||||
ctx: ctx,
|
conf: c,
|
||||||
conf: c,
|
}
|
||||||
oldToken: &nwn.t,
|
if t != nil {
|
||||||
|
tkr.refreshToken = t.RefreshToken
|
||||||
|
}
|
||||||
|
return &reuseTokenSource{
|
||||||
|
t: t,
|
||||||
|
new: tkr,
|
||||||
}
|
}
|
||||||
return nwn
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// tokenRefresher is a TokenSource that makes "grant_type"=="refresh_token"
|
// tokenRefresher is a TokenSource that makes "grant_type"=="refresh_token"
|
||||||
// HTTP requests to renew a token using a RefreshToken.
|
// HTTP requests to renew a token using a RefreshToken.
|
||||||
type tokenRefresher struct {
|
type tokenRefresher struct {
|
||||||
ctx Context // used to get HTTP requests
|
ctx context.Context // used to get HTTP requests
|
||||||
conf *Config
|
conf *Config
|
||||||
oldToken **Token // pointer to old *Token w/ RefreshToken
|
refreshToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tf tokenRefresher) Token() (*Token, error) {
|
// WARNING: Token is not safe for concurrent access, as it
|
||||||
t := *tf.oldToken
|
// updates the tokenRefresher's refreshToken field.
|
||||||
if t == nil {
|
// Within this package, it is used by reuseTokenSource which
|
||||||
return nil, errors.New("oauth2: attempted use of nil Token")
|
// synchronizes calls to this method with its own mutex.
|
||||||
}
|
func (tf *tokenRefresher) Token() (*Token, error) {
|
||||||
if t.RefreshToken == "" {
|
if tf.refreshToken == "" {
|
||||||
return nil, errors.New("oauth2: token expired and refresh token is not set")
|
return nil, errors.New("oauth2: token expired and refresh token is not set")
|
||||||
}
|
}
|
||||||
return retrieveToken(tf.ctx, tf.conf, url.Values{
|
|
||||||
|
tk, err := retrieveToken(tf.ctx, tf.conf, url.Values{
|
||||||
"grant_type": {"refresh_token"},
|
"grant_type": {"refresh_token"},
|
||||||
"refresh_token": {t.RefreshToken},
|
"refresh_token": {tf.refreshToken},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if tf.refreshToken != tk.RefreshToken {
|
||||||
|
tf.refreshToken = tk.RefreshToken
|
||||||
|
}
|
||||||
|
return tk, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// reuseTokenSource is a TokenSource that holds a single token in memory
|
// reuseTokenSource is a TokenSource that holds a single token in memory
|
||||||
// and validates its expiry before each call to retrieve it with
|
// and validates its expiry before each call to retrieve it with
|
||||||
// Token. If it's expired, it will be auto-refreshed using the
|
// Token. If it's expired, it will be auto-refreshed using the
|
||||||
// new TokenSource.
|
// new TokenSource.
|
||||||
//
|
|
||||||
// The first call to TokenRefresher must be SetToken.
|
|
||||||
type reuseTokenSource struct {
|
type reuseTokenSource struct {
|
||||||
new TokenSource // called when t is expired.
|
new TokenSource // called when t is expired.
|
||||||
|
|
||||||
|
@ -270,145 +253,25 @@ func (s *reuseTokenSource) Token() (*Token, error) {
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrieveToken(ctx Context, c *Config, v url.Values) (*Token, error) {
|
// StaticTokenSource returns a TokenSource that always returns the same token.
|
||||||
hc, err := contextClient(ctx)
|
// Because the provided token t is never refreshed, StaticTokenSource is only
|
||||||
if err != nil {
|
// useful for tokens that never expire.
|
||||||
return nil, err
|
func StaticTokenSource(t *Token) TokenSource {
|
||||||
}
|
return staticTokenSource{t}
|
||||||
v.Set("client_id", c.ClientID)
|
|
||||||
bustedAuth := !providerAuthHeaderWorks(c.Endpoint.TokenURL)
|
|
||||||
if bustedAuth && c.ClientSecret != "" {
|
|
||||||
v.Set("client_secret", c.ClientSecret)
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest("POST", c.Endpoint.TokenURL, strings.NewReader(v.Encode()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
if !bustedAuth && c.ClientSecret != "" {
|
|
||||||
req.SetBasicAuth(c.ClientID, c.ClientSecret)
|
|
||||||
}
|
|
||||||
r, err := hc.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer r.Body.Close()
|
|
||||||
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
|
||||||
}
|
|
||||||
if code := r.StatusCode; code < 200 || code > 299 {
|
|
||||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", r.Status, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
var token *Token
|
|
||||||
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 nil, err
|
|
||||||
}
|
|
||||||
token = &Token{
|
|
||||||
AccessToken: vals.Get("access_token"),
|
|
||||||
TokenType: vals.Get("token_type"),
|
|
||||||
RefreshToken: vals.Get("refresh_token"),
|
|
||||||
raw: vals,
|
|
||||||
}
|
|
||||||
e := vals.Get("expires_in")
|
|
||||||
if e == "" {
|
|
||||||
// TODO(jbd): Facebook's OAuth2 implementation is broken and
|
|
||||||
// returns expires_in field in expires. Remove the fallback to expires,
|
|
||||||
// when Facebook fixes their implementation.
|
|
||||||
e = vals.Get("expires")
|
|
||||||
}
|
|
||||||
expires, _ := strconv.Atoi(e)
|
|
||||||
if expires != 0 {
|
|
||||||
token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
var tj tokenJSON
|
|
||||||
if err = json.Unmarshal(body, &tj); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
token = &Token{
|
|
||||||
AccessToken: tj.AccessToken,
|
|
||||||
TokenType: tj.TokenType,
|
|
||||||
RefreshToken: tj.RefreshToken,
|
|
||||||
Expiry: tj.expiry(),
|
|
||||||
raw: make(map[string]interface{}),
|
|
||||||
}
|
|
||||||
json.Unmarshal(body, &token.raw) // no error checks for optional fields
|
|
||||||
}
|
|
||||||
// Don't overwrite `RefreshToken` with an empty value
|
|
||||||
// if this was a token refreshing request.
|
|
||||||
if token.RefreshToken == "" {
|
|
||||||
token.RefreshToken = v.Get("refresh_token")
|
|
||||||
}
|
|
||||||
return token, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// tokenJSON is the struct representing the HTTP response from OAuth2
|
// staticTokenSource is a TokenSource that always returns the same Token.
|
||||||
// providers returning a token in JSON form.
|
type staticTokenSource struct {
|
||||||
type tokenJSON struct {
|
t *Token
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
TokenType string `json:"token_type"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
ExpiresIn int32 `json:"expires_in"`
|
|
||||||
Expires int32 `json:"expires"` // broken Facebook spelling of expires_in
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *tokenJSON) expiry() (t time.Time) {
|
func (s staticTokenSource) Token() (*Token, error) {
|
||||||
if v := e.ExpiresIn; v != 0 {
|
return s.t, nil
|
||||||
return time.Now().Add(time.Duration(v) * time.Second)
|
|
||||||
}
|
|
||||||
if v := e.Expires; v != 0 {
|
|
||||||
return time.Now().Add(time.Duration(v) * time.Second)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func condVal(v string) []string {
|
|
||||||
if v == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return []string{v}
|
|
||||||
}
|
|
||||||
|
|
||||||
// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL
|
|
||||||
// implements the OAuth2 spec correctly
|
|
||||||
// See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
|
|
||||||
// In summary:
|
|
||||||
// - Reddit only accepts client secret in the Authorization header
|
|
||||||
// - Dropbox accepts either it in URL param or Auth header, but not both.
|
|
||||||
// - Google only accepts URL param (not spec compliant?), not Auth header
|
|
||||||
func providerAuthHeaderWorks(tokenURL string) bool {
|
|
||||||
if strings.HasPrefix(tokenURL, "https://accounts.google.com/") ||
|
|
||||||
strings.HasPrefix(tokenURL, "https://github.com/") ||
|
|
||||||
strings.HasPrefix(tokenURL, "https://api.instagram.com/") ||
|
|
||||||
strings.HasPrefix(tokenURL, "https://www.douban.com/") ||
|
|
||||||
strings.HasPrefix(tokenURL, "https://api.dropbox.com/") ||
|
|
||||||
strings.HasPrefix(tokenURL, "https://api.soundcloud.com/") ||
|
|
||||||
strings.HasPrefix(tokenURL, "https://www.linkedin.com/") {
|
|
||||||
// Some sites fail to implement the OAuth2 spec fully.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assume the provider implements the spec properly
|
|
||||||
// otherwise. We can add more exceptions as they're
|
|
||||||
// discovered. We will _not_ be adding configurable hooks
|
|
||||||
// to this package to let users select server bugs.
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPClient is the context key to use with golang.org/x/net/context's
|
// HTTPClient is the context key to use with golang.org/x/net/context's
|
||||||
// WithValue function to associate an *http.Client value with a context.
|
// WithValue function to associate an *http.Client value with a context.
|
||||||
var HTTPClient contextKey
|
var HTTPClient internal.ContextKey
|
||||||
|
|
||||||
// contextKey is just an empty struct. It exists so HTTPClient can be
|
|
||||||
// an immutable public variable with a unique type. It's immutable
|
|
||||||
// because nobody else can create a contextKey, being unexported.
|
|
||||||
type contextKey struct{}
|
|
||||||
|
|
||||||
// NewClient creates an *http.Client from a Context and TokenSource.
|
// NewClient creates an *http.Client from a Context and TokenSource.
|
||||||
// The returned client is not valid beyond the lifetime of the context.
|
// The returned client is not valid beyond the lifetime of the context.
|
||||||
|
@ -416,17 +279,17 @@ type contextKey struct{}
|
||||||
// As a special case, if src is nil, a non-OAuth2 client is returned
|
// As a special case, if src is nil, a non-OAuth2 client is returned
|
||||||
// using the provided context. This exists to support related OAuth2
|
// using the provided context. This exists to support related OAuth2
|
||||||
// packages.
|
// packages.
|
||||||
func NewClient(ctx Context, src TokenSource) *http.Client {
|
func NewClient(ctx context.Context, src TokenSource) *http.Client {
|
||||||
if src == nil {
|
if src == nil {
|
||||||
c, err := contextClient(ctx)
|
c, err := internal.ContextClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &http.Client{Transport: errorTransport{err}}
|
return &http.Client{Transport: internal.ErrorTransport{err}}
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
Transport: &Transport{
|
Transport: &Transport{
|
||||||
Base: contextTransport(ctx),
|
Base: internal.ContextTransport(ctx),
|
||||||
Source: ReuseTokenSource(nil, src),
|
Source: ReuseTokenSource(nil, src),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,16 @@
|
||||||
package oauth2
|
package oauth2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
)
|
)
|
||||||
|
@ -56,6 +61,15 @@ func TestAuthCodeURL(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthCodeURL_CustomParam(t *testing.T) {
|
||||||
|
conf := newConf("server")
|
||||||
|
param := SetAuthURLParam("foo", "bar")
|
||||||
|
url := conf.AuthCodeURL("baz", param)
|
||||||
|
if url != "server/auth?client_id=CLIENT_ID&foo=bar&redirect_uri=REDIRECT_URL&response_type=code&scope=scope1+scope2&state=baz" {
|
||||||
|
t.Errorf("Auth code URL doesn't match the expected, found: %v", url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAuthCodeURL_Optional(t *testing.T) {
|
func TestAuthCodeURL_Optional(t *testing.T) {
|
||||||
conf := &Config{
|
conf := &Config{
|
||||||
ClientID: "CLIENT_ID",
|
ClientID: "CLIENT_ID",
|
||||||
|
@ -158,6 +172,60 @@ func TestExchangeRequest_JSONResponse(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const day = 24 * time.Hour
|
||||||
|
|
||||||
|
func TestExchangeRequest_JSONResponse_Expiry(t *testing.T) {
|
||||||
|
seconds := int32(day.Seconds())
|
||||||
|
jsonNumberType := reflect.TypeOf(json.Number("0"))
|
||||||
|
for _, c := range []struct {
|
||||||
|
expires string
|
||||||
|
expect error
|
||||||
|
}{
|
||||||
|
{fmt.Sprintf(`"expires_in": %d`, seconds), nil},
|
||||||
|
{fmt.Sprintf(`"expires_in": "%d"`, seconds), nil}, // PayPal case
|
||||||
|
{fmt.Sprintf(`"expires": %d`, seconds), nil}, // Facebook case
|
||||||
|
{`"expires": false`, &json.UnmarshalTypeError{Value: "bool", Type: jsonNumberType}}, // wrong type
|
||||||
|
{`"expires": {}`, &json.UnmarshalTypeError{Value: "object", Type: jsonNumberType}}, // wrong type
|
||||||
|
{`"expires": "zzz"`, &strconv.NumError{Func: "ParseInt", Num: "zzz", Err: strconv.ErrSyntax}}, // wrong value
|
||||||
|
} {
|
||||||
|
testExchangeRequest_JSONResponse_expiry(t, c.expires, c.expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testExchangeRequest_JSONResponse_expiry(t *testing.T, exp string, expect error) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write([]byte(fmt.Sprintf(`{"access_token": "90d", "scope": "user", "token_type": "bearer", %s}`, exp)))
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
conf := newConf(ts.URL)
|
||||||
|
t1 := time.Now().Add(day)
|
||||||
|
tok, err := conf.Exchange(NoContext, "exchange-code")
|
||||||
|
t2 := time.Now().Add(day)
|
||||||
|
// Do a fmt.Sprint comparison so either side can be
|
||||||
|
// nil. fmt.Sprint just stringifies them to "<nil>", and no
|
||||||
|
// non-nil expected error ever stringifies as "<nil>", so this
|
||||||
|
// isn't terribly disgusting. We do this because Go 1.4 and
|
||||||
|
// Go 1.5 return a different deep value for
|
||||||
|
// json.UnmarshalTypeError. In Go 1.5, the
|
||||||
|
// json.UnmarshalTypeError contains a new field with a new
|
||||||
|
// non-zero value. Rather than ignore it here with reflect or
|
||||||
|
// add new files and +build tags, just look at the strings.
|
||||||
|
if fmt.Sprint(err) != fmt.Sprint(expect) {
|
||||||
|
t.Errorf("Error = %v; want %v", err, expect)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !tok.Valid() {
|
||||||
|
t.Fatalf("Token invalid. Got: %#v", tok)
|
||||||
|
}
|
||||||
|
expiry := tok.Expiry
|
||||||
|
if expiry.Before(t1) || expiry.After(t2) {
|
||||||
|
t.Errorf("Unexpected value for Expiry: %v (shold be between %v and %v)", expiry, t1, t2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExchangeRequest_BadResponse(t *testing.T) {
|
func TestExchangeRequest_BadResponse(t *testing.T) {
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
@ -210,6 +278,53 @@ func TestExchangeRequest_NonBasicAuth(t *testing.T) {
|
||||||
conf.Exchange(ctx, "code")
|
conf.Exchange(ctx, "code")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPasswordCredentialsTokenRequest(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
expected := "/token"
|
||||||
|
if r.URL.String() != expected {
|
||||||
|
t.Errorf("URL = %q; want %q", r.URL, expected)
|
||||||
|
}
|
||||||
|
headerAuth := r.Header.Get("Authorization")
|
||||||
|
expected = "Basic Q0xJRU5UX0lEOkNMSUVOVF9TRUNSRVQ="
|
||||||
|
if headerAuth != expected {
|
||||||
|
t.Errorf("Authorization header = %q; want %q", headerAuth, expected)
|
||||||
|
}
|
||||||
|
headerContentType := r.Header.Get("Content-Type")
|
||||||
|
expected = "application/x-www-form-urlencoded"
|
||||||
|
if headerContentType != expected {
|
||||||
|
t.Errorf("Content-Type header = %q; want %q", headerContentType, expected)
|
||||||
|
}
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed reading request body: %s.", err)
|
||||||
|
}
|
||||||
|
expected = "client_id=CLIENT_ID&grant_type=password&password=password1&scope=scope1+scope2&username=user1"
|
||||||
|
if string(body) != expected {
|
||||||
|
t.Errorf("res.Body = %q; want %q", string(body), expected)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w.Write([]byte("access_token=90d64460d14870c08c81352a05dedd3465940a7c&scope=user&token_type=bearer"))
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
conf := newConf(ts.URL)
|
||||||
|
tok, err := conf.PasswordCredentialsToken(NoContext, "user1", "password1")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if !tok.Valid() {
|
||||||
|
t.Fatalf("Token invalid. Got: %#v", tok)
|
||||||
|
}
|
||||||
|
expected := "90d64460d14870c08c81352a05dedd3465940a7c"
|
||||||
|
if tok.AccessToken != expected {
|
||||||
|
t.Errorf("AccessToken = %q; want %q", tok.AccessToken, expected)
|
||||||
|
}
|
||||||
|
expected = "bearer"
|
||||||
|
if tok.TokenType != expected {
|
||||||
|
t.Errorf("TokenType = %q; want %q", tok.TokenType, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTokenRefreshRequest(t *testing.T) {
|
func TestTokenRefreshRequest(t *testing.T) {
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.String() == "/somethingelse" {
|
if r.URL.String() == "/somethingelse" {
|
||||||
|
@ -258,3 +373,50 @@ func TestFetchWithNoRefreshToken(t *testing.T) {
|
||||||
t.Errorf("Fetch should return an error if no refresh token is set")
|
t.Errorf("Fetch should return an error if no refresh token is set")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRefreshToken_RefreshTokenReplacement(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write([]byte(`{"access_token":"ACCESS TOKEN", "scope": "user", "token_type": "bearer", "refresh_token": "NEW REFRESH TOKEN"}`))
|
||||||
|
return
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
conf := newConf(ts.URL)
|
||||||
|
tkr := tokenRefresher{
|
||||||
|
conf: conf,
|
||||||
|
ctx: NoContext,
|
||||||
|
refreshToken: "OLD REFRESH TOKEN",
|
||||||
|
}
|
||||||
|
tk, err := tkr.Token()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected refreshToken error returned: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tk.RefreshToken != tkr.refreshToken {
|
||||||
|
t.Errorf("tokenRefresher.refresh_token = %s; want %s", tkr.refreshToken, tk.RefreshToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigClientWithToken(t *testing.T) {
|
||||||
|
tok := &Token{
|
||||||
|
AccessToken: "abc123",
|
||||||
|
}
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if got, want := r.Header.Get("Authorization"), fmt.Sprintf("Bearer %s", tok.AccessToken); got != want {
|
||||||
|
t.Errorf("Authorization header = %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
conf := newConf(ts.URL)
|
||||||
|
|
||||||
|
c := conf.Client(NoContext, tok)
|
||||||
|
req, err := http.NewRequest("GET", ts.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
_, err = c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
16
Godeps/_workspace/src/golang.org/x/oauth2/odnoklassniki/odnoklassniki.go
generated
vendored
Normal file
16
Godeps/_workspace/src/golang.org/x/oauth2/odnoklassniki/odnoklassniki.go
generated
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// Copyright 2015 The oauth2 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 odnoklassniki provides constants for using OAuth2 to access Odnoklassniki.
|
||||||
|
package odnoklassniki
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Endpoint is Odnoklassniki's OAuth 2.0 endpoint.
|
||||||
|
var Endpoint = oauth2.Endpoint{
|
||||||
|
AuthURL: "https://www.odnoklassniki.ru/oauth/authorize",
|
||||||
|
TokenURL: "https://api.odnoklassniki.ru/oauth/token.do",
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright 2015 The oauth2 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 paypal provides constants for using OAuth2 to access PayPal.
|
||||||
|
package paypal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Endpoint is PayPal's OAuth 2.0 endpoint in live (production) environment.
|
||||||
|
var Endpoint = oauth2.Endpoint{
|
||||||
|
AuthURL: "https://www.paypal.com/webapps/auth/protocol/openidconnect/v1/authorize",
|
||||||
|
TokenURL: "https://api.paypal.com/v1/identity/openidconnect/tokenservice",
|
||||||
|
}
|
||||||
|
|
||||||
|
// SandboxEndpoint is PayPal's OAuth 2.0 endpoint in sandbox (testing) environment.
|
||||||
|
var SandboxEndpoint = oauth2.Endpoint{
|
||||||
|
AuthURL: "https://www.sandbox.paypal.com/webapps/auth/protocol/openidconnect/v1/authorize",
|
||||||
|
TokenURL: "https://api.sandbox.paypal.com/v1/identity/openidconnect/tokenservice",
|
||||||
|
}
|
|
@ -7,9 +7,18 @@ package oauth2
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"golang.org/x/oauth2/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// expiryDelta determines how earlier a token should be considered
|
||||||
|
// expired than its actual expiration time. It is used to avoid late
|
||||||
|
// expirations due to client-server time mismatches.
|
||||||
|
const expiryDelta = 10 * time.Second
|
||||||
|
|
||||||
// Token represents the crendentials used to authorize
|
// Token represents the crendentials used to authorize
|
||||||
// the requests to access protected resources on the OAuth 2.0
|
// the requests to access protected resources on the OAuth 2.0
|
||||||
// provider's backend.
|
// provider's backend.
|
||||||
|
@ -45,6 +54,15 @@ type Token struct {
|
||||||
|
|
||||||
// Type returns t.TokenType if non-empty, else "Bearer".
|
// Type returns t.TokenType if non-empty, else "Bearer".
|
||||||
func (t *Token) Type() string {
|
func (t *Token) Type() string {
|
||||||
|
if strings.EqualFold(t.TokenType, "bearer") {
|
||||||
|
return "Bearer"
|
||||||
|
}
|
||||||
|
if strings.EqualFold(t.TokenType, "mac") {
|
||||||
|
return "MAC"
|
||||||
|
}
|
||||||
|
if strings.EqualFold(t.TokenType, "basic") {
|
||||||
|
return "Basic"
|
||||||
|
}
|
||||||
if t.TokenType != "" {
|
if t.TokenType != "" {
|
||||||
return t.TokenType
|
return t.TokenType
|
||||||
}
|
}
|
||||||
|
@ -90,10 +108,36 @@ func (t *Token) expired() bool {
|
||||||
if t.Expiry.IsZero() {
|
if t.Expiry.IsZero() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return t.Expiry.Before(time.Now())
|
return t.Expiry.Add(-expiryDelta).Before(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Valid reports whether t is non-nil, has an AccessToken, and is not expired.
|
// Valid reports whether t is non-nil, has an AccessToken, and is not expired.
|
||||||
func (t *Token) Valid() bool {
|
func (t *Token) Valid() bool {
|
||||||
return t != nil && t.AccessToken != "" && !t.expired()
|
return t != nil && t.AccessToken != "" && !t.expired()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tokenFromInternal maps an *internal.Token struct into
|
||||||
|
// a *Token struct.
|
||||||
|
func tokenFromInternal(t *internal.Token) *Token {
|
||||||
|
if t == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &Token{
|
||||||
|
AccessToken: t.AccessToken,
|
||||||
|
TokenType: t.TokenType,
|
||||||
|
RefreshToken: t.RefreshToken,
|
||||||
|
Expiry: t.Expiry,
|
||||||
|
raw: t.Raw,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrieveToken takes a *Config and uses that to retrieve an *internal.Token.
|
||||||
|
// This token is then mapped from *internal.Token into an *oauth2.Token which is returned along
|
||||||
|
// with an error..
|
||||||
|
func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error) {
|
||||||
|
tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tokenFromInternal(tk), nil
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,10 @@
|
||||||
|
|
||||||
package oauth2
|
package oauth2
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
func TestTokenExtra(t *testing.T) {
|
func TestTokenExtra(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
|
@ -28,3 +31,20 @@ func TestTokenExtra(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTokenExpiry(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
tok *Token
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "12 seconds", tok: &Token{Expiry: now.Add(12 * time.Second)}, want: false},
|
||||||
|
{name: "10 seconds", tok: &Token{Expiry: now.Add(expiryDelta)}, want: true},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
if got, want := tc.tok.expired(), tc.want; got != want {
|
||||||
|
t.Errorf("expired (%q) = %v; want %v", tc.name, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -130,9 +130,3 @@ func (r *onEOFReader) runFunc() {
|
||||||
r.fn = nil
|
r.fn = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type errorTransport struct{ err error }
|
|
||||||
|
|
||||||
func (t errorTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
|
||||||
return nil, t.err
|
|
||||||
}
|
|
||||||
|
|
|
@ -32,6 +32,39 @@ func TestTransportTokenSource(t *testing.T) {
|
||||||
client.Get(server.URL)
|
client.Get(server.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test for case-sensitive token types, per https://github.com/golang/oauth2/issues/113
|
||||||
|
func TestTransportTokenSourceTypes(t *testing.T) {
|
||||||
|
const val = "abc"
|
||||||
|
tests := []struct {
|
||||||
|
key string
|
||||||
|
val string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{key: "bearer", val: val, want: "Bearer abc"},
|
||||||
|
{key: "mac", val: val, want: "MAC abc"},
|
||||||
|
{key: "basic", val: val, want: "Basic abc"},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
ts := &tokenSource{
|
||||||
|
token: &Token{
|
||||||
|
AccessToken: tc.val,
|
||||||
|
TokenType: tc.key,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tr := &Transport{
|
||||||
|
Source: ts,
|
||||||
|
}
|
||||||
|
server := newMockServer(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if got, want := r.Header.Get("Authorization"), tc.want; got != want {
|
||||||
|
t.Errorf("Authorization header (%q) = %q; want %q", val, got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
defer server.Close()
|
||||||
|
client := http.Client{Transport: tr}
|
||||||
|
client.Get(server.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTokenValidNoAccessToken(t *testing.T) {
|
func TestTokenValidNoAccessToken(t *testing.T) {
|
||||||
token := &Token{}
|
token := &Token{}
|
||||||
if token.Valid() {
|
if token.Valid() {
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
// Copyright 2015 The oauth2 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 vk provides constants for using OAuth2 to access VK.com.
|
||||||
|
package vk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Endpoint is VK's OAuth 2.0 endpoint.
|
||||||
|
var Endpoint = oauth2.Endpoint{
|
||||||
|
AuthURL: "https://oauth.vk.com/authorize",
|
||||||
|
TokenURL: "https://oauth.vk.com/access_token",
|
||||||
|
}
|
|
@ -24,19 +24,18 @@ function pop_dir {
|
||||||
}
|
}
|
||||||
|
|
||||||
KUBE_ROOT=$(dirname "${BASH_SOURCE}")/..
|
KUBE_ROOT=$(dirname "${BASH_SOURCE}")/..
|
||||||
|
source "${KUBE_ROOT}/hack/lib/init.sh"
|
||||||
|
|
||||||
if [[ -z "${1:-}" ]]; then
|
if [[ -z "${1:-}" ]]; then
|
||||||
echo "Usage: ${0} <pr-number>"
|
echo "Usage: ${0} <pr-number> [opts]"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
pushd . > /dev/null
|
pushd . > /dev/null
|
||||||
trap 'pop_dir' INT TERM EXIT
|
trap 'pop_dir' INT TERM EXIT
|
||||||
|
|
||||||
cd ${KUBE_ROOT}/contrib/release-notes
|
kube::golang::build_binaries contrib/release-notes
|
||||||
# TODO: vendor these dependencies, but using godep again will be annoying...
|
kube::golang::place_bins
|
||||||
GOPATH=$PWD go get github.com/google/go-github/github
|
releasenotes=$(kube::util::find-binary "release-notes")
|
||||||
GOPATH=$PWD go get github.com/google/go-querystring/query
|
"${releasenotes}" --last-release-pr=${1} ${@}
|
||||||
GOPATH=$PWD go build release-notes.go
|
|
||||||
./release-notes --last-release-pr=${1}
|
|
||||||
|
|
||||||
|
|
|
@ -18,24 +18,43 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/google/go-github/github"
|
"github.com/google/go-github/github"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var target = flag.Int("last-release-pr", 0, "The PR number of the last versioned release.")
|
var (
|
||||||
|
target int
|
||||||
|
token string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.IntVar(&target, "last-release-pr", 0, "The PR number of the last versioned release.")
|
||||||
|
flag.StringVar(&token, "api-token", "", "Github api token for rate limiting. See https://developer.github.com/v3/#rate-limiting.")
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
// Automatically determine this from github.
|
// Automatically determine this from github.
|
||||||
if *target == 0 {
|
if target == 0 {
|
||||||
fmt.Printf("--last-release-pr is required.\n")
|
fmt.Printf("--last-release-pr is required.\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
var tc *http.Client
|
||||||
|
|
||||||
client := github.NewClient(nil)
|
if len(token) > 0 {
|
||||||
|
tc = oauth2.NewClient(
|
||||||
|
oauth2.NoContext,
|
||||||
|
oauth2.StaticTokenSource(
|
||||||
|
&oauth2.Token{AccessToken: token}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := github.NewClient(tc)
|
||||||
|
|
||||||
done := false
|
done := false
|
||||||
|
|
||||||
|
@ -62,7 +81,7 @@ func main() {
|
||||||
if result.MergedAt == nil {
|
if result.MergedAt == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if *result.Number == *target {
|
if *result.Number == target {
|
||||||
done = true
|
done = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue