mirror of
https://github.com/davidallendj/opaal.git
synced 2025-12-20 11:37:01 -07:00
Reorganized server and client internal code
This commit is contained in:
parent
6d482cc60f
commit
1a4afd2d16
3 changed files with 221 additions and 213 deletions
152
internal/client.go
Normal file
152
internal/client.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -1,35 +1,5 @@
|
||||||
package opaal
|
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 {
|
type ActionUrls struct {
|
||||||
Identities string `yaml:"identities"`
|
Identities string `yaml:"identities"`
|
||||||
TrustedIssuers string `yaml:"trusted-issuers"`
|
TrustedIssuers string `yaml:"trusted-issuers"`
|
||||||
|
|
@ -38,189 +8,6 @@ type ActionUrls struct {
|
||||||
JwksUri string `yaml:"jwks_uri"`
|
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 {
|
func hasRequiredParams(config *Config) bool {
|
||||||
return config.Client.Id != "" && config.Client.Secret != ""
|
return config.Client.Id != "" && config.Client.Secret != ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
69
internal/server.go
Normal file
69
internal/server.go
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue