diff --git a/example/client/github/github.go b/example/client/github/github.go new file mode 100644 index 0000000..4afa2fb --- /dev/null +++ b/example/client/github/github.go @@ -0,0 +1,43 @@ +package main + +import ( + "context" + "fmt" + "github.com/caos/oidc/pkg/cli" + "github.com/caos/oidc/pkg/rp" + "github.com/google/go-github/v31/github" + githubOAuth "golang.org/x/oauth2/github" + "os" +) + +var ( + callbackPath string = "/orbctl/github/callback" + key []byte = []byte("test1234test1234") +) + +func main() { + clientID := os.Getenv("CLIENT_ID") + clientSecret := os.Getenv("CLIENT_SECRET") + port := os.Getenv("PORT") + + rpConfig := &rp.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + CallbackURL: fmt.Sprintf("http://localhost:%v%v", port, callbackPath), + Scopes: []string{"repo", "repo_deployment"}, + Endpoints: githubOAuth.Endpoint, + } + + oauth2Client := cli.CodeFlowForClient(rpConfig, key, callbackPath, port) + + client := github.NewClient(oauth2Client) + + ctx := context.Background() + _, _, err := client.Users.Get(ctx, "") + if err != nil { + fmt.Println("OAuth flow failed") + } else { + + fmt.Println("OAuth flow success") + } +} diff --git a/go.mod b/go.mod index 19b02fb..068ee02 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.13 require ( github.com/caos/logging v0.0.0-20191210002624-b3260f690a6a github.com/golang/mock v1.4.3 - github.com/golang/protobuf v1.3.2 // indirect + github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.1.1 github.com/gorilla/handlers v1.4.2 github.com/gorilla/mux v1.7.4 diff --git a/go.sum b/go.sum index dce7963..cb41e29 100644 --- a/go.sum +++ b/go.sum @@ -4,20 +4,20 @@ github.com/caos/logging v0.0.0-20191210002624-b3260f690a6a/go.mod h1:9LKiDE2ChuG github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo= +github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= @@ -42,8 +42,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= @@ -58,6 +56,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20191122200657-5d9234df094c h1:HjRaKPaiWks0f5tA6ELVF7ZfqSppfPwOEEAvsrKUTO4= golang.org/x/oauth2 v0.0.0-20191122200657-5d9234df094c/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -73,6 +72,7 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= @@ -80,8 +80,6 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/square/go-jose.v2 v2.4.0 h1:0kXPskUMGAXXWJlP05ktEMOV0vmzFQUWw6d+aZJQU8A= -gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.0 h1:OZ4sdq+Y+SHfYB7vfthi1Ei8b0vkP8ZPQgUfUwdUSqo= gopkg.in/square/go-jose.v2 v2.5.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go new file mode 100644 index 0000000..3ea3fc4 --- /dev/null +++ b/pkg/cli/cli.go @@ -0,0 +1,107 @@ +package cli + +import ( + "context" + "fmt" + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/rp" + "github.com/caos/oidc/pkg/utils" + "github.com/google/uuid" + "github.com/sirupsen/logrus" + "log" + "net/http" + "strings" + "time" +) + +func CodeFlow(rpc *rp.Config, key []byte, callbackPath string, port string) *oidc.Tokens { + cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) + provider, err := rp.NewDefaultRP(rpc, rp.WithCookieHandler(cookieHandler)) //rp.WithPKCE(cookieHandler)) //, + if err != nil { + logrus.Fatalf("error creating provider %s", err.Error()) + } + + return codeFlow(provider, callbackPath, port) +} + +func CodeFlowForClient(rpc *rp.Config, key []byte, callbackPath string, port string) *http.Client { + cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) + provider, err := rp.NewDefaultRP(rpc, rp.WithCookieHandler(cookieHandler)) //rp.WithPKCE(cookieHandler)) //, + if err != nil { + logrus.Fatalf("error creating provider %s", err.Error()) + } + token := codeFlow(provider, callbackPath, port) + + return provider.Client(context.Background(), token.Token) +} + +func codeFlow(provider rp.DelegationTokenExchangeRP, callbackPath string, port string) *oidc.Tokens { + loginPath := "/login" + portStr := port + if !strings.HasPrefix(port, ":") { + portStr = strings.Join([]string{":", portStr}, "") + } + + getToken, setToken := getAndSetTokens() + + state := uuid.New().String() + http.Handle(loginPath, provider.AuthURLHandler(state)) + + marshal := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string) { + setToken(w, tokens) + } + http.Handle(callbackPath, provider.CodeExchangeHandler(marshal)) + + // start http-server + stopHttpServer := startHttpServer(portStr) + + // open browser in different window + utils.OpenBrowser(strings.Join([]string{"http://localhost", portStr, loginPath}, "")) + + // wait until user is logged into browser + ret := getToken() + + // stop http-server as no callback is needed anymore + stopHttpServer() + + // return tokens + return ret +} + +func startHttpServer(port string) func() { + srv := &http.Server{Addr: port} + go func() { + + // always returns error. ErrServerClosed on graceful close + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + // unexpected error. port in use? + log.Fatalf("ListenAndServe(): %v", err) + } + }() + + return func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + log.Fatalf("Shutdown(): %v", err) + } + } +} + +func getAndSetTokens() (func() *oidc.Tokens, func(w http.ResponseWriter, tokens *oidc.Tokens)) { + marshalChan := make(chan *oidc.Tokens) + + getToken := func() *oidc.Tokens { + return <-marshalChan + } + setToken := func(w http.ResponseWriter, tokens *oidc.Tokens) { + marshalChan <- tokens + + msg := "

Success!

" + msg = msg + "

You are authenticated and can now return to the CLI.

" + fmt.Fprintf(w, msg) + } + + return getToken, setToken +} diff --git a/pkg/rp/default_rp.go b/pkg/rp/default_rp.go index 1f18984..6c9208d 100644 --- a/pkg/rp/default_rp.go +++ b/pkg/rp/default_rp.go @@ -40,7 +40,8 @@ type DefaultRP struct { errorHandler func(http.ResponseWriter, *http.Request, string, string, string) - verifier Verifier + verifier Verifier + onlyOAuth2 bool } //NewDefaultRP creates `DefaultRP` with the given @@ -48,17 +49,29 @@ type DefaultRP struct { //it will run discovery on the provided issuer //if no verifier is provided using the options the `DefaultVerifier` is set func NewDefaultRP(rpConfig *Config, rpOpts ...DefaultRPOpts) (DelegationTokenExchangeRP, error) { + foundOpenID := false + for _, scope := range rpConfig.Scopes { + if scope == "openid" { + foundOpenID = true + } + } + p := &DefaultRP{ config: rpConfig, httpClient: utils.DefaultHTTPClient, + onlyOAuth2: !foundOpenID, } for _, optFunc := range rpOpts { optFunc(p) } - if err := p.discover(); err != nil { - return nil, err + if rpConfig.Endpoints.TokenURL != "" && rpConfig.Endpoints.AuthURL != "" { + p.oauthConfig = p.getOAuthConfig(rpConfig.Endpoints) + } else { + if err := p.discover(); err != nil { + return nil, err + } } if p.errorHandler == nil { @@ -159,9 +172,12 @@ func (p *DefaultRP) CodeExchange(ctx context.Context, code string, opts ...CodeE //TODO: implement } - idToken, err := p.verifier.Verify(ctx, token.AccessToken, idTokenString) - if err != nil { - return nil, err //TODO: err + idToken := new(oidc.IDTokenClaims) + if !p.onlyOAuth2 { + idToken, err = p.verifier.Verify(ctx, token.AccessToken, idTokenString) + if err != nil { + return nil, err //TODO: err + } } return &oidc.Tokens{Token: token, IDTokenClaims: idToken, IDToken: idTokenString}, nil @@ -241,14 +257,18 @@ func (p *DefaultRP) discover() error { return err } p.endpoints = GetEndpoints(discoveryConfig) - p.oauthConfig = oauth2.Config{ + p.oauthConfig = p.getOAuthConfig(p.endpoints.Endpoint) + return nil +} + +func (p *DefaultRP) getOAuthConfig(endpoint oauth2.Endpoint) oauth2.Config { + return oauth2.Config{ ClientID: p.config.ClientID, ClientSecret: p.config.ClientSecret, - Endpoint: p.endpoints.Endpoint, + Endpoint: endpoint, RedirectURL: p.config.CallbackURL, Scopes: p.config.Scopes, } - return nil } func (p *DefaultRP) callTokenEndpoint(request interface{}) (newToken *oauth2.Token, err error) { @@ -285,3 +305,7 @@ func (p *DefaultRP) tryReadStateCookie(w http.ResponseWriter, r *http.Request) ( p.cookieHandler.DeleteCookie(w, stateParam) return state, nil } + +func (p *DefaultRP) Client(ctx context.Context, token *oauth2.Token) *http.Client { + return p.oauthConfig.Client(ctx, token) +} diff --git a/pkg/rp/relaying_party.go b/pkg/rp/relaying_party.go index d706839..8aba443 100644 --- a/pkg/rp/relaying_party.go +++ b/pkg/rp/relaying_party.go @@ -11,6 +11,8 @@ import ( //RelayingParty declares the minimal interface for oidc clients type RelayingParty interface { + //Client return a standard http client where the token can be used + Client(ctx context.Context, token *oauth2.Token) *http.Client //AuthURL returns the authorization endpoint with a given state AuthURL(state string, opts ...AuthURLOpt) string @@ -59,6 +61,7 @@ type Config struct { CallbackURL string Issuer string Scopes []string + Endpoints oauth2.Endpoint } type OptionFunc func(RelayingParty) diff --git a/pkg/utils/browser.go b/pkg/utils/browser.go new file mode 100644 index 0000000..dca75e4 --- /dev/null +++ b/pkg/utils/browser.go @@ -0,0 +1,26 @@ +package utils + +import ( + "fmt" + "log" + "os/exec" + "runtime" +) + +func OpenBrowser(url string) { + var err error + + switch runtime.GOOS { + case "linux": + err = exec.Command("xdg-open", url).Start() + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + err = exec.Command("open", url).Start() + default: + err = fmt.Errorf("unsupported platform") + } + if err != nil { + log.Fatal(err) + } +}