Support for external OIDC providers and implicit login flows (#727)

This commit is contained in:
Jesse Suen
2018-10-29 01:36:53 -07:00
committed by GitHub
parent a0b5af0dae
commit 5c7a3329f3
35 changed files with 1637 additions and 630 deletions

10
Gopkg.lock generated
View File

@@ -88,14 +88,6 @@
revision = "d71629e497929858300c38cd442098c178121c30"
version = "v1.5.0"
[[projects]]
digest = "1:65bad35bfcdd839cb26bb4ff31de49be39dd6bd2ade0c7c57d010f7d0412a4a5"
name = "github.com/coreos/dex"
packages = ["api"]
pruneopts = ""
revision = "218d671a96865df2a4cf7f310efb99b8bfc5a5e2"
version = "v2.10.0"
[[projects]]
branch = "v2"
digest = "1:d8ee1b165eb7f4fd9ada718e1e7eeb0bc1fd462592d0bd823df694443f448681"
@@ -1439,7 +1431,6 @@
"github.com/argoproj/pkg/time",
"github.com/casbin/casbin",
"github.com/casbin/casbin/model",
"github.com/coreos/dex/api",
"github.com/coreos/go-oidc",
"github.com/dgrijalva/jwt-go",
"github.com/dustin/go-humanize",
@@ -1453,7 +1444,6 @@
"github.com/gogo/protobuf/proto",
"github.com/gogo/protobuf/protoc-gen-gofast",
"github.com/gogo/protobuf/protoc-gen-gogofast",
"github.com/golang/glog",
"github.com/golang/protobuf/proto",
"github.com/golang/protobuf/protoc-gen-go",
"github.com/golang/protobuf/ptypes/empty",

View File

@@ -1,4 +1,4 @@
controller: go run ./cmd/argocd-application-controller/main.go
api-server: go run ./cmd/argocd-server/main.go --insecure --disable-auth
api-server: go run ./cmd/argocd-server/main.go --insecure --dex-server http://localhost:5556 --repo-server localhost:8081
repo-server: go run ./cmd/argocd-repo-server/main.go --loglevel debug
dex: sh -c "go run ./cmd/argocd-util/main.go gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p 5556:5556 -p 5557:5557 -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/coreos/dex:v2.10.0 serve /dex.yaml"
dex: sh -c "go run ./cmd/argocd-util/main.go gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p 5556:5556 -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/dexidp/dex:v2.12.0 serve /dex.yaml"

View File

@@ -1 +1 @@
0.10.0
0.11.0

View File

@@ -2,10 +2,8 @@ package main
import (
"context"
"flag"
"fmt"
"os"
"strconv"
"time"
log "github.com/sirupsen/logrus"
@@ -48,14 +46,8 @@ func newCommand() *cobra.Command {
Use: cliName,
Short: "application-controller is a controller to operate on applications CRD",
RunE: func(c *cobra.Command, args []string) error {
level, err := log.ParseLevel(logLevel)
errors.CheckError(err)
log.SetLevel(level)
// Set the glog level for the k8s go-client
_ = flag.CommandLine.Parse([]string{})
_ = flag.Lookup("logtostderr").Value.Set("true")
_ = flag.Lookup("v").Value.Set(strconv.Itoa(glogLevel))
cli.SetLogLevel(logLevel)
cli.SetGLogLevel(glogLevel)
config, err := clientConfig.ClientConfig()
errors.CheckError(err)

View File

@@ -14,6 +14,7 @@ import (
"github.com/argoproj/argo-cd/reposerver"
"github.com/argoproj/argo-cd/reposerver/repository"
"github.com/argoproj/argo-cd/util/cache"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/git"
"github.com/argoproj/argo-cd/util/ksonnet"
"github.com/argoproj/argo-cd/util/stats"
@@ -35,9 +36,7 @@ func newCommand() *cobra.Command {
Use: cliName,
Short: "Run argocd-repo-server",
RunE: func(c *cobra.Command, args []string) error {
level, err := log.ParseLevel(logLevel)
errors.CheckError(err)
log.SetLevel(level)
cli.SetLogLevel(logLevel)
tlsConfigCustomizer, err := tlsConfigCustomizerSrc()
errors.CheckError(err)

View File

@@ -2,11 +2,8 @@ package commands
import (
"context"
"flag"
"strconv"
"time"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
@@ -20,6 +17,14 @@ import (
"github.com/argoproj/argo-cd/util/tls"
)
const (
// DefaultDexServerAddr is the HTTP address of the Dex OIDC server, which we run a reverse proxy against
DefaultDexServerAddr = "http://dex-server:5556"
// DefaultRepoServerAddr is the gRPC address of the ArgoCD repo server
DefaultRepoServerAddr = "argocd-repo-server:8081"
)
// NewCommand returns a new instance of an argocd command
func NewCommand() *cobra.Command {
var (
@@ -29,6 +34,7 @@ func NewCommand() *cobra.Command {
clientConfig clientcmd.ClientConfig
staticAssetsDir string
repoServerAddress string
dexServerAddress string
disableAuth bool
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
)
@@ -37,14 +43,8 @@ func NewCommand() *cobra.Command {
Short: "Run the argocd API server",
Long: "Run the argocd API server",
Run: func(c *cobra.Command, args []string) {
level, err := log.ParseLevel(logLevel)
errors.CheckError(err)
log.SetLevel(level)
// Set the glog level for the k8s go-client
_ = flag.CommandLine.Parse([]string{})
_ = flag.Lookup("logtostderr").Value.Set("true")
_ = flag.Lookup("v").Value.Set(strconv.Itoa(glogLevel))
cli.SetLogLevel(logLevel)
cli.SetGLogLevel(glogLevel)
config, err := clientConfig.ClientConfig()
errors.CheckError(err)
@@ -66,6 +66,7 @@ func NewCommand() *cobra.Command {
KubeClientset: kubeclientset,
AppClientset: appclientset,
RepoClientset: repoclientset,
DexServerAddr: dexServerAddress,
DisableAuth: disableAuth,
TLSConfigCustomizer: tlsConfigCustomizer,
}
@@ -89,7 +90,8 @@ func NewCommand() *cobra.Command {
command.Flags().StringVar(&staticAssetsDir, "staticassets", "", "Static assets directory path")
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
command.Flags().IntVar(&glogLevel, "gloglevel", 0, "Set the glog logging level")
command.Flags().StringVar(&repoServerAddress, "repo-server", "localhost:8081", "Repo server address.")
command.Flags().StringVar(&repoServerAddress, "repo-server", DefaultRepoServerAddr, "Repo server address")
command.Flags().StringVar(&dexServerAddress, "dex-server", DefaultDexServerAddr, "Dex server address")
command.Flags().BoolVar(&disableAuth, "disable-auth", false, "Disable client authentication")
command.AddCommand(cli.NewVersionCmd(cliName))
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(command)

View File

@@ -10,7 +10,7 @@ import (
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
"github.com/argoproj/argo-cd/server/account"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/settings"
"github.com/argoproj/argo-cd/util/cli"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
)
@@ -51,7 +51,7 @@ func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *co
}
if newPassword == "" {
var err error
newPassword, err = settings.ReadAndConfirmPassword()
newPassword, err = cli.ReadAndConfirmPassword()
errors.CheckError(err)
}

View File

@@ -2,4 +2,8 @@ package commands
const (
cliName = "argocd"
// DefaultSSOLocalPort is the localhost port to listen on for the temporary web server performing
// the OAuth2 login flow.
DefaultSSOLocalPort = 8085
)

View File

@@ -2,15 +2,19 @@ package commands
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"os"
"strconv"
"time"
"github.com/argoproj/argo-cd/common"
oidc "github.com/coreos/go-oidc"
jwt "github.com/dgrijalva/jwt-go"
log "github.com/sirupsen/logrus"
"github.com/skratchdot/open-golang/open"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
"github.com/argoproj/argo-cd/server/session"
@@ -19,11 +23,8 @@ import (
"github.com/argoproj/argo-cd/util/cli"
grpc_util "github.com/argoproj/argo-cd/util/grpc"
"github.com/argoproj/argo-cd/util/localconfig"
jwt "github.com/dgrijalva/jwt-go"
log "github.com/sirupsen/logrus"
"github.com/skratchdot/open-golang/open"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
oidcutil "github.com/argoproj/argo-cd/util/oidc"
"github.com/argoproj/argo-cd/util/rand"
)
// NewLoginCommand returns a new instance of `argocd login` command
@@ -33,6 +34,7 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
username string
password string
sso bool
ssoPort int
)
var command = &cobra.Command{
Use: "login SERVER",
@@ -81,12 +83,15 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
if !sso {
tokenString = passwordLogin(acdClient, username, password)
} else {
acdSet, err := setIf.Get(context.Background(), &settings.SettingsQuery{})
ctx := context.Background()
httpClient, err := acdClient.HTTPClient()
errors.CheckError(err)
if !ssoConfigured(acdSet) {
log.Fatalf("ArgoCD instance is not configured with SSO")
}
tokenString, refreshToken = oauth2Login(server, clientOpts.PlainText)
ctx = oidc.ClientContext(ctx, httpClient)
acdSet, err := setIf.Get(ctx, &settings.SettingsQuery{})
errors.CheckError(err)
oauth2conf, provider, err := acdClient.OIDCConfig(ctx, acdSet)
errors.CheckError(err)
tokenString, refreshToken = oauth2Login(ctx, ssoPort, oauth2conf, provider)
}
parser := &jwt.Parser{
@@ -130,7 +135,8 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
command.Flags().StringVar(&ctxName, "name", "", "name to use for the context")
command.Flags().StringVar(&username, "username", "", "the username of an account to authenticate")
command.Flags().StringVar(&password, "password", "", "the password of an account to authenticate")
command.Flags().BoolVar(&sso, "sso", false, "Perform SSO login")
command.Flags().BoolVar(&sso, "sso", false, "perform SSO login")
command.Flags().IntVar(&ssoPort, "sso-port", DefaultSSOLocalPort, "port to run local OAuth2 login application")
return command
}
@@ -144,97 +150,107 @@ func userDisplayName(claims jwt.MapClaims) string {
return claims["sub"].(string)
}
func ssoConfigured(set *settings.Settings) bool {
return set.DexConfig != nil && len(set.DexConfig.Connectors) > 0
}
// getFreePort asks the kernel for a free open port that is ready to use.
func getFreePort() (int, error) {
ln, err := net.Listen("tcp", "[::]:0")
if err != nil {
return 0, err
}
return ln.Addr().(*net.TCPAddr).Port, ln.Close()
}
// oauth2Login opens a browser, runs a temporary HTTP server to delegate OAuth2 login flow and
// returns the JWT token and a refresh token (if supported)
func oauth2Login(host string, plaintext bool) (string, string) {
ctx := context.Background()
port, err := getFreePort()
func oauth2Login(ctx context.Context, port int, oauth2conf *oauth2.Config, provider *oidc.Provider) (string, string) {
oauth2conf.RedirectURL = fmt.Sprintf("http://localhost:%d/auth/callback", port)
oidcConf, err := oidcutil.ParseConfig(provider)
errors.CheckError(err)
var scheme = "https"
if plaintext {
scheme = "http"
}
conf := &oauth2.Config{
ClientID: common.ArgoCDCLIClientAppID,
Scopes: []string{"openid", "profile", "email", "groups", "offline_access"},
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("%s://%s%s/auth", scheme, host, common.DexAPIEndpoint),
TokenURL: fmt.Sprintf("%s://%s%s/token", scheme, host, common.DexAPIEndpoint),
},
RedirectURL: fmt.Sprintf("http://localhost:%d/auth/callback", port),
}
srv := &http.Server{Addr: ":" + strconv.Itoa(port)}
log.Debug("OIDC Configuration:")
log.Debugf(" supported_scopes: %v", oidcConf.ScopesSupported)
log.Debugf(" response_types_supported: %v", oidcConf.ResponseTypesSupported)
// handledRequests ensures we do not handle more requests than necessary
handledRequests := 0
// completionChan is to signal flow completed. Non-empty string indicates error
completionChan := make(chan string)
// stateNonce is an OAuth2 state nonce
stateNonce := rand.RandString(10)
var tokenString string
var refreshToken string
loginCompleted := make(chan struct{})
handleErr := func(w http.ResponseWriter, errMsg string) {
http.Error(w, errMsg, http.StatusBadRequest)
completionChan <- errMsg
}
// Authorization redirect callback from OAuth2 auth flow.
// Handles both implicit and authorization code flow
callbackHandler := func(w http.ResponseWriter, r *http.Request) {
defer func() {
loginCompleted <- struct{}{}
}()
log.Debugf("Callback: %s", r.URL)
// Authorization redirect callback from OAuth2 auth flow.
if errMsg := r.FormValue("error"); errMsg != "" {
http.Error(w, errMsg+": "+r.FormValue("error_description"), http.StatusBadRequest)
log.Fatal(errMsg)
if formErr := r.FormValue("error"); formErr != "" {
handleErr(w, formErr+": "+r.FormValue("error_description"))
return
}
code := r.FormValue("code")
if code == "" {
errMsg := fmt.Sprintf("no code in request: %q", r.Form)
http.Error(w, errMsg, http.StatusBadRequest)
log.Fatal(errMsg)
return
}
tok, err := conf.Exchange(ctx, code)
errors.CheckError(err)
log.Info("Authentication successful")
var ok bool
tokenString, ok = tok.Extra("id_token").(string)
if !ok {
errMsg := "no id_token in token response"
http.Error(w, errMsg, http.StatusInternalServerError)
log.Fatal(errMsg)
handledRequests++
if handledRequests > 2 {
// Since implicit flow will redirect back to ourselves, this counter ensures we do not
// fallinto a redirect loop (e.g. user visits the page by hand)
handleErr(w, "Unable to complete login flow: too many redirects")
return
}
refreshToken, _ = tok.Extra("refresh_token").(string)
log.Debugf("Token: %s", tokenString)
log.Debugf("Refresh Token: %s", tokenString)
if len(r.Form) == 0 {
// If we get here, no form data was set. We presume to be performing an implicit login
// flow where the id_token is contained in a URL fragment, making it inaccessible to be
// read from the request. This javascript will redirect the browser to send the
// fragments as query parameters so our callback handler can read and return token.
fmt.Fprintf(w, `<script>window.location.search = window.location.hash.substring(1)</script>`)
return
}
if state := r.FormValue("state"); state != stateNonce {
handleErr(w, "Unknown state nonce")
return
}
tokenString = r.FormValue("id_token")
if tokenString == "" {
code := r.FormValue("code")
if code == "" {
handleErr(w, fmt.Sprintf("no code in request: %q", r.Form))
return
}
tok, err := oauth2conf.Exchange(ctx, code)
if err != nil {
handleErr(w, err.Error())
return
}
var ok bool
tokenString, ok = tok.Extra("id_token").(string)
if !ok {
handleErr(w, "no id_token in token response")
return
}
refreshToken, _ = tok.Extra("refresh_token").(string)
}
successPage := `
<div style="height:100px; width:100%!; display:flex; flex-direction: column; justify-content: center; align-items:center; background-color:#2ecc71; color:white; font-size:22"><div>Authentication successful!</div></div>
<p style="margin-top:20px; font-size:18; text-align:center">Authentication was successful, you can now return to CLI. This page will close automatically</p>
<script>window.onload=function(){setTimeout(this.close, 4000)}</script>
`
fmt.Fprintf(w, successPage)
completionChan <- ""
}
srv := &http.Server{Addr: ":" + strconv.Itoa(port)}
http.HandleFunc("/auth/callback", callbackHandler)
// add transport for self-signed certificate to context
sslcli := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
ctx = context.WithValue(ctx, oauth2.HTTPClient, sslcli)
// Redirect user to login & consent page to ask for permission for the scopes specified above.
log.Info("Opening browser for authentication")
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
log.Infof("Authentication URL: %s", url)
var url string
grantType := oidcutil.InferGrantType(oauth2conf, oidcConf)
switch grantType {
case oidcutil.GrantTypeAuthorizationCode:
url = oauth2conf.AuthCodeURL(stateNonce, oauth2.AccessTypeOffline)
case oidcutil.GrantTypeImplicit:
url = oidcutil.ImplicitFlowURL(oauth2conf, stateNonce, oauth2.AccessTypeOffline)
default:
log.Fatalf("Unsupported grant type: %v", grantType)
}
log.Infof("Performing %s flow login: %s", grantType, url)
time.Sleep(1 * time.Second)
err = open.Run(url)
errors.CheckError(err)
@@ -243,8 +259,16 @@ func oauth2Login(host string, plaintext bool) (string, string) {
log.Fatalf("listen: %s\n", err)
}
}()
<-loginCompleted
errMsg := <-completionChan
if errMsg != "" {
log.Fatal(errMsg)
}
log.Info("Authentication successful")
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
log.Debugf("Token: %s", tokenString)
log.Debugf("Refresh Token: %s", refreshToken)
return tokenString, refreshToken
}

View File

@@ -1,15 +1,19 @@
package commands
import (
"context"
"fmt"
"os"
oidc "github.com/coreos/go-oidc"
jwt "github.com/dgrijalva/jwt-go"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
"github.com/argoproj/argo-cd/server/settings"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/localconfig"
"github.com/argoproj/argo-cd/util/session"
)
@@ -18,6 +22,7 @@ import (
func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
password string
ssoPort int
)
var command = &cobra.Command{
Use: "relogin",
@@ -45,19 +50,29 @@ func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comm
var tokenString string
var refreshToken string
clientOpts := argocdclient.ClientOptions{
ConfigPath: "",
ServerAddr: configCtx.Server.Server,
Insecure: configCtx.Server.Insecure,
PlainText: configCtx.Server.PlainText,
}
acdClient := argocdclient.NewClientOrDie(&clientOpts)
if claims.Issuer == session.SessionManagerClaimsIssuer {
clientOpts := argocdclient.ClientOptions{
ConfigPath: "",
ServerAddr: configCtx.Server.Server,
Insecure: configCtx.Server.Insecure,
PlainText: configCtx.Server.PlainText,
}
acdClient := argocdclient.NewClientOrDie(&clientOpts)
fmt.Printf("Relogging in as '%s'\n", claims.Subject)
tokenString = passwordLogin(acdClient, claims.Subject, password)
} else {
fmt.Println("Reinitiating SSO login")
tokenString, refreshToken = oauth2Login(configCtx.Server.Server, configCtx.Server.PlainText)
setConn, setIf := acdClient.NewSettingsClientOrDie()
defer util.Close(setConn)
ctx := context.Background()
httpClient, err := acdClient.HTTPClient()
errors.CheckError(err)
ctx = oidc.ClientContext(ctx, httpClient)
acdSet, err := setIf.Get(ctx, &settings.SettingsQuery{})
errors.CheckError(err)
oauth2conf, provider, err := acdClient.OIDCConfig(ctx, acdSet)
errors.CheckError(err)
tokenString, refreshToken = oauth2Login(ctx, ssoPort, oauth2conf, provider)
}
localCfg.UpsertUser(localconfig.User{
@@ -71,5 +86,6 @@ func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comm
},
}
command.Flags().StringVar(&password, "password", "", "the password of an account to authenticate")
command.Flags().IntVar(&ssoPort, "sso-port", DefaultSSOLocalPort, "port to run local OAuth2 login application")
return command
}

View File

@@ -1,13 +1,25 @@
package commands
import (
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
"github.com/argoproj/argo-cd/util/localconfig"
"github.com/spf13/cobra"
"k8s.io/client-go/tools/clientcmd"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/localconfig"
)
func init() {
cobra.OnInitialize(initConfig)
}
var logLevel string
func initConfig() {
cli.SetLogLevel(logLevel)
}
// NewCommand returns a new instance of an argocd command
func NewCommand() *cobra.Command {
var (
@@ -41,6 +53,6 @@ func NewCommand() *cobra.Command {
command.PersistentFlags().BoolVar(&clientOpts.Insecure, "insecure", false, "Skip server certificate and domain verification")
command.PersistentFlags().StringVar(&clientOpts.CertFile, "server-crt", "", "Server certificate file")
command.PersistentFlags().StringVar(&clientOpts.AuthToken, "auth-token", "", "Authentication token")
command.PersistentFlags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
return command
}

View File

@@ -21,7 +21,7 @@ spec:
name: static-files
containers:
- name: dex
image: quay.io/dexidp/dex:v2.11.0
image: quay.io/dexidp/dex:v2.12.0
command: [/shared/argocd-util, rundex]
ports:
- containerPort: 5556

View File

@@ -416,7 +416,7 @@ spec:
- command:
- /shared/argocd-util
- rundex
image: quay.io/dexidp/dex:v2.11.0
image: quay.io/dexidp/dex:v2.12.0
name: dex
ports:
- containerPort: 5556

View File

@@ -356,7 +356,7 @@ spec:
- command:
- /shared/argocd-util
- rundex
image: quay.io/dexidp/dex:v2.11.0
image: quay.io/dexidp/dex:v2.12.0
name: dex
ports:
- containerPort: 5556

View File

@@ -44,10 +44,16 @@ const (
MaxGRPCMessageSize = 100 * 1024 * 1024
)
var (
clientScopes = []string{"openid", "profile", "email", "groups", "offline_access"}
)
// Client defines an interface for interaction with an Argo CD server.
type Client interface {
ClientOptions() ClientOptions
NewConn() (*grpc.ClientConn, error)
HTTPClient() (*http.Client, error)
OIDCConfig(context.Context, *settings.Settings) (*oauth2.Config, *oidc.Provider, error)
NewRepoClient() (*grpc.ClientConn, repository.RepositoryServiceClient, error)
NewRepoClientOrDie() (*grpc.ClientConn, repository.RepositoryServiceClient)
NewClusterClient() (*grpc.ClientConn, cluster.ClusterServiceClient, error)
@@ -160,16 +166,62 @@ func NewClient(opts *ClientOptions) (Client, error) {
return &c, nil
}
// OIDCConfig returns OAuth2 client config and a OpenID Provider based on ArgoCD settings
// ctx can hold an appropriate http.Client to use for the exchange
func (c *client) OIDCConfig(ctx context.Context, set *settings.Settings) (*oauth2.Config, *oidc.Provider, error) {
var clientID string
var issuerURL string
if set.DexConfig != nil && len(set.DexConfig.Connectors) > 0 {
clientID = common.ArgoCDCLIClientAppID
issuerURL = fmt.Sprintf("%s%s", set.URL, common.DexAPIEndpoint)
} else if set.OIDCConfig != nil && set.OIDCConfig.Issuer != "" {
clientID = set.OIDCConfig.ClientID
issuerURL = set.OIDCConfig.Issuer
} else {
return nil, nil, fmt.Errorf("%s is not configured with SSO", c.ServerAddr)
}
provider, err := oidc.NewProvider(ctx, issuerURL)
if err != nil {
return nil, nil, fmt.Errorf("Failed to query provider %q: %v", issuerURL, err)
}
oauth2conf := oauth2.Config{
ClientID: clientID,
Scopes: clientScopes,
Endpoint: provider.Endpoint(),
}
return &oauth2conf, provider, nil
}
// HTTPClient returns a HTTPClient appropriate for performing OAuth, based on TLS settings
func (c *client) HTTPClient() (*http.Client, error) {
tlsConfig, err := c.tlsConfig()
if err != nil {
return nil, err
}
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}, nil
}
// refreshAuthToken refreshes a JWT auth token if it is invalid (e.g. expired)
func (c *client) refreshAuthToken(localCfg *localconfig.LocalConfig, ctxName, configPath string) error {
configCtx, err := localCfg.ResolveContext(ctxName)
if err != nil {
return err
}
if c.RefreshToken == "" {
// If we have no refresh token, there's no point in doing anything
return nil
}
configCtx, err := localCfg.ResolveContext(ctxName)
if err != nil {
return err
}
parser := &jwt.Parser{
SkipClaimsValidation: true,
}
@@ -184,50 +236,10 @@ func (c *client) refreshAuthToken(localCfg *localconfig.LocalConfig, ctxName, co
}
log.Debug("Auth token no longer valid. Refreshing")
tlsConfig, err := c.tlsConfig()
rawIDToken, refreshToken, err := c.redeemRefreshToken()
if err != nil {
return err
}
httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
ctx := oidc.ClientContext(context.Background(), httpClient)
var scheme string
if c.PlainText {
scheme = "http"
} else {
scheme = "https"
}
conf := &oauth2.Config{
ClientID: common.ArgoCDCLIClientAppID,
Scopes: []string{"openid", "profile", "email", "groups", "offline_access"},
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("%s://%s%s/auth", scheme, c.ServerAddr, common.DexAPIEndpoint),
TokenURL: fmt.Sprintf("%s://%s%s/token", scheme, c.ServerAddr, common.DexAPIEndpoint),
},
RedirectURL: fmt.Sprintf("%s://%s/auth/callback", scheme, c.ServerAddr),
}
t := &oauth2.Token{
RefreshToken: c.RefreshToken,
}
token, err := conf.TokenSource(ctx, t).Token()
if err != nil {
return err
}
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
return errors.New("no id_token in token response")
}
refreshToken, _ := token.Extra("refresh_token").(string)
c.AuthToken = rawIDToken
c.RefreshToken = refreshToken
localCfg.UpsertUser(localconfig.User{
@@ -242,6 +254,41 @@ func (c *client) refreshAuthToken(localCfg *localconfig.LocalConfig, ctxName, co
return nil
}
// redeemRefreshToken performs the exchange of a refresh_token for a new id_token and refresh_token
func (c *client) redeemRefreshToken() (string, string, error) {
setConn, setIf, err := c.NewSettingsClient()
if err != nil {
return "", "", err
}
defer func() { _ = setConn.Close() }()
httpClient, err := c.HTTPClient()
if err != nil {
return "", "", err
}
ctx := oidc.ClientContext(context.Background(), httpClient)
acdSet, err := setIf.Get(ctx, &settings.SettingsQuery{})
if err != nil {
return "", "", err
}
oauth2conf, _, err := c.OIDCConfig(ctx, acdSet)
if err != nil {
return "", "", err
}
t := &oauth2.Token{
RefreshToken: c.RefreshToken,
}
token, err := oauth2conf.TokenSource(ctx, t).Token()
if err != nil {
return "", "", err
}
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
return "", "", errors.New("no id_token in token response")
}
refreshToken, _ := token.Extra("refresh_token").(string)
return rawIDToken, refreshToken, nil
}
// NewClientOrDie creates a new API client from a set of config options, or fails fatally if the new client creation fails.
func NewClientOrDie(opts *ClientOptions) Client {
client, err := NewClient(opts)

View File

@@ -58,9 +58,11 @@ import (
dexutil "github.com/argoproj/argo-cd/util/dex"
grpc_util "github.com/argoproj/argo-cd/util/grpc"
"github.com/argoproj/argo-cd/util/healthz"
httputil "github.com/argoproj/argo-cd/util/http"
jsonutil "github.com/argoproj/argo-cd/util/json"
jwtutil "github.com/argoproj/argo-cd/util/jwt"
"github.com/argoproj/argo-cd/util/kube"
"github.com/argoproj/argo-cd/util/oidc"
projectutil "github.com/argoproj/argo-cd/util/project"
"github.com/argoproj/argo-cd/util/rbac"
util_session "github.com/argoproj/argo-cd/util/session"
@@ -104,7 +106,7 @@ func init() {
type ArgoCDServer struct {
ArgoCDServerOpts
ssoClientApp *dexutil.ClientApp
ssoClientApp *oidc.ClientApp
settings *settings_util.ArgoCDSettings
log *log.Entry
sessionMgr *util_session.SessionManager
@@ -121,6 +123,7 @@ type ArgoCDServerOpts struct {
DisableAuth bool
Insecure bool
Namespace string
DexServerAddr string
StaticAssetsDir string
KubeClientset kubernetes.Interface
AppClientset appclientset.Interface
@@ -307,6 +310,8 @@ func (a *ArgoCDServer) watchSettings(ctx context.Context) {
updateCh := make(chan struct{}, 1)
a.settingsMgr.Subscribe(updateCh)
prevURL := a.settings.URL
prevOIDCConfig := a.settings.OIDCConfigRAW
prevDexCfgBytes, err := dex.GenerateDexConfigYAML(a.settings)
errors.CheckError(err)
prevGitHubSecret := a.settings.WebhookGitHubSecret
@@ -325,6 +330,14 @@ func (a *ArgoCDServer) watchSettings(ctx context.Context) {
log.Infof("dex config modified. restarting")
break
}
if prevOIDCConfig != a.settings.OIDCConfigRAW {
log.Infof("odic config modified. restarting")
break
}
if prevURL != a.settings.URL {
log.Infof("url modified. restarting")
break
}
if prevGitHubSecret != a.settings.WebhookGitHubSecret {
log.Infof("github secret modified. restarting")
break
@@ -431,7 +444,7 @@ func (a *ArgoCDServer) translateGrpcCookieHeader(ctx context.Context, w http.Res
if !a.Insecure {
flags = append(flags, "Secure")
}
cookie := util_session.MakeCookieMetadata(common.AuthCookieName, sessionResp.Token, flags...)
cookie := httputil.MakeCookieMetadata(common.AuthCookieName, sessionResp.Token, flags...)
w.Header().Set("Set-Cookie", cookie)
}
return nil
@@ -524,10 +537,10 @@ func (a *ArgoCDServer) registerDexHandlers(mux *http.ServeMux) {
}
// Run dex OpenID Connect Identity Provider behind a reverse proxy (served at /api/dex)
var err error
mux.HandleFunc(common.DexAPIEndpoint+"/", dexutil.NewDexHTTPReverseProxy())
mux.HandleFunc(common.DexAPIEndpoint+"/", dexutil.NewDexHTTPReverseProxy(a.DexServerAddr))
tlsConfig := a.settings.TLSConfig()
tlsConfig.InsecureSkipVerify = true
a.ssoClientApp, err = dexutil.NewClientApp(a.settings, a.sessionMgr)
a.ssoClientApp, err = oidc.NewClientApp(a.settings)
errors.CheckError(err)
mux.HandleFunc(common.LoginEndpoint, a.ssoClientApp.HandleLogin)
mux.HandleFunc(common.CallbackEndpoint, a.ssoClientApp.HandleCallback)

View File

@@ -27,10 +27,19 @@ func (s *Server) Get(ctx context.Context, q *SettingsQuery) (*Settings, error) {
set := Settings{
URL: argoCDSettings.URL,
}
var cfg DexConfig
err = yaml.Unmarshal([]byte(argoCDSettings.DexConfig), &cfg)
if err == nil {
set.DexConfig = &cfg
if argoCDSettings.DexConfig != "" {
var cfg DexConfig
err = yaml.Unmarshal([]byte(argoCDSettings.DexConfig), &cfg)
if err == nil {
set.DexConfig = &cfg
}
}
if oidcConfig := argoCDSettings.OIDCConfig(); oidcConfig != nil {
set.OIDCConfig = &OIDCConfig{
Name: oidcConfig.Name,
Issuer: oidcConfig.Issuer,
ClientID: oidcConfig.ClientID,
}
}
return &set, nil
}

View File

@@ -42,7 +42,7 @@ func (m *SettingsQuery) Reset() { *m = SettingsQuery{} }
func (m *SettingsQuery) String() string { return proto.CompactTextString(m) }
func (*SettingsQuery) ProtoMessage() {}
func (*SettingsQuery) Descriptor() ([]byte, []int) {
return fileDescriptor_settings_71506a99e4ff7448, []int{0}
return fileDescriptor_settings_843b05f20a608370, []int{0}
}
func (m *SettingsQuery) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@@ -72,18 +72,19 @@ func (m *SettingsQuery) XXX_DiscardUnknown() {
var xxx_messageInfo_SettingsQuery proto.InternalMessageInfo
type Settings struct {
URL string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
DexConfig *DexConfig `protobuf:"bytes,2,opt,name=dexConfig" json:"dexConfig,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
URL string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
DexConfig *DexConfig `protobuf:"bytes,2,opt,name=dexConfig" json:"dexConfig,omitempty"`
OIDCConfig *OIDCConfig `protobuf:"bytes,3,opt,name=oidcConfig" json:"oidcConfig,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Settings) Reset() { *m = Settings{} }
func (m *Settings) String() string { return proto.CompactTextString(m) }
func (*Settings) ProtoMessage() {}
func (*Settings) Descriptor() ([]byte, []int) {
return fileDescriptor_settings_71506a99e4ff7448, []int{1}
return fileDescriptor_settings_843b05f20a608370, []int{1}
}
func (m *Settings) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@@ -126,6 +127,13 @@ func (m *Settings) GetDexConfig() *DexConfig {
return nil
}
func (m *Settings) GetOIDCConfig() *OIDCConfig {
if m != nil {
return m.OIDCConfig
}
return nil
}
type DexConfig struct {
Connectors []*Connector `protobuf:"bytes,1,rep,name=connectors" json:"connectors,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
@@ -137,7 +145,7 @@ func (m *DexConfig) Reset() { *m = DexConfig{} }
func (m *DexConfig) String() string { return proto.CompactTextString(m) }
func (*DexConfig) ProtoMessage() {}
func (*DexConfig) Descriptor() ([]byte, []int) {
return fileDescriptor_settings_71506a99e4ff7448, []int{2}
return fileDescriptor_settings_843b05f20a608370, []int{2}
}
func (m *DexConfig) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@@ -185,7 +193,7 @@ func (m *Connector) Reset() { *m = Connector{} }
func (m *Connector) String() string { return proto.CompactTextString(m) }
func (*Connector) ProtoMessage() {}
func (*Connector) Descriptor() ([]byte, []int) {
return fileDescriptor_settings_71506a99e4ff7448, []int{3}
return fileDescriptor_settings_843b05f20a608370, []int{3}
}
func (m *Connector) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@@ -228,11 +236,75 @@ func (m *Connector) GetType() string {
return ""
}
type OIDCConfig struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Issuer string `protobuf:"bytes,2,opt,name=issuer,proto3" json:"issuer,omitempty"`
ClientID string `protobuf:"bytes,3,opt,name=clientID,proto3" json:"clientID,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *OIDCConfig) Reset() { *m = OIDCConfig{} }
func (m *OIDCConfig) String() string { return proto.CompactTextString(m) }
func (*OIDCConfig) ProtoMessage() {}
func (*OIDCConfig) Descriptor() ([]byte, []int) {
return fileDescriptor_settings_843b05f20a608370, []int{4}
}
func (m *OIDCConfig) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *OIDCConfig) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_OIDCConfig.Marshal(b, m, deterministic)
} else {
b = b[:cap(b)]
n, err := m.MarshalTo(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
}
func (dst *OIDCConfig) XXX_Merge(src proto.Message) {
xxx_messageInfo_OIDCConfig.Merge(dst, src)
}
func (m *OIDCConfig) XXX_Size() int {
return m.Size()
}
func (m *OIDCConfig) XXX_DiscardUnknown() {
xxx_messageInfo_OIDCConfig.DiscardUnknown(m)
}
var xxx_messageInfo_OIDCConfig proto.InternalMessageInfo
func (m *OIDCConfig) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *OIDCConfig) GetIssuer() string {
if m != nil {
return m.Issuer
}
return ""
}
func (m *OIDCConfig) GetClientID() string {
if m != nil {
return m.ClientID
}
return ""
}
func init() {
proto.RegisterType((*SettingsQuery)(nil), "cluster.SettingsQuery")
proto.RegisterType((*Settings)(nil), "cluster.Settings")
proto.RegisterType((*DexConfig)(nil), "cluster.DexConfig")
proto.RegisterType((*Connector)(nil), "cluster.Connector")
proto.RegisterType((*OIDCConfig)(nil), "cluster.OIDCConfig")
}
// Reference imports to suppress errors if they are not otherwise used.
@@ -361,6 +433,16 @@ func (m *Settings) MarshalTo(dAtA []byte) (int, error) {
}
i += n1
}
if m.OIDCConfig != nil {
dAtA[i] = 0x1a
i++
i = encodeVarintSettings(dAtA, i, uint64(m.OIDCConfig.Size()))
n2, err := m.OIDCConfig.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n2
}
if m.XXX_unrecognized != nil {
i += copy(dAtA[i:], m.XXX_unrecognized)
}
@@ -433,6 +515,45 @@ func (m *Connector) MarshalTo(dAtA []byte) (int, error) {
return i, nil
}
func (m *OIDCConfig) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *OIDCConfig) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if len(m.Name) > 0 {
dAtA[i] = 0xa
i++
i = encodeVarintSettings(dAtA, i, uint64(len(m.Name)))
i += copy(dAtA[i:], m.Name)
}
if len(m.Issuer) > 0 {
dAtA[i] = 0x12
i++
i = encodeVarintSettings(dAtA, i, uint64(len(m.Issuer)))
i += copy(dAtA[i:], m.Issuer)
}
if len(m.ClientID) > 0 {
dAtA[i] = 0x1a
i++
i = encodeVarintSettings(dAtA, i, uint64(len(m.ClientID)))
i += copy(dAtA[i:], m.ClientID)
}
if m.XXX_unrecognized != nil {
i += copy(dAtA[i:], m.XXX_unrecognized)
}
return i, nil
}
func encodeVarintSettings(dAtA []byte, offset int, v uint64) int {
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
@@ -462,6 +583,10 @@ func (m *Settings) Size() (n int) {
l = m.DexConfig.Size()
n += 1 + l + sovSettings(uint64(l))
}
if m.OIDCConfig != nil {
l = m.OIDCConfig.Size()
n += 1 + l + sovSettings(uint64(l))
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
@@ -500,6 +625,27 @@ func (m *Connector) Size() (n int) {
return n
}
func (m *OIDCConfig) Size() (n int) {
var l int
_ = l
l = len(m.Name)
if l > 0 {
n += 1 + l + sovSettings(uint64(l))
}
l = len(m.Issuer)
if l > 0 {
n += 1 + l + sovSettings(uint64(l))
}
l = len(m.ClientID)
if l > 0 {
n += 1 + l + sovSettings(uint64(l))
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
return n
}
func sovSettings(x uint64) (n int) {
for {
n++
@@ -655,6 +801,39 @@ func (m *Settings) Unmarshal(dAtA []byte) error {
return err
}
iNdEx = postIndex
case 3:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field OIDCConfig", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowSettings
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthSettings
}
postIndex := iNdEx + msglen
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.OIDCConfig == nil {
m.OIDCConfig = &OIDCConfig{}
}
if err := m.OIDCConfig.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipSettings(dAtA[iNdEx:])
@@ -868,6 +1047,144 @@ func (m *Connector) Unmarshal(dAtA []byte) error {
}
return nil
}
func (m *OIDCConfig) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowSettings
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: OIDCConfig: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: OIDCConfig: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowSettings
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthSettings
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Name = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Issuer", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowSettings
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthSettings
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Issuer = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 3:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field ClientID", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowSettings
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthSettings
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.ClientID = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipSettings(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthSettings
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func skipSettings(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
@@ -974,30 +1291,34 @@ var (
)
func init() {
proto.RegisterFile("server/settings/settings.proto", fileDescriptor_settings_71506a99e4ff7448)
proto.RegisterFile("server/settings/settings.proto", fileDescriptor_settings_843b05f20a608370)
}
var fileDescriptor_settings_71506a99e4ff7448 = []byte{
// 322 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x91, 0x41, 0x4b, 0xc3, 0x40,
0x10, 0x85, 0xd9, 0x46, 0xac, 0x19, 0x91, 0xea, 0x22, 0x12, 0x8b, 0xc4, 0x92, 0x53, 0x41, 0x4c,
0xb4, 0x3d, 0x79, 0x12, 0x5a, 0x41, 0x10, 0x2f, 0xa6, 0x88, 0x20, 0x78, 0x48, 0xd3, 0x71, 0x8d,
0xb4, 0x3b, 0x65, 0xb3, 0x29, 0xf6, 0xea, 0x5f, 0xf0, 0x4f, 0x79, 0x14, 0xbc, 0x8b, 0x04, 0x7f,
0x88, 0x74, 0xdb, 0x44, 0xab, 0xb7, 0xc7, 0xf7, 0x66, 0x92, 0xb7, 0xf3, 0xc0, 0x4d, 0x51, 0x4d,
0x50, 0x05, 0x29, 0x6a, 0x9d, 0x48, 0x91, 0x96, 0xc2, 0x1f, 0x2b, 0xd2, 0xc4, 0xab, 0xf1, 0x30,
0x4b, 0x35, 0xaa, 0xfa, 0xb6, 0x20, 0x41, 0x86, 0x05, 0x33, 0x35, 0xb7, 0xeb, 0x7b, 0x82, 0x48,
0x0c, 0x31, 0x88, 0xc6, 0x49, 0x10, 0x49, 0x49, 0x3a, 0xd2, 0x09, 0xc9, 0xc5, 0xb2, 0x57, 0x83,
0x8d, 0xde, 0xe2, 0x73, 0x57, 0x19, 0xaa, 0xa9, 0x77, 0x03, 0x6b, 0x05, 0xe0, 0xbb, 0x60, 0x65,
0x6a, 0xe8, 0xb0, 0x06, 0x6b, 0xda, 0x9d, 0x6a, 0xfe, 0xb1, 0x6f, 0x5d, 0x87, 0x97, 0xe1, 0x8c,
0xf1, 0x23, 0xb0, 0x07, 0xf8, 0xd4, 0x25, 0x79, 0x9f, 0x08, 0xa7, 0xd2, 0x60, 0xcd, 0xf5, 0x16,
0xf7, 0x17, 0x41, 0xfc, 0xb3, 0xc2, 0x09, 0x7f, 0x86, 0xbc, 0x53, 0xb0, 0x4b, 0xce, 0x5b, 0x00,
0x31, 0x49, 0x89, 0xb1, 0x26, 0x95, 0x3a, 0xac, 0x61, 0x2d, 0xed, 0x77, 0x0b, 0x2b, 0xfc, 0x35,
0xe5, 0xb5, 0xc1, 0x2e, 0x0d, 0xce, 0x61, 0x45, 0x46, 0x23, 0x9c, 0x67, 0x0b, 0x8d, 0x9e, 0x31,
0x3d, 0x1d, 0xa3, 0x89, 0x63, 0x87, 0x46, 0xb7, 0xee, 0xa0, 0x56, 0x3c, 0xa7, 0x87, 0x6a, 0x92,
0xc4, 0xc8, 0x2f, 0xc0, 0x3a, 0x47, 0xcd, 0x77, 0xca, 0xdf, 0x2d, 0x1d, 0xa0, 0xbe, 0xf5, 0x8f,
0x7b, 0xce, 0xf3, 0xfb, 0xd7, 0x4b, 0x85, 0xf3, 0x4d, 0x73, 0xc4, 0xc9, 0x71, 0xd9, 0x40, 0xe7,
0xe4, 0x35, 0x77, 0xd9, 0x5b, 0xee, 0xb2, 0xcf, 0xdc, 0x65, 0xb7, 0x07, 0x22, 0xd1, 0x0f, 0x59,
0xdf, 0x8f, 0x69, 0x14, 0x44, 0xca, 0x74, 0xf1, 0x68, 0xc4, 0x61, 0x3c, 0x08, 0xfe, 0xb4, 0xd8,
0x5f, 0x35, 0x05, 0xb4, 0xbf, 0x03, 0x00, 0x00, 0xff, 0xff, 0xef, 0x0e, 0xd5, 0xb9, 0xdf, 0x01,
0x00, 0x00,
var fileDescriptor_settings_843b05f20a608370 = []byte{
// 397 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x52, 0xcd, 0x8a, 0xdb, 0x30,
0x18, 0x44, 0x71, 0x49, 0xe2, 0xaf, 0x3f, 0x69, 0xd5, 0x12, 0xdc, 0x50, 0x9c, 0xe0, 0x53, 0xa0,
0xd4, 0x6e, 0x93, 0x53, 0x4f, 0x05, 0x3b, 0x50, 0x52, 0x0a, 0xa5, 0x0a, 0xbd, 0x14, 0x7a, 0x70,
0x14, 0xd5, 0x55, 0x71, 0xa4, 0x20, 0xcb, 0xa1, 0xb9, 0xf6, 0x15, 0xf6, 0xba, 0x0f, 0xb4, 0xc7,
0x85, 0xbd, 0x87, 0xc5, 0xec, 0x83, 0x2c, 0x51, 0x6c, 0x27, 0xd9, 0xdd, 0xdb, 0x68, 0x46, 0x23,
0xbe, 0xd1, 0x37, 0xe0, 0x66, 0x4c, 0xad, 0x99, 0x0a, 0x32, 0xa6, 0x35, 0x17, 0x49, 0x56, 0x03,
0x7f, 0xa5, 0xa4, 0x96, 0xb8, 0x45, 0xd3, 0x3c, 0xd3, 0x4c, 0xf5, 0x5e, 0x25, 0x32, 0x91, 0x86,
0x0b, 0x76, 0x68, 0x2f, 0xf7, 0xde, 0x24, 0x52, 0x26, 0x29, 0x0b, 0xe2, 0x15, 0x0f, 0x62, 0x21,
0xa4, 0x8e, 0x35, 0x97, 0xa2, 0x34, 0x7b, 0x1d, 0x78, 0x3a, 0x2b, 0x9f, 0xfb, 0x9e, 0x33, 0xb5,
0xf1, 0xce, 0x11, 0xb4, 0x2b, 0x06, 0xbf, 0x06, 0x2b, 0x57, 0xa9, 0x83, 0x06, 0x68, 0x68, 0x87,
0xad, 0x62, 0xdb, 0xb7, 0x7e, 0x90, 0xaf, 0x64, 0xc7, 0xe1, 0xf7, 0x60, 0x2f, 0xd8, 0xbf, 0x48,
0x8a, 0xdf, 0x3c, 0x71, 0x1a, 0x03, 0x34, 0x7c, 0x3c, 0xc2, 0x7e, 0x39, 0x89, 0x3f, 0xa9, 0x14,
0x72, 0xb8, 0x84, 0x23, 0x00, 0xc9, 0x17, 0xb4, 0xb4, 0x58, 0xc6, 0xf2, 0xb2, 0xb6, 0x7c, 0x9b,
0x4e, 0xa2, 0xbd, 0x14, 0x3e, 0x2b, 0xb6, 0x7d, 0x38, 0x9c, 0xc9, 0x91, 0xcd, 0xfb, 0x04, 0x76,
0xfd, 0x38, 0x1e, 0x01, 0x50, 0x29, 0x04, 0xa3, 0x5a, 0xaa, 0xcc, 0x41, 0x03, 0xeb, 0x64, 0x88,
0xa8, 0x92, 0xc8, 0xd1, 0x2d, 0x6f, 0x0c, 0x76, 0x2d, 0x60, 0x0c, 0x8f, 0x44, 0xbc, 0x64, 0xfb,
0x80, 0xc4, 0xe0, 0x1d, 0xa7, 0x37, 0x2b, 0x66, 0x32, 0xd9, 0xc4, 0x60, 0x6f, 0x0e, 0x47, 0xf3,
0x3c, 0xe8, 0xea, 0x42, 0x93, 0x67, 0x59, 0xce, 0x54, 0xe9, 0x2b, 0x4f, 0x78, 0x08, 0x6d, 0x9a,
0x72, 0x26, 0xf4, 0x74, 0x62, 0x22, 0xdb, 0xe1, 0x93, 0x62, 0xdb, 0x6f, 0x47, 0x25, 0x47, 0x6a,
0x75, 0xf4, 0x0b, 0x3a, 0xd5, 0xbf, 0xcf, 0x98, 0x5a, 0x73, 0xca, 0xf0, 0x17, 0xb0, 0x3e, 0x33,
0x8d, 0xbb, 0x75, 0xa4, 0x93, 0x55, 0xf5, 0x5e, 0xdc, 0xe3, 0x3d, 0xe7, 0xff, 0xd5, 0xcd, 0x59,
0x03, 0xe3, 0xe7, 0x66, 0xdd, 0xeb, 0x0f, 0x75, 0x57, 0xc2, 0x8f, 0x17, 0x85, 0x8b, 0x2e, 0x0b,
0x17, 0x5d, 0x17, 0x2e, 0xfa, 0xf9, 0x36, 0xe1, 0xfa, 0x4f, 0x3e, 0xf7, 0xa9, 0x5c, 0x06, 0xb1,
0x32, 0xad, 0xf9, 0x6b, 0xc0, 0x3b, 0xba, 0x08, 0xee, 0xf4, 0x6d, 0xde, 0x34, 0x55, 0x19, 0xdf,
0x06, 0x00, 0x00, 0xff, 0xff, 0xbf, 0xa2, 0x52, 0xc6, 0x89, 0x02, 0x00, 0x00,
}

View File

@@ -16,6 +16,7 @@ message SettingsQuery {
message Settings {
string url = 1 [(gogoproto.customname) = "URL"];
DexConfig dexConfig = 2;
OIDCConfig oidcConfig = 3 [(gogoproto.customname) = "OIDCConfig"];
}
message DexConfig {
@@ -27,6 +28,12 @@ message Connector {
string type = 2;
}
message OIDCConfig {
string name = 1;
string issuer = 2;
string clientID = 3 [(gogoproto.customname) = "ClientID"];
}
// SettingsService
service SettingsService {

View File

@@ -1349,12 +1349,29 @@
}
}
},
"clusterOIDCConfig": {
"type": "object",
"properties": {
"clientID": {
"type": "string"
},
"issuer": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"clusterSettings": {
"type": "object",
"properties": {
"dexConfig": {
"$ref": "#/definitions/clusterDexConfig"
},
"oidcConfig": {
"$ref": "#/definitions/clusterOIDCConfig"
},
"url": {
"type": "string"
}

View File

@@ -4,16 +4,20 @@ package cli
import (
"bufio"
"flag"
"fmt"
"os"
"strconv"
"strings"
"syscall"
argocd "github.com/argoproj/argo-cd"
"github.com/argoproj/argo-cd/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
"k8s.io/client-go/tools/clientcmd"
argocd "github.com/argoproj/argo-cd"
"github.com/argoproj/argo-cd/errors"
)
// NewVersionCmd returns a new `version` command to be used as a sub-command to root
@@ -105,3 +109,39 @@ func AskToProceed(message string) bool {
}
}
}
// ReadAndConfirmPassword is a helper to read and confirm a password from stdin
func ReadAndConfirmPassword() (string, error) {
for {
fmt.Print("*** Enter new password: ")
password, err := terminal.ReadPassword(syscall.Stdin)
if err != nil {
return "", err
}
fmt.Print("\n")
fmt.Print("*** Confirm new password: ")
confirmPassword, err := terminal.ReadPassword(syscall.Stdin)
if err != nil {
return "", err
}
fmt.Print("\n")
if string(password) == string(confirmPassword) {
return string(password), nil
}
log.Error("Passwords do not match")
}
}
// SetLogLevel parses and sets a logrus log level
func SetLogLevel(logLevel string) {
level, err := log.ParseLevel(logLevel)
errors.CheckError(err)
log.SetLevel(level)
}
// SetGLogLevel set the glog level for the k8s go-client
func SetGLogLevel(glogLevel int) {
_ = flag.CommandLine.Parse([]string{})
_ = flag.Lookup("logtostderr").Value.Set("true")
_ = flag.Lookup("v").Value.Set(strconv.Itoa(glogLevel))
}

View File

@@ -11,7 +11,7 @@ import (
)
func GenerateDexConfigYAML(settings *settings.ArgoCDSettings) ([]byte, error) {
if !settings.IsSSOConfigured() {
if !settings.IsDexConfigured() {
return nil, nil
}
var dexCfg map[string]interface{}
@@ -36,7 +36,7 @@ func GenerateDexConfigYAML(settings *settings.ArgoCDSettings) ([]byte, error) {
{
"id": common.ArgoCDClientAppID,
"name": common.ArgoCDClientAppName,
"secret": settings.OAuth2ClientSecret(),
"secret": settings.DexOAuth2ClientSecret(),
"redirectURIs": []string{
settings.RedirectURL(),
},
@@ -118,10 +118,10 @@ func replaceStringSecret(val string, secretValues map[string]string) string {
// needsRedirectURI returns whether or not the given connector type needs a redirectURI
// Update this list as necessary, as new connectors are added
// https://github.com/coreos/dex/tree/master/Documentation/connectors
// https://github.com/dexidp/dex/tree/master/Documentation/connectors
func needsRedirectURI(connectorType string) bool {
switch connectorType {
case "oidc", "saml", "microsoft", "linkedin", "gitlab", "github":
case "oidc", "saml", "microsoft", "linkedin", "gitlab", "github", "bitbucket-cloud":
return true
}
return false

View File

@@ -1,56 +1,26 @@
package dex
import (
"context"
"crypto/rand"
"encoding/json"
"fmt"
"html"
"io/ioutil"
"math/big"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/coreos/dex/api"
oidc "github.com/coreos/go-oidc"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"google.golang.org/grpc"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/errors"
"github.com/argoproj/argo-cd/util/cache"
"github.com/argoproj/argo-cd/util/session"
"github.com/argoproj/argo-cd/util/settings"
)
const (
// DexReverseProxyAddr is the address of the Dex OIDC server, which we run a reverse proxy against
DexReverseProxyAddr = "http://dex-server:5556"
// DexgRPCAPIAddr is the address to the Dex gRPC API server for managing dex. This is assumed to run
// locally (as a sidecar)
DexgRPCAPIAddr = "dex-server:5557"
)
var messageRe = regexp.MustCompile(`<p>(.*)([\s\S]*?)<\/p>`)
type DexAPIClient struct {
api.DexClient
}
// NewDexHTTPReverseProxy returns a reverse proxy to the DEX server. Dex is assumed to be configured
// NewDexHTTPReverseProxy returns a reverse proxy to the Dex server. Dex is assumed to be configured
// with the external issuer URL muxed to the same path configured in server.go. In other words, if
// ArgoCD API server wants to proxy requests at /api/dex, then the dex config yaml issuer URL should
// also be /api/dex (e.g. issuer: https://argocd.example.com/api/dex)
func NewDexHTTPReverseProxy() func(writer http.ResponseWriter, request *http.Request) {
target, err := url.Parse(DexReverseProxyAddr)
func NewDexHTTPReverseProxy(serverAddr string) func(writer http.ResponseWriter, request *http.Request) {
target, err := url.Parse(serverAddr)
errors.CheckError(err)
proxy := httputil.NewSingleHostReverseProxy(target)
proxy.ModifyResponse = func(resp *http.Response) error {
@@ -82,273 +52,3 @@ func NewDexHTTPReverseProxy() func(writer http.ResponseWriter, request *http.Req
proxy.ServeHTTP(w, r)
}
}
func NewDexClient() (*DexAPIClient, error) {
conn, err := grpc.Dial(DexgRPCAPIAddr, grpc.WithInsecure())
if err != nil {
return nil, fmt.Errorf("failed to dial %s: %v", DexgRPCAPIAddr, err)
}
apiClient := DexAPIClient{
api.NewDexClient(conn),
}
return &apiClient, nil
}
// WaitUntilReady waits until the dex gRPC server is responding
func (d *DexAPIClient) WaitUntilReady() {
log.Info("Waiting for dex to become ready")
ctx := context.Background()
for {
vers, err := d.GetVersion(ctx, &api.VersionReq{})
if err == nil {
log.Infof("Dex %s (API: %d) up and running", vers.Server, vers.Api)
return
}
time.Sleep(1 * time.Second)
}
}
type ClientApp struct {
// OAuth2 client ID of this application (e.g. argo-cd)
clientID string
// OAuth2 client secret of this application
clientSecret string
// Callback URL for OAuth2 responses (e.g. https://argocd.example.com/auth/callback)
redirectURI string
// URL of the issuer (e.g. https://argocd.example.com/api/dex)
issuerURL string
// client is the HTTP client which is used to query the IDp
client *http.Client
// secureCookie indicates if the cookie should be set with the Secure flag, meaning it should
// only ever be sent over HTTPS. This value is inferred by the scheme of the redirectURI.
secureCookie bool
// settings holds ArgoCD settings
settings *settings.ArgoCDSettings
// sessionMgr holds an ArgoCD session manager
sessionMgr *session.SessionManager
// states holds temporary nonce tokens to which hold application state values
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
states cache.Cache
}
type appState struct {
// ReturnURL is the URL in which to redirect a user back to after completing an OAuth2 login
ReturnURL string `json:"returnURL"`
}
// NewClientApp will register the ArgoCD client app in Dex and return an object which has HTTP
// handlers for handling the HTTP responses for login and callback
func NewClientApp(settings *settings.ArgoCDSettings, sessionMgr *session.SessionManager) (*ClientApp, error) {
log.Infof("Creating client app (%s)", common.ArgoCDClientAppID)
a := ClientApp{
clientID: common.ArgoCDClientAppID,
clientSecret: settings.OAuth2ClientSecret(),
redirectURI: settings.RedirectURL(),
issuerURL: settings.IssuerURL(),
}
u, err := url.Parse(settings.URL)
if err != nil {
return nil, fmt.Errorf("parse redirect-uri: %v", err)
}
tlsConfig := settings.TLSConfig()
if tlsConfig != nil {
tlsConfig.InsecureSkipVerify = true
}
a.client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
// NOTE: if we ever have replicas of ArgoCD, this needs to switch to Redis cache
a.states = cache.NewInMemoryCache(3 * time.Minute)
a.secureCookie = bool(u.Scheme == "https")
a.settings = settings
a.sessionMgr = sessionMgr
return &a, nil
}
func (a *ClientApp) oauth2Config(scopes []string) (*oauth2.Config, error) {
provider, err := a.sessionMgr.OIDCProvider()
if err != nil {
return nil, err
}
return &oauth2.Config{
ClientID: a.clientID,
ClientSecret: a.clientSecret,
Endpoint: provider.Endpoint(),
Scopes: scopes,
RedirectURL: a.redirectURI,
}, nil
}
// RandString generates, from a given charset, a cryptographically-secure pseudo-random string of a given length.
// If the random number reader is unable to gather enough entropy to generate a secure random number, an error will be returned.
func randString(n int, charset string) (string, error) {
var b strings.Builder
rr := []rune(charset)
m := big.NewInt(int64(len(rr)))
for i := 0; i < n; i++ {
pos, err := rand.Int(rand.Reader, m)
if err != nil {
return b.String(), err
}
_, _ = b.WriteRune(rr[pos.Int64()])
}
return b.String(), nil
}
// generateAppState creates an app state nonce
func (a *ClientApp) generateAppState(returnURL string) string {
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
randStr, err := randString(10, letters)
if err != nil {
log.Fatalf("Could not generate entropy: %v", err)
}
if returnURL == "" {
returnURL = "/"
}
err = a.states.Set(&cache.Item{
Key: randStr,
Object: &appState{
ReturnURL: returnURL,
},
})
if err != nil {
// This should never happen with the in-memory cache
log.Errorf("Failed to set app state: %v", err)
}
return randStr
}
func (a *ClientApp) verifyAppState(state string) (*appState, error) {
var aState appState
err := a.states.Get(state, &aState)
if err != nil {
if err == cache.ErrCacheMiss {
return nil, fmt.Errorf("unknown app state %s", state)
} else {
return nil, fmt.Errorf("failed to verify app state %s: %v", state, err)
}
}
// TODO: purge the state string from the cache so that it is a true nonce
return &aState, nil
}
func (a *ClientApp) HandleLogin(w http.ResponseWriter, r *http.Request) {
var opts []oauth2.AuthCodeOption
returnURL := r.FormValue("return_url")
scopes := []string{"openid", "profile", "email", "groups"}
appState := a.generateAppState(returnURL)
if r.FormValue("offline_access") != "yes" {
// no-op
} else if a.sessionMgr.OfflineAsScope() {
scopes = append(scopes, "offline_access")
} else {
opts = append(opts, oauth2.AccessTypeOffline)
}
oauth2Config, err := a.oauth2Config(scopes)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
authCodeURL := oauth2Config.AuthCodeURL(appState, opts...)
http.Redirect(w, r, authCodeURL, http.StatusSeeOther)
}
func (a *ClientApp) HandleCallback(w http.ResponseWriter, r *http.Request) {
var (
err error
token *oauth2.Token
returnURL string
)
ctx := oidc.ClientContext(r.Context(), a.client)
oauth2Config, err := a.oauth2Config(nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
switch r.Method {
case "GET":
// Authorization redirect callback from OAuth2 auth flow.
if errMsg := r.FormValue("error"); errMsg != "" {
http.Error(w, errMsg+": "+r.FormValue("error_description"), http.StatusBadRequest)
return
}
code := r.FormValue("code")
if code == "" {
http.Error(w, fmt.Sprintf("no code in request: %q", r.Form), http.StatusBadRequest)
return
}
var aState *appState
aState, err = a.verifyAppState(r.FormValue("state"))
if err != nil {
http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest)
return
}
returnURL = aState.ReturnURL
token, err = oauth2Config.Exchange(ctx, code)
case "POST":
// Form request from frontend to refresh a token.
refresh := r.FormValue("refresh_token")
if refresh == "" {
http.Error(w, fmt.Sprintf("no refresh_token in request: %q", r.Form), http.StatusBadRequest)
return
}
t := &oauth2.Token{
RefreshToken: refresh,
Expiry: time.Now().UTC().Add(-time.Hour),
}
token, err = oauth2Config.TokenSource(ctx, t).Token()
default:
http.Error(w, fmt.Sprintf("method not implemented: %s", r.Method), http.StatusBadRequest)
return
}
if err != nil {
http.Error(w, fmt.Sprintf("failed to get token: %v", err), http.StatusInternalServerError)
return
}
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
http.Error(w, "no id_token in token response", http.StatusInternalServerError)
return
}
claims, err := a.sessionMgr.VerifyToken(rawIDToken)
if err != nil {
http.Error(w, fmt.Sprintf("invalid session token: %v", err), http.StatusInternalServerError)
return
}
flags := []string{"path=/"}
if a.secureCookie {
flags = append(flags, "Secure")
}
cookie := session.MakeCookieMetadata(common.AuthCookieName, rawIDToken, flags...)
w.Header().Set("Set-Cookie", cookie)
log.Infof("Web login successful claims: %v", claims)
if os.Getenv(common.EnvVarSSODebug) == "1" {
claimsJSON, _ := json.MarshalIndent(claims, "", " ")
renderToken(w, a.redirectURI, rawIDToken, token.RefreshToken, claimsJSON)
} else {
http.Redirect(w, r, returnURL, http.StatusSeeOther)
}
}
func (a *ClientApp) verify(tokenString string) (*oidc.IDToken, error) {
provider, err := a.sessionMgr.OIDCProvider()
if err != nil {
return nil, err
}
verifier := provider.Verifier(&oidc.Config{ClientID: a.clientID})
return verifier.Verify(context.Background(), tokenString)
}

15
util/http/http.go Normal file
View File

@@ -0,0 +1,15 @@
package http
import (
"fmt"
"strings"
)
// MakeCookieMetadata generates a string representing a Web cookie. Yum!
func MakeCookieMetadata(key, value string, flags ...string) string {
components := []string{
fmt.Sprintf("%s=%s", key, value),
}
components = append(components, flags...)
return strings.Join(components, "; ")
}

407
util/oidc/oidc.go Normal file
View File

@@ -0,0 +1,407 @@
package oidc
import (
"bytes"
"context"
"encoding/json"
"fmt"
"html/template"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
gooidc "github.com/coreos/go-oidc"
jwt "github.com/dgrijalva/jwt-go"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/util/cache"
httputil "github.com/argoproj/argo-cd/util/http"
"github.com/argoproj/argo-cd/util/rand"
"github.com/argoproj/argo-cd/util/settings"
)
const (
GrantTypeAuthorizationCode = "authorization_code"
GrantTypeImplicit = "implicit"
ResponseTypeCode = "code"
)
// OIDCConfiguration holds a subset of interested fields from the OIDC configuration spec
type OIDCConfiguration struct {
Issuer string `json:"issuer"`
ScopesSupported []string `json:"scopes_supported"`
ResponseTypesSupported []string `json:"response_types_supported"`
GrantTypesSupported []string `json:"grant_types_supported,omitempty"`
}
type ClientApp struct {
// OAuth2 client ID of this application (e.g. argo-cd)
clientID string
// OAuth2 client secret of this application
clientSecret string
// Callback URL for OAuth2 responses (e.g. https://argocd.example.com/auth/callback)
redirectURI string
// URL of the issuer (e.g. https://argocd.example.com/api/dex)
issuerURL string
// client is the HTTP client which is used to query the IDp
client *http.Client
// secureCookie indicates if the cookie should be set with the Secure flag, meaning it should
// only ever be sent over HTTPS. This value is inferred by the scheme of the redirectURI.
secureCookie bool
// settings holds ArgoCD settings
settings *settings.ArgoCDSettings
// provider is the OIDC configuration
provider *gooidc.Provider
// states holds temporary nonce tokens to which hold application state values
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
states cache.Cache
}
type appState struct {
// ReturnURL is the URL in which to redirect a user back to after completing an OAuth2 login
ReturnURL string `json:"returnURL"`
}
// NewClientApp will register the ArgoCD client app (either via Dex or external OIDC) and return an
// object which has HTTP handlers for handling the HTTP responses for login and callback
func NewClientApp(settings *settings.ArgoCDSettings) (*ClientApp, error) {
a := ClientApp{
clientID: settings.OAuth2ClientID(),
clientSecret: settings.OAuth2ClientSecret(),
redirectURI: settings.RedirectURL(),
issuerURL: settings.IssuerURL(),
}
log.Infof("Creating client app (%s)", a.clientID)
u, err := url.Parse(settings.URL)
if err != nil {
return nil, fmt.Errorf("parse redirect-uri: %v", err)
}
tlsConfig := settings.TLSConfig()
if tlsConfig != nil {
tlsConfig.InsecureSkipVerify = true
}
a.client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
// NOTE: if we ever have replicas of ArgoCD, this needs to switch to Redis cache
a.states = cache.NewInMemoryCache(3 * time.Minute)
a.secureCookie = bool(u.Scheme == "https")
a.settings = settings
return &a, nil
}
func (a *ClientApp) oauth2Config(scopes []string) (*oauth2.Config, error) {
provider, err := a.oidcProvider()
if err != nil {
return nil, err
}
return &oauth2.Config{
ClientID: a.clientID,
ClientSecret: a.clientSecret,
Endpoint: provider.Endpoint(),
Scopes: scopes,
RedirectURL: a.redirectURI,
}, nil
}
// generateAppState creates an app state nonce
func (a *ClientApp) generateAppState(returnURL string) string {
randStr := rand.RandString(10)
if returnURL == "" {
returnURL = "/"
}
err := a.states.Set(&cache.Item{
Key: randStr,
Object: &appState{
ReturnURL: returnURL,
},
})
if err != nil {
// This should never happen with the in-memory cache
log.Errorf("Failed to set app state: %v", err)
}
return randStr
}
func (a *ClientApp) verifyAppState(state string) (*appState, error) {
var aState appState
err := a.states.Get(state, &aState)
if err != nil {
if err == cache.ErrCacheMiss {
return nil, fmt.Errorf("unknown app state %s", state)
} else {
return nil, fmt.Errorf("failed to verify app state %s: %v", state, err)
}
}
// TODO: purge the state string from the cache so that it is a true nonce
return &aState, nil
}
// HandleLogin formulates the proper OAuth2 URL (auth code or implicit) and redirects the user to
// the IDp login & consent page
func (a *ClientApp) HandleLogin(w http.ResponseWriter, r *http.Request) {
provider, err := a.oidcProvider()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
oidcConf, err := ParseConfig(provider)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
scopes := []string{"openid", "profile", "email", "groups"}
oauth2Config, err := a.oauth2Config(scopes)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
returnURL := r.FormValue("return_url")
stateNonce := a.generateAppState(returnURL)
grantType := InferGrantType(oauth2Config, oidcConf)
var url string
switch grantType {
case GrantTypeAuthorizationCode:
url = oauth2Config.AuthCodeURL(stateNonce)
case GrantTypeImplicit:
url = ImplicitFlowURL(oauth2Config, stateNonce)
default:
http.Error(w, fmt.Sprintf("Unsupported grant type: %v", grantType), http.StatusInternalServerError)
return
}
log.Infof("Performing %s flow login: %s", grantType, url)
http.Redirect(w, r, url, http.StatusSeeOther)
}
// HandleCallback is the callback handler for an OAuth2 login flow
func (a *ClientApp) HandleCallback(w http.ResponseWriter, r *http.Request) {
ctx := gooidc.ClientContext(r.Context(), a.client)
oauth2Config, err := a.oauth2Config(nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Infof("Callback: %s", r.URL)
if errMsg := r.FormValue("error"); errMsg != "" {
http.Error(w, errMsg+": "+r.FormValue("error_description"), http.StatusBadRequest)
return
}
code := r.FormValue("code")
state := r.FormValue("state")
if code == "" {
// If code was not given, it implies implicit flow
a.handleImplicitFlow(w, r, state)
return
}
appState, err := a.verifyAppState(state)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
token, err := oauth2Config.Exchange(ctx, code)
if err != nil {
http.Error(w, fmt.Sprintf("failed to get token: %v", err), http.StatusInternalServerError)
return
}
idTokenRAW, ok := token.Extra("id_token").(string)
if !ok {
http.Error(w, "no id_token in token response", http.StatusInternalServerError)
return
}
idToken, err := a.verify(idTokenRAW)
if err != nil {
http.Error(w, fmt.Sprintf("invalid session token: %v", err), http.StatusInternalServerError)
return
}
flags := []string{"path=/"}
if a.secureCookie {
flags = append(flags, "Secure")
}
cookie := httputil.MakeCookieMetadata(common.AuthCookieName, idTokenRAW, flags...)
w.Header().Set("Set-Cookie", cookie)
var claims jwt.MapClaims
err = idToken.Claims(&claims)
claimsJSON, _ := json.Marshal(claims)
log.Infof("Web login successful. Claims: %s", claimsJSON)
if os.Getenv(common.EnvVarSSODebug) == "1" {
claimsJSON, _ := json.MarshalIndent(claims, "", " ")
renderToken(w, a.redirectURI, idTokenRAW, token.RefreshToken, claimsJSON)
} else {
http.Redirect(w, r, appState.ReturnURL, http.StatusSeeOther)
}
}
var implicitFlowTmpl = template.Must(template.New("implicit.html").Parse(`<script>
var hash = window.location.hash.substr(1);
var result = hash.split('&').reduce(function (result, item) {
var parts = item.split('=');
result[parts[0]] = parts[1];
return result;
}, {});
var idToken = result['id_token'];
var state = result['state'];
var returnURL = "{{ .ReturnURL }}";
if (state != "" && returnURL == "") {
window.location.href = window.location.href.split("#")[0] + "?state=" + result['state'] + window.location.hash;
} else if (returnURL != "") {
document.cookie = "{{ .CookieName }}=" + idToken + "; path=/";
window.location.href = returnURL;
}
</script>`))
// handleImplicitFlow completes an implicit OAuth2 flow. The id_token and state will be contained
// in the URL fragment. The javascript client first redirects to the callback URL, supplying the
// state nonce for verification, as well as looking up the return URL. Once verified, the client
// stores the id_token from the fragment as a cookie. Finally it performs the final redirect back to
// the return URL.
func (a *ClientApp) handleImplicitFlow(w http.ResponseWriter, r *http.Request, state string) {
type implicitFlowValues struct {
CookieName string
ReturnURL string
}
vals := implicitFlowValues{
CookieName: common.AuthCookieName,
}
if state != "" {
appState, err := a.verifyAppState(state)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
vals.ReturnURL = appState.ReturnURL
}
renderTemplate(w, implicitFlowTmpl, vals)
}
func (a *ClientApp) oidcProvider() (*gooidc.Provider, error) {
if a.provider != nil {
return a.provider, nil
}
provider, err := NewOIDCProvider(a.issuerURL, a.client)
if err != nil {
return nil, err
}
a.provider = provider
return a.provider, nil
}
func (a *ClientApp) verify(tokenString string) (*gooidc.IDToken, error) {
provider, err := a.oidcProvider()
if err != nil {
return nil, err
}
verifier := provider.Verifier(&gooidc.Config{ClientID: a.clientID})
return verifier.Verify(context.Background(), tokenString)
}
// NewOIDCProvider initializes an OIDC provider, querying the well known oidc configuration path
// http://example-argocd.com/api/dex/.well-known/openid-configuration
func NewOIDCProvider(issuerURL string, client *http.Client) (*gooidc.Provider, error) {
log.Infof("Initializing OIDC provider (issuer: %s)", issuerURL)
ctx := gooidc.ClientContext(context.Background(), client)
provider, err := gooidc.NewProvider(ctx, issuerURL)
if err != nil {
return nil, fmt.Errorf("Failed to query provider %q: %v", issuerURL, err)
}
s, _ := ParseConfig(provider)
log.Infof("OIDC supported scopes: %v", s.ScopesSupported)
return provider, nil
}
// ImplicitFlowURL is an adaptation of oauth2.Config::AuthCodeURL() which returns a URL
// appropriate for an OAuth2 implicit login flow (as opposed to authorization code flow).
func ImplicitFlowURL(c *oauth2.Config, state string, opts ...oauth2.AuthCodeOption) string {
var buf bytes.Buffer
buf.WriteString(c.Endpoint.AuthURL)
v := url.Values{
"response_type": {"id_token"},
"nonce": {rand.RandString(10)},
"client_id": {c.ClientID},
"redirect_uri": condVal(c.RedirectURL),
"scope": condVal(strings.Join(c.Scopes, " ")),
"state": condVal(state),
}
for _, opt := range opts {
switch opt {
case oauth2.AccessTypeOnline:
v.Set("access_type", "online")
case oauth2.AccessTypeOffline:
v.Set("access_type", "offline")
case oauth2.ApprovalForce:
v.Set("approval_prompt", "force")
}
}
if strings.Contains(c.Endpoint.AuthURL, "?") {
buf.WriteByte('&')
} else {
buf.WriteByte('?')
}
buf.WriteString(v.Encode())
return buf.String()
}
func condVal(v string) []string {
if v == "" {
return nil
}
return []string{v}
}
// OfflineAccess returns whether or not 'offline_access' is a supported scope
func OfflineAccess(scopes []string) bool {
if len(scopes) == 0 {
// scopes_supported is a "RECOMMENDED" discovery claim, not a required
// one. If missing, assume that the provider follows the spec and has
// an "offline_access" scope.
return true
}
// See if scopes_supported has the "offline_access" scope.
for _, scope := range scopes {
if scope == gooidc.ScopeOfflineAccess {
return true
}
}
return false
}
// ParseConfig parses the OIDC Config into the concrete datastructure
func ParseConfig(provider *gooidc.Provider) (*OIDCConfiguration, error) {
var conf OIDCConfiguration
err := provider.Claims(&conf)
if err != nil {
return nil, err
}
return &conf, nil
}
// InferGrantType infers the proper grant flow depending on the OAuth2 client config and OIDC configuration.
// Returns either: "authorization_code" or "implicit"
func InferGrantType(oauth2conf *oauth2.Config, oidcConf *OIDCConfiguration) string {
if oauth2conf.ClientSecret != "" {
// If we know the client secret, we are using the 'authorization_code' flow
return GrantTypeAuthorizationCode
}
if len(oidcConf.ResponseTypesSupported) == 1 && oidcConf.ResponseTypesSupported[0] == ResponseTypeCode {
// If we don't have the secret, check the supported response types. If the list is a single
// response type of type 'code', then grant type is 'authorization_code'. This is the Dex
// case, which does not support implicit login flow (https://github.com/dexidp/dex/issues/1254)
return GrantTypeAuthorizationCode
}
// If we don't have the client secret (e.g. SPA app), we can assume to be implicit
return GrantTypeImplicit
}

49
util/oidc/oidc_test.go Normal file
View File

@@ -0,0 +1,49 @@
package oidc
import (
"encoding/json"
"io/ioutil"
"testing"
"golang.org/x/oauth2"
"github.com/stretchr/testify/assert"
)
var (
spaOauth2Conf = &oauth2.Config{
ClientID: "spa-id",
}
webOauth2Conf = &oauth2.Config{
ClientID: "spa-id",
ClientSecret: "my-super-secret",
}
)
func TestInferGrantType(t *testing.T) {
var grantType string
dexRAW, err := ioutil.ReadFile("testdata/dex.json")
assert.NoError(t, err)
var dexConfig OIDCConfiguration
err = json.Unmarshal(dexRAW, &dexConfig)
assert.NoError(t, err)
grantType = InferGrantType(spaOauth2Conf, &dexConfig)
// Dex does not support implicit login flow (https://github.com/dexidp/dex/issues/1254)
assert.Equal(t, GrantTypeAuthorizationCode, grantType)
grantType = InferGrantType(webOauth2Conf, &dexConfig)
assert.Equal(t, GrantTypeAuthorizationCode, grantType)
testFiles := []string{"testdata/okta.json", "testdata/auth0.json", "testdata/onelogin.json"}
for _, path := range testFiles {
oktaRAW, err := ioutil.ReadFile(path)
assert.NoError(t, err)
var oktaConfig OIDCConfiguration
err = json.Unmarshal(oktaRAW, &oktaConfig)
assert.NoError(t, err)
grantType = InferGrantType(spaOauth2Conf, &oktaConfig)
assert.Equal(t, GrantTypeImplicit, grantType)
grantType = InferGrantType(webOauth2Conf, &oktaConfig)
assert.Equal(t, GrantTypeAuthorizationCode, grantType)
}
}

View File

@@ -1,4 +1,4 @@
package dex
package oidc
import (
"html/template"

70
util/oidc/testdata/auth0.json vendored Normal file
View File

@@ -0,0 +1,70 @@
{
"issuer": "https://argocd-test.auth0.com/",
"authorization_endpoint": "https://argocd-test.auth0.com/authorize",
"token_endpoint": "https://argocd-test.auth0.com/oauth/token",
"userinfo_endpoint": "https://argocd-test.auth0.com/userinfo",
"mfa_challenge_endpoint": "https://argocd-test.auth0.com/mfa/challenge",
"jwks_uri": "https://argocd-test.auth0.com/.well-known/jwks.json",
"registration_endpoint": "https://argocd-test.auth0.com/oidc/register",
"revocation_endpoint": "https://argocd-test.auth0.com/oauth/revoke",
"scopes_supported": [
"openid",
"profile",
"offline_access",
"name",
"given_name",
"family_name",
"nickname",
"email",
"email_verified",
"picture",
"created_at",
"identities",
"phone",
"address"
],
"response_types_supported": [
"code",
"token",
"id_token",
"code token",
"code id_token",
"token id_token",
"code token id_token"
],
"response_modes_supported": [
"query",
"fragment",
"form_post"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"HS256",
"RS256"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
],
"claims_supported": [
"aud",
"auth_time",
"created_at",
"email",
"email_verified",
"exp",
"family_name",
"given_name",
"iat",
"identities",
"iss",
"name",
"nickname",
"phone_number",
"picture",
"sub"
],
"request_uri_parameter_supported": false
}

36
util/oidc/testdata/dex.json vendored Normal file
View File

@@ -0,0 +1,36 @@
{
"issuer": "https://argocd.example.com/api/dex",
"authorization_endpoint": "https://argocd.example.com/api/dex/auth",
"token_endpoint": "https://argocd.example.com/api/dex/token",
"jwks_uri": "https://argocd.example.com/api/dex/keys",
"response_types_supported": [
"code"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"scopes_supported": [
"openid",
"email",
"groups",
"profile",
"offline_access"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic"
],
"claims_supported": [
"aud",
"email",
"email_verified",
"exp",
"iat",
"iss",
"locale",
"name",
"sub"
]
}

115
util/oidc/testdata/okta.json vendored Normal file
View File

@@ -0,0 +1,115 @@
{
"issuer": "https://dev-123456.oktapreview.com",
"authorization_endpoint": "https://dev-123456.oktapreview.com/oauth2/v1/authorize",
"token_endpoint": "https://dev-123456.oktapreview.com/oauth2/v1/token",
"userinfo_endpoint": "https://dev-123456.oktapreview.com/oauth2/v1/userinfo",
"registration_endpoint": "https://dev-123456.oktapreview.com/oauth2/v1/clients",
"jwks_uri": "https://dev-123456.oktapreview.com/oauth2/v1/keys",
"response_types_supported": [
"code",
"id_token",
"code id_token",
"code token",
"id_token token",
"code id_token token"
],
"response_modes_supported": [
"query",
"fragment",
"form_post",
"okta_post_message"
],
"grant_types_supported": [
"authorization_code",
"implicit",
"refresh_token",
"password"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"scopes_supported": [
"openid",
"email",
"profile",
"address",
"phone",
"offline_access",
"groups"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt",
"none"
],
"claims_supported": [
"iss",
"ver",
"sub",
"aud",
"iat",
"exp",
"jti",
"auth_time",
"amr",
"idp",
"nonce",
"name",
"nickname",
"preferred_username",
"given_name",
"middle_name",
"family_name",
"email",
"email_verified",
"profile",
"zoneinfo",
"locale",
"address",
"phone_number",
"picture",
"website",
"gender",
"birthdate",
"updated_at",
"at_hash",
"c_hash"
],
"code_challenge_methods_supported": [
"S256"
],
"introspection_endpoint": "https://dev-123456.oktapreview.com/oauth2/v1/introspect",
"introspection_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt",
"none"
],
"revocation_endpoint": "https://dev-123456.oktapreview.com/oauth2/v1/revoke",
"revocation_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt",
"none"
],
"end_session_endpoint": "https://dev-123456.oktapreview.com/oauth2/v1/logout",
"request_parameter_supported": true,
"request_object_signing_alg_values_supported": [
"HS256",
"HS384",
"HS512",
"RS256",
"RS384",
"RS512",
"ES256",
"ES384",
"ES512"
]
}

87
util/oidc/testdata/onelogin.json vendored Normal file
View File

@@ -0,0 +1,87 @@
{
"acr_values_supported": [
"onelogin:nist:level:1:re-auth"
],
"authorization_endpoint": "https://argocd-dev.onelogin.com/oidc/auth",
"claims_parameter_supported": true,
"claims_supported": [
"acr",
"auth_time",
"company",
"custom_fields",
"department",
"email",
"family_name",
"given_name",
"groups",
"iss",
"locale_code",
"name",
"phone_number",
"preferred_username",
"sub",
"title",
"updated_at"
],
"grant_types_supported": [
"authorization_code",
"implicit",
"refresh_token",
"password"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"issuer": "https://openid-connect.onelogin.com/oidc",
"jwks_uri": "https://argocd-dev.onelogin.com/oidc/certs",
"request_parameter_supported": false,
"request_uri_parameter_supported": false,
"response_modes_supported": [
"form_post",
"fragment",
"query"
],
"response_types_supported": [
"code",
"id_token token",
"id_token"
],
"scopes_supported": [
"openid",
"name",
"profile",
"groups",
"email",
"phone"
],
"subject_types_supported": [
"public"
],
"token_endpoint": "https://argocd-dev.onelogin.com/oidc/token",
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"none"
],
"userinfo_endpoint": "https://argocd-dev.onelogin.com/oidc/me",
"userinfo_signing_alg_values_supported": [],
"code_challenge_methods_supported": [
"plain",
"S256"
],
"introspection_endpoint": "https://argocd-dev.onelogin.com/oidc/token/introspection",
"introspection_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"none"
],
"revocation_endpoint": "https://argocd-dev.onelogin.com/oidc/token/revocation",
"revocation_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"none"
],
"claim_types_supported": [
"normal"
]
}

37
util/rand/rand.go Normal file
View File

@@ -0,0 +1,37 @@
package rand
import (
"math/rand"
"time"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
var src = rand.NewSource(time.Now().UnixNano())
// RandString generates, from a given charset, a cryptographically-secure pseudo-random string of a given length.
func RandString(n int) string {
return RandStringCharset(n, letterBytes)
}
func RandStringCharset(n int, charset string) string {
b := make([]byte, n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(charset) {
b[i] = charset[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return string(b)
}

View File

@@ -1,4 +1,4 @@
package dex
package rand
import (
"testing"
@@ -6,20 +6,11 @@ import (
func TestRandString(t *testing.T) {
var ss string
var err error
ss, err = randString(10, "A")
if err != nil {
t.Fatalf("Could not generate entropy: %v", err)
}
ss = RandStringCharset(10, "A")
if ss != "AAAAAAAAAA" {
t.Errorf("Expected 10 As, but got %q", ss)
}
ss, err = randString(5, "ABC123")
if err != nil {
t.Fatalf("Could not generate entropy: %v", err)
}
ss = RandStringCharset(5, "ABC123")
if len(ss) != 5 {
t.Errorf("Expected random string of length 10, but got %q", ss)
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/argoproj/argo-cd/common"
jwtutil "github.com/argoproj/argo-cd/util/jwt"
oidcutil "github.com/argoproj/argo-cd/util/oidc"
passwordutil "github.com/argoproj/argo-cd/util/password"
"github.com/argoproj/argo-cd/util/settings"
)
@@ -27,10 +28,6 @@ type SessionManager struct {
settings *settings.ArgoCDSettings
client *http.Client
provider *oidc.Provider
// Does the provider use "offline_access" scope to request a refresh token
// or does it use "access_type=offline" (e.g. Google)?
offlineAsScope bool
}
const (
@@ -153,7 +150,7 @@ func (mgr *SessionManager) VerifyToken(tokenString string) (jwt.Claims, error) {
return mgr.Parse(tokenString)
default:
// Dex signed token
provider, err := mgr.OIDCProvider()
provider, err := mgr.oidcProvider()
if err != nil {
return nil, err
}
@@ -208,20 +205,11 @@ func Username(ctx context.Context) string {
}
}
// MakeCookieMetadata generates a string representing a Web cookie. Yum!
func MakeCookieMetadata(key, value string, flags ...string) string {
components := []string{
fmt.Sprintf("%s=%s", key, value),
}
components = append(components, flags...)
return strings.Join(components, "; ")
}
// OIDCProvider lazily initializes, memoizes, and returns the OIDC provider.
// We have to initialize the provider lazily since ArgoCD is an OIDC client to itself, which
// presents a chicken-and-egg problem of (1) serving dex over HTTP, and (2) querying the OIDC
// provider (ourselves) to initialize the app.
func (mgr *SessionManager) OIDCProvider() (*oidc.Provider, error) {
// oidcProvider lazily initializes, memoizes, and returns the OIDC provider.
// We have to initialize the provider lazily since ArgoCD can be an OIDC client to itself (in the
// case of dex reverse proxy), which presents a chicken-and-egg problem of (1) serving dex over
// HTTP, and (2) querying the OIDC provider (ourself) to initialize the app.
func (mgr *SessionManager) oidcProvider() (*oidc.Provider, error) {
if mgr.provider != nil {
return mgr.provider, nil
}
@@ -234,48 +222,14 @@ func (mgr *SessionManager) initializeOIDCProvider() (*oidc.Provider, error) {
if !mgr.settings.IsSSOConfigured() {
return nil, fmt.Errorf("SSO is not configured")
}
issuerURL := mgr.settings.IssuerURL()
log.Infof("Initializing OIDC provider (issuer: %s)", issuerURL)
ctx := oidc.ClientContext(context.Background(), mgr.client)
provider, err := oidc.NewProvider(ctx, issuerURL)
provider, err := oidcutil.NewOIDCProvider(mgr.settings.IssuerURL(), mgr.client)
if err != nil {
return nil, fmt.Errorf("Failed to query provider %q: %v", issuerURL, err)
}
// Returns the scopes the provider supports
// See: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
var s struct {
ScopesSupported []string `json:"scopes_supported"`
}
if err := provider.Claims(&s); err != nil {
return nil, fmt.Errorf("Failed to parse provider scopes_supported: %v", err)
}
log.Infof("OpenID supported scopes: %v", s.ScopesSupported)
offlineAsScope := false
if len(s.ScopesSupported) == 0 {
// scopes_supported is a "RECOMMENDED" discovery claim, not a required
// one. If missing, assume that the provider follows the spec and has
// an "offline_access" scope.
offlineAsScope = true
} else {
// See if scopes_supported has the "offline_access" scope.
for _, scope := range s.ScopesSupported {
if scope == oidc.ScopeOfflineAccess {
offlineAsScope = true
break
}
}
return nil, err
}
mgr.provider = provider
mgr.offlineAsScope = offlineAsScope
return mgr.provider, nil
}
// OfflineAsScope returns whether or not the OIDC provider supports offline as a scope
func (mgr *SessionManager) OfflineAsScope() bool {
_, _ = mgr.OIDCProvider() // forces offlineAsScope to be determined
return mgr.offlineAsScope
}
type debugTransport struct {
t http.RoundTripper
}

View File

@@ -8,12 +8,10 @@ import (
"encoding/base64"
"fmt"
"sync"
"syscall"
"time"
"github.com/ghodss/yaml"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh/terminal"
apiv1 "k8s.io/api/core/v1"
apierr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -24,6 +22,7 @@ import (
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/password"
tlsutil "github.com/argoproj/argo-cd/util/tls"
)
@@ -36,8 +35,10 @@ type ArgoCDSettings struct {
// Admin superuser password storage
AdminPasswordHash string `json:"adminPasswordHash,omitempty"`
AdminPasswordMtime time.Time `json:"adminPasswordMtime,omitempty"`
// DexConfig is contains portions of a dex config yaml
// DexConfig contains portions of a dex config yaml
DexConfig string `json:"dexConfig,omitempty"`
// OIDCConfigRAW holds OIDC configuration as a raw string
OIDCConfigRAW string `json:"oidcConfig,omitempty"`
// ServerSignature holds the key used to generate JWT tokens.
ServerSignature []byte `json:"serverSignature,omitempty"`
// Certificate holds the certificate/private key for the ArgoCD API server.
@@ -53,6 +54,13 @@ type ArgoCDSettings struct {
Secrets map[string]string `json:"secrets,omitempty"`
}
type OIDCConfig struct {
Name string `json:"name,omitempty"`
Issuer string `json:"issuer,omitempty"`
ClientID string `json:"clientID,omitempty"`
ClientSecret string `json:"clientSecret,omitempty"`
}
const (
// settingAdminPasswordHashKey designates the key for a root password hash inside a Kubernetes secret.
settingAdminPasswordHashKey = "admin.password"
@@ -68,6 +76,8 @@ const (
settingURLKey = "url"
// settingDexConfigKey designates the key for the dex config
settingDexConfigKey = "dex.config"
// settingsOIDCConfigKey designates the key for OIDC config
settingsOIDCConfigKey = "oidc.config"
// settingsWebhookGitHubSecret is the key for the GitHub shared webhook secret
settingsWebhookGitHubSecretKey = "webhook.github.secret"
// settingsWebhookGitLabSecret is the key for the GitLab shared webhook secret
@@ -115,6 +125,7 @@ func (mgr *SettingsManager) GetSettings() (*ArgoCDSettings, error) {
func updateSettingsFromConfigMap(settings *ArgoCDSettings, argoCDCM *apiv1.ConfigMap) {
settings.DexConfig = argoCDCM.Data[settingDexConfigKey]
settings.OIDCConfigRAW = argoCDCM.Data[settingsOIDCConfigKey]
settings.URL = argoCDCM.Data[settingURLKey]
}
@@ -183,8 +194,22 @@ func (mgr *SettingsManager) SaveSettings(settings *ArgoCDSettings) error {
if argoCDCM.Data == nil {
argoCDCM.Data = make(map[string]string)
}
argoCDCM.Data[settingURLKey] = settings.URL
argoCDCM.Data[settingDexConfigKey] = settings.DexConfig
if settings.URL != "" {
argoCDCM.Data[settingURLKey] = settings.URL
} else {
delete(argoCDCM.Data, settingURLKey)
}
if settings.DexConfig != "" {
argoCDCM.Data[settingDexConfigKey] = settings.DexConfig
} else {
delete(argoCDCM.Data, settings.DexConfig)
}
if settings.OIDCConfigRAW != "" {
argoCDCM.Data[settingsOIDCConfigKey] = settings.OIDCConfigRAW
} else {
delete(argoCDCM.Data, settingsOIDCConfigKey)
}
if createCM {
_, err = mgr.clientset.CoreV1().ConfigMaps(mgr.namespace).Create(argoCDCM)
} else {
@@ -253,6 +278,16 @@ func NewSettingsManager(clientset kubernetes.Interface, namespace string) *Setti
// IsSSOConfigured returns whether or not single-sign-on is configured
func (a *ArgoCDSettings) IsSSOConfigured() bool {
if a.IsDexConfigured() {
return true
}
if a.OIDCConfig() != nil {
return true
}
return false
}
func (a *ArgoCDSettings) IsDexConfigured() bool {
if a.URL == "" {
return false
}
@@ -265,6 +300,19 @@ func (a *ArgoCDSettings) IsSSOConfigured() bool {
return len(dexCfg) > 0
}
func (a *ArgoCDSettings) OIDCConfig() *OIDCConfig {
if a.OIDCConfigRAW == "" {
return nil
}
var oidcConfig OIDCConfig
err := yaml.Unmarshal([]byte(a.OIDCConfigRAW), &oidcConfig)
if err != nil {
log.Warnf("invalid oidc config: %v", err)
return nil
}
return &oidcConfig
}
// TLSConfig returns a tls.Config with the configured certificates
func (a *ArgoCDSettings) TLSConfig() *tls.Config {
if a.Certificate == nil {
@@ -282,18 +330,44 @@ func (a *ArgoCDSettings) TLSConfig() *tls.Config {
}
func (a *ArgoCDSettings) IssuerURL() string {
return a.URL + common.DexAPIEndpoint
if oidcConfig := a.OIDCConfig(); oidcConfig != nil {
return oidcConfig.Issuer
}
if a.DexConfig != "" {
return a.URL + common.DexAPIEndpoint
}
return ""
}
func (a *ArgoCDSettings) OAuth2ClientID() string {
if oidcConfig := a.OIDCConfig(); oidcConfig != nil {
return oidcConfig.ClientID
}
if a.DexConfig != "" {
return common.ArgoCDClientAppID
}
return ""
}
func (a *ArgoCDSettings) OAuth2ClientSecret() string {
if oidcConfig := a.OIDCConfig(); oidcConfig != nil {
return oidcConfig.ClientSecret
}
if a.DexConfig != "" {
return a.DexOAuth2ClientSecret()
}
return ""
}
func (a *ArgoCDSettings) RedirectURL() string {
return a.URL + common.CallbackEndpoint
}
// OAuth2ClientSecret calculates an arbitrary, but predictable OAuth2 client secret string derived
// DexOAuth2ClientSecret calculates an arbitrary, but predictable OAuth2 client secret string derived
// from the server secret. This is called by the dex startup wrapper (argocd-util rundex), as well
// as the API server, such that they both independently come to the same conclusion of what the
// OAuth2 shared client secret should be.
func (a *ArgoCDSettings) OAuth2ClientSecret() string {
func (a *ArgoCDSettings) DexOAuth2ClientSecret() string {
h := sha256.New()
_, err := h.Write(a.ServerSignature)
if err != nil {
@@ -407,27 +481,6 @@ func (mgr *SettingsManager) notifySubscribers() {
}
}
func ReadAndConfirmPassword() (string, error) {
for {
fmt.Print("*** Enter new password: ")
password, err := terminal.ReadPassword(syscall.Stdin)
if err != nil {
return "", err
}
fmt.Print("\n")
fmt.Print("*** Confirm new password: ")
confirmPassword, err := terminal.ReadPassword(syscall.Stdin)
if err != nil {
return "", err
}
fmt.Print("\n")
if string(password) == string(confirmPassword) {
return string(password), nil
}
log.Error("Passwords do not match")
}
}
func isIncompleteSettingsError(err error) bool {
_, ok := err.(*incompleteSettingsError)
return ok
@@ -454,7 +507,7 @@ func UpdateSettings(defaultPassword string, settingsMgr *SettingsManager, update
if cdSettings.AdminPasswordHash == "" || updateSuperuser {
passwordRaw := defaultPassword
if passwordRaw == "" {
passwordRaw, err = ReadAndConfirmPassword()
passwordRaw, err = cli.ReadAndConfirmPassword()
if err != nil {
return nil, err
}