From 1a4afd2d166ca1d4ad7181b8dd3d595f3052460a Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Sun, 25 Feb 2024 09:59:20 -0700 Subject: [PATCH] Reorganized server and client internal code --- internal/client.go | 152 ++++++++++++++++++++++++++++++++ internal/opaal.go | 213 --------------------------------------------- internal/server.go | 69 +++++++++++++++ 3 files changed, 221 insertions(+), 213 deletions(-) create mode 100644 internal/client.go create mode 100644 internal/server.go diff --git a/internal/client.go b/internal/client.go new file mode 100644 index 0000000..08de8ff --- /dev/null +++ b/internal/client.go @@ -0,0 +1,152 @@ +package opaal + +import ( + "bytes" + "davidallendj/opaal/internal/oidc" + "davidallendj/opaal/internal/util" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/cookiejar" + "net/url" + "strings" + "time" + + "golang.org/x/net/publicsuffix" +) + +type Client struct { + http.Client + Id string `yaml:"id"` + Secret string `yaml:"secret"` + RedirectUris []string `yaml:"redirect-uris"` +} + +func NewClientWithConfig(config *Config) *Client { + jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + return &Client{ + Id: config.Client.Id, + Secret: config.Client.Secret, + RedirectUris: config.Client.RedirectUris, + Client: http.Client{Jar: jar}, + } +} + +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, ",")) + + "&response_type=" + responseType + + "&state=" + state + + "&scope=" + strings.Join(scope, "+") +} + +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, ",")}, + } + res, err := http.PostForm(remoteUrl, data) + if err != nil { + return nil, fmt.Errorf("failed to get ID token: %s", err) + } + defer res.Body.Close() + + return io.ReadAll(res.Body) +} + +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") + + "&assertion=" + jwt + + "&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() + + 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: /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) + } + data := []byte(fmt.Sprintf(`{ + "allow_any_subject": true, + "issuer": "%s", + "subject": "%s" + "expires_at": "%v" + "jwk": %v, + "scope": [ %s ], + }`, idp.Issuer, subject, time.Now().Add(duration), string(jwkstr), strings.Join(scope, ","))) + + 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) 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) +} diff --git a/internal/opaal.go b/internal/opaal.go index ec2c1da..b82cbea 100644 --- a/internal/opaal.go +++ b/internal/opaal.go @@ -1,35 +1,5 @@ package opaal -import ( - "bytes" - "davidallendj/opaal/internal/oidc" - "davidallendj/opaal/internal/util" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/cookiejar" - "net/url" - "os" - "strings" - "time" - - "golang.org/x/net/publicsuffix" -) - -type Server struct { - http.Server - Host string `yaml:"host"` - Port int `yaml:"port"` -} - -type Client struct { - http.Client - Id string `yaml:"id"` - Secret string `yaml:"secret"` - RedirectUris []string `yaml:"redirect-uris"` -} - type ActionUrls struct { Identities string `yaml:"identities"` TrustedIssuers string `yaml:"trusted-issuers"` @@ -38,189 +8,6 @@ type ActionUrls struct { JwksUri string `yaml:"jwks_uri"` } -func NewServerWithConfig(config *Config) *Server { - host := config.Server.Host - port := config.Server.Port - server := &Server{ - Host: host, - Port: port, - } - server.Addr = fmt.Sprintf("%s:%d", host, port) - return server -} - -func NewClientWithConfig(config *Config) *Client { - jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) - return &Client{ - Id: config.Client.Id, - Secret: config.Client.Secret, - RedirectUris: config.Client.RedirectUris, - Client: http.Client{Jar: jar}, - } -} - -func (s *Server) SetListenAddr(host string, port int) { - s.Host = host - s.Port = port - s.Addr = s.GetListenAddr() -} - -func (s *Server) GetListenAddr() string { - return fmt.Sprintf("%s:%d", s.Host, s.Port) -} - -func (s *Server) WaitForAuthorizationCode(loginUrl string) (string, error) { - var code string - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/login", http.StatusSeeOther) - }) - http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { - // show login page with notice to redirect - loginPage, err := os.ReadFile("pages/index.html") - if err != nil { - fmt.Printf("failed to load login page: %v\n", err) - } - loginPage = []byte(strings.ReplaceAll(string(loginPage), "{{loginUrl}}", loginUrl)) - w.WriteHeader(http.StatusSeeOther) - w.Write(loginPage) - }) - http.HandleFunc("/oidc/callback", func(w http.ResponseWriter, r *http.Request) { - // get the code from the OIDC provider - if r != nil { - code = r.URL.Query().Get("code") - fmt.Printf("Authorization code: %v\n", code) - } - http.Redirect(w, r, s.Addr+"/success", http.StatusSeeOther) - s.Close() - }) - return code, s.ListenAndServe() -} - -func (s *Server) ShowSuccessPage() error { - http.HandleFunc("/success", func(w http.ResponseWriter, r *http.Request) { - - }) - return s.ListenAndServe() -} - -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, ",")) + - "&response_type=" + responseType + - "&state=" + state + - "&scope=" + strings.Join(scope, "+") -} - -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, ",")}, - } - res, err := http.PostForm(remoteUrl, data) - if err != nil { - return nil, fmt.Errorf("failed to get ID token: %s", err) - } - defer res.Body.Close() - - return io.ReadAll(res.Body) -} - -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") + - "&assertion=" + jwt + - "&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() - - 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: /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) - } - data := []byte(fmt.Sprintf(`{ - "allow_any_subject": true, - "issuer": "%s", - "subject": "%s" - "expires_at": "%v" - "jwk": %v, - "scope": [ %s ], - }`, idp.Issuer, subject, time.Now().Add(duration), string(jwkstr), strings.Join(scope, ","))) - - 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) 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) -} - func hasRequiredParams(config *Config) bool { return config.Client.Id != "" && config.Client.Secret != "" } diff --git a/internal/server.go b/internal/server.go new file mode 100644 index 0000000..3936b00 --- /dev/null +++ b/internal/server.go @@ -0,0 +1,69 @@ +package opaal + +import ( + "fmt" + "net/http" + "os" + "strings" +) + +type Server struct { + http.Server + Host string `yaml:"host"` + Port int `yaml:"port"` +} + +func NewServerWithConfig(config *Config) *Server { + host := config.Server.Host + port := config.Server.Port + server := &Server{ + Host: host, + Port: port, + } + server.Addr = fmt.Sprintf("%s:%d", host, port) + return server +} + +func (s *Server) SetListenAddr(host string, port int) { + s.Host = host + s.Port = port + s.Addr = s.GetListenAddr() +} + +func (s *Server) GetListenAddr() string { + return fmt.Sprintf("%s:%d", s.Host, s.Port) +} + +func (s *Server) WaitForAuthorizationCode(loginUrl string) (string, error) { + var code string + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/login", http.StatusSeeOther) + }) + http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { + // show login page with notice to redirect + loginPage, err := os.ReadFile("pages/index.html") + if err != nil { + fmt.Printf("failed to load login page: %v\n", err) + } + loginPage = []byte(strings.ReplaceAll(string(loginPage), "{{loginUrl}}", loginUrl)) + w.WriteHeader(http.StatusSeeOther) + w.Write(loginPage) + }) + http.HandleFunc("/oidc/callback", func(w http.ResponseWriter, r *http.Request) { + // get the code from the OIDC provider + if r != nil { + code = r.URL.Query().Get("code") + fmt.Printf("Authorization code: %v\n", code) + } + http.Redirect(w, r, s.Addr+"/success", http.StatusSeeOther) + s.Close() + }) + return code, s.ListenAndServe() +} + +func (s *Server) ShowSuccessPage() error { + http.HandleFunc("/success", func(w http.ResponseWriter, r *http.Request) { + + }) + return s.ListenAndServe() +}