From 4f5930ad562c5930b7b0af88c8a14545cf8f2008 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Fri, 12 Apr 2024 09:44:29 -0600 Subject: [PATCH 01/33] Fixed static pages not being included in container --- .goreleaser.yaml | 2 ++ Dockerfile | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index eeb228b..72956fa 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -23,6 +23,7 @@ dockers: extra_files: - LICENSE.md - README.md + - pages/ archives: - format: tar.gz rlcp: true @@ -37,6 +38,7 @@ archives: files: - LICENSE.md - README.md + - pages/ checksum: name_template: 'checksums.txt' snapshot: diff --git a/Dockerfile b/Dockerfile index 8b4bb3b..03ad18a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM cgr.dev/chainguard/wolfi-base RUN apk add --no-cache tini bash curl -RUN mkdir /opaal +RUN mkdir -p /opaal/pages/static/stylesheets RUN chown 65534:65534 /opaal WORKDIR /opaal @@ -10,6 +10,8 @@ WORKDIR /opaal USER 65534:65534 COPY opaal /opaal/opaal +COPY pages/* /opaal/pages/ + CMD [ "/opaal/opaal" ] From 13a35081d7a3f8179ce3b8f50aedb8d645595682 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Wed, 17 Apr 2024 17:19:11 -0600 Subject: [PATCH 02/33] Updated go deps --- go.mod | 12 ++++++------ go.sum | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index d2a924b..d4bdb1a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module davidallendj/opaal go 1.22.0 require ( - github.com/davidallendj/go-utils v0.0.0-20240310194826-5a1300f3bcbf + github.com/davidallendj/go-utils v0.0.0-20240417195221-95765f3b9bad github.com/go-chi/chi/v5 v5.0.12 github.com/google/uuid v1.6.0 github.com/jmoiron/sqlx v1.3.5 @@ -11,12 +11,12 @@ require ( github.com/mattn/go-sqlite3 v1.14.22 github.com/nikolalohinski/gonja/v2 v2.2.0 github.com/spf13/cobra v1.8.0 - golang.org/x/net v0.22.0 + golang.org/x/net v0.24.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-logr/logr v1.3.0 // indirect github.com/goccy/go-json v0.10.2 // indirect @@ -39,8 +39,8 @@ require ( github.com/segmentio/asm v1.2.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index a9ef5e3..9abfacf 100644 --- a/go.sum +++ b/go.sum @@ -8,10 +8,10 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davidallendj/go-utils v0.0.0-20240310194826-5a1300f3bcbf h1:gY89rDLnc+70S0JcyHoPGU+XFpMwY1iVYNQzdC/qAHc= -github.com/davidallendj/go-utils v0.0.0-20240310194826-5a1300f3bcbf/go.mod h1:kiv3jEnBbeueMNNJclaMMJULL/tjqJ6wc136d+uxqSs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/davidallendj/go-utils v0.0.0-20240417195221-95765f3b9bad h1:WODRnqFS2CZfraXy7Nvh5qekM42/L5kvLoLMqNr50e8= +github.com/davidallendj/go-utils v0.0.0-20240417195221-95765f3b9bad/go.mod h1:kiv3jEnBbeueMNNJclaMMJULL/tjqJ6wc136d+uxqSs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= @@ -96,20 +96,20 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= -golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= -golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From af79cfe254196bece4f0c50d373046708ad8863a Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Wed, 17 Apr 2024 17:23:36 -0600 Subject: [PATCH 03/33] Changed default expiration for token --- internal/flows/jwt_bearer.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/internal/flows/jwt_bearer.go b/internal/flows/jwt_bearer.go index f36effa..652944b 100644 --- a/internal/flows/jwt_bearer.go +++ b/internal/flows/jwt_bearer.go @@ -60,7 +60,7 @@ func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (s } } - // 2. Check if we are already registered as a trusted issuer with authorization server... + // TODO: 2. Check if we are already registered as a trusted issuer with authorization server... // 3.a if not, create a new JWKS (or just JWK) to be verified var ( @@ -77,7 +77,7 @@ func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (s if err != nil { return "", fmt.Errorf("failed to generate new RSA key: %v", err) } - privateJwk, publicJwk, err = cryptox.GenerateJwkKeyPairFromPrivateKey(privateKey) + privateJwk, publicJwk, err = GenerateJwkKeyPairFromPrivateKey(privateKey) // FIXME: needs to pull correct version from cryptox if err != nil { return "", fmt.Errorf("failed to generate JWK pair from private key: %v", err) } @@ -130,12 +130,13 @@ func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (s if err != nil { return "", fmt.Errorf("failed to parse ID token: %v", err) } + payload := parsedIdToken.PrivateClaims() payload["iss"] = trustedIssuer.Issuer payload["aud"] = []string{eps.Token} payload["iat"] = time.Now().Unix() payload["nbf"] = time.Now().Unix() - payload["exp"] = time.Now().Add(time.Second * 3600).Unix() + payload["exp"] = time.Now().Add(time.Second * 3600 * 16).Unix() payload["sub"] = "opaal" // include the offline_access scope if refresh tokens are enabled @@ -339,3 +340,15 @@ func ForwardToken(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) error } return nil } + +func GenerateJwkKeyPairFromPrivateKey(privateKey *rsa.PrivateKey) (jwk.Key, jwk.Key, error) { + privateJwk, err := jwk.FromRaw(privateKey) + if err != nil { + return nil, nil, fmt.Errorf("failed to create private JWK: %v", err) + } + publicJwk, err := jwk.PublicKeyOf(privateJwk) + if err != nil { + return nil, nil, fmt.Errorf("failed to create public JWK: %v", err) + } + return privateJwk, publicJwk, nil +} From d7990807f019c23dba7b349e2b112199a0e7667c Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Wed, 17 Apr 2024 17:24:56 -0600 Subject: [PATCH 04/33] Minor changes --- internal/login.go | 5 +++-- internal/new.go | 4 ++++ internal/oauth/authenticate.go | 3 +-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/login.go b/internal/login.go index 2ab0b04..e9c6374 100644 --- a/internal/login.go +++ b/internal/login.go @@ -41,7 +41,7 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider // print the authorization URL for sharing var authorizationUrl = client.BuildAuthorizationUrl(provider.Endpoints.Authorization, state) s := NewServerWithConfig(config) - fmt.Printf("Login with identity provider:\n\n %s/login\n %s\n\n", + fmt.Printf("Login with external identity provider:\n\n %s/login\n %s\n\n", s.GetListenAddr(), authorizationUrl, ) @@ -87,7 +87,7 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider Client: authzClient, }, } - err = s.Start(button, provider, client, params) + err = s.StartLogin(button, provider, client, params) if errors.Is(err, http.ErrServerClosed) { fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n") } else if err != nil { @@ -110,6 +110,7 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider } func MakeButton(url string, text string) string { + // check if we have http:// a html := " Date: Wed, 17 Apr 2024 17:25:28 -0600 Subject: [PATCH 05/33] Updated static web pages --- pages/index.html | 1 + pages/static/stylesheets/styles.css | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/pages/index.html b/pages/index.html index ed5c813..cfea742 100644 --- a/pages/index.html +++ b/pages/index.html @@ -7,6 +7,7 @@
Log in using the option(s) below for an access token.

+ {{loginForm}} {{loginButtons}}
\ No newline at end of file diff --git a/pages/static/stylesheets/styles.css b/pages/static/stylesheets/styles.css index 2f008f4..aa47071 100644 --- a/pages/static/stylesheets/styles.css +++ b/pages/static/stylesheets/styles.css @@ -27,4 +27,10 @@ input[type=text] { padding: 12px 20px; margin: 8px 0; box-sizing: border-box; +} +input[type=password] { + width: 80%; + padding: 12px 20px; + margin: 8px 0; + box-sizing: border-box; } \ No newline at end of file From 059fb37aaf24accbd7164bd3940cc9ccc82896a3 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Wed, 17 Apr 2024 17:26:03 -0600 Subject: [PATCH 06/33] Added serve command to start an identity provider server --- cmd/serve.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 cmd/serve.go diff --git a/cmd/serve.go b/cmd/serve.go new file mode 100644 index 0000000..67cec1a --- /dev/null +++ b/cmd/serve.go @@ -0,0 +1,31 @@ +package cmd + +import ( + opaal "davidallendj/opaal/internal" + "errors" + "fmt" + "net/http" + + "github.com/spf13/cobra" +) + +var exampleCmd = &cobra.Command{ + Use: "serve", + Short: "Start an simple identity provider server", + Long: "The built-in identity provider is not (nor meant to be) a complete OIDC implementation and behaves like an external IdP", + Run: func(cmd *cobra.Command, args []string) { + s := opaal.NewServerWithConfig(&config) + // FIXME: change how the server address is set with `NewServerWithConfig` + s.Server.Addr = fmt.Sprintf("%s:%d", s.Issuer.Host, s.Issuer.Port) + err := s.StartIdentityProvider() + if errors.Is(err, http.ErrServerClosed) { + fmt.Printf("Identity provider server closed.\n") + } else if err != nil { + fmt.Errorf("failed to start server: %v", err) + } + }, +} + +func init() { + rootCmd.AddCommand(exampleCmd) +} From 0ca88e1a84972457f41ab886505de19e57365945 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Wed, 17 Apr 2024 17:26:29 -0600 Subject: [PATCH 07/33] Added identity provider server --- internal/server/server.go | 285 +++++++++++++++++++++++++++++++++++++- 1 file changed, 279 insertions(+), 6 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index 556b55b..66c45d4 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,16 +1,28 @@ package server import ( + "crypto/rand" + "crypto/rsa" "davidallendj/opaal/internal/flows" "davidallendj/opaal/internal/oauth" "davidallendj/opaal/internal/oidc" "encoding/json" "fmt" "net/http" + "os" + "slices" + "strings" + "time" + "github.com/davidallendj/go-utils/cryptox" "github.com/davidallendj/go-utils/httpx" + "github.com/davidallendj/go-utils/util" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v2/jwt" "github.com/nikolalohinski/gonja/v2" "github.com/nikolalohinski/gonja/v2/exec" ) @@ -21,6 +33,12 @@ type Server struct { Port int `yaml:"port"` Callback string `yaml:"callback"` State string `yaml:"state"` + Issuer Issuer `yaml:"issuer"` +} + +type Issuer struct { + Host string `yaml:"host"` + Port int `yaml:"port"` } type ServerParams struct { @@ -40,12 +58,15 @@ func (s *Server) GetListenAddr() string { return fmt.Sprintf("%s:%d", s.Host, s.Port) } -func (s *Server) Start(buttons string, provider *oidc.IdentityProvider, client *oauth.Client, params ServerParams) error { - var target = "" +func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, client *oauth.Client, params ServerParams) error { + var ( + target = "" + callback = "" + ) // check if callback is set if s.Callback == "" { - s.Callback = "/oidc/callback" + callback = "/oidc/callback" } var code string @@ -79,7 +100,13 @@ func (s *Server) Start(buttons string, provider *oidc.IdentityProvider, client * panic(err) } + // form, err := os.ReadFile("pages/login.html") + // if err != nil { + // fmt.Printf("failed to load login form: %v", err) + // } + data := exec.NewContext(map[string]interface{}{ + // "loginForm": string(form), "loginButtons": buttons, }) @@ -116,7 +143,6 @@ func (s *Server) Start(buttons string, provider *oidc.IdentityProvider, client * http.Redirect(w, r, "/error", http.StatusInternalServerError) return } - } // forward the JWKS from the authorization server @@ -126,7 +152,6 @@ func (s *Server) Start(buttons string, provider *oidc.IdentityProvider, client * return } w.Write(jwks) - }) r.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { // use refresh token provided to do a refresh token grant @@ -162,7 +187,7 @@ func (s *Server) Start(buttons string, provider *oidc.IdentityProvider, client * w.Write([]byte(accessToken)) } }) - r.HandleFunc(s.Callback, func(w http.ResponseWriter, r *http.Request) { + r.HandleFunc(callback, func(w http.ResponseWriter, r *http.Request) { // get the code from the OIDC provider if r != nil { code = r.URL.Query().Get("code") @@ -268,3 +293,251 @@ func (s *Server) Start(buttons string, provider *oidc.IdentityProvider, client * s.Handler = r return s.ListenAndServe() } + +func (s *Server) StartIdentityProvider() error { + // NOTE: this example does NOT implement CSRF tokens nor use them + + // create an example identity provider + var ( + r = chi.NewRouter() + // clients = []oauth.Client{} + callback = "" + activeCodes = []string{} + ) + + // check if callback is set + if s.Callback == "" { + callback = "/oidc/callback" + } + + // generate key pair used to sign JWKS and create JWTs + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return fmt.Errorf("failed to generate new RSA key: %v", err) + } + privateJwk, publicJwk, err := cryptox.GenerateJwkKeyPairFromPrivateKey(privateKey) + if err != nil { + return fmt.Errorf("failed to generate JWK pair from private key: %v", err) + } + kid, _ := privateJwk.Get("kid") + publicJwk.Set("kid", kid) + publicJwk.Set("use", "sig") + publicJwk.Set("kty", "RSA") + publicJwk.Set("alg", "RS256") + if err := publicJwk.Validate(); err != nil { + return fmt.Errorf("failed to validate public JWK: %v", err) + } + + // TODO: create .well-known JWKS endpoint with json + r.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) { + // TODO: generate new JWKs from a private key + + jwks := map[string]any{ + "keys": []jwk.Key{ + publicJwk, + }, + } + b, err := json.Marshal(jwks) + if err != nil { + return + } + w.Write(b) + }) + + // TODO: create .well-known openid configuration + r.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { + // create config JSON to serve with GET request + config := map[string]any{ + "issuer": "http://" + s.Addr, + "authorization_endpoint": "http://" + s.Addr + "/oauth/authorize", + "token_endpoint": "http://" + s.Addr + "/oauth/token", + "jwks_uri": "http://" + s.Addr + "/.well-known/jwks.json", + "scopes_supported": []string{ + "openid", + "profile", + "email", + }, + "response_types_supported": []string{ + "code", + }, + "grant_types_supported": []string{ + "authorization_code", + }, + "id_token_signing_alg_values_supported": []string{ + "RS256", + }, + "claims_supported": []string{ + "iss", + "sub", + "aud", + "exp", + "iat", + "name", + "email", + }, + } + + b, err := json.Marshal(config) + if err != nil { + return + } + w.Write(b) + }) + r.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) { + // serve up a simple login page + }) + r.HandleFunc("/consent", func(w http.ResponseWriter, r *http.Request) { + // give consent for app to use + }) + r.HandleFunc("/browser/login", func(w http.ResponseWriter, r *http.Request) { + // serve up a login page for user creds + form, err := os.ReadFile("pages/login.html") + if err != nil { + fmt.Printf("failed to load login form: %v", err) + } + w.Write(form) + }) + r.HandleFunc("/api/login", func(w http.ResponseWriter, r *http.Request) { + // check for example identity with POST request + r.ParseForm() + username := r.Form.Get("username") + password := r.Form.Get("password") + + // example username and password so do simplified authorization code flow + if username == "ochami" && password == "ochami" { + client := oauth.Client{ + Id: "ochami", + Secret: "ochami", + Name: "ochami", + Issuer: "http://127.0.0.1:3333", + RedirectUris: []string{fmt.Sprintf("http://%s:%d%s", s.Host, s.Port, callback)}, + } + + // check if there are any redirect URIs supplied + if len(client.RedirectUris) <= 0 { + fmt.Printf("no redirect URIs found") + return + } + for _, url := range client.RedirectUris { + // send an authorization code to each URI + code := util.RandomString(64) + activeCodes = append(activeCodes, code) + redirectUrl := fmt.Sprintf("%s?code=%s", url, code) + fmt.Printf("redirect URL: %s\n", redirectUrl) + http.Redirect(w, r, redirectUrl, http.StatusFound) + // _, _, err := httpx.MakeHttpRequest(fmt.Sprintf("%s?code=%s", url, code), http.MethodGet, nil, nil) + // if err != nil { + // fmt.Printf("failed to make request: %v\n", err) + // continue + // } + } + } else { + w.Write([]byte("error logging in")) + http.Redirect(w, r, "/browser/login", http.StatusUnauthorized) + } + }) + r.HandleFunc("/oauth/token", func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + + // check for authorization code and make sure it's valid + var code = r.Form.Get("code") + index := slices.IndexFunc(activeCodes, func(s string) bool { return s == code }) + if index < 0 { + fmt.Printf("invalid authorization code: %s\n", code) + return + } + + // now create and return a JWT that can be verified with by authorization server + iat := time.Now().Unix() + exp := time.Now().Add(time.Second * 3600 * 16).Unix() + t := jwt.New() + t.Set(jwt.IssuerKey, s.Addr) + t.Set(jwt.SubjectKey, "ochami") + t.Set(jwt.AudienceKey, "ochami") + t.Set(jwt.IssuedAtKey, iat) + t.Set(jwt.ExpirationKey, exp) + t.Set("name", "ochami") + t.Set("email", "example@ochami.org") + t.Set("email_verified", true) + t.Set("scope", []string{ + "openid", + "profile", + "email", + "example", + }) + // payload := map[string]any{} + // payload["iss"] = s.Addr + // payload["aud"] = "ochami" + // payload["iat"] = iat + // payload["nbf"] = iat + // payload["exp"] = exp + // payload["sub"] = "ochami" + // payload["name"] = "ochami" + // payload["email"] = "example@ochami.org" + // payload["email_verified"] = true + // payload["scope"] = []string{ + // "openid", + // "profile", + // "email", + // "example", + // } + payloadJson, err := json.MarshalIndent(t, "", "\t") + if err != nil { + fmt.Printf("failed to marshal payload: %v", err) + return + } + signed, err := jws.Sign(payloadJson, jws.WithKey(jwa.RS256, privateJwk)) + if err != nil { + fmt.Printf("failed to sign token: %v\n", err) + return + } + + // construct the bearer token with required fields + scope, _ := t.Get("scope") + bearer := map[string]any{ + "token_type": "Bearer", + "id_token": string(signed), + "expires_in": exp, + "created_at": iat, + "scope": strings.Join(scope.([]string), " "), + } + + b, err := json.MarshalIndent(bearer, "", "\t") + if err != nil { + fmt.Printf("failed to marshal bearer token: %v\n", err) + return + } + fmt.Printf("bearer: %s\n", string(b)) + w.Write(b) + }) + r.HandleFunc("/oauth/authorize", func(w http.ResponseWriter, r *http.Request) { + var ( + responseType = r.URL.Query().Get("response_type") + clientId = r.URL.Query().Get("client_id") + redirectUris = r.URL.Query().Get("redirect_uri") + ) + + // check for required authorization code params + if responseType != "code" { + fmt.Printf("invalid response type\n") + return + } + + // check that we're using the default registered client + if clientId != "ochami" { + fmt.Printf("invalid client\n") + return + } + + // TODO: check that our redirect URIs all match + for _, uri := range redirectUris { + _ = uri + } + + // redirect to browser login since we don't do session management here + http.Redirect(w, r, "/browser/login", http.StatusFound) + }) + + s.Handler = r + return s.ListenAndServe() +} From 57166c5ee48c0722375adef56f09ad74b9f13b40 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Wed, 17 Apr 2024 17:27:03 -0600 Subject: [PATCH 08/33] Added a login page for identity provider server --- pages/login.html | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 pages/login.html diff --git a/pages/login.html b/pages/login.html new file mode 100644 index 0000000..e55f5cf --- /dev/null +++ b/pages/login.html @@ -0,0 +1,13 @@ + + +
+

Login to your account

+
+
+
+
+ Forgot Username?
+ +
+
+ \ No newline at end of file From b45821e587d915fc7f7d89949654f0f0291d6767 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Thu, 18 Apr 2024 12:40:25 -0600 Subject: [PATCH 09/33] Changed .well-known endpoints to write status codes --- internal/server/server.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/server/server.go b/internal/server/server.go index 66c45d4..31116b4 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -341,6 +341,7 @@ func (s *Server) StartIdentityProvider() error { if err != nil { return } + w.WriteHeader(200) w.Write(b) }) @@ -381,6 +382,7 @@ func (s *Server) StartIdentityProvider() error { if err != nil { return } + w.WriteHeader(200) w.Write(b) }) r.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) { From 2762a95da5751fba6c72b1098dc1a35e51314464 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Thu, 18 Apr 2024 12:55:16 -0600 Subject: [PATCH 10/33] Update README.md about internal IDP --- README.md | 60 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index bcabb1b..1ca42c7 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,39 @@ These commands will create a default config, then start the login process. Maybe - [Gitlab](https://about.gitlab.com/) - [Forgejo](https://forgejo.org/) (fork of Gitea) +The tool is now able to run an internal example identity provider using the `serve` subcommand. + +```bash +./opaal serve --config config.yaml +``` + +This will start a server that allows you to login with `opaal` itself. Currently, it is only has one example user to use for log in. The username and password combination is `ochami:ochami`. It uses the same config file as before with additional parameters set in the config file: + +```yaml +server: + ... + issuer: + host: "127.0.0.1" + port: 3332 + +authentication: + clients: + - id: "ochami" + secret: "ochami" + name: "ochami" + issuer: "http://127.0.0.1:3332" + redirect-uris: + - "http://127.0.0.1:3333/oidc/callback" +``` + +See the [Configuration](#configuration) section for the entire config file. + ### Authorization Code Flow `opaal` has the ability to completely execute the authorization code and return an access token from an authorization server using social sign-in. The process works as follows: 1. Click the authorization link or navigate to the hosted endpoint in your browser (127.0.0.1:3333 by default) - - Alternatively, you can use a link produced + - Alternatively, you can use a link produced 2. Login using identity provider credentials 3. Authorize application registered with IdP 4. IdP redirects to specified redirect URI @@ -41,27 +68,29 @@ These commands will create a default config, then start the login process. Maybe - ...verifying the authenticity of the ID token from identity provider with its JWKS - ...adds itself as a trusted issuer to the authorization server with it's own JWK - ...creates a new signed JWT to send to the authorization server with the `urn:ietf:params:oauth:grant-type:jwt-bearer` grant type - - ... returns an access token that can be used by services protected by the authorization server + - ... returns an access token that can be used by services protected by the authorization server *After receiving the ID token, the rest of the flow requires the appropriate URLs to be set to continue. ### Client Credentials Flow -`opaal` also has - ## Configuration Here is an example configuration file: ```yaml -version: "0.0.1" +version: "0.3.2" server: host: "127.0.0.1" port: 3333 callback: "/oidc/callback" + issuer: + host: "127.0.0.1" + port: 3332 providers: + opaal: "https://127.0.0.1:3332" forgejo: "http://127.0.0.1:3000" authentication: @@ -83,7 +112,17 @@ authentication: client-credentials: authorization: - urls: + token: + forwarding: false + refresh: false + duration: 16h + scope: + - smd.read + key-path: ./keys + endpoints: + issuer: http://127.0.0.1:4444 + config: http://127.0.0.1:4444/.well-known/openid-configuration + jwks: http://127.0.0.1:4444/.well-known/jwks.json #identities: http://127.0.0.1:4434/admin/identities trusted-issuers: http://127.0.0.1:4445/admin/trust/grants/jwt-bearer/issuers login: http://127.0.0.1:4433/self-service/login/api @@ -91,17 +130,14 @@ authorization: authorize: http://127.0.0.1:4444/oauth2/auth register: http://127.0.0.1:4444/oauth2/register token: http://127.0.0.1:4444/oauth2/token - clients: - - id: bss - secret: IAMBSS options: - decode-id-token: true - decode-access-token: true run-once: true open-browser: false - forward: false + flow: authorization_code + cache-only: false + verbose: true ``` ## Troubleshooting From 2e117bea360edb2d46b8123d9aa0ff62ec5a3804 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Thu, 18 Apr 2024 12:55:38 -0600 Subject: [PATCH 11/33] Removed write headers for .well-known endpoints --- internal/server/server.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index 31116b4..66c45d4 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -341,7 +341,6 @@ func (s *Server) StartIdentityProvider() error { if err != nil { return } - w.WriteHeader(200) w.Write(b) }) @@ -382,7 +381,6 @@ func (s *Server) StartIdentityProvider() error { if err != nil { return } - w.WriteHeader(200) w.Write(b) }) r.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) { From 61a35c165dbb7ede545c0419a9a09babd405133a Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Thu, 18 Apr 2024 16:02:43 -0600 Subject: [PATCH 12/33] WIP refactoring login --- cmd/login.go | 15 +++++++++++++++ internal/server/server.go | 1 + 2 files changed, 16 insertions(+) diff --git a/cmd/login.go b/cmd/login.go index abe3452..09b0b58 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -85,6 +85,11 @@ var loginCmd = &cobra.Command{ os.Exit(1) } + // use clients to make SSO buttons that + for _, client := range config.Authentication.Clients { + MakeButton() + } + // start the listener err := opaal.Login(&config, &client, provider) if err != nil { @@ -115,3 +120,13 @@ func init() { loginCmd.MarkFlagsMutuallyExclusive("target.name", "target.index") rootCmd.AddCommand(loginCmd) } + +func MakeButton(url string, text string) string { + // check if we have http:// a + html := " " + text + "" +} diff --git a/internal/server/server.go b/internal/server/server.go index 66c45d4..84c6aa4 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -93,6 +93,7 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli // add target if query exists if r != nil { target = r.URL.Query().Get("target") + sso := r.URL.Query().Get("sso") } // show login page with notice to redirect template, err := gonja.FromFile("pages/index.html") From 6d2f488a6b570b6f5cc130b259725fa8908b2890 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 23 Apr 2024 13:17:41 -0600 Subject: [PATCH 13/33] Refactored login page and process --- cmd/login.go | 120 +++++++++++++++------------------ internal/flows/jwt_bearer.go | 36 +++++----- internal/login.go | 35 ++-------- internal/new.go | 4 +- internal/oauth/authenticate.go | 13 ++-- internal/oauth/client.go | 19 +++--- internal/oidc/oidc.go | 26 +++---- internal/server/server.go | 86 ++++++++++++++++++----- 8 files changed, 179 insertions(+), 160 deletions(-) diff --git a/cmd/login.go b/cmd/login.go index 09b0b58..855a0e8 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -2,12 +2,9 @@ package cmd import ( opaal "davidallendj/opaal/internal" - cache "davidallendj/opaal/internal/cache/sqlite" "davidallendj/opaal/internal/oauth" - "davidallendj/opaal/internal/oidc" "fmt" "os" - "slices" "github.com/spf13/cobra" ) @@ -25,73 +22,68 @@ var loginCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { for { // try and find client with valid identity provider config - var provider *oidc.IdentityProvider - if target != "" { - // only try to use client with name give - index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool { - return target == c.Name - }) - if index < 0 { - fmt.Printf("could not find the target client listed by name") - os.Exit(1) - } - client := config.Authentication.Clients[index] - _, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer) - if err != nil { + // var provider *oidc.IdentityProvider + // if target != "" { + // // only try to use client with name give + // index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool { + // return target == c.Name + // }) + // if index < 0 { + // fmt.Printf("could not find the target client listed by name") + // os.Exit(1) + // } + // client := config.Authentication.Clients[index] + // _, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer) + // if err != nil { - } + // } - } else if targetIndex >= 0 { - // only try to use client by index - targetCount := len(config.Authentication.Clients) - 1 - if targetIndex > targetCount { - fmt.Printf("target index out of range (found %d)", targetCount) - } - client := config.Authentication.Clients[targetIndex] - _, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer) - if err != nil { + // } else if targetIndex >= 0 { + // // only try to use client by index + // targetCount := len(config.Authentication.Clients) - 1 + // if targetIndex > targetCount { + // fmt.Printf("target index out of range (found %d)", targetCount) + // } + // client := config.Authentication.Clients[targetIndex] + // _, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer) + // if err != nil { - } - } else { - for _, c := range config.Authentication.Clients { - // try to get identity provider info locally first - _, err := cache.GetIdentityProvider(config.Options.CachePath, c.Issuer) - if err != nil && !config.Options.CacheOnly { - fmt.Printf("fetching config from issuer: %v\n", c.Issuer) - // try to get info remotely by fetching - provider, err = oidc.FetchServerConfig(c.Issuer) - if err != nil { - fmt.Printf("failed to fetch server config: %v\n", err) - continue - } - client = c - // fetch the provider's JWKS - err := provider.FetchJwks() - if err != nil { - fmt.Printf("failed to fetch JWKS: %v\n", err) - } - break - } - // only test the first if --run-all flag is not set - if !config.Authentication.TestAllClients { - fmt.Printf("stopping after first test...\n\n\n") - break - } - } - } + // } + // } else { + // for _, c := range config.Authentication.Clients { + // // try to get identity provider info locally first + // _, err := cache.GetIdentityProvider(config.Options.CachePath, c.Issuer) + // if err != nil && !config.Options.CacheOnly { + // fmt.Printf("fetching config from issuer: %v\n", c.Issuer) + // // try to get info remotely by fetching + // provider, err = oidc.FetchServerConfig(c.Issuer) + // if err != nil { + // fmt.Printf("failed to fetch server config: %v\n", err) + // continue + // } + // client = c + // // fetch the provider's JWKS + // err := provider.FetchJwks() + // if err != nil { + // fmt.Printf("failed to fetch JWKS: %v\n", err) + // } + // break + // } + // // only test the first if --run-all flag is not set + // if !config.Authentication.TestAllClients { + // fmt.Printf("stopping after first test...\n\n\n") + // break + // } + // } + // } - if provider == nil { - fmt.Printf("failed to retrieve provider config\n") - os.Exit(1) - } - - // use clients to make SSO buttons that - for _, client := range config.Authentication.Clients { - MakeButton() - } + // if provider == nil { + // fmt.Printf("failed to retrieve provider config\n") + // os.Exit(1) + // } // start the listener - err := opaal.Login(&config, &client, provider) + err := opaal.Login(&config) if err != nil { fmt.Printf("%v\n", err) os.Exit(1) diff --git a/internal/flows/jwt_bearer.go b/internal/flows/jwt_bearer.go index 652944b..b73ebdc 100644 --- a/internal/flows/jwt_bearer.go +++ b/internal/flows/jwt_bearer.go @@ -4,7 +4,6 @@ import ( "crypto/rand" "crypto/rsa" "davidallendj/opaal/internal/oauth" - "davidallendj/opaal/internal/oidc" "encoding/json" "fmt" "os" @@ -19,14 +18,14 @@ import ( ) type JwtBearerFlowParams struct { - AccessToken string - IdToken string - IdentityProvider *oidc.IdentityProvider - TrustedIssuer *oauth.TrustedIssuer - Client *oauth.Client - Refresh bool - Verbose bool - KeyPath string + AccessToken string + IdToken string + // IdentityProvider *oidc.IdentityProvider + TrustedIssuer *oauth.TrustedIssuer + Client *oauth.Client + Refresh bool + Verbose bool + KeyPath string } type JwtBearerFlowEndpoints struct { @@ -39,22 +38,27 @@ type JwtBearerFlowEndpoints struct { func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (string, error) { // 1. verify that the JWT from the issuer is valid using all keys var ( - idp = params.IdentityProvider + // idp = params.IdentityProvider accessToken = params.AccessToken idToken = params.IdToken client = params.Client trustedIssuer = params.TrustedIssuer verbose = params.Verbose ) + + // pre-condition checks to make sure certain variables are set + if client == nil { + return "", fmt.Errorf("invalid client (client is nil)") + } if accessToken != "" { - _, err := jws.Verify([]byte(accessToken), jws.WithKeySet(idp.KeySet), jws.WithValidateKey(true)) + _, err := jws.Verify([]byte(accessToken), jws.WithKeySet(client.Provider.KeySet), jws.WithValidateKey(true)) if err != nil { return "", fmt.Errorf("failed to verify access token: %v", err) } } if idToken != "" { - _, err := jws.Verify([]byte(idToken), jws.WithKeySet(idp.KeySet), jws.WithValidateKey(true)) + _, err := jws.Verify([]byte(idToken), jws.WithKeySet(client.Provider.KeySet), jws.WithValidateKey(true)) if err != nil { return "", fmt.Errorf("failed to verify ID token: %v", err) } @@ -126,7 +130,7 @@ func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (s // TODO: add trusted issuer to cache if successful // 4. create a new JWT based on the claims from the identity provider and sign - parsedIdToken, err := jwt.ParseString(idToken, jwt.WithKeySet(idp.KeySet)) + parsedIdToken, err := jwt.ParseString(idToken, jwt.WithKeySet(client.Provider.KeySet)) if err != nil { return "", fmt.Errorf("failed to parse ID token: %v", err) } @@ -242,7 +246,7 @@ func ForwardToken(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) error var ( client = params.Client idToken = params.IdToken - idp = params.IdentityProvider + // idp = params.IdentityProvider verbose = params.Verbose ) @@ -250,7 +254,7 @@ func ForwardToken(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) error if verbose { fmt.Printf("Fetching JWKS from authentication server for verification...\n") } - err := idp.FetchJwks() + err := client.Provider.FetchJwks() if err != nil { return fmt.Errorf("failed to fetch JWK: %v", err) } else { @@ -260,7 +264,7 @@ func ForwardToken(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) error } ti := &oauth.TrustedIssuer{ - Issuer: idp.Issuer, + Issuer: client.Provider.Issuer, Subject: "1", ExpiresAt: time.Now().Add(time.Second * 3600), } diff --git a/internal/login.go b/internal/login.go index e9c6374..d542974 100644 --- a/internal/login.go +++ b/internal/login.go @@ -12,19 +12,11 @@ import ( "time" ) -func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider) error { +func Login(config *Config) error { if config == nil { return fmt.Errorf("invalid config") } - if client == nil { - return fmt.Errorf("invalid client") - } - - if provider == nil { - return fmt.Errorf("invalid identity provider") - } - // make cache if it's not where expect _, err := cache.CreateIdentityProvidersIfNotExists(config.Options.CachePath) if err != nil { @@ -39,18 +31,12 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider } // print the authorization URL for sharing - var authorizationUrl = client.BuildAuthorizationUrl(provider.Endpoints.Authorization, state) s := NewServerWithConfig(config) - fmt.Printf("Login with external identity provider:\n\n %s/login\n %s\n\n", - s.GetListenAddr(), authorizationUrl, - ) + s.State = state - var button = MakeButton(authorizationUrl, "Login with "+client.Name) var authzClient = oauth.NewClient() authzClient.Scope = config.Authorization.Token.Scope - // authorize oauth client and listen for callback from provider - fmt.Printf("Waiting for authorization code redirect @%s/oidc/callback...\n", s.GetListenAddr()) params := server.ServerParams{ Verbose: config.Options.Verbose, AuthProvider: &oidc.IdentityProvider{ @@ -66,8 +52,7 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider Register: config.Authorization.Endpoints.Register, }, JwtBearerParams: flows.JwtBearerFlowParams{ - Client: authzClient, - IdentityProvider: provider, + Client: authzClient, TrustedIssuer: &oauth.TrustedIssuer{ AllowAnySubject: false, Issuer: s.Addr, @@ -87,7 +72,7 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider Client: authzClient, }, } - err = s.StartLogin(button, provider, client, params) + err = s.StartLogin(config.Authentication.Clients, params) if errors.Is(err, http.ErrServerClosed) { fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n") } else if err != nil { @@ -96,7 +81,7 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider } else if config.Options.FlowType == "client_credentials" { params := flows.ClientCredentialsFlowParams{ - Client: client, + Client: nil, // # FIXME: need to do something about this being nil I think } _, err := NewClientCredentialsFlowWithConfig(config, params) if err != nil { @@ -108,13 +93,3 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider return nil } - -func MakeButton(url string, text string) string { - // check if we have http:// a - html := " " + text + "" -} diff --git a/internal/new.go b/internal/new.go index 2b2bad8..55d9cda 100644 --- a/internal/new.go +++ b/internal/new.go @@ -29,7 +29,7 @@ func NewClientWithConfig(config *Config) *oauth.Client { Id: clients[0].Id, Secret: clients[0].Secret, Name: clients[0].Name, - Issuer: clients[0].Issuer, + Provider: clients[0].Provider, Scope: clients[0].Scope, RedirectUris: clients[0].RedirectUris, } @@ -53,7 +53,7 @@ func NewClientWithConfigByName(config *Config, name string) *oauth.Client { func NewClientWithConfigByProvider(config *Config, issuer string) *oauth.Client { index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool { - return c.Issuer == issuer + return c.Provider.Issuer == issuer }) if index >= 0 { diff --git a/internal/oauth/authenticate.go b/internal/oauth/authenticate.go index 5724526..b579e8e 100644 --- a/internal/oauth/authenticate.go +++ b/internal/oauth/authenticate.go @@ -16,12 +16,15 @@ func (client *Client) IsFlowInitiated() bool { return client.FlowId != "" } -func (client *Client) BuildAuthorizationUrl(issuer string, state string) string { - return issuer + "?" + "client_id=" + client.Id + +func (client *Client) BuildAuthorizationUrl(state string) string { + url := client.Provider.Endpoints.Authorization + "?client_id=" + client.Id + "&redirect_uri=" + url.QueryEscape(strings.Join(client.RedirectUris, ",")) + "&response_type=code" + // this has to be set to "code" - "&state=" + state + "&scope=" + strings.Join(client.Scope, "+") + if state != "" { + url += "&state=" + state + } + return url } func (client *Client) InitiateLoginFlow(loginUrl string) error { @@ -90,7 +93,7 @@ func (client *Client) FetchCSRFToken(flowUrl string) error { return fmt.Errorf("failed to extract CSRF token: not found") } -func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl string, state string) ([]byte, error) { +func (client *Client) FetchTokenFromAuthenticationServer(code string, state string) ([]byte, error) { body := url.Values{ "grant_type": {"authorization_code"}, "client_id": {client.Id}, @@ -104,7 +107,7 @@ func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl if state != "" { body["state"] = []string{state} } - res, err := http.PostForm(remoteUrl, body) + res, err := http.PostForm(client.Provider.Endpoints.Token, body) if err != nil { return nil, fmt.Errorf("failed to get ID token: %s", err) } diff --git a/internal/oauth/client.go b/internal/oauth/client.go index 2040117..78ab9cc 100644 --- a/internal/oauth/client.go +++ b/internal/oauth/client.go @@ -1,6 +1,7 @@ package oauth import ( + "davidallendj/opaal/internal/oidc" "encoding/json" "fmt" "net/http" @@ -24,15 +25,15 @@ const ( type Client struct { http.Client - Id string `db:"id" yaml:"id"` - Secret string `db:"secret" yaml:"secret"` - Name string `db:"name" yaml:"name"` - Description string `db:"description" yaml:"description"` - Issuer string `db:"issuer" yaml:"issuer"` - RegistrationAccessToken string `db:"registration_access_token" yaml:"registration-access-token"` - RedirectUris []string `db:"redirect_uris" yaml:"redirect-uris"` - Scope []string `db:"scope" yaml:"scope"` - Audience []string `db:"audience" yaml:"audience"` + Id string `db:"id" yaml:"id"` + Secret string `db:"secret" yaml:"secret"` + Name string `db:"name" yaml:"name"` + Description string `db:"description" yaml:"description"` + Provider oidc.IdentityProvider `db:"issuer" yaml:"provider"` + RegistrationAccessToken string `db:"registration_access_token" yaml:"registration-access-token"` + RedirectUris []string `db:"redirect_uris" yaml:"redirect-uris"` + Scope []string `db:"scope" yaml:"scope"` + Audience []string `db:"audience" yaml:"audience"` FlowId string CsrfToken string } diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index af813ab..2ec1f9c 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -111,26 +111,11 @@ func (p *IdentityProvider) LoadServerConfig(path string) error { } func (p *IdentityProvider) FetchServerConfig() error { - // make a request to a server's openid-configuration - req, err := http.NewRequest(http.MethodGet, p.Issuer+"/.well-known/openid-configuration", bytes.NewBuffer([]byte{})) + tmp, err := FetchServerConfig(p.Issuer) if err != nil { - return fmt.Errorf("failed to create a new request: %v", err) - } - - client := &http.Client{} // temp client to get info and not used in flow - res, err := client.Do(req) - if err != nil { - return fmt.Errorf("failed to do request: %v", err) - } - - body, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("failed to read response body: %v", err) - } - err = p.ParseServerConfig(body) - if err != nil { - return fmt.Errorf("failed to parse server config: %v", err) + return err } + p = tmp return nil } @@ -147,10 +132,15 @@ func FetchServerConfig(issuer string) (*IdentityProvider, error) { return nil, fmt.Errorf("failed to do request: %v", err) } + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP status code: %d", res.StatusCode) + } + body, err := io.ReadAll(res.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %v", err) } + var p IdentityProvider err = p.ParseServerConfig(body) if err != nil { diff --git a/internal/server/server.go b/internal/server/server.go index 84c6aa4..3409630 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -58,10 +58,12 @@ func (s *Server) GetListenAddr() string { return fmt.Sprintf("%s:%d", s.Host, s.Port) } -func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, client *oauth.Client, params ServerParams) error { +func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error { var ( - target = "" - callback = "" + target string + callback string + client *oauth.Client + sso string ) // check if callback is set @@ -69,6 +71,29 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli callback = "/oidc/callback" } + // make the login page SSO buttons and authorization URLs to write to stdout + buttons := "" + fmt.Printf("Login with external identity providers: \n") + for i, client := range clients { + // fetch provider configuration before adding button + p, err := oidc.FetchServerConfig(client.Provider.Issuer) + if err != nil { + fmt.Printf("failed to fetch server config: %v\n", err) + continue + } + + // if we're able to get the config, go ahead and try to fetch jwks too + if err = p.FetchJwks(); err != nil { + fmt.Printf("failed to fetch JWKS: %v\n", err) + continue + } + + clients[i].Provider = *p + buttons += makeButton(fmt.Sprintf("/login?sso=%s", client.Id), client.Name) + url := client.BuildAuthorizationUrl(s.State) + fmt.Printf("\t%s\n", url) + } + var code string var accessToken string r := chi.NewRouter() @@ -93,21 +118,32 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli // add target if query exists if r != nil { target = r.URL.Query().Get("target") - sso := r.URL.Query().Get("sso") + sso = r.URL.Query().Get("sso") + + // TODO: get client from list and build the authorization URL string + index := slices.IndexFunc(clients, func(c oauth.Client) bool { + return c.Id == sso + }) + + // TODO: redirect the user to authorization URL and return from func + foundClient := index >= 0 + if foundClient { + client = &clients[index] + + url := client.BuildAuthorizationUrl(s.State) + fmt.Printf("Redirect URL: %s\n", url) + http.Redirect(w, r, url, http.StatusFound) + return + } } + // show login page with notice to redirect template, err := gonja.FromFile("pages/index.html") if err != nil { panic(err) } - // form, err := os.ReadFile("pages/login.html") - // if err != nil { - // fmt.Printf("failed to load login form: %v", err) - // } - data := exec.NewContext(map[string]interface{}{ - // "loginForm": string(form), "loginButtons": buttons, }) @@ -158,7 +194,7 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli // use refresh token provided to do a refresh token grant refreshToken := r.URL.Query().Get("refresh-token") if refreshToken != "" { - _, err := params.JwtBearerParams.Client.PerformRefreshTokenGrant(provider.Endpoints.Token, refreshToken) + _, err := params.JwtBearerParams.Client.PerformRefreshTokenGrant(client.Provider.Endpoints.Token, refreshToken) if err != nil { fmt.Printf("failed to perform refresh token grant: %v\n", err) http.Redirect(w, r, "/error", http.StatusInternalServerError) @@ -196,10 +232,15 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli fmt.Printf("Authorization code: %v\n", code) } + // make sure we have the correct client to use + if client == nil { + fmt.Printf("failed to find valid client") + return + } + // use code from response and exchange for bearer token (with ID token) bearerToken, err := client.FetchTokenFromAuthenticationServer( code, - provider.Endpoints.Token, s.State, ) if err != nil { @@ -229,6 +270,7 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli // complete JWT bearer flow to receive access token from authorization server // fmt.Printf("bearer: %v\n", string(bearerToken)) params.JwtBearerParams.IdToken = data["id_token"].(string) + params.JwtBearerParams.Client = client accessToken, err = flows.NewJwtBearerFlow(params.JwtBearerEndpoints, params.JwtBearerParams) if err != nil { fmt.Printf("failed to complete JWT bearer flow: %v\n", err) @@ -407,10 +449,12 @@ func (s *Server) StartIdentityProvider() error { // example username and password so do simplified authorization code flow if username == "ochami" && password == "ochami" { client := oauth.Client{ - Id: "ochami", - Secret: "ochami", - Name: "ochami", - Issuer: "http://127.0.0.1:3333", + Id: "ochami", + Secret: "ochami", + Name: "ochami", + Provider: oidc.IdentityProvider{ + Issuer: "http://127.0.0.1:3333", + }, RedirectUris: []string{fmt.Sprintf("http://%s:%d%s", s.Host, s.Port, callback)}, } @@ -542,3 +586,13 @@ func (s *Server) StartIdentityProvider() error { s.Handler = r return s.ListenAndServe() } + +func makeButton(url string, text string) string { + // check if we have http:// a + html := "", text) + return html + // return " " + text + "" +} From bea426c47d2eab70871201a2b2893cec52489b0c Mon Sep 17 00:00:00 2001 From: Devon Bautista Date: Wed, 24 Apr 2024 10:04:15 -0600 Subject: [PATCH 14/33] Goreleaser: Add arm64 builds --- .goreleaser.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 72956fa..dab79bc 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -8,6 +8,7 @@ builds: - linux goarch: - amd64 + - arm64 dockers: - image_templates: - ghcr.io/openchami/{{.ProjectName}}:latest From 78ef71e94cdb1505b85b4686c50146f8134f3df2 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Wed, 24 Apr 2024 13:59:06 -0400 Subject: [PATCH 15/33] Update build_release.yml Removing broken attestation for the moment and adding a manual workflow trigger --- .github/workflows/build_release.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index 5b7ff5e..a1336da 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -4,6 +4,7 @@ name: Release with goreleaser on: + workflow_dispatch: push: tags: - v* @@ -52,7 +53,3 @@ jobs: echo "fs.writeFileSync('digest.txt', firstNonNullDigest);" >> process.js node process.js echo "digest=$(cat digest.txt)" >> $GITHUB_OUTPUT - - name: Attest opaal binary - uses: github-early-access/generate-build-provenance@main - with: - subject-path: opaal \ No newline at end of file From 0dc5754b4d15f40027b19c4e27459f9aa212ec56 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Wed, 24 Apr 2024 14:08:15 -0400 Subject: [PATCH 16/33] Backing off arm64 build --- .goreleaser.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index dab79bc..8e9d87d 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -8,7 +8,6 @@ builds: - linux goarch: - amd64 - - arm64 dockers: - image_templates: - ghcr.io/openchami/{{.ProjectName}}:latest @@ -54,4 +53,4 @@ changelog: # github: # name_template: "{{.Version}}" # prerelease: auto -# mode: append \ No newline at end of file +# mode: append From 20206ea4c130032643e539ace5724a345e0475a4 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Fri, 26 Apr 2024 15:37:09 -0600 Subject: [PATCH 17/33] Changed login button to use tags with no JS --- internal/server/server.go | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index 3409630..1cb7558 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -212,17 +212,19 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error { http.Redirect(w, r, "/error", http.StatusInternalServerError) return } - } else { - // perform a client credentials grant and return a token - var err error - accessToken, err = flows.NewClientCredentialsFlow(params.ClientCredentialsEndpoints, params.ClientCredentialsParams) - if err != nil { - fmt.Printf("failed to perform client credentials flow: %v\n", err) - http.Redirect(w, r, "/error", http.StatusInternalServerError) - return - } - w.Write([]byte(accessToken)) } + // FIXME: I think this probably needs to reworked or removed + // else { + // // perform a client credentials grant and return a token + // var err error + // accessToken, err = flows.NewClientCredentialsFlow(params.ClientCredentialsEndpoints, params.ClientCredentialsParams) + // if err != nil { + // fmt.Printf("failed to perform client credentials flow: %v\n", err) + // http.Redirect(w, r, "/error", http.StatusInternalServerError) + // return + // } + // w.Write([]byte(accessToken)) + // } }) r.HandleFunc(callback, func(w http.ResponseWriter, r *http.Request) { // get the code from the OIDC provider @@ -589,10 +591,12 @@ func (s *Server) StartIdentityProvider() error { func makeButton(url string, text string) string { // check if we have http:// a - html := "", text) + html := "", text) + html += fmt.Sprintf("href=\"%s\">%s", url, text) + html += "" return html - // return " " + text + "" } From 447c9fb5e9294a946481108017f23226bebe68da Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Fri, 26 Apr 2024 15:56:25 -0600 Subject: [PATCH 18/33] Reverted token fetch in server --- internal/server/server.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index 1cb7558..65e0f80 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -212,19 +212,19 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error { http.Redirect(w, r, "/error", http.StatusInternalServerError) return } + } else { + // FIXME: I think this probably needs to reworked or removed + // NOTE: this logic fetches a token for services to retrieve like BSS + // perform a client credentials grant and return a token + var err error + accessToken, err = flows.NewClientCredentialsFlow(params.ClientCredentialsEndpoints, params.ClientCredentialsParams) + if err != nil { + fmt.Printf("failed to perform client credentials flow: %v\n", err) + http.Redirect(w, r, "/error", http.StatusInternalServerError) + return + } + w.Write([]byte(accessToken)) } - // FIXME: I think this probably needs to reworked or removed - // else { - // // perform a client credentials grant and return a token - // var err error - // accessToken, err = flows.NewClientCredentialsFlow(params.ClientCredentialsEndpoints, params.ClientCredentialsParams) - // if err != nil { - // fmt.Printf("failed to perform client credentials flow: %v\n", err) - // http.Redirect(w, r, "/error", http.StatusInternalServerError) - // return - // } - // w.Write([]byte(accessToken)) - // } }) r.HandleFunc(callback, func(w http.ResponseWriter, r *http.Request) { // get the code from the OIDC provider From 20ba7bc7355b3d8b28169d6a7b4af70ea2922657 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Sun, 28 Apr 2024 13:23:25 -0600 Subject: [PATCH 19/33] Separated server and IDP code into different files --- internal/server/server.go | 261 -------------------------------------- 1 file changed, 261 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index 65e0f80..3c7507b 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,28 +1,17 @@ package server import ( - "crypto/rand" - "crypto/rsa" "davidallendj/opaal/internal/flows" "davidallendj/opaal/internal/oauth" "davidallendj/opaal/internal/oidc" "encoding/json" "fmt" "net/http" - "os" "slices" - "strings" - "time" - "github.com/davidallendj/go-utils/cryptox" "github.com/davidallendj/go-utils/httpx" - "github.com/davidallendj/go-utils/util" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jws" - "github.com/lestrrat-go/jwx/v2/jwt" "github.com/nikolalohinski/gonja/v2" "github.com/nikolalohinski/gonja/v2/exec" ) @@ -339,256 +328,6 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error { return s.ListenAndServe() } -func (s *Server) StartIdentityProvider() error { - // NOTE: this example does NOT implement CSRF tokens nor use them - - // create an example identity provider - var ( - r = chi.NewRouter() - // clients = []oauth.Client{} - callback = "" - activeCodes = []string{} - ) - - // check if callback is set - if s.Callback == "" { - callback = "/oidc/callback" - } - - // generate key pair used to sign JWKS and create JWTs - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return fmt.Errorf("failed to generate new RSA key: %v", err) - } - privateJwk, publicJwk, err := cryptox.GenerateJwkKeyPairFromPrivateKey(privateKey) - if err != nil { - return fmt.Errorf("failed to generate JWK pair from private key: %v", err) - } - kid, _ := privateJwk.Get("kid") - publicJwk.Set("kid", kid) - publicJwk.Set("use", "sig") - publicJwk.Set("kty", "RSA") - publicJwk.Set("alg", "RS256") - if err := publicJwk.Validate(); err != nil { - return fmt.Errorf("failed to validate public JWK: %v", err) - } - - // TODO: create .well-known JWKS endpoint with json - r.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) { - // TODO: generate new JWKs from a private key - - jwks := map[string]any{ - "keys": []jwk.Key{ - publicJwk, - }, - } - b, err := json.Marshal(jwks) - if err != nil { - return - } - w.Write(b) - }) - - // TODO: create .well-known openid configuration - r.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { - // create config JSON to serve with GET request - config := map[string]any{ - "issuer": "http://" + s.Addr, - "authorization_endpoint": "http://" + s.Addr + "/oauth/authorize", - "token_endpoint": "http://" + s.Addr + "/oauth/token", - "jwks_uri": "http://" + s.Addr + "/.well-known/jwks.json", - "scopes_supported": []string{ - "openid", - "profile", - "email", - }, - "response_types_supported": []string{ - "code", - }, - "grant_types_supported": []string{ - "authorization_code", - }, - "id_token_signing_alg_values_supported": []string{ - "RS256", - }, - "claims_supported": []string{ - "iss", - "sub", - "aud", - "exp", - "iat", - "name", - "email", - }, - } - - b, err := json.Marshal(config) - if err != nil { - return - } - w.Write(b) - }) - r.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) { - // serve up a simple login page - }) - r.HandleFunc("/consent", func(w http.ResponseWriter, r *http.Request) { - // give consent for app to use - }) - r.HandleFunc("/browser/login", func(w http.ResponseWriter, r *http.Request) { - // serve up a login page for user creds - form, err := os.ReadFile("pages/login.html") - if err != nil { - fmt.Printf("failed to load login form: %v", err) - } - w.Write(form) - }) - r.HandleFunc("/api/login", func(w http.ResponseWriter, r *http.Request) { - // check for example identity with POST request - r.ParseForm() - username := r.Form.Get("username") - password := r.Form.Get("password") - - // example username and password so do simplified authorization code flow - if username == "ochami" && password == "ochami" { - client := oauth.Client{ - Id: "ochami", - Secret: "ochami", - Name: "ochami", - Provider: oidc.IdentityProvider{ - Issuer: "http://127.0.0.1:3333", - }, - RedirectUris: []string{fmt.Sprintf("http://%s:%d%s", s.Host, s.Port, callback)}, - } - - // check if there are any redirect URIs supplied - if len(client.RedirectUris) <= 0 { - fmt.Printf("no redirect URIs found") - return - } - for _, url := range client.RedirectUris { - // send an authorization code to each URI - code := util.RandomString(64) - activeCodes = append(activeCodes, code) - redirectUrl := fmt.Sprintf("%s?code=%s", url, code) - fmt.Printf("redirect URL: %s\n", redirectUrl) - http.Redirect(w, r, redirectUrl, http.StatusFound) - // _, _, err := httpx.MakeHttpRequest(fmt.Sprintf("%s?code=%s", url, code), http.MethodGet, nil, nil) - // if err != nil { - // fmt.Printf("failed to make request: %v\n", err) - // continue - // } - } - } else { - w.Write([]byte("error logging in")) - http.Redirect(w, r, "/browser/login", http.StatusUnauthorized) - } - }) - r.HandleFunc("/oauth/token", func(w http.ResponseWriter, r *http.Request) { - r.ParseForm() - - // check for authorization code and make sure it's valid - var code = r.Form.Get("code") - index := slices.IndexFunc(activeCodes, func(s string) bool { return s == code }) - if index < 0 { - fmt.Printf("invalid authorization code: %s\n", code) - return - } - - // now create and return a JWT that can be verified with by authorization server - iat := time.Now().Unix() - exp := time.Now().Add(time.Second * 3600 * 16).Unix() - t := jwt.New() - t.Set(jwt.IssuerKey, s.Addr) - t.Set(jwt.SubjectKey, "ochami") - t.Set(jwt.AudienceKey, "ochami") - t.Set(jwt.IssuedAtKey, iat) - t.Set(jwt.ExpirationKey, exp) - t.Set("name", "ochami") - t.Set("email", "example@ochami.org") - t.Set("email_verified", true) - t.Set("scope", []string{ - "openid", - "profile", - "email", - "example", - }) - // payload := map[string]any{} - // payload["iss"] = s.Addr - // payload["aud"] = "ochami" - // payload["iat"] = iat - // payload["nbf"] = iat - // payload["exp"] = exp - // payload["sub"] = "ochami" - // payload["name"] = "ochami" - // payload["email"] = "example@ochami.org" - // payload["email_verified"] = true - // payload["scope"] = []string{ - // "openid", - // "profile", - // "email", - // "example", - // } - payloadJson, err := json.MarshalIndent(t, "", "\t") - if err != nil { - fmt.Printf("failed to marshal payload: %v", err) - return - } - signed, err := jws.Sign(payloadJson, jws.WithKey(jwa.RS256, privateJwk)) - if err != nil { - fmt.Printf("failed to sign token: %v\n", err) - return - } - - // construct the bearer token with required fields - scope, _ := t.Get("scope") - bearer := map[string]any{ - "token_type": "Bearer", - "id_token": string(signed), - "expires_in": exp, - "created_at": iat, - "scope": strings.Join(scope.([]string), " "), - } - - b, err := json.MarshalIndent(bearer, "", "\t") - if err != nil { - fmt.Printf("failed to marshal bearer token: %v\n", err) - return - } - fmt.Printf("bearer: %s\n", string(b)) - w.Write(b) - }) - r.HandleFunc("/oauth/authorize", func(w http.ResponseWriter, r *http.Request) { - var ( - responseType = r.URL.Query().Get("response_type") - clientId = r.URL.Query().Get("client_id") - redirectUris = r.URL.Query().Get("redirect_uri") - ) - - // check for required authorization code params - if responseType != "code" { - fmt.Printf("invalid response type\n") - return - } - - // check that we're using the default registered client - if clientId != "ochami" { - fmt.Printf("invalid client\n") - return - } - - // TODO: check that our redirect URIs all match - for _, uri := range redirectUris { - _ = uri - } - - // redirect to browser login since we don't do session management here - http.Redirect(w, r, "/browser/login", http.StatusFound) - }) - - s.Handler = r - return s.ListenAndServe() -} - func makeButton(url string, text string) string { // check if we have http:// a // html := " Date: Mon, 29 Apr 2024 14:50:48 -0600 Subject: [PATCH 20/33] Added audience override for token sent to authorization server --- internal/config.go | 4 +++- internal/flows/jwt_bearer.go | 6 ++++++ internal/login.go | 5 +++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/internal/config.go b/internal/config.go index 1515013..8e2139f 100644 --- a/internal/config.go +++ b/internal/config.go @@ -45,6 +45,7 @@ type TokenOptions struct { Forwarding bool `yaml:"forwarding"` Refresh bool `yaml:"refresh"` Scope []string `yaml:"scope"` + //TODO: allow specifying audience in returned token } type Authentication struct { @@ -55,9 +56,10 @@ type Authentication struct { } type Authorization struct { + Token TokenOptions `yaml:"token"` Endpoints Endpoints `yaml:"endpoints"` KeyPath string `yaml:"key-path"` - Token TokenOptions `yaml:"token"` + Audience []string `yaml:"audience"` // NOTE: overrides the "aud" claim in token sent to authorization server } type Config struct { diff --git a/internal/flows/jwt_bearer.go b/internal/flows/jwt_bearer.go index b73ebdc..2e93265 100644 --- a/internal/flows/jwt_bearer.go +++ b/internal/flows/jwt_bearer.go @@ -23,6 +23,7 @@ type JwtBearerFlowParams struct { // IdentityProvider *oidc.IdentityProvider TrustedIssuer *oauth.TrustedIssuer Client *oauth.Client + Audience []string Refresh bool Verbose bool KeyPath string @@ -143,6 +144,11 @@ func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (s payload["exp"] = time.Now().Add(time.Second * 3600 * 16).Unix() payload["sub"] = "opaal" + // if an "audience" value is set, then override the token endpoint value + if len(params.Audience) > 0 { + payload["aud"] = params.Audience + } + // include the offline_access scope if refresh tokens are enabled if params.Refresh { v, ok := payload["scope"] diff --git a/internal/login.go b/internal/login.go index d542974..ab51747 100644 --- a/internal/login.go +++ b/internal/login.go @@ -60,8 +60,9 @@ func Login(config *Config) error { ExpiresAt: time.Now().Add(config.Authorization.Token.Duration), Scope: []string{}, }, - Verbose: config.Options.Verbose, - Refresh: config.Authorization.Token.Refresh, + Verbose: config.Options.Verbose, + Refresh: config.Authorization.Token.Refresh, + Audience: config.Authorization.Audience, }, ClientCredentialsEndpoints: flows.ClientCredentialsFlowEndpoints{ Clients: config.Authorization.Endpoints.Clients, From f10a771db6e363c0d8906a6b460b83fc8c3c58aa Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Mon, 29 Apr 2024 14:58:51 -0600 Subject: [PATCH 21/33] Added missing IDP function back --- internal/server/idp.go | 273 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 internal/server/idp.go diff --git a/internal/server/idp.go b/internal/server/idp.go new file mode 100644 index 0000000..67c70b1 --- /dev/null +++ b/internal/server/idp.go @@ -0,0 +1,273 @@ +package server + +import ( + "crypto/rand" + "crypto/rsa" + "davidallendj/opaal/internal/oauth" + "davidallendj/opaal/internal/oidc" + "encoding/json" + "fmt" + "net/http" + "os" + "slices" + "strings" + "time" + + "github.com/davidallendj/go-utils/cryptox" + "github.com/davidallendj/go-utils/util" + "github.com/go-chi/chi/v5" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jws" + "github.com/lestrrat-go/jwx/v2/jwt" +) + +func (s *Server) StartIdentityProvider() error { + // NOTE: this example does NOT implement CSRF tokens nor use them + + // create an example identity provider + var ( + r = chi.NewRouter() + // clients = []oauth.Client{} + callback = "" + activeCodes = []string{} + ) + + // check if callback is set + if s.Callback == "" { + callback = "/oidc/callback" + } + + // generate key pair used to sign JWKS and create JWTs + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return fmt.Errorf("failed to generate new RSA key: %v", err) + } + privateJwk, publicJwk, err := cryptox.GenerateJwkKeyPairFromPrivateKey(privateKey) + if err != nil { + return fmt.Errorf("failed to generate JWK pair from private key: %v", err) + } + kid, _ := privateJwk.Get("kid") + publicJwk.Set("kid", kid) + publicJwk.Set("use", "sig") + publicJwk.Set("kty", "RSA") + publicJwk.Set("alg", "RS256") + if err := publicJwk.Validate(); err != nil { + return fmt.Errorf("failed to validate public JWK: %v", err) + } + + // TODO: create .well-known JWKS endpoint with json + r.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) { + // TODO: generate new JWKs from a private key + + jwks := map[string]any{ + "keys": []jwk.Key{ + publicJwk, + }, + } + b, err := json.Marshal(jwks) + if err != nil { + return + } + w.Write(b) + }) + + // TODO: create .well-known openid configuration + r.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { + // create config JSON to serve with GET request + config := map[string]any{ + "issuer": "http://" + s.Addr, + "authorization_endpoint": "http://" + s.Addr + "/oauth/authorize", + "token_endpoint": "http://" + s.Addr + "/oauth/token", + "jwks_uri": "http://" + s.Addr + "/.well-known/jwks.json", + "scopes_supported": []string{ + "openid", + "profile", + "email", + }, + "response_types_supported": []string{ + "code", + }, + "grant_types_supported": []string{ + "authorization_code", + }, + "id_token_signing_alg_values_supported": []string{ + "RS256", + }, + "claims_supported": []string{ + "iss", + "sub", + "aud", + "exp", + "iat", + "name", + "email", + }, + } + + b, err := json.Marshal(config) + if err != nil { + return + } + w.Write(b) + }) + r.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) { + // serve up a simple login page + }) + r.HandleFunc("/consent", func(w http.ResponseWriter, r *http.Request) { + // give consent for app to use + }) + r.HandleFunc("/browser/login", func(w http.ResponseWriter, r *http.Request) { + // serve up a login page for user creds + form, err := os.ReadFile("pages/login.html") + if err != nil { + fmt.Printf("failed to load login form: %v", err) + } + w.Write(form) + }) + r.HandleFunc("/api/login", func(w http.ResponseWriter, r *http.Request) { + // check for example identity with POST request + r.ParseForm() + username := r.Form.Get("username") + password := r.Form.Get("password") + + // example username and password so do simplified authorization code flow + if username == "ochami" && password == "ochami" { + client := oauth.Client{ + Id: "ochami", + Secret: "ochami", + Name: "ochami", + Provider: oidc.IdentityProvider{ + Issuer: "http://127.0.0.1:3333", + }, + RedirectUris: []string{fmt.Sprintf("http://%s:%d%s", s.Host, s.Port, callback)}, + } + + // check if there are any redirect URIs supplied + if len(client.RedirectUris) <= 0 { + fmt.Printf("no redirect URIs found") + return + } + for _, url := range client.RedirectUris { + // send an authorization code to each URI + code := util.RandomString(64) + activeCodes = append(activeCodes, code) + redirectUrl := fmt.Sprintf("%s?code=%s", url, code) + fmt.Printf("redirect URL: %s\n", redirectUrl) + http.Redirect(w, r, redirectUrl, http.StatusFound) + // _, _, err := httpx.MakeHttpRequest(fmt.Sprintf("%s?code=%s", url, code), http.MethodGet, nil, nil) + // if err != nil { + // fmt.Printf("failed to make request: %v\n", err) + // continue + // } + } + } else { + w.Write([]byte("error logging in")) + http.Redirect(w, r, "/browser/login", http.StatusUnauthorized) + } + }) + r.HandleFunc("/oauth/token", func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + + // check for authorization code and make sure it's valid + var code = r.Form.Get("code") + index := slices.IndexFunc(activeCodes, func(s string) bool { return s == code }) + if index < 0 { + fmt.Printf("invalid authorization code: %s\n", code) + return + } + + // now create and return a JWT that can be verified with by authorization server + iat := time.Now().Unix() + exp := time.Now().Add(time.Second * 3600 * 16).Unix() + t := jwt.New() + t.Set(jwt.IssuerKey, s.Addr) + t.Set(jwt.SubjectKey, "ochami") + t.Set(jwt.AudienceKey, "ochami") + t.Set(jwt.IssuedAtKey, iat) + t.Set(jwt.ExpirationKey, exp) + t.Set("name", "ochami") + t.Set("email", "example@ochami.org") + t.Set("email_verified", true) + t.Set("scope", []string{ + "openid", + "profile", + "email", + "example", + }) + // payload := map[string]any{} + // payload["iss"] = s.Addr + // payload["aud"] = "ochami" + // payload["iat"] = iat + // payload["nbf"] = iat + // payload["exp"] = exp + // payload["sub"] = "ochami" + // payload["name"] = "ochami" + // payload["email"] = "example@ochami.org" + // payload["email_verified"] = true + // payload["scope"] = []string{ + // "openid", + // "profile", + // "email", + // "example", + // } + payloadJson, err := json.MarshalIndent(t, "", "\t") + if err != nil { + fmt.Printf("failed to marshal payload: %v", err) + return + } + signed, err := jws.Sign(payloadJson, jws.WithKey(jwa.RS256, privateJwk)) + if err != nil { + fmt.Printf("failed to sign token: %v\n", err) + return + } + + // construct the bearer token with required fields + scope, _ := t.Get("scope") + bearer := map[string]any{ + "token_type": "Bearer", + "id_token": string(signed), + "expires_in": exp, + "created_at": iat, + "scope": strings.Join(scope.([]string), " "), + } + + b, err := json.MarshalIndent(bearer, "", "\t") + if err != nil { + fmt.Printf("failed to marshal bearer token: %v\n", err) + return + } + fmt.Printf("bearer: %s\n", string(b)) + w.Write(b) + }) + r.HandleFunc("/oauth/authorize", func(w http.ResponseWriter, r *http.Request) { + var ( + responseType = r.URL.Query().Get("response_type") + clientId = r.URL.Query().Get("client_id") + redirectUris = r.URL.Query().Get("redirect_uri") + ) + + // check for required authorization code params + if responseType != "code" { + fmt.Printf("invalid response type\n") + return + } + + // check that we're using the default registered client + if clientId != "ochami" { + fmt.Printf("invalid client\n") + return + } + + // TODO: check that our redirect URIs all match + for _, uri := range redirectUris { + _ = uri + } + + // redirect to browser login since we don't do session management here + http.Redirect(w, r, "/browser/login", http.StatusFound) + }) + + s.Handler = r + return s.ListenAndServe() +} From 73e4e50d44e75e0232ea6858ca839b4d86dcd548 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Mon, 29 Apr 2024 18:45:30 -0600 Subject: [PATCH 22/33] Made it possible to override certain example IDP endpoints --- cmd/serve.go | 19 ++++++++++++++----- internal/oidc/oidc.go | 22 ++++++++++++++++++++++ internal/server/idp.go | 17 ++++++++++++----- internal/server/server.go | 17 +++++++++-------- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 67cec1a..5318c87 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -2,6 +2,7 @@ package cmd import ( opaal "davidallendj/opaal/internal" + "davidallendj/opaal/internal/oidc" "errors" "fmt" "net/http" @@ -9,23 +10,31 @@ import ( "github.com/spf13/cobra" ) -var exampleCmd = &cobra.Command{ +var ( + endpoints oidc.Endpoints +) + +var serveCmd = &cobra.Command{ Use: "serve", - Short: "Start an simple identity provider server", + Short: "Start an simple, bare minimal identity provider server", Long: "The built-in identity provider is not (nor meant to be) a complete OIDC implementation and behaves like an external IdP", Run: func(cmd *cobra.Command, args []string) { s := opaal.NewServerWithConfig(&config) // FIXME: change how the server address is set with `NewServerWithConfig` s.Server.Addr = fmt.Sprintf("%s:%d", s.Issuer.Host, s.Issuer.Port) - err := s.StartIdentityProvider() + err := s.StartIdentityProvider(endpoints) if errors.Is(err, http.ErrServerClosed) { fmt.Printf("Identity provider server closed.\n") } else if err != nil { - fmt.Errorf("failed to start server: %v", err) + fmt.Printf("failed to start server: %v", err) } }, } func init() { - rootCmd.AddCommand(exampleCmd) + serveCmd.Flags().StringVar(&endpoints.Authorization, "endpoints.authorization", "", "set the authorization endpoint for the identity provider") + serveCmd.Flags().StringVar(&endpoints.Token, "endpoints.token", "", "set the token endpoint for the identity provider") + serveCmd.Flags().StringVar(&endpoints.JwksUri, "endpoints.jwks_uri", "", "set the JWKS endpoints for the identity provider") + + rootCmd.AddCommand(serveCmd) } diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index 2ec1f9c..f3382fc 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -164,3 +164,25 @@ func (p *IdentityProvider) FetchJwks() error { return nil } + +func (p *IdentityProvider) UpdateEndpoints(other *IdentityProvider) { + UpdateEndpoints(&p.Endpoints, &other.Endpoints) +} + +func UpdateEndpoints(eps *Endpoints, other *Endpoints) { + // only update endpoints that are not empty + var UpdateIf = func(ep *string, s string) { + if ep != nil { + if *ep != "" { + *ep = s + } + } + } + UpdateIf(&eps.Config, other.Config) + UpdateIf(&eps.Authorization, other.Authorization) + UpdateIf(&eps.Token, other.Token) + UpdateIf(&eps.Revocation, other.Revocation) + UpdateIf(&eps.Introspection, other.Introspection) + UpdateIf(&eps.UserInfo, other.UserInfo) + UpdateIf(&eps.JwksUri, other.JwksUri) +} diff --git a/internal/server/idp.go b/internal/server/idp.go index 67c70b1..4822e91 100644 --- a/internal/server/idp.go +++ b/internal/server/idp.go @@ -22,7 +22,7 @@ import ( "github.com/lestrrat-go/jwx/v2/jwt" ) -func (s *Server) StartIdentityProvider() error { +func (s *Server) StartIdentityProvider(eps oidc.Endpoints) error { // NOTE: this example does NOT implement CSRF tokens nor use them // create an example identity provider @@ -38,6 +38,14 @@ func (s *Server) StartIdentityProvider() error { callback = "/oidc/callback" } + // update endpoints that have values set + defaultEps := oidc.Endpoints{ + Authorization: "http://" + s.Addr + "/oauth/authorize", + Token: "http://" + s.Addr + "/oauth/token", + JwksUri: "http://" + s.Addr + "/.well-known/jwks.json", + } + oidc.UpdateEndpoints(&eps, &defaultEps) + // generate key pair used to sign JWKS and create JWTs privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { @@ -72,14 +80,13 @@ func (s *Server) StartIdentityProvider() error { w.Write(b) }) - // TODO: create .well-known openid configuration r.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { // create config JSON to serve with GET request config := map[string]any{ "issuer": "http://" + s.Addr, - "authorization_endpoint": "http://" + s.Addr + "/oauth/authorize", - "token_endpoint": "http://" + s.Addr + "/oauth/token", - "jwks_uri": "http://" + s.Addr + "/.well-known/jwks.json", + "authorization_endpoint": eps.Authorization, + "token_endpoint": eps.Token, + "jwks_uri": eps.JwksUri, "scopes_supported": []string{ "openid", "profile", diff --git a/internal/server/server.go b/internal/server/server.go index 3c7507b..c829a34 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -18,16 +18,17 @@ import ( type Server struct { *http.Server - Host string `yaml:"host"` - Port int `yaml:"port"` - Callback string `yaml:"callback"` - State string `yaml:"state"` - Issuer Issuer `yaml:"issuer"` + Host string `yaml:"host"` + Port int `yaml:"port"` + Callback string `yaml:"callback"` + State string `yaml:"state"` + Issuer IdentityProviderServer `yaml:"issuer"` } -type Issuer struct { - Host string `yaml:"host"` - Port int `yaml:"port"` +type IdentityProviderServer struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + Endpoints oidc.Endpoints `yaml:"endpoints"` } type ServerParams struct { From 67683e9fcaefc4a09536e5a6ec3dcbfdf845e949 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 30 Apr 2024 09:01:05 -0600 Subject: [PATCH 23/33] Fixed small issues with not building --- internal/login.go | 5 +++-- internal/new.go | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/login.go b/internal/login.go index ab51747..b6aaf84 100644 --- a/internal/login.go +++ b/internal/login.go @@ -42,8 +42,9 @@ func Login(config *Config) error { AuthProvider: &oidc.IdentityProvider{ Issuer: config.Authorization.Endpoints.Issuer, Endpoints: oidc.Endpoints{ - Config: config.Authorization.Endpoints.Config, - JwksUri: config.Authorization.Endpoints.JwksUri, + Config: config.Authorization.Endpoints.Config, + Authorization: config.Authorization.Endpoints.Authorize, + JwksUri: config.Authorization.Endpoints.JwksUri, }, }, JwtBearerEndpoints: flows.JwtBearerFlowEndpoints{ diff --git a/internal/new.go b/internal/new.go index 55d9cda..ce20aee 100644 --- a/internal/new.go +++ b/internal/new.go @@ -90,7 +90,7 @@ func NewServerWithConfig(conf *Config) *server.Server { }, Host: host, Port: port, - Issuer: server.Issuer{ + Issuer: server.IdentityProviderServer{ Host: conf.Server.Issuer.Host, Port: conf.Server.Issuer.Port, }, From e940dc2dd9a51ec38ec8179d1127dc57956668ce Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 30 Apr 2024 11:32:28 -0600 Subject: [PATCH 24/33] Fixed IDP endpoint overrides not working correctly --- cmd/serve.go | 2 +- internal/config.go | 11 ++++++++++- internal/new.go | 5 +++-- internal/oidc/oidc.go | 19 ++++++++++--------- internal/server/idp.go | 10 +++++----- 5 files changed, 29 insertions(+), 18 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 5318c87..b68b814 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -22,7 +22,7 @@ var serveCmd = &cobra.Command{ s := opaal.NewServerWithConfig(&config) // FIXME: change how the server address is set with `NewServerWithConfig` s.Server.Addr = fmt.Sprintf("%s:%d", s.Issuer.Host, s.Issuer.Port) - err := s.StartIdentityProvider(endpoints) + err := s.StartIdentityProvider() if errors.Is(err, http.ErrServerClosed) { fmt.Printf("Identity provider server closed.\n") } else if err != nil { diff --git a/internal/config.go b/internal/config.go index 8e2139f..cb7bef1 100644 --- a/internal/config.go +++ b/internal/config.go @@ -2,6 +2,7 @@ package opaal import ( "davidallendj/opaal/internal/oauth" + "davidallendj/opaal/internal/oidc" "log" "os" "path/filepath" @@ -72,11 +73,18 @@ type Config struct { } func NewConfig() Config { - return Config{ + config := Config{ Version: goutil.GetCommit(), Server: server.Server{ Host: "127.0.0.1", Port: 3333, + Issuer: server.IdentityProviderServer{ + Endpoints: oidc.Endpoints{ + Authorization: "http://127.0.0.1/oauth/authorize", + Token: "http://127.0.0.1/oauth/token", + JwksUri: "http://127.0.0.1/.well-known/jwks.json", + }, + }, }, Options: Options{ RunOnce: true, @@ -99,6 +107,7 @@ func NewConfig() Config { }, }, } + return config } func LoadConfig(path string) Config { diff --git a/internal/new.go b/internal/new.go index ce20aee..7e3e35c 100644 --- a/internal/new.go +++ b/internal/new.go @@ -91,8 +91,9 @@ func NewServerWithConfig(conf *Config) *server.Server { Host: host, Port: port, Issuer: server.IdentityProviderServer{ - Host: conf.Server.Issuer.Host, - Port: conf.Server.Issuer.Port, + Host: conf.Server.Issuer.Host, + Port: conf.Server.Issuer.Port, + Endpoints: conf.Server.Issuer.Endpoints, }, } return server diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index f3382fc..206f68f 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -171,18 +171,19 @@ func (p *IdentityProvider) UpdateEndpoints(other *IdentityProvider) { func UpdateEndpoints(eps *Endpoints, other *Endpoints) { // only update endpoints that are not empty - var UpdateIf = func(ep *string, s string) { + var UpdateIfEmpty = func(ep *string, s string) { if ep != nil { - if *ep != "" { + if *ep == "" { *ep = s + fmt.Printf("updated %s\n", s) } } } - UpdateIf(&eps.Config, other.Config) - UpdateIf(&eps.Authorization, other.Authorization) - UpdateIf(&eps.Token, other.Token) - UpdateIf(&eps.Revocation, other.Revocation) - UpdateIf(&eps.Introspection, other.Introspection) - UpdateIf(&eps.UserInfo, other.UserInfo) - UpdateIf(&eps.JwksUri, other.JwksUri) + UpdateIfEmpty(&eps.Config, other.Config) + UpdateIfEmpty(&eps.Authorization, other.Authorization) + UpdateIfEmpty(&eps.Token, other.Token) + UpdateIfEmpty(&eps.Revocation, other.Revocation) + UpdateIfEmpty(&eps.Introspection, other.Introspection) + UpdateIfEmpty(&eps.UserInfo, other.UserInfo) + UpdateIfEmpty(&eps.JwksUri, other.JwksUri) } diff --git a/internal/server/idp.go b/internal/server/idp.go index 4822e91..f763778 100644 --- a/internal/server/idp.go +++ b/internal/server/idp.go @@ -22,7 +22,7 @@ import ( "github.com/lestrrat-go/jwx/v2/jwt" ) -func (s *Server) StartIdentityProvider(eps oidc.Endpoints) error { +func (s *Server) StartIdentityProvider() error { // NOTE: this example does NOT implement CSRF tokens nor use them // create an example identity provider @@ -44,7 +44,7 @@ func (s *Server) StartIdentityProvider(eps oidc.Endpoints) error { Token: "http://" + s.Addr + "/oauth/token", JwksUri: "http://" + s.Addr + "/.well-known/jwks.json", } - oidc.UpdateEndpoints(&eps, &defaultEps) + oidc.UpdateEndpoints(&s.Issuer.Endpoints, &defaultEps) // generate key pair used to sign JWKS and create JWTs privateKey, err := rsa.GenerateKey(rand.Reader, 2048) @@ -84,9 +84,9 @@ func (s *Server) StartIdentityProvider(eps oidc.Endpoints) error { // create config JSON to serve with GET request config := map[string]any{ "issuer": "http://" + s.Addr, - "authorization_endpoint": eps.Authorization, - "token_endpoint": eps.Token, - "jwks_uri": eps.JwksUri, + "authorization_endpoint": s.Issuer.Endpoints.Authorization, + "token_endpoint": s.Issuer.Endpoints.Token, + "jwks_uri": s.Issuer.Endpoints.JwksUri, "scopes_supported": []string{ "openid", "profile", From 2edc624c017b4a55210b9db82b499f6dcbe63fb3 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 30 Apr 2024 12:28:19 -0600 Subject: [PATCH 25/33] Resetted the default IDP endpoint values --- internal/config.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/config.go b/internal/config.go index cb7bef1..9108ac1 100644 --- a/internal/config.go +++ b/internal/config.go @@ -80,9 +80,9 @@ func NewConfig() Config { Port: 3333, Issuer: server.IdentityProviderServer{ Endpoints: oidc.Endpoints{ - Authorization: "http://127.0.0.1/oauth/authorize", - Token: "http://127.0.0.1/oauth/token", - JwksUri: "http://127.0.0.1/.well-known/jwks.json", + Authorization: "", + Token: "", + JwksUri: "", }, }, }, From bc5e6934254e236a0e67192d5c95045ff89ea352 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 30 Apr 2024 12:45:02 -0600 Subject: [PATCH 26/33] Changed the IDP oauth endpoints to oauth2 --- internal/server/idp.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/server/idp.go b/internal/server/idp.go index f763778..35b4051 100644 --- a/internal/server/idp.go +++ b/internal/server/idp.go @@ -173,7 +173,7 @@ func (s *Server) StartIdentityProvider() error { http.Redirect(w, r, "/browser/login", http.StatusUnauthorized) } }) - r.HandleFunc("/oauth/token", func(w http.ResponseWriter, r *http.Request) { + r.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) { r.ParseForm() // check for authorization code and make sure it's valid @@ -247,7 +247,7 @@ func (s *Server) StartIdentityProvider() error { fmt.Printf("bearer: %s\n", string(b)) w.Write(b) }) - r.HandleFunc("/oauth/authorize", func(w http.ResponseWriter, r *http.Request) { + r.HandleFunc("/oauth2/authorize", func(w http.ResponseWriter, r *http.Request) { var ( responseType = r.URL.Query().Get("response_type") clientId = r.URL.Query().Get("client_id") From cbb3e6f851d54c2fb5bc3161dbd0e77e822bc8d7 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 30 Apr 2024 14:43:50 -0600 Subject: [PATCH 27/33] Updated login page hint --- pages/login.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/login.html b/pages/login.html index e55f5cf..c25f8c3 100644 --- a/pages/login.html +++ b/pages/login.html @@ -7,7 +7,7 @@

Forgot Username?
- + \ No newline at end of file From 7022801fe92cd45e86491d61626107e59d433ce3 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 30 Apr 2024 14:44:57 -0600 Subject: [PATCH 28/33] Implemented IDP registered clients and callbacks --- internal/server/idp.go | 42 ++++++++++++++++++++++----------------- internal/server/server.go | 6 ------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/internal/server/idp.go b/internal/server/idp.go index 35b4051..e7fea40 100644 --- a/internal/server/idp.go +++ b/internal/server/idp.go @@ -3,7 +3,6 @@ package server import ( "crypto/rand" "crypto/rsa" - "davidallendj/opaal/internal/oauth" "davidallendj/opaal/internal/oidc" "encoding/json" "fmt" @@ -22,6 +21,22 @@ import ( "github.com/lestrrat-go/jwx/v2/jwt" ) +// TODO: make this a completely separate server +type IdentityProviderServer struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + Endpoints oidc.Endpoints `yaml:"endpoints"` + Clients []RegisteredClient `yaml:"clients"` +} + +// NOTE: could we use a oauth.Client here instead?? +type RegisteredClient struct { + Id string `yaml:"id"` + Secret string `yaml:"secret"` + Name string `yaml:"name"` + RedirectUris []string `yaml:"redirect-uris"` +} + func (s *Server) StartIdentityProvider() error { // NOTE: this example does NOT implement CSRF tokens nor use them @@ -29,15 +44,9 @@ func (s *Server) StartIdentityProvider() error { var ( r = chi.NewRouter() // clients = []oauth.Client{} - callback = "" activeCodes = []string{} ) - // check if callback is set - if s.Callback == "" { - callback = "/oidc/callback" - } - // update endpoints that have values set defaultEps := oidc.Endpoints{ Authorization: "http://" + s.Addr + "/oauth/authorize", @@ -138,21 +147,18 @@ func (s *Server) StartIdentityProvider() error { username := r.Form.Get("username") password := r.Form.Get("password") + if len(s.Issuer.Clients) <= 0 { + fmt.Printf("no registered clients found with identity provider (add them in config)\n") + return + } + // example username and password so do simplified authorization code flow - if username == "ochami" && password == "ochami" { - client := oauth.Client{ - Id: "ochami", - Secret: "ochami", - Name: "ochami", - Provider: oidc.IdentityProvider{ - Issuer: "http://127.0.0.1:3333", - }, - RedirectUris: []string{fmt.Sprintf("http://%s:%d%s", s.Host, s.Port, callback)}, - } + if username == "openchami" && password == "openchami" { + client := s.Issuer.Clients[0] // check if there are any redirect URIs supplied if len(client.RedirectUris) <= 0 { - fmt.Printf("no redirect URIs found") + fmt.Printf("no redirect URIs found for client %s (ID: %s)\n", client.Name, client.Id) return } for _, url := range client.RedirectUris { diff --git a/internal/server/server.go b/internal/server/server.go index c829a34..30eac4d 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -25,12 +25,6 @@ type Server struct { Issuer IdentityProviderServer `yaml:"issuer"` } -type IdentityProviderServer struct { - Host string `yaml:"host"` - Port int `yaml:"port"` - Endpoints oidc.Endpoints `yaml:"endpoints"` -} - type ServerParams struct { AuthProvider *oidc.IdentityProvider Verbose bool From e929fac09e7af89ab639180ccdf68296543cc6cd Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 30 Apr 2024 16:03:23 -0600 Subject: [PATCH 29/33] Fixed some minor issues --- internal/new.go | 1 + internal/oidc/oidc.go | 1 - internal/server/idp.go | 14 +++++++++----- internal/server/server.go | 9 +++++---- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/internal/new.go b/internal/new.go index 7e3e35c..2799d88 100644 --- a/internal/new.go +++ b/internal/new.go @@ -94,6 +94,7 @@ func NewServerWithConfig(conf *Config) *server.Server { Host: conf.Server.Issuer.Host, Port: conf.Server.Issuer.Port, Endpoints: conf.Server.Issuer.Endpoints, + Clients: conf.Server.Issuer.Clients, }, } return server diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index 206f68f..f52e0c4 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -175,7 +175,6 @@ func UpdateEndpoints(eps *Endpoints, other *Endpoints) { if ep != nil { if *ep == "" { *ep = s - fmt.Printf("updated %s\n", s) } } } diff --git a/internal/server/idp.go b/internal/server/idp.go index e7fea40..bb82a28 100644 --- a/internal/server/idp.go +++ b/internal/server/idp.go @@ -49,8 +49,8 @@ func (s *Server) StartIdentityProvider() error { // update endpoints that have values set defaultEps := oidc.Endpoints{ - Authorization: "http://" + s.Addr + "/oauth/authorize", - Token: "http://" + s.Addr + "/oauth/token", + Authorization: "http://" + s.Addr + "/oauth2/authorize", + Token: "http://" + s.Addr + "/oauth2/token", JwksUri: "http://" + s.Addr + "/.well-known/jwks.json", } oidc.UpdateEndpoints(&s.Issuer.Endpoints, &defaultEps) @@ -266,9 +266,13 @@ func (s *Server) StartIdentityProvider() error { return } - // check that we're using the default registered client - if clientId != "ochami" { - fmt.Printf("invalid client\n") + // find a valid client + index := slices.IndexFunc(s.Issuer.Clients, func(c RegisteredClient) bool { + fmt.Printf("%s ? %s\n", c.Id, clientId) + return c.Id == clientId + }) + if index < 0 { + fmt.Printf("no valid client found") return } diff --git a/internal/server/server.go b/internal/server/server.go index 30eac4d..16b7b51 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -57,7 +57,7 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error { // make the login page SSO buttons and authorization URLs to write to stdout buttons := "" - fmt.Printf("Login with external identity providers: \n") + fmt.Printf("Login with an identity provider: \n") for i, client := range clients { // fetch provider configuration before adding button p, err := oidc.FetchServerConfig(client.Provider.Issuer) @@ -74,8 +74,7 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error { clients[i].Provider = *p buttons += makeButton(fmt.Sprintf("/login?sso=%s", client.Id), client.Name) - url := client.BuildAuthorizationUrl(s.State) - fmt.Printf("\t%s\n", url) + fmt.Printf("\t%s: /login?sso=%s\n", client.Name, client.Id) } var code string @@ -115,7 +114,9 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error { client = &clients[index] url := client.BuildAuthorizationUrl(s.State) - fmt.Printf("Redirect URL: %s\n", url) + if params.Verbose { + fmt.Printf("Redirect URL: %s\n", url) + } http.Redirect(w, r, url, http.StatusFound) return } From b304361ce9085665af0bcbf4ac2a6b1f0e75cab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiziano=20M=C3=BCller?= Date: Fri, 24 May 2024 16:34:38 +0200 Subject: [PATCH 30/33] server: fix error reporting and logic for /keys handler restores proper error reporting to include the host dialed, and fixes the tautological comparison `jwks == nil` in the recovery path to properly refetch the server config and try again as intended --- internal/server/server.go | 49 +++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index 16b7b51..3fdae97 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -141,38 +141,47 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error { p = params.AuthProvider jwks []byte ) - // try and get the JWKS from param first - if p.Endpoints.JwksUri != "" { - err := p.FetchJwks() + + fetchAndMarshal := func() (err error) { + err = p.FetchJwks() if err != nil { - fmt.Printf("failed to fetch keys using JWKS url...trying to fetch config and try again...\n") + fmt.Printf("failed to fetch keys: %v\n", err) + return } jwks, err = json.Marshal(p.KeySet) if err != nil { fmt.Printf("failed to marshal JWKS: %v\n", err) } - } else if p.Endpoints.Config != "" && jwks == nil { - // otherwise, try and fetch the whole config and try again - err := p.FetchServerConfig() - if err != nil { - fmt.Printf("failed to fetch server config: %v\n", err) - http.Redirect(w, r, "/error", http.StatusInternalServerError) - return - } - err = p.FetchJwks() - if err != nil { - fmt.Printf("failed to fetch JWKS after fetching server config: %v\n", err) - http.Redirect(w, r, "/error", http.StatusInternalServerError) + return + } + + // try and get the JWKS from param first + if p.Endpoints.JwksUri != "" { + if err := fetchAndMarshal(); err != nil { + w.Write(jwks) return } } - // forward the JWKS from the authorization server - if jwks == nil { - fmt.Printf("no JWKS was fetched from authorization server\n") - http.Redirect(w, r, "/error", http.StatusInternalServerError) + // otherwise or if fetching the JWKS failed, try and fetch the whole config first and try again + if p.Endpoints.Config != "" { + if err := p.FetchServerConfig(); err != nil { + fmt.Printf("failed to fetch server config: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + } else { + fmt.Printf("getting JWKS from param failed and endpoints config unavailable\n") + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } + + if err := fetchAndMarshal(); err != nil { + fmt.Printf("failed to fetch and marshal JWKS after config update: %v\n", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + w.Write(jwks) }) r.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { From 8c01ba897ff72c81cc49b8ced6f1f310c1b930a1 Mon Sep 17 00:00:00 2001 From: David Allen Date: Wed, 12 Jun 2024 12:50:53 -0600 Subject: [PATCH 31/33] Added verbose print to show ID and access tokens from IDP --- internal/flows/jwt_bearer.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/flows/jwt_bearer.go b/internal/flows/jwt_bearer.go index 2e93265..a0287d9 100644 --- a/internal/flows/jwt_bearer.go +++ b/internal/flows/jwt_bearer.go @@ -51,6 +51,9 @@ func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (s if client == nil { return "", fmt.Errorf("invalid client (client is nil)") } + if verbose { + fmt.Printf("ID token (IDP): %s\n access token (IDP): %s", accessToken, idToken) + } if accessToken != "" { _, err := jws.Verify([]byte(accessToken), jws.WithKeySet(client.Provider.KeySet), jws.WithValidateKey(true)) if err != nil { From a7e0e73e4560a979151a0c57037034c64b61810b Mon Sep 17 00:00:00 2001 From: David Allen Date: Wed, 12 Jun 2024 14:01:24 -0600 Subject: [PATCH 32/33] Added response body print to debug ID token --- internal/oauth/authenticate.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/oauth/authenticate.go b/internal/oauth/authenticate.go index b579e8e..175aad1 100644 --- a/internal/oauth/authenticate.go +++ b/internal/oauth/authenticate.go @@ -109,12 +109,14 @@ func (client *Client) FetchTokenFromAuthenticationServer(code string, state stri } res, err := http.PostForm(client.Provider.Endpoints.Token, body) if err != nil { - return nil, fmt.Errorf("failed to get ID token: %s", err) + return nil, fmt.Errorf("failed to get ID token: %v", err) } + b, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %v", err) + } + fmt.Printf("%s\n", string(b)) defer res.Body.Close() - // domain, _ := url.Parse("http://127.0.0.1") - // client.Jar.SetCookies(domain, res.Cookies()) - return io.ReadAll(res.Body) } From e0a8d434211c29be3b4808e3dfcedaa2a713bf8a Mon Sep 17 00:00:00 2001 From: David Allen Date: Wed, 12 Jun 2024 14:42:47 -0600 Subject: [PATCH 33/33] Fixed token fetch from IDP --- internal/oauth/authenticate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/oauth/authenticate.go b/internal/oauth/authenticate.go index 175aad1..4af65cb 100644 --- a/internal/oauth/authenticate.go +++ b/internal/oauth/authenticate.go @@ -118,5 +118,5 @@ func (client *Client) FetchTokenFromAuthenticationServer(code string, state stri fmt.Printf("%s\n", string(b)) defer res.Body.Close() - return io.ReadAll(res.Body) + return b, nil }