From 681cfc4f598c13d469c8ce935ad39b13cf47416e Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 27 Feb 2024 08:11:48 -0700 Subject: [PATCH 01/26] Enabled dynamic registration in Hydra config --- docker/configs/hydra/hydra.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/configs/hydra/hydra.yml b/docker/configs/hydra/hydra.yml index 06772f6..765cebb 100644 --- a/docker/configs/hydra/hydra.yml +++ b/docker/configs/hydra/hydra.yml @@ -18,6 +18,8 @@ secrets: - youReallyNeedToChangeThis oidc: + dynamic_client_registration: + enabled: true subject_identifiers: supported_types: - pairwise From 3b7819c7f66a4d8bc97ed69d6003e4a98a6547c3 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 27 Feb 2024 08:12:28 -0700 Subject: [PATCH 02/26] Removed audience from RegisterOAuthClient for teseting --- internal/client.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/client.go b/internal/client.go index 781151c..d08d352 100644 --- a/internal/client.go +++ b/internal/client.go @@ -62,6 +62,9 @@ func (client *Client) InitiateLoginFlow(loginUrl string) error { // get the flow ID from response body, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %v", err) + } var flowData map[string]any err = json.Unmarshal(body, &flowData) @@ -218,8 +221,10 @@ func (client *Client) RegisterOAuthClient(registerUrl string, audience []string) "scope": "openid email profile", "grant_types": ["client_credentials", "urn:ietf:params:oauth:grant-type:jwt-bearer"], "response_types": ["token"], - "audience": [%s] - }`, client.Id, client.Secret, strings.Join(audience, ","))) + }`, client.Id, client.Secret, + // strings.Join(audience, ",") + )) + // "audience": [%s] req, err := http.NewRequest("POST", registerUrl, bytes.NewBuffer(data)) if err != nil { From 2be7c3123f95fc0114853fc326bb4183edb41e21 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 27 Feb 2024 08:49:54 -0700 Subject: [PATCH 03/26] Changed 'Use' in root cmd --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index ad2cbbd..156b29c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -14,7 +14,7 @@ var ( config opaal.Config ) var rootCmd = &cobra.Command{ - Use: "oidc", + Use: "opaal", Short: "An experimental OIDC helper tool for handling logins", Run: func(cmd *cobra.Command, args []string) { From 1e5982763ff4795fc479b1dd675c75a8d3b175fe Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 27 Feb 2024 19:45:06 -0700 Subject: [PATCH 04/26] Removed utils in favor of common utils --- cmd/config.go | 3 +- cmd/root.go | 2 +- config.yaml | 27 +++++++++++ go.mod | 3 +- go.sum | 2 + internal/client.go | 12 +++-- internal/config.go | 9 ++-- internal/login.go | 3 +- internal/util/util.go | 109 ------------------------------------------ 9 files changed, 51 insertions(+), 119 deletions(-) create mode 100755 config.yaml delete mode 100644 internal/util/util.go diff --git a/cmd/config.go b/cmd/config.go index 6f9ddbc..75068a5 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -2,9 +2,10 @@ package cmd import ( opaal "davidallendj/opaal/internal" - "davidallendj/opaal/internal/util" "fmt" + "github.com/davidallendj/go-utils/util" + "github.com/spf13/cobra" ) diff --git a/cmd/root.go b/cmd/root.go index 156b29c..c1fb355 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,10 +2,10 @@ package cmd import ( opaal "davidallendj/opaal/internal" - "davidallendj/opaal/internal/util" "fmt" "os" + "github.com/davidallendj/go-utils/util" "github.com/spf13/cobra" ) diff --git a/config.yaml b/config.yaml new file mode 100755 index 0000000..992ac3b --- /dev/null +++ b/config.yaml @@ -0,0 +1,27 @@ +server: + host: 127.0.0.1 + port: 3333 +client: + id: 7527e7b4-c96a-4df0-8fc5-00fde18bb65d + secret: gto_cc5uvpb5lsdczkwnbarvwmbpv5kcjwg7nhbc75zt65yrfh2ldenq + redirect-uris: + - "http://127.0.0.1:3333/oidc/callback" +oidc: + issuer: "http://git.towk.local:3000/" +urls: + #identities: http://127.0.0.1:4434/admin/identities + trusted-issuers: http://127.0.0.1:4445/admin/trust/grants/jwt-bearer/issuers + access-token: http://127.0.0.1:4444/oauth2/token + server-config: http://git.towk.local:3000/.well-known/openid-configuration + jwks_uri: http://git.towk.local:3000/login/oauth/keys + login: http://127.0.0.1:4433/self-service/login/api + login-flow-id: http://127.0.0.1:4433/self-service/login/flows?id={id} +state: "" +response-type: code +decode-id-token: true +decode-access-token: true +run-once: true +scope: +- openid +- profile +- email diff --git a/go.mod b/go.mod index e459e10..f9921a3 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module davidallendj/opaal go 1.22.0 require ( + github.com/davidallendj/go-utils v0.0.0-20240228023108-a3401c328af8 github.com/go-chi/chi v1.5.5 github.com/go-chi/chi/v5 v5.0.12 - github.com/golang-jwt/jwt v3.2.2+incompatible github.com/lestrrat-go/jwx v1.2.28 github.com/spf13/cobra v1.8.0 golang.org/x/net v0.10.0 @@ -15,6 +15,7 @@ require ( require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect diff --git a/go.sum b/go.sum index d7a787d..2ae2491 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t 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-20240228023108-a3401c328af8 h1:2TS/l1tLIJ5Qwh3owCM9kxvnGC186dZK9FllgmlDTXo= +github.com/davidallendj/go-utils v0.0.0-20240228023108-a3401c328af8/go.mod h1:dOlDYHzYfiz3Skh133YHRm9IiKxBdkMkvXip54057x8= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 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= diff --git a/internal/client.go b/internal/client.go index d08d352..355207e 100644 --- a/internal/client.go +++ b/internal/client.go @@ -3,7 +3,6 @@ package opaal import ( "bytes" "davidallendj/opaal/internal/oidc" - "davidallendj/opaal/internal/util" "encoding/json" "fmt" "io" @@ -13,6 +12,7 @@ import ( "strings" "time" + "github.com/davidallendj/go-utils/util" "golang.org/x/net/publicsuffix" ) @@ -41,7 +41,7 @@ func (client *Client) IsFlowInitiated() bool { func (client *Client) BuildAuthorizationUrl(authEndpoint string, state string, responseType string, scope []string) string { return authEndpoint + "?" + "client_id=" + client.Id + - "&redirect_uri=" + util.URLEscape(strings.Join(client.RedirectUris, ",")) + + "&redirect_uri=" + url.QueryEscape(strings.Join(client.RedirectUris, ",")) + "&response_type=" + responseType + "&state=" + state + "&scope=" + strings.Join(scope, "+") + @@ -148,7 +148,7 @@ func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl func (client *Client) FetchTokenFromAuthorizationServer(remoteUrl string, jwt string, scope []string) ([]byte, error) { // hydra endpoint: /oauth/token - data := "grant_type=" + util.URLEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") + + data := "grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") + "&client_id=" + client.Id + "&client_secret=" + client.Secret + "&scope=" + strings.Join(scope, "+") + @@ -211,6 +211,12 @@ func (client *Client) AddTrustedIssuer(remoteUrl string, idp *oidc.IdentityProvi return io.ReadAll(res.Body) } +func (client *Client) AuthorizeClient(authorizeUrl string) ([]byte, error) { + bytes := []byte{} + + return bytes, nil +} + func (client *Client) RegisterOAuthClient(registerUrl string, audience []string) ([]byte, error) { // hydra endpoint: POST /clients audience = util.QuoteArrayStrings(audience) diff --git a/internal/config.go b/internal/config.go index b9c3946..d887ed9 100644 --- a/internal/config.go +++ b/internal/config.go @@ -2,11 +2,12 @@ package opaal import ( "davidallendj/opaal/internal/oidc" - "davidallendj/opaal/internal/util" "log" "os" "path/filepath" + goutil "github.com/davidallendj/go-utils/util" + "gopkg.in/yaml.v2" ) @@ -23,11 +24,12 @@ type Config struct { DecodeIdToken bool `yaml:"decode-id-token"` DecodeAccessToken bool `yaml:"decode-access-token"` RunOnce bool `yaml:"run-once"` + GrantType string `yaml:"grant-type"` } func NewConfig() Config { return Config{ - Version: util.GetCommit(), + Version: goutil.GetCommit(), Server: Server{ Host: "127.0.0.1", Port: 3333, @@ -38,7 +40,7 @@ func NewConfig() Config { RedirectUris: []string{""}, }, IdentityProvider: *oidc.NewIdentityProvider(), - State: util.RandomString(20), + State: goutil.RandomString(20), ResponseType: "code", Scope: []string{"openid", "profile", "email"}, ActionUrls: ActionUrls{ @@ -51,6 +53,7 @@ func NewConfig() Config { DecodeIdToken: false, DecodeAccessToken: false, RunOnce: true, + GrantType: "authorization_code", } } diff --git a/internal/login.go b/internal/login.go index 0beb0c5..048b262 100644 --- a/internal/login.go +++ b/internal/login.go @@ -2,13 +2,14 @@ package opaal import ( "davidallendj/opaal/internal/oidc" - "davidallendj/opaal/internal/util" "encoding/json" "errors" "fmt" "net/http" "reflect" "time" + + "github.com/davidallendj/go-utils/util" ) func Login(config *Config) error { diff --git a/internal/util/util.go b/internal/util/util.go deleted file mode 100644 index 4ba8156..0000000 --- a/internal/util/util.go +++ /dev/null @@ -1,109 +0,0 @@ -package util - -import ( - "encoding/base64" - "math/rand" - "net/url" - "os" - "os/exec" - "runtime" - "strings" - - "github.com/golang-jwt/jwt" -) - -func RandomString(n int) string { - const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - const ( - letterIdxBits = 6 // 6 bits to represent a letter index - letterIdxMask = 1<= 0; { - if remain == 0 { - cache, remain = rand.Int63(), letterIdxMax - } - if idx := int(cache & letterIdxMask); idx < len(letterBytes) { - b[i] = letterBytes[idx] - i-- - } - cache >>= letterIdxBits - remain-- - } - - return string(b) -} - -func URLEscape(s string) string { - return url.QueryEscape(s) -} - -func EncodeBase64(s string) string { - return base64.StdEncoding.EncodeToString([]byte(s)) -} - -func DecodeJwt(encoded string) ([][]byte, error) { - // split the string into 3 segments and decode - segments := strings.Split(encoded, ".") - decoded := [][]byte{} - for _, segment := range segments { - bytes, _ := jwt.DecodeSegment(segment) - decoded = append(decoded, bytes) - } - return decoded, nil -} - -func PathExists(path string) (bool, error) { - _, err := os.Stat(path) - if err == nil { - return true, nil - } - if os.IsNotExist(err) { - return false, nil - } - return false, err -} - -// https://stackoverflow.com/questions/39320371/how-start-web-server-to-open-page-in-browser-in-golang -// open opens the specified URL in the default browser of the user. -func OpenUrl(url string) error { - var cmd string - var args []string - - switch runtime.GOOS { - case "windows": - cmd = "cmd" - args = []string{"/c", "start"} - case "darwin": - cmd = "open" - default: // "linux", "freebsd", "openbsd", "netbsd" - cmd = "xdg-open" - } - args = append(args, url) - return exec.Command(cmd, args...).Start() -} - -func GetCommit() string { - bytes, err := exec.Command("git", "rev --parse HEAD").Output() - if err != nil { - return "" - } - return string(bytes) -} - -func Tokenize(s string) map[string]any { - tokens := make(map[string]any) - - // find token enclosed in curly brackets - - return tokens -} - -func QuoteArrayStrings(arr []string) []string { - for i, v := range arr { - arr[i] = "\"" + v + "\"" - } - return arr -} From b447b0e149790a85eac4cbbc427a0abf56d83732 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 27 Feb 2024 20:43:10 -0700 Subject: [PATCH 05/26] Updated imports and dependencies --- cmd/config.go | 4 ++-- cmd/root.go | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 75068a5..52ff313 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -4,7 +4,7 @@ import ( opaal "davidallendj/opaal/internal" "fmt" - "github.com/davidallendj/go-utils/util" + "github.com/davidallendj/go-utils/pathx" "github.com/spf13/cobra" ) @@ -16,7 +16,7 @@ var configCmd = &cobra.Command{ // create a new config at all args (paths) for _, path := range args { // check and make sure something doesn't exist first - if exists, err := util.PathExists(path); exists || err != nil { + if exists, err := pathx.PathExists(path); exists || err != nil { fmt.Printf("file or directory exists\n") continue } diff --git a/cmd/root.go b/cmd/root.go index c1fb355..e6b5245 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,7 +5,7 @@ import ( "fmt" "os" - "github.com/davidallendj/go-utils/util" + "github.com/davidallendj/go-utils/pathx" "github.com/spf13/cobra" ) @@ -24,7 +24,7 @@ var rootCmd = &cobra.Command{ func initConfig() { // load config if found or create a new one if configPath != "" { - exists, err := util.PathExists(configPath) + exists, err := pathx.PathExists(configPath) if err != nil { fmt.Printf("failed to load config") os.Exit(1) diff --git a/go.mod b/go.mod index f9921a3..4baf6c6 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-20240228023108-a3401c328af8 + github.com/davidallendj/go-utils v0.0.0-20240228033512-fd54787e0e90 github.com/go-chi/chi v1.5.5 github.com/go-chi/chi/v5 v5.0.12 github.com/lestrrat-go/jwx v1.2.28 diff --git a/go.sum b/go.sum index 2ae2491..4fd2d0f 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t 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-20240228023108-a3401c328af8 h1:2TS/l1tLIJ5Qwh3owCM9kxvnGC186dZK9FllgmlDTXo= -github.com/davidallendj/go-utils v0.0.0-20240228023108-a3401c328af8/go.mod h1:dOlDYHzYfiz3Skh133YHRm9IiKxBdkMkvXip54057x8= +github.com/davidallendj/go-utils v0.0.0-20240228033512-fd54787e0e90 h1:WFcUklDav28/vMjXTwGj9fiH953LySvu1WoNY1YqaPU= +github.com/davidallendj/go-utils v0.0.0-20240228033512-fd54787e0e90/go.mod h1:m86DMxVAMSau3PdVCkBh56m6HIxzTDvR+opoX/gu9/A= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 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= From f912890a2dcaeba95203b2eba167df3e3b4e7fda Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 27 Feb 2024 20:43:37 -0700 Subject: [PATCH 06/26] Added AuthorizedClient for client credentials grant --- internal/client.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/internal/client.go b/internal/client.go index 355207e..a0324df 100644 --- a/internal/client.go +++ b/internal/client.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/davidallendj/go-utils/httpx" "github.com/davidallendj/go-utils/util" "golang.org/x/net/publicsuffix" ) @@ -212,9 +213,24 @@ func (client *Client) AddTrustedIssuer(remoteUrl string, idp *oidc.IdentityProvi } func (client *Client) AuthorizeClient(authorizeUrl string) ([]byte, error) { - bytes := []byte{} + // encode ID and secret for authorization header basic authentication + basicAuth := util.EncodeBase64( + fmt.Sprintf("%s:%s", + url.QueryEscape(client.Id), + url.QueryEscape(client.Secret), + ), + ) + body := httpx.Body("grant_type=client_credentials&scope=read") + headers := httpx.Headers{ + "Authorization": basicAuth, + "Content-Type": "application/x-www-form-urlencoded", + } + _, b, err := httpx.MakeHTTPRequest(authorizeUrl, http.MethodPost, body, headers) + if err != nil { + return nil, fmt.Errorf("failed to make HTTP request: %v", err) + } - return bytes, nil + return b, nil } func (client *Client) RegisterOAuthClient(registerUrl string, audience []string) ([]byte, error) { From f490eb4fc4d58d89e04f83cc5a14b010486e0b32 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Thu, 29 Feb 2024 20:14:53 -0700 Subject: [PATCH 07/26] Refactored and added client credentials flow --- cmd/login.go | 5 ++-- cmd/root.go | 24 +++++++-------- config.yaml | 2 ++ internal/client.go | 22 ++++++++++---- internal/config.go | 13 ++++++--- .../{login.go => flows/authorization_code.go} | 15 +++------- internal/flows/client_credentials.go | 29 +++++++++++++++++++ internal/flows/login.go | 28 ++++++++++++++++++ internal/opaal.go | 16 +++++----- 9 files changed, 113 insertions(+), 41 deletions(-) rename internal/{login.go => flows/authorization_code.go} (96%) create mode 100644 internal/flows/client_credentials.go create mode 100644 internal/flows/login.go diff --git a/cmd/login.go b/cmd/login.go index 1817a7c..43cdd36 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -1,7 +1,7 @@ package cmd import ( - opaal "davidallendj/opaal/internal" + "davidallendj/opaal/internal/flows" "fmt" "os" @@ -13,7 +13,7 @@ var loginCmd = &cobra.Command{ Short: "Start the login flow", Run: func(cmd *cobra.Command, args []string) { for { - err := opaal.Login(&config) + err := flows.Login(&config) if err != nil { fmt.Printf("%v\n", err) os.Exit(1) @@ -37,5 +37,6 @@ func init() { loginCmd.Flags().BoolVar(&config.DecodeIdToken, "decode-id-token", config.DecodeIdToken, "decode and print ID token from identity provider") loginCmd.Flags().BoolVar(&config.DecodeAccessToken, "decore-access-token", config.DecodeAccessToken, "decode and print access token from authorization server") loginCmd.Flags().BoolVar(&config.RunOnce, "once", config.RunOnce, "set whether to run login once and exit") + loginCmd.Flags().StringVar(&config.GrantType, "grant-type", config.GrantType, "set the grant-type/authorization flow") rootCmd.AddCommand(loginCmd) } diff --git a/cmd/root.go b/cmd/root.go index e6b5245..eace300 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -21,6 +21,18 @@ var rootCmd = &cobra.Command{ }, } +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "failed to start CLI: %s", err) + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + rootCmd.PersistentFlags().StringVar(&configPath, "config", "", "set the config path") +} + func initConfig() { // load config if found or create a new one if configPath != "" { @@ -35,15 +47,3 @@ func initConfig() { } } } - -func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Fprintf(os.Stderr, "failed to start CLI: %s", err) - os.Exit(1) - } -} - -func init() { - cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().StringVar(&configPath, "config", "", "set the config path") -} diff --git a/config.yaml b/config.yaml index 992ac3b..c2530ec 100755 --- a/config.yaml +++ b/config.yaml @@ -16,6 +16,8 @@ urls: jwks_uri: http://git.towk.local:3000/login/oauth/keys login: http://127.0.0.1:4433/self-service/login/api login-flow-id: http://127.0.0.1:4433/self-service/login/flows?id={id} + register-client: http://127.0.0.1:4445/clients + authorize-client: http://127.0.0.1:4444/oauth2/authorize state: "" response-type: code decode-id-token: true diff --git a/internal/client.go b/internal/client.go index a0324df..48bb689 100644 --- a/internal/client.go +++ b/internal/client.go @@ -129,12 +129,17 @@ func (client *Client) FetchCSRFToken(flowUrl string) error { func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl string, state string) ([]byte, error) { data := url.Values{ "grant_type": {"authorization_code"}, - "code": {code}, "client_id": {client.Id}, "client_secret": {client.Secret}, - "state": {state}, "redirect_uri": {strings.Join(client.RedirectUris, ",")}, } + // add optional params if valid + if code != "" { + data["code"] = []string{code} + } + if state != "" { + data["state"] = []string{state} + } res, err := http.PostForm(remoteUrl, data) if err != nil { return nil, fmt.Errorf("failed to get ID token: %s", err) @@ -151,9 +156,16 @@ func (client *Client) FetchTokenFromAuthorizationServer(remoteUrl string, jwt st // hydra endpoint: /oauth/token data := "grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") + "&client_id=" + client.Id + - "&client_secret=" + client.Secret + - "&scope=" + strings.Join(scope, "+") + - "&assertion=" + jwt + "&client_secret=" + client.Secret + + // add optional params if valid + if jwt != "" { + data += "&assertion=" + jwt + } + if scope != nil || len(scope) > 0 { + data += "&scope=" + strings.Join(scope, "+") + } + fmt.Printf("encoded params: %v\n\n", data) req, err := http.NewRequest("POST", remoteUrl, bytes.NewBuffer([]byte(data))) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") diff --git a/internal/config.go b/internal/config.go index d887ed9..f0f5d49 100644 --- a/internal/config.go +++ b/internal/config.go @@ -44,10 +44,15 @@ func NewConfig() Config { ResponseType: "code", Scope: []string{"openid", "profile", "email"}, ActionUrls: ActionUrls{ - Identities: "", - AccessToken: "", - TrustedIssuers: "", - ServerConfig: "", + Identities: "", + AccessToken: "", + TrustedIssuers: "", + ServerConfig: "", + JwksUri: "", + Login: "", + LoginFlowId: "", + RegisterClient: "", + AuthorizeClient: "", }, OpenBrowser: false, DecodeIdToken: false, diff --git a/internal/login.go b/internal/flows/authorization_code.go similarity index 96% rename from internal/login.go rename to internal/flows/authorization_code.go index 048b262..cf5d5a3 100644 --- a/internal/login.go +++ b/internal/flows/authorization_code.go @@ -1,6 +1,7 @@ -package opaal +package flows import ( + opaal "davidallendj/opaal/internal" "davidallendj/opaal/internal/oidc" "encoding/json" "errors" @@ -12,15 +13,7 @@ import ( "github.com/davidallendj/go-utils/util" ) -func Login(config *Config) error { - if config == nil { - return fmt.Errorf("config is not valid") - } - - // initialize client that will be used throughout login flow - server := NewServerWithConfig(config) - client := NewClientWithConfig(config) - +func AuthorizationCode(config *opaal.Config, server *opaal.Server, client *opaal.Client) error { // initiate the login flow and get a flow ID and CSRF token { err := client.InitiateLoginFlow(config.ActionUrls.Login) @@ -49,7 +42,7 @@ func Login(config *Config) error { } // check if all appropriate parameters are set in config - if !HasRequiredParams(config) { + if !opaal.HasRequiredParams(config) { return fmt.Errorf("client ID must be set") } diff --git a/internal/flows/client_credentials.go b/internal/flows/client_credentials.go new file mode 100644 index 0000000..6c59037 --- /dev/null +++ b/internal/flows/client_credentials.go @@ -0,0 +1,29 @@ +package flows + +import ( + opaal "davidallendj/opaal/internal" + "fmt" +) + +func ClientCredentials(config *opaal.Config, server *opaal.Server, client *opaal.Client) error { + // register a new OAuth 2 client with authorization srever + _, err := client.RegisterOAuthClient(config.ActionUrls.RegisterClient, nil) + if err != nil { + return fmt.Errorf("failed to register OAuth client: %v", err) + } + + // authorize the client + _, err = client.AuthorizeClient(config.ActionUrls.AuthorizeClient) + if err != nil { + return fmt.Errorf("failed to authorize client: %v", err) + } + + // request a token from the authorization server + res, err := client.FetchTokenFromAuthorizationServer(config.ActionUrls.AccessToken, "", nil) + if err != nil { + return fmt.Errorf("failed to fetch token from authorization server: %v", err) + } + + fmt.Printf("token: %v\n", string(res)) + return nil +} diff --git a/internal/flows/login.go b/internal/flows/login.go new file mode 100644 index 0000000..1ee953b --- /dev/null +++ b/internal/flows/login.go @@ -0,0 +1,28 @@ +package flows + +import ( + opaal "davidallendj/opaal/internal" + "fmt" +) + +func Login(config *opaal.Config) error { + if config == nil { + return fmt.Errorf("config is not valid") + } + + // initialize client that will be used throughout login flow + server := opaal.NewServerWithConfig(config) + client := opaal.NewClientWithConfig(config) + + fmt.Printf("grant type: %v\n", config.GrantType) + + if config.GrantType == "authorization_code" { + AuthorizationCode(config, server, client) + } else if config.GrantType == "client_credentials" { + ClientCredentials(config, server, client) + } else { + return fmt.Errorf("invalid grant type") + } + + return nil +} diff --git a/internal/opaal.go b/internal/opaal.go index 67dcf81..ee9cb11 100644 --- a/internal/opaal.go +++ b/internal/opaal.go @@ -1,13 +1,15 @@ package opaal type ActionUrls struct { - Identities string `yaml:"identities"` - TrustedIssuers string `yaml:"trusted-issuers"` - AccessToken string `yaml:"access-token"` - ServerConfig string `yaml:"server-config"` - JwksUri string `yaml:"jwks_uri"` - Login string `yaml:"login"` - LoginFlowId string `yaml:"login-flow-id"` + Identities string `yaml:"identities"` + TrustedIssuers string `yaml:"trusted-issuers"` + AccessToken string `yaml:"access-token"` + ServerConfig string `yaml:"server-config"` + JwksUri string `yaml:"jwks_uri"` + Login string `yaml:"login"` + LoginFlowId string `yaml:"login-flow-id"` + RegisterClient string `yaml:"register-client"` + AuthorizeClient string `yaml:"authorize-client"` } func HasRequiredParams(config *Config) bool { From e24610f412f52ad1d9ae939cbed0a8bbc308d531 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Sun, 3 Mar 2024 18:17:48 -0700 Subject: [PATCH 08/26] Updated README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b8a0e49..47f2b74 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # OIDC Provider Authentication/Authorization Login (OPAAL) -This is a small, simple, experimental OIDC login helper tool that automates the authorization code lohin flow defined by [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1) for social sign-in with identity providers (IdP) like Google, Facebook, or GitHub. This tool is made to work when your identity provider is separate from your authorization server, and we only need the IdP to receive an ID token. In this document, the identity provider (or authentication server) is strictly the OIDC implementation that identifies the resource owner (ID token) whereas the resource provider (or authorization server) is the OIDC implementation that grants access to a resource (access token). This tool is tested with Ory Kratos and Hydra for user identity and session management and OAuth2/OIDC implementation respectively. +This is a small, simple, experimental OIDC login helper tool that automates the authorization flows defined by [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1) for social sign-in with identity providers (IdP) like Google, Facebook, or GitHub. This tool is made to work when your identity provider is separate from your authorization server, and we only need the IdP to receive an ID token. In this document, the identity provider (or authentication server) is strictly the OIDC implementation that identifies the resource owner (ID token) whereas the resource provider (or authorization server) is the OIDC implementation that grants access to a resource (access token). OPAAL assumes that the authentication server is external and the authorization server is owned. This tool is tested with Ory Kratos and Hydra for user identity and session management and OAuth2/OIDC implementation respectively. -Note: This tool acts as an OAuth client, contains client secrets, and is not to be exposed to users! +Note: This tool acts as an OAuth client, contains client secrets, and should not to be exposed to users! It would probably also be a good idea to use a reverse proxy and firewall to protect admin endpoints. ## Build and Usage From 781702cb8d424e74a337e5c290b275401a83087f Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Sun, 3 Mar 2024 18:19:04 -0700 Subject: [PATCH 09/26] Updated go.* files --- go.mod | 8 +++++--- go.sum | 20 ++++++++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 4baf6c6..26682a8 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,13 @@ module davidallendj/opaal go 1.22.0 require ( - github.com/davidallendj/go-utils v0.0.0-20240228033512-fd54787e0e90 + github.com/davidallendj/go-utils v0.0.0-20240302194916-fe292bcf24a4 github.com/go-chi/chi v1.5.5 github.com/go-chi/chi/v5 v5.0.12 + github.com/jmoiron/sqlx v1.3.5 github.com/lestrrat-go/jwx v1.2.28 github.com/spf13/cobra v1.8.0 - golang.org/x/net v0.10.0 + golang.org/x/net v0.21.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -24,5 +25,6 @@ require ( github.com/lestrrat-go/option v1.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/crypto v0.17.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect ) diff --git a/go.sum b/go.sum index 4fd2d0f..60a1152 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t 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-20240228033512-fd54787e0e90 h1:WFcUklDav28/vMjXTwGj9fiH953LySvu1WoNY1YqaPU= -github.com/davidallendj/go-utils v0.0.0-20240228033512-fd54787e0e90/go.mod h1:m86DMxVAMSau3PdVCkBh56m6HIxzTDvR+opoX/gu9/A= +github.com/davidallendj/go-utils v0.0.0-20240302194916-fe292bcf24a4 h1:6LeOczLfpq27cDfu4r6bRU3zGeBER9fy+iecHG5dDSA= +github.com/davidallendj/go-utils v0.0.0-20240302194916-fe292bcf24a4/go.mod h1:/hcpHd4um12taX6iLuMmwxosoyN6E2Ws8QxDpnY07oo= github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 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= @@ -11,12 +11,16 @@ github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= @@ -30,6 +34,10 @@ github.com/lestrrat-go/jwx v1.2.28/go.mod h1:nF+91HEMh/MYFVwKPl5HHsBGMPscqbQb+8I github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -50,16 +58,20 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From c1b268536ac46cc110524f687b4781ab21e6ce7a Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Sun, 3 Mar 2024 18:19:42 -0700 Subject: [PATCH 10/26] Added identity provider caching --- internal/db/sqlite.go | 155 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 internal/db/sqlite.go diff --git a/internal/db/sqlite.go b/internal/db/sqlite.go new file mode 100644 index 0000000..37d34b3 --- /dev/null +++ b/internal/db/sqlite.go @@ -0,0 +1,155 @@ +package db + +import ( + "davidallendj/opaal/internal/oidc" + "fmt" + + "github.com/jmoiron/sqlx" +) + +func CreateIdentityProvidersIfNotExists(path string) (*sqlx.DB, error) { + schema := ` + CREATE TABLE IF NOT EXISTS identity_providers ( + issuer TEXT NOT NULL, + authorization_endpoint TEXT, + token_endpoint TEXT, + revocation_endpoint TEXT, + introspection_endpoint TEXT, + userinfo_endpoint TEXT, + jwks_uri TEXT, + response_types_supported TEXT, + response_modes_supported TEXT, + grant_types_supported TEXT, + token_endpoint_auth_methods_supported TEXT, + subject_types_supported TEXT, + id_token_signing_alg_values_supported TEXT, + claim_types_supported TEXT, + claims_supported TEXT, + jwks TEXT, + + PRIMARY KEY (issuer) + ); + ` + db, err := sqlx.Open("sqlite3", path) + if err != nil { + return nil, fmt.Errorf("could not open database: %v", err) + } + db.MustExec(schema) + return db, nil +} + +func InsertIdentityProviders(path string, providers *[]oidc.IdentityProvider) error { + if providers == nil { + return fmt.Errorf("states == nil") + } + + // create database if it doesn't already exist + db, err := CreateIdentityProvidersIfNotExists(path) + if err != nil { + return err + } + + // insert all probe states into db + tx := db.MustBegin() + for _, state := range *providers { + sql := `INSERT OR REPLACE INTO identity_providers + ( + issuer, + authorization_endpoint, + token_endpoint, + revocation_endpoint, + introspection_endpoint, + userinfo_endpoint, + jwks_uri, + response_types_supported, + response_modes_supported, + grant_types_supported, + token_endpoint_auth_methods_supported, + subject_types_supported, + id_token_signing_alg_values_supported, + claim_types_supported, + claims_supported, + jwks + ) + VALUES + ( + :issuer, + :authorization_endpoint, + :token_endpoint, + :revocation_endpoint, + :introspection_endpoint, + :userinfo_endpoint, + :jwks_uri, + :response_types_supported, + :response_modes_supported, + :grant_types_supported, + :token_endpoint_auth_methods_supported, + :subject_types_supported, + :id_token_signing_alg_values_supported, + :claim_types_supported, + :claims_supported, + :jwks + );` + _, err := tx.NamedExec(sql, &state) + if err != nil { + fmt.Printf("could not execute transaction: %v\n", err) + } + } + err = tx.Commit() + if err != nil { + return fmt.Errorf("could not commit transaction: %v", err) + } + return nil +} + +func GetIdentityProvider(path string, issuer string) (*oidc.IdentityProvider, error) { + db, err := sqlx.Open("sqlite3", path) + if err != nil { + return nil, fmt.Errorf("could not open database: %v", err) + } + + results := &oidc.IdentityProvider{} + err = db.Select(&results, "SELECT * FROM magellan_scanned_ports ORDER BY host ASC, port ASC LIMIT 1;") + if err != nil { + return nil, fmt.Errorf("could not retrieve probes: %v", err) + } + return results, nil +} + +func GetIdentityProviders(path string) ([]oidc.IdentityProvider, error) { + db, err := sqlx.Open("sqlite3", path) + if err != nil { + return nil, fmt.Errorf("could not open database: %v", err) + } + + results := []oidc.IdentityProvider{} + err = db.Select(&results, "SELECT * FROM magellan_scanned_ports ORDER BY host ASC, port ASC;") + if err != nil { + return nil, fmt.Errorf("could not retrieve probes: %v", err) + } + return results, nil +} + +func DeleteIdentityProviders(path string, results *[]oidc.IdentityProvider) error { + if results == nil { + return fmt.Errorf("no probe results found") + } + db, err := sqlx.Open("sqlite3", path) + if err != nil { + return fmt.Errorf("could not open database: %v", err) + } + tx := db.MustBegin() + for _, state := range *results { + sql := `DELETE FROM identity_providers WHERE host = :issuer;` + _, err := tx.NamedExec(sql, &state) + if err != nil { + fmt.Printf("could not execute transaction: %v\n", err) + } + } + + err = tx.Commit() + if err != nil { + return fmt.Errorf("could not commit transaction: %v", err) + } + return nil +} From 92f624239d9fc7b5686cc4a8f7cdc8671130b2e6 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Sun, 3 Mar 2024 18:20:38 -0700 Subject: [PATCH 11/26] Changed when to fetch identity provider server config --- cmd/login.go | 58 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/cmd/login.go b/cmd/login.go index 43cdd36..b318993 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -1,23 +1,50 @@ package cmd import ( - "davidallendj/opaal/internal/flows" + opaal "davidallendj/opaal/internal" + "davidallendj/opaal/internal/db" + "davidallendj/opaal/internal/oidc" "fmt" "os" "github.com/spf13/cobra" ) +var ( + client opaal.Client +) + var loginCmd = &cobra.Command{ Use: "login", Short: "Start the login flow", Run: func(cmd *cobra.Command, args []string) { for { - err := flows.Login(&config) + // try and find client with valid identity provider config + var provider *oidc.IdentityProvider + for _, c := range config.Authentication.Clients { + _, err := db.GetIdentityProvider(config.Options.CachePath, c.Issuer) + if err != nil && !config.Options.LocalOnly { + 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 { + continue + } + client = c + break + } + } + + if provider == nil { + fmt.Printf("failed to retrieve provider config\n") + os.Exit(1) + } + + err := opaal.Login(&config, &client, provider) if err != nil { fmt.Printf("%v\n", err) os.Exit(1) - } else if config.RunOnce { + } else if config.Options.RunOnce { break } } @@ -25,18 +52,17 @@ var loginCmd = &cobra.Command{ } func init() { - loginCmd.Flags().StringVar(&config.Client.Id, "client.id", config.Client.Id, "set the client ID") - loginCmd.Flags().StringVar(&config.Client.Secret, "client.secret", config.Client.Secret, "set the client secret") - loginCmd.Flags().StringSliceVar(&config.Client.RedirectUris, "redirect-uri", config.Client.RedirectUris, "set the redirect URI") - loginCmd.Flags().StringVar(&config.ResponseType, "response-type", config.ResponseType, "set the response-type") - loginCmd.Flags().StringSliceVar(&config.Scope, "scope", config.Scope, "set the scopes") - loginCmd.Flags().StringVar(&config.State, "state", config.State, "set the state") - loginCmd.Flags().StringVar(&config.Server.Host, "host", config.Server.Host, "set the listening host") - loginCmd.Flags().IntVar(&config.Server.Port, "port", config.Server.Port, "set the listening port") - loginCmd.Flags().BoolVar(&config.OpenBrowser, "open-browser", config.OpenBrowser, "automatically open link in browser") - loginCmd.Flags().BoolVar(&config.DecodeIdToken, "decode-id-token", config.DecodeIdToken, "decode and print ID token from identity provider") - loginCmd.Flags().BoolVar(&config.DecodeAccessToken, "decore-access-token", config.DecodeAccessToken, "decode and print access token from authorization server") - loginCmd.Flags().BoolVar(&config.RunOnce, "once", config.RunOnce, "set whether to run login once and exit") - loginCmd.Flags().StringVar(&config.GrantType, "grant-type", config.GrantType, "set the grant-type/authorization flow") + loginCmd.Flags().StringVar(&client.Id, "client.id", client.Id, "set the client ID") + loginCmd.Flags().StringVar(&client.Secret, "client.secret", client.Secret, "set the client secret") + loginCmd.Flags().StringSliceVar(&client.RedirectUris, "client.redirect-uris", client.RedirectUris, "set the redirect URI") + loginCmd.Flags().StringSliceVar(&client.Scope, "client.scope", client.Scope, "set the scopes") + loginCmd.Flags().StringVar(&config.Server.Host, "server.host", config.Server.Host, "set the listening host") + loginCmd.Flags().IntVar(&config.Server.Port, "server.port", config.Server.Port, "set the listening port") + loginCmd.Flags().BoolVar(&config.Options.OpenBrowser, "open-browser", config.Options.OpenBrowser, "automatically open link in browser") + loginCmd.Flags().BoolVar(&config.Options.DecodeIdToken, "decode-id-token", config.Options.DecodeIdToken, "decode and print ID token from identity provider") + loginCmd.Flags().BoolVar(&config.Options.DecodeAccessToken, "decore-access-token", config.Options.DecodeAccessToken, "decode and print access token from authorization server") + loginCmd.Flags().BoolVar(&config.Options.RunOnce, "once", config.Options.RunOnce, "set whether to run login once and exit") + loginCmd.Flags().StringVar(&config.Options.FlowType, "flow", config.Options.FlowType, "set the grant-type/authorization flow") + loginCmd.Flags().BoolVar(&config.Options.LocalOnly, "local", config.Options.LocalOnly, "only fetch identity provider configs stored locally") rootCmd.AddCommand(loginCmd) } From f2e5720aaa067cf95a58cd687f008edd93d8f5ee Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Sun, 3 Mar 2024 18:21:06 -0700 Subject: [PATCH 12/26] Added cache path flag --- cmd/root.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index eace300..4ec5905 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -10,8 +10,8 @@ import ( ) var ( - configPath = "" - config opaal.Config + confPath = "" + config opaal.Config ) var rootCmd = &cobra.Command{ Use: "opaal", @@ -30,18 +30,19 @@ func Execute() { func init() { cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().StringVar(&configPath, "config", "", "set the config path") + rootCmd.PersistentFlags().StringVar(&confPath, "config", "", "set the config path") + rootCmd.PersistentFlags().StringVar(&config.Options.CachePath, "cache", "", "set the cache path") } func initConfig() { // load config if found or create a new one - if configPath != "" { - exists, err := pathx.PathExists(configPath) + if confPath != "" { + exists, err := pathx.PathExists(confPath) if err != nil { fmt.Printf("failed to load config") os.Exit(1) } else if exists { - config = opaal.LoadConfig(configPath) + config = opaal.LoadConfig(confPath) } else { config = opaal.NewConfig() } From 53d1a8cc35e971cad80947f14f5ed04bfb7c6577 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Sun, 3 Mar 2024 18:22:07 -0700 Subject: [PATCH 13/26] Separated authorization code and client credentials flows --- internal/authorization_code.go | 291 +++++++++++++++++++++++++++++++++ internal/client_credentials.go | 48 ++++++ 2 files changed, 339 insertions(+) create mode 100644 internal/authorization_code.go create mode 100644 internal/client_credentials.go diff --git a/internal/authorization_code.go b/internal/authorization_code.go new file mode 100644 index 0000000..26d4a2f --- /dev/null +++ b/internal/authorization_code.go @@ -0,0 +1,291 @@ +package opaal + +import ( + "davidallendj/opaal/internal/oidc" + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "time" + + "github.com/davidallendj/go-utils/util" +) + +// TODO: change authorization code flow to use these instead +type AuthorizationCodeFlowEndpoints struct { + Login string + Token string + Identities string + TrustedIssuer string + Register string +} + +func AuthorizationCodeWithConfig(config *Config, server *Server, client *Client, idp *oidc.IdentityProvider) error { + // check preconditions are met + err := verifyParams(config, server, client, idp) + if err != nil { + return err + } + + // build the authorization URL to redirect user for social sign-in + state := config.Authentication.Flows["authorization_code"]["state"] + var authorizationUrl = client.BuildAuthorizationUrl(idp.Endpoints.Authorization, state) + + // print the authorization URL for sharing + fmt.Printf("Login with identity provider:\n\n %s/login\n %s\n\n", + server.GetListenAddr(), authorizationUrl, + ) + + // automatically open browser to initiate login flow (only useful for testing and debugging) + if config.Options.OpenBrowser { + util.OpenUrl(authorizationUrl) + } + + // authorize oauth client and listen for callback from provider + fmt.Printf("Waiting for authorization code redirect @%s/oidc/callback...\n", server.GetListenAddr()) + code, err := server.WaitForAuthorizationCode(authorizationUrl, "") + if errors.Is(err, http.ErrServerClosed) { + fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n") + } else if err != nil { + return fmt.Errorf("failed to start server: %s", err) + } + + // start up another server in background to listen for success or failures + d := StartListener(server) + + // use code from response and exchange for bearer token (with ID token) + bearerToken, err := client.FetchTokenFromAuthenticationServer( + code, + idp.Endpoints.Token, + state, + ) + if err != nil { + return fmt.Errorf("failed to fetch token from issuer: %v", err) + } + // fmt.Printf("%v\n", string(bearerToken)) + + // unmarshal data to get id_token and access_token + var data map[string]any + err = json.Unmarshal([]byte(bearerToken), &data) + if err != nil || data == nil { + return fmt.Errorf("failed to unmarshal token: %v", err) + } + + // make sure we have an ID token + if data["id_token"] == nil { + return fmt.Errorf("no ID token found...aborting") + } + + // extract ID token from bearer as JSON string for easy consumption + idToken := data["id_token"].(string) + idJwtSegments, err := util.DecodeJwt(idToken) + if err != nil { + fmt.Printf("failed to parse ID token: %v\n", err) + } else { + fmt.Printf("id_token: %v\n", idToken) + if config.Options.DecodeIdToken { + if err != nil { + fmt.Printf("failed to decode JWT: %v\n", err) + } else { + for i, segment := range idJwtSegments { + // don't print last segment (signatures) + if i == len(idJwtSegments)-1 { + break + } + fmt.Printf("%s\n", string(segment)) + } + } + } + fmt.Println() + } + + // extract the access token to get the scopes + accessToken := data["access_token"].(string) + accessJwtSegments, err := util.DecodeJwt(accessToken) + if err != nil || len(accessJwtSegments) <= 0 { + fmt.Printf("failed to parse access token: %v\n", err) + } else { + fmt.Printf("access_token (from identity provider): %v\n", accessToken) + if config.Options.DecodeIdToken { + if err != nil { + fmt.Printf("failed to decode JWT: %v\n", err) + } else { + for i, segment := range accessJwtSegments { + // don't print last segment (signatures) + if i == len(accessJwtSegments)-1 { + break + } + fmt.Printf("%s\n", string(segment)) + } + } + } + fmt.Println() + } + + // extract the scope from access token claims + // var scope []string + // var accessJsonPayload map[string]any + // var accessJwtPayload []byte = accessJwtSegments[1] + // if accessJsonPayload != nil { + // err := json.Unmarshal(accessJwtPayload, &accessJsonPayload) + // if err != nil { + // return fmt.Errorf("failed to unmarshal JWT: %v", err) + // } + // scope = idJsonPayload["scope"].([]string) + // } + + // create a new identity with identity and session manager if url is provided + // if config.RequestUrls.Identities != "" { + // fmt.Printf("Attempting to create a new identity...\n") + // err := client.CreateIdentity(config.RequestUrls.Identities, idToken) + // if err != nil { + // return fmt.Errorf("failed to create new identity: %v", err) + // } + // _, err = client.FetchIdentities(config.RequestUrls.Identities) + // if err != nil { + // return fmt.Errorf("failed to fetch identities: %v", err) + // } + // fmt.Printf("Created new identity successfully.\n\n") + // } + + // extract the subject from ID token claims + var subject string + var audience []string + var idJsonPayload map[string]any + var idJwtPayload []byte = idJwtSegments[1] + if idJwtPayload != nil { + err := json.Unmarshal(idJwtPayload, &idJsonPayload) + if err != nil { + return fmt.Errorf("failed to unmarshal JWT: %v", err) + } + subject = idJsonPayload["sub"].(string) + audType := reflect.ValueOf(idJsonPayload["aud"]) + switch audType.Kind() { + case reflect.String: + audience = append(audience, idJsonPayload["aud"].(string)) + case reflect.Array: + audience = idJsonPayload["aud"].([]string) + } + } else { + return fmt.Errorf("failed to extract subject from ID token claims") + } + + // fetch JWKS and add issuer to authentication server to submit ID token + fmt.Printf("Fetching JWKS from authentication server for verification...\n") + err = idp.FetchJwk() + if err != nil { + return fmt.Errorf("failed to fetch JWK: %v", err) + } else { + fmt.Printf("Successfully retrieved JWK from authentication server.\n\n") + fmt.Printf("Attempting to add issuer to authorization server...\n") + res, err := client.AddTrustedIssuer( + config.Authorization.RequestUrls.TrustedIssuers, + idp, + subject, + time.Duration(1000), + ) + if err != nil { + return fmt.Errorf("failed to add trusted issuer: %v", err) + } + fmt.Printf("%v\n", string(res)) + } + + // add client ID to audience + audience = append(audience, client.Id) + audience = append(audience, "http://127.0.0.1:4444/oauth2/token") + + // try and register a new client with authorization server + fmt.Printf("Registering new OAuth2 client with authorization server...\n") + res, err := client.RegisterOAuthClient(config.Authorization.RequestUrls.Register, audience) + if err != nil { + return fmt.Errorf("failed to register client: %v", err) + } + fmt.Printf("%v\n", string(res)) + + // extract the client info from response + var clientData map[string]any + err = json.Unmarshal(res, &clientData) + if err != nil { + return fmt.Errorf("failed to unmarshal client data: %v", err) + } else { + // check for error first + errJson := clientData["error"] + if errJson == nil { + client.Id = clientData["client_id"].(string) + client.Secret = clientData["client_secret"].(string) + } else { + // delete client and create again + fmt.Printf("Attempting to delete client...\n") + err := client.DeleteOAuthClient(config.Authorization.RequestUrls.Clients) + if err != nil { + return fmt.Errorf("failed to delete OAuth client: %v", err) + } + fmt.Printf("Attempting to re-create client...\n") + res, err := client.CreateOAuthClient(config.Authorization.RequestUrls.Clients, audience) + if err != nil { + return fmt.Errorf("failed to register client: %v", err) + } + fmt.Printf("%v\n", string(res)) + } + } + + // authorize the client + // fmt.Printf("Attempting to authorize client...\n") + // res, err = client.AuthorizeOAuthClient(config.Authorization.RequestUrls.Authorize) + // if err != nil { + // return fmt.Errorf("failed to authorize client: %v", err) + // } + // fmt.Printf("%v\n", string(res)) + + // use ID token/user info to fetch access token from authentication server + if config.Authorization.RequestUrls.Token != "" { + fmt.Printf("Fetching access token from authorization server...\n") + res, err := client.PerformTokenGrant(config.Authorization.RequestUrls.Token, idToken) + if err != nil { + return fmt.Errorf("failed to fetch access token: %v", err) + } + fmt.Printf("%s\n", res) + } + var access_token []byte + d <- access_token + return nil +} + +func verifyParams(config *Config, server *Server, client *Client, idp *oidc.IdentityProvider) error { + // make sure we have a valid server and client + if server == nil { + return fmt.Errorf("server not initialized or valid (server == nil)") + } + if client == nil { + return fmt.Errorf("client not initialized or valid (client == nil)") + } + if idp == nil { + return fmt.Errorf("identity provider not initialized or valid (idp == nil)") + } + // check if all appropriate parameters are set in config + if !HasRequiredConfigParams(config) { + return fmt.Errorf("required params not set correctly or missing") + } + return nil +} + +func StartListener(server *Server) chan []byte { + d := make(chan []byte) + quit := make(chan bool) + + go server.Serve(d) + go func() { + select { + case <-d: + fmt.Printf("got access token") + quit <- true + case <-quit: + close(d) + close(quit) + return + default: + } + }() + return d +} diff --git a/internal/client_credentials.go b/internal/client_credentials.go new file mode 100644 index 0000000..efe20eb --- /dev/null +++ b/internal/client_credentials.go @@ -0,0 +1,48 @@ +package opaal + +import ( + "fmt" +) + +type ClientCredentialsFlowParams struct { + State string `yaml:"state"` + ResponseType string `yaml:"response-type"` +} + +type ClientCredentialsFlowEndpoints struct { + Create string + Authorize string + Token string +} + +func ClientCredentials(eps ClientCredentialsFlowEndpoints, client *Client) error { + // register a new OAuth 2 client with authorization srever + _, err := client.CreateOAuthClient(eps.Create, nil) + if err != nil { + return fmt.Errorf("failed to register OAuth client: %v", err) + } + + // authorize the client + _, err = client.AuthorizeOAuthClient(eps.Authorize) + if err != nil { + return fmt.Errorf("failed to authorize client: %v", err) + } + + // request a token from the authorization server + res, err := client.PerformTokenGrant(eps.Token, "") + if err != nil { + return fmt.Errorf("failed to fetch token from authorization server: %v", err) + } + + fmt.Printf("token: %v\n", string(res)) + return nil +} + +func ClientCredentialsWithConfig(config *Config, client *Client) error { + eps := ClientCredentialsFlowEndpoints{ + Create: config.Authorization.RequestUrls.Clients, + Authorize: config.Authorization.RequestUrls.Authorize, + Token: config.Authorization.RequestUrls.Token, + } + return ClientCredentials(eps, client) +} From 4bca62ec2fb2cfc80a84d3588004163b000d6e03 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Sun, 3 Mar 2024 18:23:35 -0700 Subject: [PATCH 14/26] Refactor and added ability to use include multiple providers in config --- config.yaml | 98 +++++--- internal/authenticate.go | 118 +++++++++ internal/authorize.go | 241 +++++++++++++++++++ internal/client.go | 342 +++++---------------------- internal/config.go | 99 +++++--- internal/flows/authorization_code.go | 254 -------------------- internal/flows/client_credentials.go | 29 --- internal/flows/login.go | 28 --- internal/identities.go | 32 +++ internal/login.go | 37 +++ internal/oidc/oidc.go | 57 ++--- internal/opaal.go | 17 -- internal/server.go | 20 +- 13 files changed, 660 insertions(+), 712 deletions(-) create mode 100644 internal/authenticate.go create mode 100644 internal/authorize.go delete mode 100644 internal/flows/authorization_code.go delete mode 100644 internal/flows/client_credentials.go delete mode 100644 internal/flows/login.go create mode 100644 internal/identities.go create mode 100644 internal/login.go delete mode 100644 internal/opaal.go diff --git a/config.yaml b/config.yaml index c2530ec..02c1c0a 100755 --- a/config.yaml +++ b/config.yaml @@ -1,29 +1,73 @@ +version: "0.0.1" server: - host: 127.0.0.1 + host: "127.0.0.1" port: 3333 -client: - id: 7527e7b4-c96a-4df0-8fc5-00fde18bb65d - secret: gto_cc5uvpb5lsdczkwnbarvwmbpv5kcjwg7nhbc75zt65yrfh2ldenq - redirect-uris: - - "http://127.0.0.1:3333/oidc/callback" -oidc: - issuer: "http://git.towk.local:3000/" -urls: - #identities: http://127.0.0.1:4434/admin/identities - trusted-issuers: http://127.0.0.1:4445/admin/trust/grants/jwt-bearer/issuers - access-token: http://127.0.0.1:4444/oauth2/token - server-config: http://git.towk.local:3000/.well-known/openid-configuration - jwks_uri: http://git.towk.local:3000/login/oauth/keys - login: http://127.0.0.1:4433/self-service/login/api - login-flow-id: http://127.0.0.1:4433/self-service/login/flows?id={id} - register-client: http://127.0.0.1:4445/clients - authorize-client: http://127.0.0.1:4444/oauth2/authorize -state: "" -response-type: code -decode-id-token: true -decode-access-token: true -run-once: true -scope: -- openid -- profile -- email + callback: "/oidc/callback" + +providers: + facebook: "http://facebook.com" + forgejo: "http://git.towk.local:3000" + gitlab: "https://gitlab.newmexicoconsortium.org" + github: "https://github.com" + +authentication: + clients: + - id: "1135541217802147" + secret: "b3a3123e8235de1dbab448369bc3d024" + issuer: "https://www.facebook.com" + scope: + - "openid" + - "name" + - "email" + redirect-uris: + - "http://127.0.0.1:3333/oidc/callback" + - id: "978b48059dd4916f53b4" + secret: "eb54b533eb6afd695e3a1b3f363ab2b29acc7425" + issuer: "https://github.com" + scope: + - "openid" + - "profile" + redirect-uris: + - "http://127.0.0.1:3333/oidc/callback" + - id: "7527e7b4-c96a-4df0-8fc5-00fde18bb65d" + secret: "gto_cc5uvpb5lsdczkwnbarvwmbpv5kcjwg7nhbc75zt65yrfh2ldenq" + name: "forgejo" + issuer: "http://git.towk.local:3000" + scope: + - "openid" + - "profile" + - "read" + - "email" + redirect-uris: + - "http://127.0.0.1:3333/oidc/callback" + - id: "7c0fab1153674a258a705976fcb9468350df3addd91de4ec622fc9ed24bfbcdd" + secret: "a9a8bc55b0cd99236756093adc00ab17855fa507ce106b8038e7f9390ef2ad99" + name: "gitlab" + issuer: "http://gitlab.newmexicoconsortium.org" + scope: + - "openid" + - "profile" + - "email" + redirect-uris: + - "http://127.0.0.1:3333/oidc/callback" + flows: + authorization-code: + state: "" + client-credentials: + +authorization: + urls: + #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 + clients: http://127.0.0.1:4445/admin/clients + 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 + + +options: + decode-id-token: true + decode-access-token: true + run-once: true + open-browser: false diff --git a/internal/authenticate.go b/internal/authenticate.go new file mode 100644 index 0000000..91f5940 --- /dev/null +++ b/internal/authenticate.go @@ -0,0 +1,118 @@ +package opaal + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/davidallendj/go-utils/httpx" +) + +func (client *Client) IsFlowInitiated() bool { + return client.FlowId != "" +} + +func (client *Client) BuildAuthorizationUrl(issuer string, state string) string { + return issuer + "?" + "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, "+") + + "&resource=" + url.QueryEscape("http://127.0.0.1:4444/oauth2/token") +} + +func (client *Client) InitiateLoginFlow(loginUrl string) error { + // kratos: GET /self-service/login/api + req, err := http.NewRequest("GET", loginUrl, bytes.NewBuffer([]byte{})) + if err != nil { + return fmt.Errorf("failed to make request: %v", err) + } + res, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to do request: %v", err) + } + defer res.Body.Close() + + // get the flow ID from response + body, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %v", err) + } + + var flowData map[string]any + err = json.Unmarshal(body, &flowData) + if err != nil { + return fmt.Errorf("failed to unmarshal flow data: %v\n%v", err, string(body)) + } else { + client.FlowId = flowData["id"].(string) + } + return nil +} + +func (client *Client) FetchFlowData(url string) (map[string]any, error) { + //kratos: GET /self-service/login/flows?id={flowId} + + // replace {id} in string with actual value + url = strings.ReplaceAll(url, "{id}", client.FlowId) + _, b, err := httpx.MakeHttpRequest(url, http.MethodGet, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to make request: %v", err) + } + + var flowData map[string]any + err = json.Unmarshal(b, &flowData) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal flow data: %v", err) + } + return flowData, nil +} + +func (client *Client) FetchCSRFToken(flowUrl string) error { + data, err := client.FetchFlowData(flowUrl) + if err != nil { + return fmt.Errorf("failed to fetch flow data: %v", err) + } + + // iterate through nodes and extract the CSRF token attribute from the flow data + ui := data["ui"].(map[string]any) + nodes := ui["nodes"].([]any) + for _, node := range nodes { + attrs := node.(map[string]any)["attributes"].(map[string]any) + name := attrs["name"].(string) + if name == "csrf_token" { + client.CsrfToken = attrs["value"].(string) + return nil + } + } + return fmt.Errorf("failed to extract CSRF token: not found") +} + +func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl string, state string) ([]byte, error) { + body := url.Values{ + "grant_type": {"authorization_code"}, + "client_id": {client.Id}, + "client_secret": {client.Secret}, + "redirect_uri": {strings.Join(client.RedirectUris, ",")}, + } + // add optional params if valid + if code != "" { + body["code"] = []string{code} + } + if state != "" { + body["state"] = []string{state} + } + res, err := http.PostForm(remoteUrl, body) + if err != nil { + return nil, fmt.Errorf("failed to get ID token: %s", err) + } + defer res.Body.Close() + + // domain, _ := url.Parse("http://127.0.0.1") + // client.Jar.SetCookies(domain, res.Cookies()) + + return io.ReadAll(res.Body) +} diff --git a/internal/authorize.go b/internal/authorize.go new file mode 100644 index 0000000..8190b6d --- /dev/null +++ b/internal/authorize.go @@ -0,0 +1,241 @@ +package opaal + +import ( + "bytes" + "davidallendj/opaal/internal/oidc" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "slices" + "strings" + "time" + + "github.com/davidallendj/go-utils/httpx" + "github.com/davidallendj/go-utils/util" +) + +func (client *Client) AddTrustedIssuer(url string, idp *oidc.IdentityProvider, subject string, duration time.Duration) ([]byte, error) { + // hydra endpoint: POST /admin/trust/grants/jwt-bearer/issuers + if idp == nil { + return nil, fmt.Errorf("identity provided is nil") + } + jwkstr, err := json.Marshal(idp.Key) + if err != nil { + return nil, fmt.Errorf("failed to marshal JWK: %v", err) + } + quotedScopes := make([]string, len(client.Scope)) + for i, s := range client.Scope { + quotedScopes[i] = fmt.Sprintf("\"%s\"", s) + } + // NOTE: Can also include "jwks_uri" instead + data := []byte(fmt.Sprintf("{"+ + "\"allow_any_subject\": false,"+ + "\"issuer\": \"%s\","+ + "\"subject\": \"%s\","+ + "\"expires_at\": \"%v\","+ + "\"jwk\": %v,"+ + "\"scope\": [ %s ]"+ + "}", idp.Issuer, subject, time.Now().Add(duration).Format(time.RFC3339), string(jwkstr), strings.Join(quotedScopes, ","))) + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) + // req.Header.Add("X-CSRF-Token", client.CsrfToken.Value) + if err != nil { + return nil, fmt.Errorf("failed to make request: %v", err) + } + req.Header.Add("Content-Type", "application/json") + // req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken)) + res, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to do request: %v", err) + } + defer res.Body.Close() + + return io.ReadAll(res.Body) +} + +func (client *Client) IsOAuthClientRegistered(clientUrl string) (bool, error) { + _, _, err := httpx.MakeHttpRequest(clientUrl, http.MethodGet, nil, nil) + if err != nil { + return false, fmt.Errorf("failed to make request: %v", err) + } + // TODO: need to check contents of actual response + return true, nil +} + +func (client *Client) GetOAuthClient(clientUrl string) error { + _, b, err := httpx.MakeHttpRequest(clientUrl, http.MethodGet, nil, nil) + if err != nil { + return fmt.Errorf("failed to make request: %v", err) + } + + fmt.Printf("GetOAuthClient: %v\n", string(b)) + + var data []map[string]any + err = json.Unmarshal(b, &data) + if err != nil { + return fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + index := slices.IndexFunc(data, func(c map[string]any) bool { + if c["client_id"] == nil { + return false + } + return c["client_id"].(string) == client.Id + }) + if index < 0 { + return fmt.Errorf("client not found") + } + + // cast the redirect_uris from []any to []string and extract registration token + foundClient := data[index] + for _, uri := range foundClient["redirect_uris"].([]any) { + client.RedirectUris = append(client.RedirectUris, uri.(string)) + } + if foundClient["registration-access-token"] != nil { + client.RegistrationAccessToken = foundClient["registration-access-token"].(string) + } + + return nil +} + +func (client *Client) CreateOAuthClient(registerUrl string, audience []string) ([]byte, error) { + // hydra endpoint: POST /clients + audience = util.QuoteArrayStrings(audience) + body := httpx.Body(fmt.Sprintf(`{ + "client_id": "%s", + "client_name": "%s", + "client_secret": "%s", + "token_endpoint_auth_method": "client_secret_post", + "scope": "%s", + "grant_types": ["urn:ietf:params:oauth:grant-type:jwt-bearer"], + "response_types": ["token"], + "redirect_uris": ["http://127.0.0.1:3333/callback"], + "state": 12345678910, + "audience": [%s] + }`, client.Id, client.Id, client.Secret, strings.Join(client.Scope, " "), strings.Join(audience, ","), + )) + headers := httpx.Headers{ + "Content-Type": "application/json", + } + + _, b, err := httpx.MakeHttpRequest(registerUrl, http.MethodPost, []byte(body), headers) + if err != nil { + return nil, fmt.Errorf("failed to make request: %v", err) + } + var rjson map[string]any + err = json.Unmarshal(b, &rjson) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal response body: %v", err) + } + + // check for error first + errJson := rjson["error"] + if errJson == nil { + // set the client ID and secret of registered client + client.Id = rjson["client_id"].(string) + client.Secret = rjson["client_secret"].(string) + client.RegistrationAccessToken = rjson["registration_access_token"].(string) + } else { + return b, nil + } + + return b, err +} + +func (client *Client) RegisterOAuthClient(registerUrl string, audience []string) ([]byte, error) { + // hydra endpoint: POST /oauth2/register + audience = util.QuoteArrayStrings(audience) + body := httpx.Body(fmt.Sprintf(`{ + "client_name": "opaal", + "token_endpoint_auth_method": "client_secret_post", + "scope": "%s", + "grant_types": ["urn:ietf:params:oauth:grant-type:jwt-bearer"], + "response_types": ["token"], + "redirect_uris": ["http://127.0.0.1:3333/callback"], + "state": 12345678910, + "audience": [%s] + }`, strings.Join(client.Scope, " "), strings.Join(audience, ","), + )) + headers := httpx.Headers{ + "Content-Type": "application/json", + } + _, b, err := httpx.MakeHttpRequest(registerUrl, http.MethodPost, body, headers) + if err != nil { + return nil, fmt.Errorf("failed to make request: %v", err) + } + var rjson map[string]any + err = json.Unmarshal(b, &rjson) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal response body: %v", err) + } + + // check for error first + errJson := rjson["error"] + if errJson == nil { + // set the client ID and secret of registered client + client.Id = rjson["client_id"].(string) + client.Secret = rjson["client_secret"].(string) + client.RegistrationAccessToken = rjson["registration_access_token"].(string) + } else { + return b, nil + } + return b, err +} + +func (client *Client) AuthorizeOAuthClient(authorizeUrl string) ([]byte, error) { + // set the authorization header + body := []byte("grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") + + "&scope=" + strings.Join(client.Scope, "+") + + "&client_id=" + client.Id + + "&client_secret=" + client.Secret + + "&redirect_uri=" + url.QueryEscape("http://127.0.0.1:3333/callback") + // FIXME: needs to not be hardcorded + "&response_type=token" + + "&state=12345678910", + ) + headers := httpx.Headers{ + "Authorization": "Bearer " + client.RegistrationAccessToken, + "Content-Type": "application/x-www-form-urlencoded", + } + _, b, err := httpx.MakeHttpRequest(authorizeUrl, http.MethodPost, body, headers) + if err != nil { + return nil, fmt.Errorf("failed to make HTTP request: %v", err) + } + + return b, nil +} + +func (client *Client) PerformTokenGrant(clientUrl string, jwt string) ([]byte, error) { + // hydra endpoint: /oauth/token + body := "grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") + + "&client_id=" + client.Id + + "&client_secret=" + client.Secret + + "&redirect_uri=" + url.QueryEscape("http://127.0.0.1:3333/callback") + // add optional params if valid + if jwt != "" { + body += "&assertion=" + jwt + } + if client.Scope != nil || len(client.Scope) > 0 { + body += "&scope=" + strings.Join(client.Scope, "+") + } + headers := httpx.Headers{ + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": "Bearer " + client.RegistrationAccessToken, + } + + _, b, err := httpx.MakeHttpRequest(clientUrl, http.MethodPost, []byte(body), headers) + + // set flow ID back to empty string to indicate a completed flow + client.FlowId = "" + + return b, err +} + +func (client *Client) DeleteOAuthClient(clientUrl string) error { + _, _, err := httpx.MakeHttpRequest(clientUrl+"/"+client.Id, http.MethodDelete, nil, nil) + if err != nil { + return fmt.Errorf("failed to make request: %v", err) + } + return nil +} diff --git a/internal/client.go b/internal/client.go index 48bb689..d9746f4 100644 --- a/internal/client.go +++ b/internal/client.go @@ -1,316 +1,90 @@ package opaal import ( - "bytes" - "davidallendj/opaal/internal/oidc" - "encoding/json" - "fmt" - "io" "net/http" "net/http/cookiejar" - "net/url" - "strings" - "time" + "slices" - "github.com/davidallendj/go-utils/httpx" - "github.com/davidallendj/go-utils/util" + "github.com/davidallendj/go-utils/mathx" "golang.org/x/net/publicsuffix" ) type Client struct { http.Client - Id string `yaml:"id"` - Secret string `yaml:"secret"` - RedirectUris []string `yaml:"redirect-uris"` - FlowId string - CsrfToken string + Id string `yaml:"id"` + Secret string `yaml:"secret"` + Name string `yaml:"name"` + Description string `yaml:"description"` + Issuer string `yaml:"issuer"` + RegistrationAccessToken string `yaml:"registration-access-token"` + RedirectUris []string `yaml:"redirect-uris"` + Scope []string `yaml:"scope"` + FlowId string + CsrfToken string +} + +func NewClient() *Client { + return &Client{} } func NewClientWithConfig(config *Config) *Client { - jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + // make sure config is valid + if config == nil { + return nil + } + + // make sure we have at least one client + clients := config.Authentication.Clients + if len(clients) <= 0 { + return nil + } + + // use the first client found by default return &Client{ - Id: config.Client.Id, - Secret: config.Client.Secret, - RedirectUris: config.Client.RedirectUris, - Client: http.Client{Jar: jar}, + Id: clients[0].Id, + Secret: clients[0].Secret, + Name: clients[0].Name, + Issuer: clients[0].Issuer, + Scope: clients[0].Scope, + RedirectUris: clients[0].RedirectUris, } } -func (client *Client) IsFlowInitiated() bool { - return client.FlowId != "" +func NewClientWithConfigByIndex(config *Config, index int) *Client { + size := len(config.Authentication.Clients) + index = mathx.Clamp(index, 0, size) + return nil } -func (client *Client) BuildAuthorizationUrl(authEndpoint string, state string, responseType string, scope []string) string { - return authEndpoint + "?" + "client_id=" + client.Id + - "&redirect_uri=" + url.QueryEscape(strings.Join(client.RedirectUris, ",")) + - "&response_type=" + responseType + - "&state=" + state + - "&scope=" + strings.Join(scope, "+") + - "&audience=http://127.0.0.1:4444/oauth2/token" -} - -func (client *Client) InitiateLoginFlow(loginUrl string) error { - // kratos: GET /self-service/login/api - req, err := http.NewRequest("GET", loginUrl, bytes.NewBuffer([]byte{})) - if err != nil { - return fmt.Errorf("failed to make request: %v", err) - } - res, err := client.Do(req) - if err != nil { - return fmt.Errorf("failed to do request: %v", err) - } - defer res.Body.Close() - - // get the flow ID from response - body, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("failed to read response body: %v", err) - } - - var flowData map[string]any - err = json.Unmarshal(body, &flowData) - if err != nil { - return fmt.Errorf("failed to unmarshal flow data: %v\n%v", err, string(body)) - } else { - client.FlowId = flowData["id"].(string) +func NewClientWithConfigByName(config *Config, name string) *Client { + index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool { + return c.Name == name + }) + if index >= 0 { + return &config.Authentication.Clients[index] } return nil } -func (client *Client) FetchFlowData(flowUrl string) (map[string]any, error) { - //kratos: GET /self-service/login/flows?id={flowId} +func NewClientWithConfigByProvider(config *Config, issuer string) *Client { + index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool { + return c.Issuer == issuer + }) - // replace {id} in string with actual value - flowUrl = strings.ReplaceAll(flowUrl, "{id}", client.FlowId) - req, err := http.NewRequest("GET", flowUrl, nil) - if err != nil { - return nil, fmt.Errorf("failed to make request: %v", err) + if index >= 0 { + return &config.Authentication.Clients[index] } - res, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to do request: %v", err) - } - defer res.Body.Close() - - // get the flow data from response - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %v", err) - } - - var flowData map[string]any - err = json.Unmarshal(body, &flowData) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal flow data: %v", err) - } - return flowData, nil + return nil } -func (client *Client) FetchCSRFToken(flowUrl string) error { - data, err := client.FetchFlowData(flowUrl) - if err != nil { - return fmt.Errorf("failed to fetch flow data: %v", err) +func NewClientWithConfigById(config *Config, id string) *Client { + index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool { + return c.Id == id + }) + if index >= 0 { + return &config.Authentication.Clients[index] } - - // iterate through nodes and extract the CSRF token attribute from the flow data - ui := data["ui"].(map[string]any) - nodes := ui["nodes"].([]any) - for _, node := range nodes { - attrs := node.(map[string]any)["attributes"].(map[string]any) - name := attrs["name"].(string) - if name == "csrf_token" { - client.CsrfToken = attrs["value"].(string) - return nil - } - } - return fmt.Errorf("failed to extract CSRF token: not found") -} - -func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl string, state string) ([]byte, error) { - data := url.Values{ - "grant_type": {"authorization_code"}, - "client_id": {client.Id}, - "client_secret": {client.Secret}, - "redirect_uri": {strings.Join(client.RedirectUris, ",")}, - } - // add optional params if valid - if code != "" { - data["code"] = []string{code} - } - if state != "" { - data["state"] = []string{state} - } - res, err := http.PostForm(remoteUrl, data) - if err != nil { - return nil, fmt.Errorf("failed to get ID token: %s", err) - } - defer res.Body.Close() - - domain, _ := url.Parse("http://127.0.0.1") - client.Jar.SetCookies(domain, res.Cookies()) - - return io.ReadAll(res.Body) -} - -func (client *Client) FetchTokenFromAuthorizationServer(remoteUrl string, jwt string, scope []string) ([]byte, error) { - // hydra endpoint: /oauth/token - data := "grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") + - "&client_id=" + client.Id + - "&client_secret=" + client.Secret - - // add optional params if valid - if jwt != "" { - data += "&assertion=" + jwt - } - if scope != nil || len(scope) > 0 { - data += "&scope=" + strings.Join(scope, "+") - } - - fmt.Printf("encoded params: %v\n\n", data) - req, err := http.NewRequest("POST", remoteUrl, bytes.NewBuffer([]byte(data))) - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - if err != nil { - return nil, fmt.Errorf("failed to make request: %s", err) - } - res, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to do request: %v", err) - } - defer res.Body.Close() - - // set flow ID back to empty string to indicate a completed flow - client.FlowId = "" - - return io.ReadAll(res.Body) -} - -func (client *Client) AddTrustedIssuer(remoteUrl string, idp *oidc.IdentityProvider, subject string, duration time.Duration, scope []string) ([]byte, error) { - // hydra endpoint: POST /admin/trust/grants/jwt-bearer/issuers - if idp == nil { - return nil, fmt.Errorf("identity provided is nil") - } - jwkstr, err := json.Marshal(idp.Key) - if err != nil { - return nil, fmt.Errorf("failed to marshal JWK: %v", err) - } - quotedScopes := make([]string, len(scope)) - for i, s := range scope { - quotedScopes[i] = fmt.Sprintf("\"%s\"", s) - } - // NOTE: Can also include "jwks_uri" instead - data := []byte(fmt.Sprintf("{"+ - "\"allow_any_subject\": false,"+ - "\"issuer\": \"%s\","+ - "\"subject\": \"%s\","+ - "\"expires_at\": \"%v\","+ - "\"jwk\": %v,"+ - "\"scope\": [ %s ]"+ - "}", idp.Issuer, subject, time.Now().Add(duration).Format(time.RFC3339), string(jwkstr), strings.Join(quotedScopes, ","))) - fmt.Printf("%v\n", string(data)) - - req, err := http.NewRequest("POST", remoteUrl, bytes.NewBuffer(data)) - // req.Header.Add("X-CSRF-Token", client.CsrfToken.Value) - if err != nil { - return nil, fmt.Errorf("failed to make request: %v", err) - } - req.Header.Add("Content-Type", "application/json") - // req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken)) - res, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to do request: %v", err) - } - defer res.Body.Close() - - return io.ReadAll(res.Body) -} - -func (client *Client) AuthorizeClient(authorizeUrl string) ([]byte, error) { - // encode ID and secret for authorization header basic authentication - basicAuth := util.EncodeBase64( - fmt.Sprintf("%s:%s", - url.QueryEscape(client.Id), - url.QueryEscape(client.Secret), - ), - ) - body := httpx.Body("grant_type=client_credentials&scope=read") - headers := httpx.Headers{ - "Authorization": basicAuth, - "Content-Type": "application/x-www-form-urlencoded", - } - _, b, err := httpx.MakeHTTPRequest(authorizeUrl, http.MethodPost, body, headers) - if err != nil { - return nil, fmt.Errorf("failed to make HTTP request: %v", err) - } - - return b, nil -} - -func (client *Client) RegisterOAuthClient(registerUrl string, audience []string) ([]byte, error) { - // hydra endpoint: POST /clients - audience = util.QuoteArrayStrings(audience) - data := []byte(fmt.Sprintf(`{ - "client_name": "%s", - "client_secret": "%s", - "token_endpoint_auth_method": "client_secret_post", - "scope": "openid email profile", - "grant_types": ["client_credentials", "urn:ietf:params:oauth:grant-type:jwt-bearer"], - "response_types": ["token"], - }`, client.Id, client.Secret, - // strings.Join(audience, ",") - )) - // "audience": [%s] - - req, err := http.NewRequest("POST", registerUrl, bytes.NewBuffer(data)) - if err != nil { - return nil, fmt.Errorf("failed to make request: %v", err) - } - req.Header.Add("Content-Type", "application/json") - // req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken)) - res, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to do request: %v", err) - } - defer res.Body.Close() - - return io.ReadAll(res.Body) -} - -func (client *Client) CreateIdentity(remoteUrl string, idToken string) ([]byte, error) { - // kratos endpoint: /admin/identities - data := []byte(`{ - "schema_id": "preset://email", - "traits": { - "email": "docs@example.org" - } - }`) - - req, err := http.NewRequest("POST", remoteUrl, bytes.NewBuffer(data)) - if err != nil { - return nil, fmt.Errorf("failed to create a new request: %v", err) - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken)) - // req.Header.Add("X-CSRF-Token", client.CsrfToken.Value) - res, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to do request: %v", err) - } - - return io.ReadAll(res.Body) -} - -func (client *Client) FetchIdentities(remoteUrl string) ([]byte, error) { - req, err := http.NewRequest("GET", remoteUrl, bytes.NewBuffer([]byte{})) - if err != nil { - return nil, fmt.Errorf("failed to create a new request: %v", err) - } - - res, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to do request: %v", err) - } - - return io.ReadAll(res.Body) + return nil } func (client *Client) ClearCookies() { diff --git a/internal/config.go b/internal/config.go index f0f5d49..362f151 100644 --- a/internal/config.go +++ b/internal/config.go @@ -1,7 +1,6 @@ package opaal import ( - "davidallendj/opaal/internal/oidc" "log" "os" "path/filepath" @@ -11,20 +10,46 @@ import ( "gopkg.in/yaml.v2" ) +type FlowOptions map[string]string +type Flows map[string]FlowOptions +type Providers map[string]string + +type Options struct { + DecodeIdToken bool `yaml:"decode-id-token"` + DecodeAccessToken bool `yaml:"decode-access-token"` + RunOnce bool `yaml:"run-once"` + OpenBrowser bool `yaml:"open-browser"` + FlowType string `yaml:"flow"` + CachePath string `yaml:"cache"` + LocalOnly bool `yaml:"local-only"` +} + +type RequestUrls struct { + Identities string `yaml:"identities"` + TrustedIssuers string `yaml:"trusted-issuers"` + Login string `yaml:"login"` + Clients string `yaml:"clients"` + Token string `yaml:"token"` + Authorize string `yaml:"authorize"` + Register string `yaml:"register"` +} + +type Authentication struct { + Clients []Client `yaml:"clients"` + Flows Flows `yaml:"flows"` +} + +type Authorization struct { + RequestUrls RequestUrls `yaml:"urls"` +} + type Config struct { - Version string `yaml:"version"` - Server Server `yaml:"server"` - Client Client `yaml:"client"` - IdentityProvider oidc.IdentityProvider `yaml:"oidc"` - State string `yaml:"state"` - ResponseType string `yaml:"response-type"` - Scope []string `yaml:"scope"` - ActionUrls ActionUrls `yaml:"urls"` - OpenBrowser bool `yaml:"open-browser"` - DecodeIdToken bool `yaml:"decode-id-token"` - DecodeAccessToken bool `yaml:"decode-access-token"` - RunOnce bool `yaml:"run-once"` - GrantType string `yaml:"grant-type"` + Version string `yaml:"version"` + Server Server `yaml:"server"` + Providers Providers `yaml:"providers"` + Options Options `yaml:"options"` + Authentication Authentication `yaml:"authentication"` + Authorization Authorization `yaml:"authorization"` } func NewConfig() Config { @@ -34,31 +59,17 @@ func NewConfig() Config { Host: "127.0.0.1", Port: 3333, }, - Client: Client{ - Id: "", - Secret: "", - RedirectUris: []string{""}, + Options: Options{ + DecodeIdToken: true, + DecodeAccessToken: true, + RunOnce: true, + OpenBrowser: false, + CachePath: "opaal.db", + FlowType: "authorization_code", + LocalOnly: false, }, - IdentityProvider: *oidc.NewIdentityProvider(), - State: goutil.RandomString(20), - ResponseType: "code", - Scope: []string{"openid", "profile", "email"}, - ActionUrls: ActionUrls{ - Identities: "", - AccessToken: "", - TrustedIssuers: "", - ServerConfig: "", - JwksUri: "", - Login: "", - LoginFlowId: "", - RegisterClient: "", - AuthorizeClient: "", - }, - OpenBrowser: false, - DecodeIdToken: false, - DecodeAccessToken: false, - RunOnce: true, - GrantType: "authorization_code", + Authentication: Authentication{}, + Authorization: Authorization{}, } } @@ -94,3 +105,15 @@ func SaveDefaultConfig(path string) { return } } + +func HasRequiredConfigParams(config *Config) bool { + // must have athe requirements to perform login + hasClients := len(config.Authentication.Clients) > 0 + hasServer := config.Server.Host != "" && config.Server.Port != 0 && config.Server.Callback != "" + hasEndpoints := config.Authorization.RequestUrls.TrustedIssuers != "" && + config.Authorization.RequestUrls.Login != "" && + config.Authorization.RequestUrls.Clients != "" && + config.Authorization.RequestUrls.Authorize != "" && + config.Authorization.RequestUrls.Token != "" + return hasClients && hasServer && hasEndpoints +} diff --git a/internal/flows/authorization_code.go b/internal/flows/authorization_code.go deleted file mode 100644 index cf5d5a3..0000000 --- a/internal/flows/authorization_code.go +++ /dev/null @@ -1,254 +0,0 @@ -package flows - -import ( - opaal "davidallendj/opaal/internal" - "davidallendj/opaal/internal/oidc" - "encoding/json" - "errors" - "fmt" - "net/http" - "reflect" - "time" - - "github.com/davidallendj/go-utils/util" -) - -func AuthorizationCode(config *opaal.Config, server *opaal.Server, client *opaal.Client) error { - // initiate the login flow and get a flow ID and CSRF token - { - err := client.InitiateLoginFlow(config.ActionUrls.Login) - if err != nil { - return fmt.Errorf("failed to initiate login flow: %v", err) - } - err = client.FetchCSRFToken(config.ActionUrls.LoginFlowId) - if err != nil { - return fmt.Errorf("failed to fetch CSRF token: %v", err) - } - } - - // try and fetch server configuration if provided URL - idp := oidc.NewIdentityProvider() - if config.ActionUrls.ServerConfig != "" { - fmt.Printf("Fetching server configuration: %s\n", config.ActionUrls.ServerConfig) - err := idp.FetchServerConfig(config.ActionUrls.ServerConfig) - if err != nil { - return fmt.Errorf("failed to fetch server config: %v", err) - } - } else { - // otherwise, use what's provided in config file - idp.Issuer = config.IdentityProvider.Issuer - idp.Endpoints = config.IdentityProvider.Endpoints - idp.Supported = config.IdentityProvider.Supported - } - - // check if all appropriate parameters are set in config - if !opaal.HasRequiredParams(config) { - return fmt.Errorf("client ID must be set") - } - - // build the authorization URL to redirect user for social sign-in - var authorizationUrl = client.BuildAuthorizationUrl( - idp.Endpoints.Authorization, - config.State, - config.ResponseType, - config.Scope, - ) - - // print the authorization URL for sharing - fmt.Printf("Login with identity provider:\n\n %s/login\n %s\n\n", - server.GetListenAddr(), authorizationUrl, - ) - - // automatically open browser to initiate login flow (only useful for testing) - if config.OpenBrowser { - util.OpenUrl(authorizationUrl) - } - - // authorize oauth client and listen for callback from provider - fmt.Printf("Waiting for authorization code redirect @%s/oidc/callback...\n", server.GetListenAddr()) - code, err := server.WaitForAuthorizationCode(authorizationUrl) - if errors.Is(err, http.ErrServerClosed) { - fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n") - } else if err != nil { - return fmt.Errorf("failed to start server: %s", err) - } - - if client == nil { - fmt.Printf("client did not initialize\n") - } - - // start up another serve in background to listen for success or failures - d := make(chan []byte) - quit := make(chan bool) - var access_token []byte - go server.Serve(d) - go func() { - select { - case <-d: - fmt.Printf("got access token") - quit <- true - case <-quit: - close(d) - close(quit) - return - default: - } - }() - - // use code from response and exchange for bearer token (with ID token) - bearerToken, err := client.FetchTokenFromAuthenticationServer( - code, - idp.Endpoints.Token, - config.State, - ) - if err != nil { - return fmt.Errorf("failed to fetch token from issuer: %v", err) - } - - // unmarshal data to get id_token and access_token - var data map[string]any - err = json.Unmarshal([]byte(bearerToken), &data) - if err != nil || data == nil { - return fmt.Errorf("failed to unmarshal token: %v", err) - } - - // extract ID token from bearer as JSON string for easy consumption - idToken := data["id_token"].(string) - idJwtSegments, err := util.DecodeJwt(idToken) - if err != nil { - fmt.Printf("failed to parse ID token: %v\n", err) - } else { - fmt.Printf("id_token: %v\n", idToken) - if config.DecodeIdToken { - if err != nil { - fmt.Printf("failed to decode JWT: %v\n", err) - } else { - for i, segment := range idJwtSegments { - // don't print last segment (signatures) - if i == len(idJwtSegments)-1 { - break - } - fmt.Printf("%s\n", string(segment)) - } - } - } - fmt.Println() - } - - // extract the access token to get the scopes - accessToken := data["access_token"].(string) - accessJwtSegments, err := util.DecodeJwt(accessToken) - if err != nil || len(accessJwtSegments) <= 0 { - fmt.Printf("failed to parse access token: %v\n", err) - } else { - fmt.Printf("access_token: %v\n", accessToken) - if config.DecodeIdToken { - if err != nil { - fmt.Printf("failed to decode JWT: %v\n", err) - } else { - for i, segment := range accessJwtSegments { - // don't print last segment (signatures) - if i == len(accessJwtSegments)-1 { - break - } - fmt.Printf("%s\n", string(segment)) - } - } - } - fmt.Println() - } - - // extract the scope from access token claims - // var scope []string - // var accessJsonPayload map[string]any - // var accessJwtPayload []byte = accessJwtSegments[1] - // if accessJsonPayload != nil { - // err := json.Unmarshal(accessJwtPayload, &accessJsonPayload) - // if err != nil { - // return fmt.Errorf("failed to unmarshal JWT: %v", err) - // } - // scope = idJsonPayload["scope"].([]string) - // } - - // create a new identity with identity and session manager if url is provided - if config.ActionUrls.Identities != "" { - fmt.Printf("Attempting to create a new identity...\n") - _, err := client.CreateIdentity(config.ActionUrls.Identities, idToken) - if err != nil { - return fmt.Errorf("failed to create new identity: %v", err) - } - _, err = client.FetchIdentities(config.ActionUrls.Identities) - if err != nil { - return fmt.Errorf("failed to fetch identities: %v", err) - } - fmt.Printf("Created new identity successfully.\n\n") - } - - // extract the subject from ID token claims - var subject string - var audience []string - var idJsonPayload map[string]any - var idJwtPayload []byte = idJwtSegments[1] - if idJwtPayload != nil { - err := json.Unmarshal(idJwtPayload, &idJsonPayload) - if err != nil { - return fmt.Errorf("failed to unmarshal JWT: %v", err) - } - subject = idJsonPayload["sub"].(string) - audType := reflect.ValueOf(idJsonPayload["aud"]) - switch audType.Kind() { - case reflect.String: - audience = append(audience, idJsonPayload["aud"].(string)) - case reflect.Array: - audience = idJsonPayload["aud"].([]string) - } - } else { - return fmt.Errorf("failed to extract subject from ID token claims") - } - - // fetch JWKS and add issuer to authentication server to submit ID token - fmt.Printf("Fetching JWKS from authentication server for verification...\n") - err = idp.FetchJwk(config.ActionUrls.JwksUri) - if err != nil { - return fmt.Errorf("failed to fetch JWK: %v", err) - } else { - fmt.Printf("Successfully retrieved JWK from authentication server.\n\n") - fmt.Printf("Attempting to add issuer to authorization server...\n") - res, err := client.AddTrustedIssuer(config.ActionUrls.TrustedIssuers, idp, subject, time.Duration(1000), config.Scope) - if err != nil { - return fmt.Errorf("failed to add trusted issuer: %v", err) - } - fmt.Printf("%v\n", string(res)) - } - - // try and register a new client with authorization server - fmt.Printf("Registering new OAuth2 client with authorization server...\n") - res, err := client.RegisterOAuthClient("http://127.0.0.1:4445/clients", audience) - if err != nil { - return fmt.Errorf("failed to register client: %v", err) - } - fmt.Printf("%v\n", string(res)) - - // extract the client info from response - var clientData map[string]any - err = json.Unmarshal(res, &clientData) - if err != nil { - return fmt.Errorf("failed to unmarshal client data: %v", err) - } else { - client.Id = clientData["client_id"].(string) - client.Secret = clientData["client_secret"].(string) - } - - // use ID token/user info to fetch access token from authentication server - if config.ActionUrls.AccessToken != "" { - fmt.Printf("Fetching access token from authorization server...\n") - res, err := client.FetchTokenFromAuthorizationServer(config.ActionUrls.AccessToken, idToken, config.Scope) - if err != nil { - return fmt.Errorf("failed to fetch access token: %v", err) - } - fmt.Printf("%s\n", res) - } - - d <- access_token - return nil -} diff --git a/internal/flows/client_credentials.go b/internal/flows/client_credentials.go deleted file mode 100644 index 6c59037..0000000 --- a/internal/flows/client_credentials.go +++ /dev/null @@ -1,29 +0,0 @@ -package flows - -import ( - opaal "davidallendj/opaal/internal" - "fmt" -) - -func ClientCredentials(config *opaal.Config, server *opaal.Server, client *opaal.Client) error { - // register a new OAuth 2 client with authorization srever - _, err := client.RegisterOAuthClient(config.ActionUrls.RegisterClient, nil) - if err != nil { - return fmt.Errorf("failed to register OAuth client: %v", err) - } - - // authorize the client - _, err = client.AuthorizeClient(config.ActionUrls.AuthorizeClient) - if err != nil { - return fmt.Errorf("failed to authorize client: %v", err) - } - - // request a token from the authorization server - res, err := client.FetchTokenFromAuthorizationServer(config.ActionUrls.AccessToken, "", nil) - if err != nil { - return fmt.Errorf("failed to fetch token from authorization server: %v", err) - } - - fmt.Printf("token: %v\n", string(res)) - return nil -} diff --git a/internal/flows/login.go b/internal/flows/login.go deleted file mode 100644 index 1ee953b..0000000 --- a/internal/flows/login.go +++ /dev/null @@ -1,28 +0,0 @@ -package flows - -import ( - opaal "davidallendj/opaal/internal" - "fmt" -) - -func Login(config *opaal.Config) error { - if config == nil { - return fmt.Errorf("config is not valid") - } - - // initialize client that will be used throughout login flow - server := opaal.NewServerWithConfig(config) - client := opaal.NewClientWithConfig(config) - - fmt.Printf("grant type: %v\n", config.GrantType) - - if config.GrantType == "authorization_code" { - AuthorizationCode(config, server, client) - } else if config.GrantType == "client_credentials" { - ClientCredentials(config, server, client) - } else { - return fmt.Errorf("invalid grant type") - } - - return nil -} diff --git a/internal/identities.go b/internal/identities.go new file mode 100644 index 0000000..e1ddee5 --- /dev/null +++ b/internal/identities.go @@ -0,0 +1,32 @@ +package opaal + +import ( + "fmt" + "net/http" + + "github.com/davidallendj/go-utils/httpx" +) + +func (client *Client) CreateIdentity(url string, idToken string) error { + // kratos endpoint: /admin/identities + body := []byte(`{ + "schema_id": "preset://email", + "traits": { + "email": "docs@example.org" + } + }`) + headers := httpx.Headers{ + "Content-Type": "application/json", + "Authorization": fmt.Sprintf("Bearer %s", idToken), + } + _, _, err := httpx.MakeHttpRequest(url, http.MethodPost, body, headers) + if err != nil { + return fmt.Errorf("failed to read response body: %v", err) + } + return nil +} + +func (client *Client) FetchIdentities(remoteUrl string) ([]byte, error) { + _, b, err := httpx.MakeHttpRequest(remoteUrl, http.MethodGet, []byte{}, httpx.Headers{}) + return b, err +} diff --git a/internal/login.go b/internal/login.go new file mode 100644 index 0000000..686dbed --- /dev/null +++ b/internal/login.go @@ -0,0 +1,37 @@ +package opaal + +import ( + "davidallendj/opaal/internal/db" + "davidallendj/opaal/internal/oidc" + "fmt" +) + +func Login(config *Config, client *Client, provider *oidc.IdentityProvider) error { + if config == nil { + return fmt.Errorf("config is not valid") + } + + // make cache if it's not where expect + _, err := db.CreateIdentityProvidersIfNotExists(config.Options.CachePath) + if err != nil { + fmt.Printf("failed to create cache: %v\n", err) + } + + if config.Options.FlowType == "authorization_code" { + // create a server if doing authorization code flow + server := NewServerWithConfig(config) + err := AuthorizationCodeWithConfig(config, server, client, provider) + if err != nil { + fmt.Printf("failed to complete authorization code flow: %v\n", err) + } + } else if config.Options.FlowType == "client_credentials" { + err := ClientCredentialsWithConfig(config, client) + if err != nil { + fmt.Printf("failed to complete client credentials flow: %v", err) + } + } else { + return fmt.Errorf("invalid grant type (options: authorization_code, client_credentials)") + } + + return nil +} diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index e17a120..4f509a9 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -13,29 +13,29 @@ import ( ) type IdentityProvider struct { - Issuer string `json:"issuer" yaml:"issuer"` - Endpoints Endpoints `json:"endpoints" yaml:"endpoints"` - Supported Supported `json:"supported" yaml:"supported"` + Issuer string `db:"issuer" json:"issuer" yaml:"issuer"` + Endpoints Endpoints `db:"endpoints" json:"endpoints" yaml:"endpoints"` + Supported Supported `db:"supported" json:"supported" yaml:"supported"` Key jwk.Key } type Endpoints struct { - Authorization string `json:"authorization_endpoint" yaml:"authorization"` - Token string `json:"token_endpoint" yaml:"token"` - Revocation string `json:"revocation_endpoint" yaml:"revocation"` - Introspection string `json:"introspection_endpoint" yaml:"introspection"` - UserInfo string `json:"userinfo_endpoint" yaml:"userinfo"` - Jwks string `json:"jwks_uri" yaml:"jwks_uri"` + Authorization string `db:"authorization_endpoint" json:"authorization_endpoint" yaml:"authorization"` + Token string `db:"token_endpoint" json:"token_endpoint" yaml:"token"` + Revocation string `db:"revocation_endpoint" json:"revocation_endpoint" yaml:"revocation"` + Introspection string `db:"introspection_endpoint" json:"introspection_endpoint" yaml:"introspection"` + UserInfo string `db:"userinfo_endpoint" json:"userinfo_endpoint" yaml:"userinfo"` + Jwks string `db:"jwks_uri" json:"jwks_uri" yaml:"jwks_uri"` } type Supported struct { - ResponseTypes []string `json:"response_types_supported"` - ResponseModes []string `json:"response_modes_supported"` - GrantTypes []string `json:"grant_types_supported"` - TokenEndpointAuthMethods []string `json:"token_endpoint_auth_methods_supported"` - SubjectTypes []string `json:"subject_types_supported"` - IdTokenSigningAlgValues []string `json:"id_token_signing_alg_values_supported"` - ClaimTypes []string `json:"claim_types_supported"` - Claims []string `json:"claims_supported"` + ResponseTypes []string `db:"response_types_supported" json:"response_types_supported"` + ResponseModes []string `db:"response_modes_supported" json:"response_modes_supported"` + GrantTypes []string `db:"grant_types_supported" json:"grant_types_supported"` + TokenEndpointAuthMethods []string `db:"token_endpoint_auth_methods_supported" json:"token_endpoint_auth_methods_supported"` + SubjectTypes []string `db:"subject_types_supported" json:"subject_types_supported"` + IdTokenSigningAlgValues []string `db:"id_token_signing_alg_values_supported" json:"id_token_signing_alg_values_supported"` + ClaimTypes []string `db:"claim_types_supported" json:"claim_types_supported"` + Claims []string `db:"claims_supported" json:"claims_supported"` } func NewIdentityProvider() *IdentityProvider { @@ -109,38 +109,39 @@ func (p *IdentityProvider) LoadServerConfig(path string) error { return nil } -func (p *IdentityProvider) FetchServerConfig(url string) error { +func FetchServerConfig(issuer string) (*IdentityProvider, error) { // make a request to a server's openid-configuration - req, err := http.NewRequest("GET", url, bytes.NewBuffer([]byte{})) + req, err := http.NewRequest(http.MethodGet, issuer+"/.well-known/openid-configuration", bytes.NewBuffer([]byte{})) if err != nil { - return fmt.Errorf("failed to create a new request: %v", err) + return nil, 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) + return nil, 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) + return nil, fmt.Errorf("failed to read response body: %v", err) } + var p IdentityProvider err = p.ParseServerConfig(body) if err != nil { - return fmt.Errorf("failed to parse server config: %v", err) + return nil, fmt.Errorf("failed to parse server config: %v", err) } - return nil + return &p, nil } -func (p *IdentityProvider) FetchJwk(url string) error { - if url == "" { - url = p.Endpoints.Jwks +func (p *IdentityProvider) FetchJwk() error { + if p.Endpoints.Jwks == "" { + return fmt.Errorf("JWKS endpoint not set") } // fetch JWKS from identity provider ctx, cancel := context.WithCancel(context.Background()) defer cancel() - set, err := jwk.Fetch(ctx, url) + set, err := jwk.Fetch(ctx, p.Endpoints.Jwks) if err != nil { return fmt.Errorf("%v", err) } diff --git a/internal/opaal.go b/internal/opaal.go deleted file mode 100644 index ee9cb11..0000000 --- a/internal/opaal.go +++ /dev/null @@ -1,17 +0,0 @@ -package opaal - -type ActionUrls struct { - Identities string `yaml:"identities"` - TrustedIssuers string `yaml:"trusted-issuers"` - AccessToken string `yaml:"access-token"` - ServerConfig string `yaml:"server-config"` - JwksUri string `yaml:"jwks_uri"` - Login string `yaml:"login"` - LoginFlowId string `yaml:"login-flow-id"` - RegisterClient string `yaml:"register-client"` - AuthorizeClient string `yaml:"authorize-client"` -} - -func HasRequiredParams(config *Config) bool { - return config.Client.Id != "" && config.Client.Secret != "" -} diff --git a/internal/server.go b/internal/server.go index c048592..4984902 100644 --- a/internal/server.go +++ b/internal/server.go @@ -12,13 +12,14 @@ import ( type Server struct { *http.Server - Host string `yaml:"host"` - Port int `yaml:"port"` + Host string `yaml:"host"` + Port int `yaml:"port"` + Callback string `yaml:"callback"` } -func NewServerWithConfig(config *Config) *Server { - host := config.Server.Host - port := config.Server.Port +func NewServerWithConfig(conf *Config) *Server { + host := conf.Server.Host + port := conf.Server.Port server := &Server{ Server: &http.Server{ Addr: fmt.Sprintf("%s:%d", host, port), @@ -37,7 +38,12 @@ func (s *Server) GetListenAddr() string { return fmt.Sprintf("%s:%d", s.Host, s.Port) } -func (s *Server) WaitForAuthorizationCode(loginUrl string) (string, error) { +func (s *Server) WaitForAuthorizationCode(loginUrl string, callback string) (string, error) { + // check if callback is set + if callback == "" { + callback = "/oidc/callback" + } + var code string r := chi.NewRouter() r.Use(middleware.RedirectSlashes) @@ -53,7 +59,7 @@ func (s *Server) WaitForAuthorizationCode(loginUrl string) (string, error) { loginPage = []byte(strings.ReplaceAll(string(loginPage), "{{loginUrl}}", loginUrl)) w.Write(loginPage) }) - r.HandleFunc("/oidc/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") From 3b5fea2299d2442c998356be26dfa344a377d61a Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 5 Mar 2024 20:38:58 -0700 Subject: [PATCH 15/26] Updated dependencies --- go.mod | 17 ++++++++-- go.sum | 102 +++++++++++++++++++++++++++++++-------------------------- 2 files changed, 70 insertions(+), 49 deletions(-) diff --git a/go.mod b/go.mod index 26682a8..e0ad618 100644 --- a/go.mod +++ b/go.mod @@ -6,25 +6,38 @@ require ( github.com/davidallendj/go-utils v0.0.0-20240302194916-fe292bcf24a4 github.com/go-chi/chi v1.5.5 github.com/go-chi/chi/v5 v5.0.12 + github.com/google/uuid v1.6.0 github.com/jmoiron/sqlx v1.3.5 - github.com/lestrrat-go/jwx v1.2.28 + github.com/lestrrat-go/jwx/v2 v2.0.20 + github.com/mattn/go-sqlite3 v1.14.6 + github.com/nikolalohinski/gonja/v2 v2.2.0 github.com/spf13/cobra v1.8.0 golang.org/x/net v0.21.0 gopkg.in/yaml.v2 v2.4.0 + k8s.io/apimachinery v0.29.2 ) require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect + 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.19.0 // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 60a1152..ff8a3de 100644 --- a/go.sum +++ b/go.sum @@ -1,108 +1,116 @@ +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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-20240302194916-fe292bcf24a4 h1:6LeOczLfpq27cDfu4r6bRU3zGeBER9fy+iecHG5dDSA= github.com/davidallendj/go-utils v0.0.0-20240302194916-fe292bcf24a4/go.mod h1:/hcpHd4um12taX6iLuMmwxosoyN6E2Ws8QxDpnY07oo= -github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 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/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 v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= -github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= -github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= +github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx v1.2.28 h1:uadI6o0WpOVrBSf498tRXZIwPpEtLnR9CvqPFXeI5sA= -github.com/lestrrat-go/jwx v1.2.28/go.mod h1:nF+91HEMh/MYFVwKPl5HHsBGMPscqbQb+8IDQdIazP8= -github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/jwx/v2 v2.0.20 h1:sAgXuWS/t8ykxS9Bi2Qtn5Qhpakw1wrcjxChudjolCc= +github.com/lestrrat-go/jwx/v2 v2.0.20/go.mod h1:UlCSmKqw+agm5BsOBfEAbTvKsEApaGNqHAEUTv5PJC4= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nikolalohinski/gonja/v2 v2.2.0 h1:tAs3BDHNjvPj48F2BL5t7iVhN32HhgeldAl3EmdsLh8= +github.com/nikolalohinski/gonja/v2 v2.2.0/go.mod h1:l9DuWJvT/BddBr2SsmEimESD6msSqRw7u5HzI2Um+sc= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.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.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= 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= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= +k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= From 3d273fb0562e448e8a9e64dc10cc72872ac34a70 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 5 Mar 2024 20:39:32 -0700 Subject: [PATCH 16/26] Updated .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d372638..bf06519 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.json +*.db opaal From 3d3678e9d910b857de5d4870b0792bff8b2f624e Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 5 Mar 2024 20:39:51 -0700 Subject: [PATCH 17/26] Updated index.html --- pages/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/index.html b/pages/index.html index 5548a5d..ee880f1 100644 --- a/pages/index.html +++ b/pages/index.html @@ -2,5 +2,5 @@ Welcome to Opaal's default login in page! Click the link below to log in with your identity provider:

- Login + Login \ No newline at end of file From 98521f0b043e8ac89abfdba09a368647824a4f1c Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 5 Mar 2024 20:41:32 -0700 Subject: [PATCH 18/26] Added stratgies.access_token to Hydra config --- docker/configs/hydra/hydra.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/configs/hydra/hydra.yml b/docker/configs/hydra/hydra.yml index 765cebb..ae49165 100644 --- a/docker/configs/hydra/hydra.yml +++ b/docker/configs/hydra/hydra.yml @@ -36,3 +36,6 @@ oauth2: log: leak_sensitive_values: true + +strategies: + access_token: jwt From 082c891ac4a9d84496d18c122e9b220f7fa63d73 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 5 Mar 2024 20:42:10 -0700 Subject: [PATCH 19/26] Added necessary dependency for SQLite --- internal/db/sqlite.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/db/sqlite.go b/internal/db/sqlite.go index 37d34b3..147fc72 100644 --- a/internal/db/sqlite.go +++ b/internal/db/sqlite.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" ) func CreateIdentityProvidersIfNotExists(path string) (*sqlx.DB, error) { From 1e2242bad146629b0d088b15b1a15b1053df3c00 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 5 Mar 2024 20:42:58 -0700 Subject: [PATCH 20/26] Fixed issue with JWKS fetching --- cmd/login.go | 7 +++++++ internal/oidc/oidc.go | 33 ++++++++++----------------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/cmd/login.go b/cmd/login.go index b318993..187f266 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -22,15 +22,22 @@ var loginCmd = &cobra.Command{ // try and find client with valid identity provider config var provider *oidc.IdentityProvider for _, c := range config.Authentication.Clients { + // try to get identity provider info locally first _, err := db.GetIdentityProvider(config.Options.CachePath, c.Issuer) if err != nil && !config.Options.LocalOnly { 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 } } diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index 4f509a9..543905b 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -9,14 +9,14 @@ import ( "net/http" "os" - "github.com/lestrrat-go/jwx/jwk" + "github.com/lestrrat-go/jwx/v2/jwk" ) type IdentityProvider struct { Issuer string `db:"issuer" json:"issuer" yaml:"issuer"` Endpoints Endpoints `db:"endpoints" json:"endpoints" yaml:"endpoints"` Supported Supported `db:"supported" json:"supported" yaml:"supported"` - Key jwk.Key + Jwks jwk.Set } type Endpoints struct { @@ -25,7 +25,7 @@ type Endpoints struct { Revocation string `db:"revocation_endpoint" json:"revocation_endpoint" yaml:"revocation"` Introspection string `db:"introspection_endpoint" json:"introspection_endpoint" yaml:"introspection"` UserInfo string `db:"userinfo_endpoint" json:"userinfo_endpoint" yaml:"userinfo"` - Jwks string `db:"jwks_uri" json:"jwks_uri" yaml:"jwks_uri"` + JwksUri string `db:"jwks_uri" json:"jwks_uri" yaml:"jwks_uri"` } type Supported struct { ResponseTypes []string `db:"response_types_supported" json:"response_types_supported"` @@ -46,7 +46,7 @@ func NewIdentityProvider() *IdentityProvider { Revocation: p.Issuer + "/oauth/revocation", Introspection: p.Issuer + "/oauth/introspect", UserInfo: p.Issuer + "/oauth/userinfo", - Jwks: p.Issuer + "/oauth/discovery/keys", + JwksUri: p.Issuer + "/oauth/discovery/keys", } p.Supported = Supported{ ResponseTypes: []string{"code"}, @@ -134,31 +134,18 @@ func FetchServerConfig(issuer string) (*IdentityProvider, error) { return &p, nil } -func (p *IdentityProvider) FetchJwk() error { - if p.Endpoints.Jwks == "" { +func (p *IdentityProvider) FetchJwks() error { + if p.Endpoints.JwksUri == "" { return fmt.Errorf("JWKS endpoint not set") } // fetch JWKS from identity provider ctx, cancel := context.WithCancel(context.Background()) defer cancel() - set, err := jwk.Fetch(ctx, p.Endpoints.Jwks) + var err error + p.Jwks, err = jwk.Fetch(ctx, p.Endpoints.JwksUri) if err != nil { - return fmt.Errorf("%v", err) - } - // get the first JWK from set - for it := set.Iterate(context.Background()); it.Next(context.Background()); { - pair := it.Pair() - p.Key = pair.Value.(jwk.Key) - return nil + return fmt.Errorf("failed to fetch JWKS: %v", err) } - return fmt.Errorf("failed to load public key: %v", err) -} - -func (p *IdentityProvider) GetRawJwk() (any, error) { - var rawkey any - if err := p.Key.Raw(&rawkey); err != nil { - return nil, fmt.Errorf("failed to get raw key: %v", err) - } - return rawkey, nil + return nil } From d2c5ec8dfcfb6f328c689a36cdc3c4518e78ae73 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 5 Mar 2024 20:43:33 -0700 Subject: [PATCH 21/26] Updated .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bf06519..2c385f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.json *.db +*.code-workspace opaal From cfba9e4bd0f50c2f04823c6468a764c67fbcdb4e Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 5 Mar 2024 20:44:33 -0700 Subject: [PATCH 22/26] Updated example config --- config.yaml | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/config.yaml b/config.yaml index 02c1c0a..48308d3 100755 --- a/config.yaml +++ b/config.yaml @@ -1,5 +1,5 @@ version: "0.0.1" -server: +server: host: "127.0.0.1" port: 3333 callback: "/oidc/callback" @@ -12,23 +12,6 @@ providers: authentication: clients: - - id: "1135541217802147" - secret: "b3a3123e8235de1dbab448369bc3d024" - issuer: "https://www.facebook.com" - scope: - - "openid" - - "name" - - "email" - redirect-uris: - - "http://127.0.0.1:3333/oidc/callback" - - id: "978b48059dd4916f53b4" - secret: "eb54b533eb6afd695e3a1b3f363ab2b29acc7425" - issuer: "https://github.com" - scope: - - "openid" - - "profile" - redirect-uris: - - "http://127.0.0.1:3333/oidc/callback" - id: "7527e7b4-c96a-4df0-8fc5-00fde18bb65d" secret: "gto_cc5uvpb5lsdczkwnbarvwmbpv5kcjwg7nhbc75zt65yrfh2ldenq" name: "forgejo" From 53de1ca726707e104a5bcfa298b5813413c5a203 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 5 Mar 2024 20:45:15 -0700 Subject: [PATCH 23/26] Added flag to forward ID token --- internal/config.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/config.go b/internal/config.go index 362f151..f4e6c4e 100644 --- a/internal/config.go +++ b/internal/config.go @@ -22,6 +22,7 @@ type Options struct { FlowType string `yaml:"flow"` CachePath string `yaml:"cache"` LocalOnly bool `yaml:"local-only"` + ForwardToken bool `yaml:"forward-token"` } type RequestUrls struct { @@ -67,6 +68,7 @@ func NewConfig() Config { CachePath: "opaal.db", FlowType: "authorization_code", LocalOnly: false, + ForwardToken: false, }, Authentication: Authentication{}, Authorization: Authorization{}, From 64f75345cdacffd4bd130c7f2afc67702cea9e94 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 5 Mar 2024 20:46:17 -0700 Subject: [PATCH 24/26] Minor changes --- internal/authorize.go | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/internal/authorize.go b/internal/authorize.go index 8190b6d..a778606 100644 --- a/internal/authorize.go +++ b/internal/authorize.go @@ -14,21 +14,19 @@ import ( "github.com/davidallendj/go-utils/httpx" "github.com/davidallendj/go-utils/util" + "github.com/lestrrat-go/jwx/v2/jwk" ) -func (client *Client) AddTrustedIssuer(url string, idp *oidc.IdentityProvider, subject string, duration time.Duration) ([]byte, error) { +func (client *Client) AddTrustedIssuer(url string, issuer string, key jwk.Key, subject string, expires time.Duration) ([]byte, error) { // hydra endpoint: POST /admin/trust/grants/jwt-bearer/issuers - if idp == nil { - return nil, fmt.Errorf("identity provided is nil") - } - jwkstr, err := json.Marshal(idp.Key) - if err != nil { - return nil, fmt.Errorf("failed to marshal JWK: %v", err) - } quotedScopes := make([]string, len(client.Scope)) for i, s := range client.Scope { quotedScopes[i] = fmt.Sprintf("\"%s\"", s) } + jwkstr, err := json.Marshal(key) + if err != nil { + return nil, fmt.Errorf("failed to marshal JWK: %v", err) + } // NOTE: Can also include "jwks_uri" instead data := []byte(fmt.Sprintf("{"+ "\"allow_any_subject\": false,"+ @@ -37,7 +35,7 @@ func (client *Client) AddTrustedIssuer(url string, idp *oidc.IdentityProvider, s "\"expires_at\": \"%v\","+ "\"jwk\": %v,"+ "\"scope\": [ %s ]"+ - "}", idp.Issuer, subject, time.Now().Add(duration).Format(time.RFC3339), string(jwkstr), strings.Join(quotedScopes, ","))) + "}", issuer, subject, time.Now().Add(expires).Format(time.RFC3339), string(jwkstr), strings.Join(quotedScopes, ","))) req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) // req.Header.Add("X-CSRF-Token", client.CsrfToken.Value) @@ -55,6 +53,15 @@ func (client *Client) AddTrustedIssuer(url string, idp *oidc.IdentityProvider, s return io.ReadAll(res.Body) } +func (client *Client) AddTrustedIssuerWithIdentityProvider(url string, idp *oidc.IdentityProvider, subject string, expires time.Duration) ([]byte, error) { + // hydra endpoint: POST /admin/trust/grants/jwt-bearer/issuers + key, ok := idp.Jwks.Key(0) + if !ok { + return nil, fmt.Errorf("no keys found in key set") + } + return client.AddTrustedIssuer(url, idp.Issuer, key, subject, expires) +} + func (client *Client) IsOAuthClientRegistered(clientUrl string) (bool, error) { _, _, err := httpx.MakeHttpRequest(clientUrl, http.MethodGet, nil, nil) if err != nil { @@ -206,15 +213,15 @@ func (client *Client) AuthorizeOAuthClient(authorizeUrl string) ([]byte, error) return b, nil } -func (client *Client) PerformTokenGrant(clientUrl string, jwt string) ([]byte, error) { +func (client *Client) PerformTokenGrant(clientUrl string, encodedJwt string) ([]byte, error) { // hydra endpoint: /oauth/token body := "grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") + "&client_id=" + client.Id + "&client_secret=" + client.Secret + "&redirect_uri=" + url.QueryEscape("http://127.0.0.1:3333/callback") // add optional params if valid - if jwt != "" { - body += "&assertion=" + jwt + if encodedJwt != "" { + body += "&assertion=" + encodedJwt } if client.Scope != nil || len(client.Scope) > 0 { body += "&scope=" + strings.Join(client.Scope, "+") From dc195afc61d9f6b953e188d2d8c7e35e6e29f593 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 5 Mar 2024 20:47:02 -0700 Subject: [PATCH 25/26] Changed replace text in HTML to use gonja template --- internal/server.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/internal/server.go b/internal/server.go index 4984902..7c11426 100644 --- a/internal/server.go +++ b/internal/server.go @@ -8,6 +8,8 @@ import ( "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" + "github.com/nikolalohinski/gonja/v2" + "github.com/nikolalohinski/gonja/v2/exec" ) type Server struct { @@ -52,12 +54,18 @@ func (s *Server) WaitForAuthorizationCode(loginUrl string, callback string) (str }) r.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { // show login page with notice to redirect - loginPage, err := os.ReadFile("pages/index.html") + template, err := gonja.FromFile("pages/index.html") if err != nil { - fmt.Printf("failed to load login page: %v\n", err) + panic(err) + } + + data := exec.NewContext(map[string]interface{}{ + "loginUrl": loginUrl, + }) + + if err = template.Execute(w, data); err != nil { // Prints: Hello Bob! + panic(err) } - loginPage = []byte(strings.ReplaceAll(string(loginPage), "{{loginUrl}}", loginUrl)) - w.Write(loginPage) }) r.HandleFunc(callback, func(w http.ResponseWriter, r *http.Request) { // get the code from the OIDC provider From d910e98f72d979723c71ff2e8ec9f693950cc242 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Tue, 5 Mar 2024 20:49:26 -0700 Subject: [PATCH 26/26] Reworked flow to consume ID token and to use own JWT --- internal/authorization_code.go | 325 +++++++++++++++++++++++---------- 1 file changed, 229 insertions(+), 96 deletions(-) diff --git a/internal/authorization_code.go b/internal/authorization_code.go index 26d4a2f..efc8ae2 100644 --- a/internal/authorization_code.go +++ b/internal/authorization_code.go @@ -1,6 +1,8 @@ package opaal import ( + "crypto/rand" + "crypto/rsa" "davidallendj/opaal/internal/oidc" "encoding/json" "errors" @@ -10,6 +12,11 @@ import ( "time" "github.com/davidallendj/go-utils/util" + "github.com/google/uuid" + "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" ) // TODO: change authorization code flow to use these instead @@ -123,129 +130,255 @@ func AuthorizationCodeWithConfig(config *Config, server *Server, client *Client, fmt.Println() } - // extract the scope from access token claims - // var scope []string - // var accessJsonPayload map[string]any - // var accessJwtPayload []byte = accessJwtSegments[1] - // if accessJsonPayload != nil { - // err := json.Unmarshal(accessJwtPayload, &accessJsonPayload) - // if err != nil { - // return fmt.Errorf("failed to unmarshal JWT: %v", err) - // } - // scope = idJsonPayload["scope"].([]string) - // } + if !config.Options.ForwardToken { - // create a new identity with identity and session manager if url is provided - // if config.RequestUrls.Identities != "" { - // fmt.Printf("Attempting to create a new identity...\n") - // err := client.CreateIdentity(config.RequestUrls.Identities, idToken) - // if err != nil { - // return fmt.Errorf("failed to create new identity: %v", err) - // } - // _, err = client.FetchIdentities(config.RequestUrls.Identities) - // if err != nil { - // return fmt.Errorf("failed to fetch identities: %v", err) - // } - // fmt.Printf("Created new identity successfully.\n\n") - // } + // TODO: implement our own JWT to send to Hydra + // 1. verify that the JWT from the issuer is valid + key, ok := idp.Jwks.Key(0) + if !ok { + return fmt.Errorf("no key found in key set") + } - // extract the subject from ID token claims - var subject string - var audience []string - var idJsonPayload map[string]any - var idJwtPayload []byte = idJwtSegments[1] - if idJwtPayload != nil { - err := json.Unmarshal(idJwtPayload, &idJsonPayload) + parsedIdToken, err := jwt.ParseString(idToken, jwt.WithKey(jwa.RS256, key)) if err != nil { - return fmt.Errorf("failed to unmarshal JWT: %v", err) + return fmt.Errorf("failed to parse ID token: %v", err) } - subject = idJsonPayload["sub"].(string) - audType := reflect.ValueOf(idJsonPayload["aud"]) - switch audType.Kind() { - case reflect.String: - audience = append(audience, idJsonPayload["aud"].(string)) - case reflect.Array: - audience = idJsonPayload["aud"].([]string) + _, err = jwt.ParseString(accessToken, jwt.WithKeySet(idp.Jwks)) + if err != nil { + return fmt.Errorf("failed to parse access token: %v", err) } - } else { - return fmt.Errorf("failed to extract subject from ID token claims") - } - // fetch JWKS and add issuer to authentication server to submit ID token - fmt.Printf("Fetching JWKS from authentication server for verification...\n") - err = idp.FetchJwk() - if err != nil { - return fmt.Errorf("failed to fetch JWK: %v", err) - } else { - fmt.Printf("Successfully retrieved JWK from authentication server.\n\n") + _, err = jws.Verify([]byte(idToken), jws.WithKeySet(idp.Jwks), jws.WithValidateKey(true)) + if err != nil { + return fmt.Errorf("failed to verify JWT: %v", err) + } + + // 2. create a new JWKS (or just JWK) to be verified + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return fmt.Errorf("failed to generate private RSA k-ey: %v", err) + } + privateJwk, err := jwk.FromRaw(privateKey) + if err != nil { + return fmt.Errorf("failed to create private JWK: %v", err) + } + publicJwk, err := jwk.PublicKeyOf(privateJwk) + if err != nil { + return fmt.Errorf("failed to create public JWK: %v", err) + } + publicJwk.Set("kid", uuid.New().String()) + + // 3. add opaal's server host as a trusted issuer with JWK fmt.Printf("Attempting to add issuer to authorization server...\n") res, err := client.AddTrustedIssuer( config.Authorization.RequestUrls.TrustedIssuers, - idp, - subject, - time.Duration(1000), + server.Addr, + publicJwk, + "1", + time.Second*3600, ) if err != nil { return fmt.Errorf("failed to add trusted issuer: %v", err) } fmt.Printf("%v\n", string(res)) - } - // add client ID to audience - audience = append(audience, client.Id) - audience = append(audience, "http://127.0.0.1:4444/oauth2/token") + // 4. create a new JWT based on the claims from the identity provider and sign + payload := parsedIdToken.PrivateClaims() + payload["iss"] = server.Addr + payload["aud"] = []string{config.Authorization.RequestUrls.Token} + payload["iat"] = time.Now().Unix() + payload["nbf"] = time.Now().Unix() + payload["exp"] = time.Now().Add(time.Second * 3600).Unix() + payload["sub"] = "1" + payloadJson, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("failed to marshal payload: %v", err) + } + newToken, err := jws.Sign(payloadJson, jws.WithJSON(), jws.WithKey(jwa.RS256, privateJwk)) + if err != nil { + return fmt.Errorf("failed to sign token: %v", err) + } - // try and register a new client with authorization server - fmt.Printf("Registering new OAuth2 client with authorization server...\n") - res, err := client.RegisterOAuthClient(config.Authorization.RequestUrls.Register, audience) - if err != nil { - return fmt.Errorf("failed to register client: %v", err) - } - fmt.Printf("%v\n", string(res)) + // sig = rsasha256(b64urlencode(header) + "." + b64urlencode(payload)) + // signature := util.EncodeBase64() + util.EncodeBase64() + - // extract the client info from response - var clientData map[string]any - err = json.Unmarshal(res, &clientData) - if err != nil { - return fmt.Errorf("failed to unmarshal client data: %v", err) - } else { - // check for error first - errJson := clientData["error"] - if errJson == nil { - client.Id = clientData["client_id"].(string) - client.Secret = clientData["client_secret"].(string) + // 5. dynamically register new OAuth client and authorize it + fmt.Printf("Registering new OAuth2 client with authorization server...\n") + res, err = client.RegisterOAuthClient(config.Authorization.RequestUrls.Register, []string{}) + if err != nil { + return fmt.Errorf("failed to register client: %v", err) + } + fmt.Printf("%v\n", string(res)) + + // extract the client info from response + var clientData map[string]any + err = json.Unmarshal(res, &clientData) + if err != nil { + return fmt.Errorf("failed to unmarshal client data: %v", err) } else { - // delete client and create again - fmt.Printf("Attempting to delete client...\n") - err := client.DeleteOAuthClient(config.Authorization.RequestUrls.Clients) - if err != nil { - return fmt.Errorf("failed to delete OAuth client: %v", err) + // check for error first + errJson := clientData["error"] + if errJson == nil { + client.Id = clientData["client_id"].(string) + client.Secret = clientData["client_secret"].(string) + } else { + // delete client and create again + fmt.Printf("Attempting to delete client...\n") + err := client.DeleteOAuthClient(config.Authorization.RequestUrls.Clients) + if err != nil { + return fmt.Errorf("failed to delete OAuth client: %v", err) + } + fmt.Printf("Attempting to re-create client...\n") + res, err := client.CreateOAuthClient(config.Authorization.RequestUrls.Clients, []string{}) + if err != nil { + return fmt.Errorf("failed to register client: %v", err) + } + fmt.Printf("%v\n", string(res)) } - fmt.Printf("Attempting to re-create client...\n") - res, err := client.CreateOAuthClient(config.Authorization.RequestUrls.Clients, audience) + } + + // authorize the client + // fmt.Printf("Attempting to authorize client...\n") + // res, err = client.AuthorizeOAuthClient(config.Authorization.RequestUrls.Authorize) + // if err != nil { + // return fmt.Errorf("failed to authorize client: %v", err) + // } + // fmt.Printf("%v\n", string(res)) + + // 6. send JWT to authorization server and receive a access token + if config.Authorization.RequestUrls.Token != "" { + fmt.Printf("Fetching access token from authorization server...\n") + res, err := client.PerformTokenGrant(config.Authorization.RequestUrls.Token, string(newToken)) if err != nil { - return fmt.Errorf("failed to register client: %v", err) + return fmt.Errorf("failed to fetch access token: %v", err) + } + fmt.Printf("%s\n", res) + } + } else { + // extract the scope from access token claims + // var scope []string + // var accessJsonPayload map[string]any + // var accessJwtPayload []byte = accessJwtSegments[1] + // if accessJsonPayload != nil { + // err := json.Unmarshal(accessJwtPayload, &accessJsonPayload) + // if err != nil { + // return fmt.Errorf("failed to unmarshal JWT: %v", err) + // } + // scope = idJsonPayload["scope"].([]string) + // } + + // create a new identity with identity and session manager if url is provided + // if config.RequestUrls.Identities != "" { + // fmt.Printf("Attempting to create a new identity...\n") + // err := client.CreateIdentity(config.RequestUrls.Identities, idToken) + // if err != nil { + // return fmt.Errorf("failed to create new identity: %v", err) + // } + // _, err = client.FetchIdentities(config.RequestUrls.Identities) + // if err != nil { + // return fmt.Errorf("failed to fetch identities: %v", err) + // } + // fmt.Printf("Created new identity successfully.\n\n") + // } + + // extract the subject from ID token claims + var subject string + var audience []string + var idJsonPayload map[string]any + var idJwtPayload []byte = idJwtSegments[1] + if idJwtPayload != nil { + err := json.Unmarshal(idJwtPayload, &idJsonPayload) + if err != nil { + return fmt.Errorf("failed to unmarshal JWT: %v", err) + } + subject = idJsonPayload["sub"].(string) + audType := reflect.ValueOf(idJsonPayload["aud"]) + switch audType.Kind() { + case reflect.String: + audience = append(audience, idJsonPayload["aud"].(string)) + case reflect.Array: + audience = idJsonPayload["aud"].([]string) + } + } else { + return fmt.Errorf("failed to extract subject from ID token claims") + } + + // fetch JWKS and add issuer to authentication server to submit ID token + fmt.Printf("Fetching JWKS from authentication server for verification...\n") + err = idp.FetchJwks() + if err != nil { + return fmt.Errorf("failed to fetch JWK: %v", err) + } else { + fmt.Printf("Successfully retrieved JWK from authentication server.\n\n") + fmt.Printf("Attempting to add issuer to authorization server...\n") + res, err := client.AddTrustedIssuerWithIdentityProvider( + config.Authorization.RequestUrls.TrustedIssuers, + idp, + subject, + time.Duration(1000), + ) + if err != nil { + return fmt.Errorf("failed to add trusted issuer: %v", err) } fmt.Printf("%v\n", string(res)) } - } - // authorize the client - // fmt.Printf("Attempting to authorize client...\n") - // res, err = client.AuthorizeOAuthClient(config.Authorization.RequestUrls.Authorize) - // if err != nil { - // return fmt.Errorf("failed to authorize client: %v", err) - // } - // fmt.Printf("%v\n", string(res)) + // add client ID to audience + audience = append(audience, client.Id) + audience = append(audience, "http://127.0.0.1:4444/oauth2/token") - // use ID token/user info to fetch access token from authentication server - if config.Authorization.RequestUrls.Token != "" { - fmt.Printf("Fetching access token from authorization server...\n") - res, err := client.PerformTokenGrant(config.Authorization.RequestUrls.Token, idToken) + // try and register a new client with authorization server + fmt.Printf("Registering new OAuth2 client with authorization server...\n") + res, err := client.RegisterOAuthClient(config.Authorization.RequestUrls.Register, audience) if err != nil { - return fmt.Errorf("failed to fetch access token: %v", err) + return fmt.Errorf("failed to register client: %v", err) + } + fmt.Printf("%v\n", string(res)) + + // extract the client info from response + var clientData map[string]any + err = json.Unmarshal(res, &clientData) + if err != nil { + return fmt.Errorf("failed to unmarshal client data: %v", err) + } else { + // check for error first + errJson := clientData["error"] + if errJson == nil { + client.Id = clientData["client_id"].(string) + client.Secret = clientData["client_secret"].(string) + } else { + // delete client and create again + fmt.Printf("Attempting to delete client...\n") + err := client.DeleteOAuthClient(config.Authorization.RequestUrls.Clients) + if err != nil { + return fmt.Errorf("failed to delete OAuth client: %v", err) + } + fmt.Printf("Attempting to re-create client...\n") + res, err := client.CreateOAuthClient(config.Authorization.RequestUrls.Clients, audience) + if err != nil { + return fmt.Errorf("failed to register client: %v", err) + } + fmt.Printf("%v\n", string(res)) + } + } + + // authorize the client + // fmt.Printf("Attempting to authorize client...\n") + // res, err = client.AuthorizeOAuthClient(config.Authorization.RequestUrls.Authorize) + // if err != nil { + // return fmt.Errorf("failed to authorize client: %v", err) + // } + // fmt.Printf("%v\n", string(res)) + + // use ID token/user info to fetch access token from authentication server + if config.Authorization.RequestUrls.Token != "" { + fmt.Printf("Fetching access token from authorization server...\n") + res, err := client.PerformTokenGrant(config.Authorization.RequestUrls.Token, idToken) + if err != nil { + return fmt.Errorf("failed to fetch access token: %v", err) + } + fmt.Printf("%s\n", res) } - fmt.Printf("%s\n", res) } var access_token []byte d <- access_token