Merge branch 'service-accounts' of github.com:caos/oidc into service-accounts

This commit is contained in:
adlerhurst 2020-09-16 14:12:45 +02:00
commit ad0966c1ab
6 changed files with 128 additions and 219 deletions

View file

@ -10,7 +10,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/rp"
@ -30,17 +29,13 @@ func main() {
ctx := context.Background() ctx := context.Background()
rpConfig := &rp.Configuration{ redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
Issuer: issuer, scopes := []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail}
Config: &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: fmt.Sprintf("http://localhost:%v%v", port, callbackPath),
Scopes: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail},
},
}
cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
provider, err := rp.NewRelayingParty(rpConfig, rp.WithCookieHandler(cookieHandler), rp.WithPKCE(cookieHandler), rp.WithVerifierOpts(rp.WithIssuedAtOffset(-3*time.Minute))) //, provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes,
rp.WithPKCE(cookieHandler),
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5*time.Second)),
)
if err != nil { if err != nil {
logrus.Fatalf("error creating provider %s", err.Error()) logrus.Fatalf("error creating provider %s", err.Error())
} }

View file

@ -5,10 +5,14 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/caos/oidc/pkg/cli"
"github.com/caos/oidc/pkg/rp"
"github.com/google/go-github/v31/github" "github.com/google/go-github/v31/github"
"github.com/google/uuid"
"golang.org/x/oauth2"
githubOAuth "golang.org/x/oauth2/github" githubOAuth "golang.org/x/oauth2/github"
"github.com/caos/oidc/pkg/rp"
"github.com/caos/oidc/pkg/rp/cli"
"github.com/caos/oidc/pkg/utils"
) )
var ( var (
@ -21,23 +25,32 @@ func main() {
clientSecret := os.Getenv("CLIENT_SECRET") clientSecret := os.Getenv("CLIENT_SECRET")
port := os.Getenv("PORT") port := os.Getenv("PORT")
rpConfig := &rp.Config{ rpConfig := &oauth2.Config{
ClientID: clientID, ClientID: clientID,
ClientSecret: clientSecret, ClientSecret: clientSecret,
CallbackURL: fmt.Sprintf("http://localhost:%v%v", port, callbackPath), RedirectURL: fmt.Sprintf("http://localhost:%v%v", port, callbackPath),
Scopes: []string{"repo", "repo_deployment"}, Scopes: []string{"repo", "repo_deployment"},
Endpoints: githubOAuth.Endpoint, Endpoint: githubOAuth.Endpoint,
} }
oauth2Client := cli.CodeFlowForClient(rpConfig, key, callbackPath, port)
client := github.NewClient(oauth2Client)
ctx := context.Background() ctx := context.Background()
_, _, err := client.Users.Get(ctx, "") cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
relayingParty, err := rp.NewRelayingPartyOAuth(rpConfig, rp.WithCookieHandler(cookieHandler))
if err != nil { if err != nil {
fmt.Println("OAuth flow failed") fmt.Printf("error creating relaying party: %v", err)
} else { return
fmt.Println("OAuth flow success")
} }
state := func() string {
return uuid.New().String()
}
token := cli.CodeFlow(relayingParty, callbackPath, port, state)
client := github.NewClient(relayingParty.Client(ctx, token.Token))
_, _, err = client.Users.Get(ctx, "")
if err != nil {
fmt.Printf("error %v", err)
return
}
fmt.Println("call succeeded")
} }

View file

@ -1,121 +0,0 @@
package cli
import (
"context"
"fmt"
"log"
"net/http"
"strings"
"time"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/rp"
"github.com/caos/oidc/pkg/utils"
)
func CodeFlow(rpc *rp.Configuration, key []byte, callbackPath string, port string) *oidc.Tokens {
cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
provider, err := rp.NewRelayingParty(rpc, rp.WithCookieHandler(cookieHandler))
if err != nil {
logrus.Fatalf("error creating provider %s", err.Error())
}
return codeFlow(provider, callbackPath, port)
}
func TokenForClient(rpc *rp.Configuration, key []byte, token *oidc.Tokens) *http.Client {
cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
provider, err := rp.NewRelayingParty(rpc, rp.WithCookieHandler(cookieHandler))
if err != nil {
logrus.Fatalf("error creating provider %s", err.Error())
}
return provider.Client(context.Background(), token.Token)
}
func CodeFlowForClient(rpc *rp.Configuration, key []byte, callbackPath string, port string) *http.Client {
cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
provider, err := rp.NewRelayingParty(rpc, rp.WithCookieHandler(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.RelayingParty, callbackPath string, port string) *oidc.Tokens {
loginPath := "/login"
portStr := port
if !strings.HasPrefix(port, ":") {
portStr = strings.Join([]string{":", portStr}, "")
}
getToken, setToken := getAndSetTokens()
state := func() string {
return uuid.New().String()
}
http.Handle(loginPath, rp.AuthURLHandler(state, provider))
marshal := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string) {
setToken(w, tokens)
}
http.Handle(callbackPath, rp.CodeExchangeHandler(marshal, provider))
// 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 := "<p><strong>Success!</strong></p>"
msg = msg + "<p>You are authenticated and can now return to the CLI.</p>"
fmt.Fprintf(w, msg)
}
return getToken, setToken
}

35
pkg/rp/cli/cli.go Normal file
View file

@ -0,0 +1,35 @@
package cli
import (
"context"
"net/http"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/rp"
"github.com/caos/oidc/pkg/utils"
)
const (
loginPath = "/login"
)
func CodeFlow(relayingParty rp.RelayingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var token *oidc.Tokens
callback := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string) {
token = tokens
msg := "<p><strong>Success!</strong></p>"
msg = msg + "<p>You are authenticated and can now return to the CLI.</p>"
w.Write([]byte(msg))
}
http.Handle(loginPath, rp.AuthURLHandler(stateProvider, relayingParty))
http.Handle(callbackPath, rp.CodeExchangeHandler(callback, relayingParty))
utils.StartServer(ctx, port)
utils.OpenBrowser("http://localhost:" + port + loginPath)
return token
}

View file

@ -40,30 +40,31 @@ type RelayingParty interface {
ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string)
} }
type ErrorHandler func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string)
var ( var (
DefaultErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) { DefaultErrorHandler ErrorHandler = func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError) http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError)
} }
) )
type relayingParty struct { type relayingParty struct {
endpoints Endpoints issuer string
endpoints Endpoints
config *Configuration oauthConfig *oauth2.Config
pkce bool oauth2Only bool
pkce bool
httpClient *http.Client httpClient *http.Client
cookieHandler *utils.CookieHandler cookieHandler *utils.CookieHandler
errorHandler func(http.ResponseWriter, *http.Request, string, string, string)
errorHandler func(http.ResponseWriter, *http.Request, string, string, string)
idTokenVerifier IDTokenVerifier idTokenVerifier IDTokenVerifier
verifierOpts []VerifierOption verifierOpts []VerifierOption
oauth2Only bool
} }
func (rp *relayingParty) OAuthConfig() *oauth2.Config { func (rp *relayingParty) OAuthConfig() *oauth2.Config {
return rp.config.Config return rp.oauthConfig
} }
func (rp *relayingParty) IsPKCE() bool { func (rp *relayingParty) IsPKCE() bool {
@ -84,97 +85,69 @@ func (rp *relayingParty) IsOAuth2Only() bool {
func (rp *relayingParty) IDTokenVerifier() IDTokenVerifier { func (rp *relayingParty) IDTokenVerifier() IDTokenVerifier {
if rp.idTokenVerifier == nil { if rp.idTokenVerifier == nil {
rp.idTokenVerifier = NewIDTokenVerifier(rp.config.Issuer, rp.config.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...) rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...)
} }
return rp.idTokenVerifier return rp.idTokenVerifier
} }
func (rp *relayingParty) Client(ctx context.Context, token *oauth2.Token) *http.Client { func (rp *relayingParty) Client(ctx context.Context, token *oauth2.Token) *http.Client {
return rp.config.Config.Client(ctx, token) return rp.oauthConfig.Client(ctx, token)
} }
func (rp *relayingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) { func (rp *relayingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) {
if rp.errorHandler == nil {
rp.errorHandler = DefaultErrorHandler
}
return rp.errorHandler return rp.errorHandler
} }
//NewRelayingParty creates a DelegationTokenExchangeRP with the given //NewRelayingPartyOAuth creates an (OAuth2) RelayingParty with the given
//Config and possible configOptions //OAuth2 Config and possible configOptions
//it will run discovery on the provided issuer if AuthURL and TokenURL are not set //it will use the AuthURL and TokenURL set in config
//if no verifier is provided using the options the `DefaultVerifier` is set func NewRelayingPartyOAuth(config *oauth2.Config, options ...Option) (RelayingParty, error) {
func NewRelayingParty(config *Configuration, options ...Option) (RelayingParty, error) {
isOpenID := isOpenID(config.Scopes)
rp := &relayingParty{ rp := &relayingParty{
config: config, oauthConfig: config,
httpClient: utils.DefaultHTTPClient, httpClient: utils.DefaultHTTPClient,
oauth2Only: !isOpenID, oauth2Only: true,
} }
for _, optFunc := range options { for _, optFunc := range options {
optFunc(rp) optFunc(rp)
} }
if isOpenID && config.Endpoint.AuthURL == "" && config.Endpoint.TokenURL == "" {
endpoints, err := Discover(config.Issuer, rp.httpClient)
if err != nil {
return nil, err
}
rp.config.Endpoint = endpoints.Endpoint
rp.endpoints = endpoints
}
if rp.errorHandler == nil {
rp.errorHandler = DefaultErrorHandler
}
if isOpenID && rp.idTokenVerifier == nil {
rp.idTokenVerifier = NewIDTokenVerifier(config.Issuer, config.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL))
}
return rp, nil return rp, nil
} }
func NewRelayingParty2(clientID, clientSecret, redirectURI string, options ...Option) (RelayingParty, error) { //NewRelayingPartyOIDC creates an (OIDC) RelayingParty with the given
//issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions
//it will run discovery on the provided issuer and use the found endpoints
func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelayingParty, error) {
rp := &relayingParty{ rp := &relayingParty{
config: &Configuration{ issuer: issuer,
Config: &oauth2.Config{ oauthConfig: &oauth2.Config{
ClientID: clientID, ClientID: clientID,
ClientSecret: clientSecret, ClientSecret: clientSecret,
RedirectURL: redirectURI, RedirectURL: redirectURI,
}, Scopes: scopes,
}, },
httpClient: utils.DefaultHTTPClient, httpClient: utils.DefaultHTTPClient,
oauth2Only: true, oauth2Only: false,
} }
for _, optFunc := range options { for _, optFunc := range options {
optFunc(rp) optFunc(rp)
} }
if !rp.oauth2Only && rp.config.Endpoint.AuthURL == "" && rp.config.Endpoint.TokenURL == "" { endpoints, err := Discover(rp.issuer, rp.httpClient)
endpoints, err := Discover(rp.config.Issuer, rp.httpClient) if err != nil {
if err != nil { return nil, err
return nil, err
}
rp.config.Endpoint = endpoints.Endpoint
rp.endpoints = endpoints
}
if rp.errorHandler == nil {
rp.errorHandler = DefaultErrorHandler
} }
rp.oauthConfig.Endpoint = endpoints.Endpoint
rp.endpoints = endpoints
return rp, nil return rp, nil
} }
func WithOIDC(issuer string, scopes []string) Option {
return func(rp *relayingParty) {
rp.config.Issuer = issuer
rp.config.Scopes = scopes
rp.oauth2Only = false
}
}
//DefaultRPOpts is the type for providing dynamic options to the DefaultRP //DefaultRPOpts is the type for providing dynamic options to the DefaultRP
type Option func(*relayingParty) type Option func(*relayingParty)
@ -202,6 +175,12 @@ func WithHTTPClient(client *http.Client) Option {
} }
} }
func WithErrorHandler(errorHandler ErrorHandler) Option {
return func(rp *relayingParty) {
rp.errorHandler = errorHandler
}
}
func WithVerifierOpts(opts ...VerifierOption) Option { func WithVerifierOpts(opts ...VerifierOption) Option {
return func(rp *relayingParty) { return func(rp *relayingParty) {
rp.verifierOpts = opts rp.verifierOpts = opts
@ -433,12 +412,3 @@ func WithCodeVerifier(codeVerifier string) CodeExchangeOpt {
return []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)} return []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)}
} }
} }
func isOpenID(scopes []string) bool {
for _, scope := range scopes {
if scope == oidc.ScopeOpenID {
return true
}
}
return false
}

View file

@ -1,9 +1,11 @@
package utils package utils
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -79,3 +81,18 @@ func URLEncodeResponse(resp interface{}, encoder Encoder) (string, error) {
v := url.Values(values) v := url.Values(values)
return v.Encode(), nil return v.Encode(), nil
} }
func StartServer(ctx context.Context, port string) {
server := &http.Server{Addr: port}
go func() {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("ListenAndServe(): %v", err)
}
}()
go func() {
<-ctx.Done()
err := server.Shutdown(ctx)
log.Fatalf("Shutdown(): %v", err)
}()
}