From 8b5d9ab6dd467967ec5322ad1cd38eb1992b8ed4 Mon Sep 17 00:00:00 2001
From: "David J. Allen"
Date: Sun, 10 Mar 2024 20:14:38 -0600
Subject: [PATCH 01/11] Updated README.md
---
README.md | 19 +++++++++++++++++--
1 file changed, 17 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index e3a95a1..34952fb 100644
--- a/README.md
+++ b/README.md
@@ -23,9 +23,17 @@ To start the authentication flow, run the following commands:
./opaal login --flow authorization_code --config config.yaml
```
-These commands will create a default config, then start the login process. Maybe sure to change the config file to match your setup!
+These commands will create a default config, then start the login process. Maybe sure to change the config file to match your setup! The tool has been tested and confirmed to work with the following identity providers so far:
+
+- [Gitlab](https://about.gitlab.com/)
+- [Forgejo](https://forgejo.org/) (fork of Gitea)
+
+### Authorization Code Flow
+
+`opaal` has the ability to completely execute the authorization code and return an access token from an authorization server using social sign-in. The process works as follows:
1. Click the authorization link or navigate to the hosted endpoint in your browser (127.0.0.1:3333 by default)
+ - Alternatively, you can use a link produced
2. Login using identity provider credentials
3. Authorize application registered with IdP
4. IdP redirects to specified redirect URI
@@ -37,6 +45,11 @@ These commands will create a default config, then start the login process. Maybe
*After receiving the ID token, the rest of the flow requires the appropriate URLs to be set to continue.
+### Client Credentials Flow
+
+`opaal` also has
+
+
## Configuration
Here is an example configuration file:
@@ -105,4 +118,6 @@ options:
- Add details about configuration parameters
- Implement client credentials flow to easily fetch tokens
- Fix how OAuth clients are managed with the authorization server
-- Fix how the trusted issuer is added to the authorization server
\ No newline at end of file
+- Fix how the trusted issuer is added to the authorization server
+- Allow signing JWTs by supplying key pair
+- Separate `jwt_bearer` grant type from the authorization code flow
\ No newline at end of file
From fa090dbf5b6fa149a39247e1adf1c14a100b59b9 Mon Sep 17 00:00:00 2001
From: "David J. Allen"
Date: Sun, 10 Mar 2024 20:15:19 -0600
Subject: [PATCH 02/11] Added cache files
---
internal/cache/sqlite/clients.go | 154 ++++++++++++++++++++++++++++
internal/cache/sqlite/providers.go | 156 +++++++++++++++++++++++++++++
internal/cache/sqlite/trusted.go | 107 ++++++++++++++++++++
3 files changed, 417 insertions(+)
create mode 100644 internal/cache/sqlite/clients.go
create mode 100644 internal/cache/sqlite/providers.go
create mode 100644 internal/cache/sqlite/trusted.go
diff --git a/internal/cache/sqlite/clients.go b/internal/cache/sqlite/clients.go
new file mode 100644
index 0000000..00af1aa
--- /dev/null
+++ b/internal/cache/sqlite/clients.go
@@ -0,0 +1,154 @@
+package cache
+
+import (
+ "davidallendj/opaal/internal/oauth"
+ "fmt"
+
+ "github.com/jmoiron/sqlx"
+)
+
+func CreateOAuthClientsIfNotExists(path string) (*sqlx.DB, error) {
+ schema := `
+ CREATE TABLE IF NOT EXISTS oauth_clients (
+ id TEXT NOT NULL,
+ secret TEXT NOT NULL,
+ name TEXT,
+ description TEXT,
+ issuer TEXT,
+ registration_access_token TEXT,
+ redirect_uris TEXT,
+ scope TEXT
+ PRIMARY KEY (id)
+ );
+ `
+ 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 InsertOAuthClients(path string, clients *[]oauth.Client) error {
+ if clients == nil {
+ return fmt.Errorf("states == nil")
+ }
+
+ // create database if it doesn't already exist
+ db, err := CreateOAuthClientsIfNotExists(path)
+ if err != nil {
+ return err
+ }
+
+ // insert all probe states into db
+ tx := db.MustBegin()
+ for _, state := range *clients {
+ sql := `INSERT OR REPLACE INTO oauth_clients
+ (
+ id,
+ secret,
+ name,
+ description,
+ issuer,
+ registration_access_token,
+ redirect_uris,
+ scope
+ )
+ VALUES
+ (
+ :id,
+ :secret,
+ :name,
+ :description,
+ :issuer,
+ :registration_access_token,
+ :redirect_uris,
+ :scope
+ );`
+ _, 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 GetOAuthClient(path string, id string) (*oauth.Client, error) {
+ db, err := sqlx.Open("sqlite3", path)
+ if err != nil {
+ return nil, fmt.Errorf("could not open database: %v", err)
+ }
+
+ results := &oauth.Client{}
+ err = db.Select(&results, "SELECT * FROM oauth_clients 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 GetOAuthClients(path string) ([]oauth.Client, error) {
+ db, err := sqlx.Open("sqlite3", path)
+ if err != nil {
+ return nil, fmt.Errorf("could not open database: %v", err)
+ }
+
+ results := []oauth.Client{}
+ err = db.Select(&results, "SELECT * FROM oauth_clients;")
+ if err != nil {
+ return nil, fmt.Errorf("could not retrieve probes: %v", err)
+ }
+ return results, nil
+}
+
+func UpdateOAuthClient(path string, clients *[]oauth.Client) error {
+ if clients == nil {
+ return fmt.Errorf("clients is nil")
+ }
+ db, err := sqlx.Open("sqlite3", path)
+ if err != nil {
+ return fmt.Errorf("could not open database: %v", err)
+ }
+ tx := db.MustBegin()
+ for _, state := range *clients {
+ sql := `UPDATE FROM identity_providers WHERE client_id = :client_id;`
+ _, 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 DeleteOAuthClients(path string, clientIds []string) error {
+ if clientIds == 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 clientIds {
+ sql := `DELETE FROM identity_providers WHERE client_id = :client_id;`
+ _, 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
+}
diff --git a/internal/cache/sqlite/providers.go b/internal/cache/sqlite/providers.go
new file mode 100644
index 0000000..893cc03
--- /dev/null
+++ b/internal/cache/sqlite/providers.go
@@ -0,0 +1,156 @@
+package cache
+
+import (
+ "davidallendj/opaal/internal/oidc"
+ "fmt"
+
+ "github.com/jmoiron/sqlx"
+ _ "github.com/mattn/go-sqlite3"
+)
+
+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
+}
diff --git a/internal/cache/sqlite/trusted.go b/internal/cache/sqlite/trusted.go
new file mode 100644
index 0000000..ecc6145
--- /dev/null
+++ b/internal/cache/sqlite/trusted.go
@@ -0,0 +1,107 @@
+package cache
+
+import (
+ "davidallendj/opaal/internal/oauth"
+ "fmt"
+
+ "github.com/jmoiron/sqlx"
+)
+
+func CreateTrustedIfNotExists(path string) (*sqlx.DB, error) {
+ schema := `
+ CREATE TABLE IF NOT EXISTS trusted_issuers (
+ id TEXT NOT NULL,
+ allow_any_subject NUMBER,
+ expires_at NUMBER,
+ issuer TEXT,
+ public_key NUMBER,
+ scope TEXT,
+ subject TEXT,
+ PRIMARY KEY (id)
+ );
+ `
+ 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 InsertTrustedIssuer(path string, issuer *oauth.TrustedIssuer) error {
+ // create database if it doesn't already exist
+ db, err := CreateOAuthClientsIfNotExists(path)
+ if err != nil {
+ return err
+ }
+
+ // insert all probe states into db
+ tx := db.MustBegin()
+ sql := `INSERT OR REPLACE INTO trusted_issuers
+ (
+ id,
+ allow_any_subject
+ expires_at,
+ issuer,
+ public_key,
+ scope,
+ subject
+ )
+ VALUES
+ (
+ :id,
+ :allow_any_subject,
+ :expires_at,
+ :issuer,
+ :public_key,
+ :scope,
+ :subject
+ );`
+ _, err = tx.NamedExec(sql, &issuer)
+ 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 GetTrustedIssuer(path string, issuer string) (*oauth.TrustedIssuer, error) {
+ db, err := sqlx.Open("sqlite3", path)
+ if err != nil {
+ return nil, fmt.Errorf("could not open database: %v", err)
+ }
+
+ results := &oauth.TrustedIssuer{}
+ err = db.Select(&results, "SELECT * FROM trusted_issuers 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 DeleteTrustedIssuer(path string, ids []string) error {
+ if ids == 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 ids {
+ sql := `DELETE FROM identity_providers WHERE id = :id;`
+ _, 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 9bdca024a61bc2d507d0bce1714946d109d9b48c Mon Sep 17 00:00:00 2001
From: "David J. Allen"
Date: Sun, 10 Mar 2024 20:16:03 -0600
Subject: [PATCH 03/11] Moved ctor functions with config to separate file
---
internal/new.go | 420 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 420 insertions(+)
create mode 100644 internal/new.go
diff --git a/internal/new.go b/internal/new.go
new file mode 100644
index 0000000..0c2d3aa
--- /dev/null
+++ b/internal/new.go
@@ -0,0 +1,420 @@
+package opaal
+
+import (
+ "davidallendj/opaal/internal/oauth"
+ "fmt"
+ "net/http"
+ "slices"
+
+ "davidallendj/opaal/internal/flows"
+ "davidallendj/opaal/internal/server"
+
+ "github.com/davidallendj/go-utils/mathx"
+)
+
+func NewClientWithConfig(config *Config) *oauth.Client {
+ // 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 &oauth.Client{
+ 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 NewClientWithConfigByIndex(config *Config, index int) *oauth.Client {
+ size := len(config.Authentication.Clients)
+ index = mathx.Clamp(index, 0, size)
+ return nil
+}
+
+func NewClientWithConfigByName(config *Config, name string) *oauth.Client {
+ index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool {
+ return c.Name == name
+ })
+ if index >= 0 {
+ return &config.Authentication.Clients[index]
+ }
+ return nil
+}
+
+func NewClientWithConfigByProvider(config *Config, issuer string) *oauth.Client {
+ index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool {
+ return c.Issuer == issuer
+ })
+
+ if index >= 0 {
+ return &config.Authentication.Clients[index]
+ }
+ return nil
+}
+
+func NewClientWithConfigById(config *Config, id string) *oauth.Client {
+ index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool {
+ return c.Id == id
+ })
+ if index >= 0 {
+ return &config.Authentication.Clients[index]
+ }
+ return nil
+}
+
+func NewClientCredentialsFlowWithConfig(config *Config, client *oauth.Client) error {
+ eps := flows.ClientCredentialsFlowEndpoints{
+ Create: config.Authorization.Endpoints.Clients,
+ Authorize: config.Authorization.Endpoints.Authorize,
+ Token: config.Authorization.Endpoints.Token,
+ }
+ return flows.NewClientCredentialsFlow(eps, client)
+}
+
+func NewServerWithConfig(conf *Config) *server.Server {
+ host := conf.Server.Host
+ port := conf.Server.Port
+ server := &server.Server{
+ Server: &http.Server{
+ Addr: fmt.Sprintf("%s:%d", host, port),
+ },
+ Host: host,
+ Port: port,
+ }
+ return server
+}
+
+// func NewAuthorizationCodeFlowWithConfig(config *Config, client *oauth.Client, idp *oidc.IdentityProvider) error {
+// // create new server and client to use for flow
+// server := NewServerWithConfig(config)
+
+// // 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())
+// err := server.Login(authorizationUrl, c)
+// 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)
+// }
+
+// // 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)
+// 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)
+// fmt.Println()
+// }
+
+// if !config.Options.TokenForwarding {
+// // 1. verify that the JWT from the issuer is valid using all keys
+// _, err = jws.Verify([]byte(idToken), jws.WithKeySet(idp.KeySet), jws.WithValidateKey(true))
+// if err != nil {
+// return fmt.Errorf("failed to verify JWT: %v", err)
+// }
+
+// // 2. Check if we are already registered as a trusted issuer with authorization server...
+
+// // 3.a if not, create a new JWKS (or just JWK) to be verified
+// var (
+// keyPath string
+// privateJwk jwk.Key
+// publicJwk jwk.Key
+// )
+// if config.Authorization.KeyPath != "" {
+// keyPath = config.Authorization.Endpoints.Authorize
+// }
+// privateKey, err := os.ReadFile(keyPath)
+// if err != nil {
+// privateJwk, publicJwk, err = cryptox.GenerateJwkKeyPair()
+// if err != nil {
+// return fmt.Errorf("failed to generate JWK pair: %v", err)
+// }
+// } else {
+// privateJwk, publicJwk, err = cryptox.GenerateJwkKeyPairFromPrivateKey(privateKey)
+// if err != nil {
+// return fmt.Errorf("failed to generate JWK pair from private key: %v", err)
+// }
+// }
+// privateJwk.Set("kid", uuid.New().String())
+// publicJwk.Set("kid", uuid.New().String())
+
+// // 3.b ...and then, add opaal's server host as a trusted issuer with JWK
+// fmt.Printf("Attempting to add issuer to authorization server...\n")
+// ti := oauth.NewTrustedIssuer()
+// ti.Issuer = server.Addr
+// ti.PublicKey = publicJwk
+// ti.Subject = "1"
+// ti.ExpiresAt = time.Now().Add(time.Second * 3600)
+// res, err := client.AddTrustedIssuer(
+// config.Authorization.Endpoints.TrustedIssuers,
+// ti,
+// )
+// if err != nil {
+// return fmt.Errorf("failed to add trusted issuer: %v", err)
+// }
+// fmt.Printf("%v\n", string(res))
+
+// // 4. create a new JWT based on the claims from the identity provider and sign
+// parsedIdToken, err := jwt.ParseString(idToken, jwt.WithKeySet(idp.KeySet))
+// if err != nil {
+// return fmt.Errorf("failed to parse ID token: %v", err)
+// }
+// payload := parsedIdToken.PrivateClaims()
+// payload["iss"] = server.Addr
+// payload["aud"] = []string{config.Authorization.Endpoints.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)
+// }
+
+// // 5. dynamically register new OAuth client and authorize it to make jwt_bearer request
+// fmt.Printf("Registering new OAuth2 client with authorization server...\n")
+// res, err = client.RegisterOAuthClient(config.Authorization.Endpoints.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 {
+// // 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 try to create again
+// fmt.Printf("Attempting to delete client...\n")
+// err := client.DeleteOAuthClient(config.Authorization.Endpoints.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.Endpoints.Clients, []string{})
+// 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))
+
+// // 6. send JWT to authorization server and receive a access token
+// if config.Authorization.Endpoints.Token != "" {
+// fmt.Printf("Fetching access token from authorization server...\n")
+// res, err := client.PerformTokenGrant(config.Authorization.Endpoints.Token, string(newToken))
+// if err != nil {
+// 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.Endpoints.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.Endpoints.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.Endpoints.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.Endpoints.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.Endpoints.Token != "" {
+// fmt.Printf("Fetching access token from authorization server...\n")
+// res, err := client.PerformTokenGrant(config.Authorization.Endpoints.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
+// }
From 72adbe1f0d853c1bcabe577147b575ecb7a2bfd0 Mon Sep 17 00:00:00 2001
From: "David J. Allen"
Date: Sun, 10 Mar 2024 20:16:47 -0600
Subject: [PATCH 04/11] Updated dependencies
---
go.mod | 23 +++++++++++++----------
go.sum | 44 +++++++++++++++++++++++++-------------------
2 files changed, 38 insertions(+), 29 deletions(-)
diff --git a/go.mod b/go.mod
index e0ad618..9960b14 100644
--- a/go.mod
+++ b/go.mod
@@ -3,41 +3,44 @@ module davidallendj/opaal
go 1.22.0
require (
- github.com/davidallendj/go-utils v0.0.0-20240302194916-fe292bcf24a4
- github.com/go-chi/chi v1.5.5
+ github.com/davidallendj/go-utils v0.0.0-20240310194826-5a1300f3bcbf
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/v2 v2.0.20
- github.com/mattn/go-sqlite3 v1.14.6
+ github.com/lestrrat-go/jwx/v2 v2.0.21
+ github.com/mattn/go-sqlite3 v1.14.22
github.com/nikolalohinski/gonja/v2 v2.2.0
github.com/spf13/cobra v1.8.0
- golang.org/x/net v0.21.0
+ golang.org/x/net v0.22.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/go-logr/logr v1.3.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
+ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/kr/text v0.2.0 // indirect
+ github.com/kr/pretty v0.3.1 // 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/httprc v1.0.5 // 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/onsi/ginkgo/v2 v2.13.0 // indirect
+ github.com/onsi/gomega v1.29.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
+ github.com/rogpeppe/go-internal v1.10.0 // 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/crypto v0.21.0 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
- golang.org/x/sys v0.17.0 // indirect
+ golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
)
diff --git a/go.sum b/go.sum
index ff8a3de..aea6024 100644
--- a/go.sum
+++ b/go.sum
@@ -1,18 +1,21 @@
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
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/davidallendj/go-utils v0.0.0-20240310034007-5fa47be83de0 h1:nWyYmigcFeC2iCDxylGv3FFLSfh1so00iUYVzwZUJP4=
+github.com/davidallendj/go-utils v0.0.0-20240310034007-5fa47be83de0/go.mod h1:kiv3jEnBbeueMNNJclaMMJULL/tjqJ6wc136d+uxqSs=
+github.com/davidallendj/go-utils v0.0.0-20240310194826-5a1300f3bcbf h1:gY89rDLnc+70S0JcyHoPGU+XFpMwY1iVYNQzdC/qAHc=
+github.com/davidallendj/go-utils v0.0.0-20240310194826-5a1300f3bcbf/go.mod h1:kiv3jEnBbeueMNNJclaMMJULL/tjqJ6wc136d+uxqSs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/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=
@@ -32,6 +35,7 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJY
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/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
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=
@@ -46,18 +50,19 @@ github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N
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/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk=
+github.com/lestrrat-go/httprc v1.0.5/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/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/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55FHrR0=
+github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM=
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/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
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=
@@ -69,10 +74,12 @@ 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/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
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=
@@ -89,17 +96,18 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
-golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
+golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
@@ -112,5 +120,3 @@ 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 6d63211d35277dbd6aa5c850d305c49a13313487 Mon Sep 17 00:00:00 2001
From: "David J. Allen"
Date: Sun, 10 Mar 2024 20:19:29 -0600
Subject: [PATCH 05/11] Major refactoring and code restructure
---
internal/authorization_code.go | 424 ---------------------
internal/client.go | 93 -----
internal/db/sqlite.go | 156 --------
internal/{ => flows}/client_credentials.go | 16 +-
internal/flows/jwt_bearer.go | 319 ++++++++++++++++
internal/{ => oauth}/authenticate.go | 2 +-
internal/{authorize.go => oauth/client.go} | 81 ++--
internal/{ => oauth}/identities.go | 2 +-
internal/oauth/trusted.go | 99 +++++
internal/server.go | 121 ------
10 files changed, 454 insertions(+), 859 deletions(-)
delete mode 100644 internal/authorization_code.go
delete mode 100644 internal/client.go
delete mode 100644 internal/db/sqlite.go
rename internal/{ => flows}/client_credentials.go (63%)
create mode 100644 internal/flows/jwt_bearer.go
rename internal/{ => oauth}/authenticate.go (99%)
rename internal/{authorize.go => oauth/client.go} (74%)
rename internal/{ => oauth}/identities.go (98%)
create mode 100644 internal/oauth/trusted.go
delete mode 100644 internal/server.go
diff --git a/internal/authorization_code.go b/internal/authorization_code.go
deleted file mode 100644
index efc8ae2..0000000
--- a/internal/authorization_code.go
+++ /dev/null
@@ -1,424 +0,0 @@
-package opaal
-
-import (
- "crypto/rand"
- "crypto/rsa"
- "davidallendj/opaal/internal/oidc"
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
- "reflect"
- "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
-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()
- }
-
- if !config.Options.ForwardToken {
-
- // 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")
- }
-
- parsedIdToken, err := jwt.ParseString(idToken, jwt.WithKey(jwa.RS256, key))
- if err != nil {
- return fmt.Errorf("failed to parse ID token: %v", err)
- }
- _, err = jwt.ParseString(accessToken, jwt.WithKeySet(idp.Jwks))
- if err != nil {
- return fmt.Errorf("failed to parse access token: %v", err)
- }
-
- _, 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,
- 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))
-
- // 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)
- }
-
- // sig = rsasha256(b64urlencode(header) + "." + b64urlencode(payload))
- // signature := util.EncodeBase64() + util.EncodeBase64() +
-
- // 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 {
- // 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))
- }
- }
-
- // 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 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))
- }
-
- // 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.go b/internal/client.go
deleted file mode 100644
index d9746f4..0000000
--- a/internal/client.go
+++ /dev/null
@@ -1,93 +0,0 @@
-package opaal
-
-import (
- "net/http"
- "net/http/cookiejar"
- "slices"
-
- "github.com/davidallendj/go-utils/mathx"
- "golang.org/x/net/publicsuffix"
-)
-
-type Client struct {
- http.Client
- 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 {
- // 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: clients[0].Id,
- Secret: clients[0].Secret,
- Name: clients[0].Name,
- Issuer: clients[0].Issuer,
- Scope: clients[0].Scope,
- RedirectUris: clients[0].RedirectUris,
- }
-}
-
-func NewClientWithConfigByIndex(config *Config, index int) *Client {
- size := len(config.Authentication.Clients)
- index = mathx.Clamp(index, 0, size)
- return nil
-}
-
-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 NewClientWithConfigByProvider(config *Config, issuer string) *Client {
- index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool {
- return c.Issuer == issuer
- })
-
- if index >= 0 {
- return &config.Authentication.Clients[index]
- }
- return nil
-}
-
-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]
- }
- return nil
-}
-
-func (client *Client) ClearCookies() {
- jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
- client.Jar = jar
-}
diff --git a/internal/db/sqlite.go b/internal/db/sqlite.go
deleted file mode 100644
index 147fc72..0000000
--- a/internal/db/sqlite.go
+++ /dev/null
@@ -1,156 +0,0 @@
-package db
-
-import (
- "davidallendj/opaal/internal/oidc"
- "fmt"
-
- "github.com/jmoiron/sqlx"
- _ "github.com/mattn/go-sqlite3"
-)
-
-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
-}
diff --git a/internal/client_credentials.go b/internal/flows/client_credentials.go
similarity index 63%
rename from internal/client_credentials.go
rename to internal/flows/client_credentials.go
index efe20eb..e5789c2 100644
--- a/internal/client_credentials.go
+++ b/internal/flows/client_credentials.go
@@ -1,6 +1,7 @@
-package opaal
+package flows
import (
+ "davidallendj/opaal/internal/oauth"
"fmt"
)
@@ -15,9 +16,9 @@ type ClientCredentialsFlowEndpoints struct {
Token string
}
-func ClientCredentials(eps ClientCredentialsFlowEndpoints, client *Client) error {
+func NewClientCredentialsFlow(eps ClientCredentialsFlowEndpoints, client *oauth.Client) error {
// register a new OAuth 2 client with authorization srever
- _, err := client.CreateOAuthClient(eps.Create, nil)
+ _, err := client.CreateOAuthClient(eps.Create)
if err != nil {
return fmt.Errorf("failed to register OAuth client: %v", err)
}
@@ -37,12 +38,3 @@ func ClientCredentials(eps ClientCredentialsFlowEndpoints, client *Client) error
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)
-}
diff --git a/internal/flows/jwt_bearer.go b/internal/flows/jwt_bearer.go
new file mode 100644
index 0000000..4b0f085
--- /dev/null
+++ b/internal/flows/jwt_bearer.go
@@ -0,0 +1,319 @@
+package flows
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "davidallendj/opaal/internal/oauth"
+ "davidallendj/opaal/internal/oidc"
+ "encoding/json"
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/davidallendj/go-utils/cryptox"
+ "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"
+)
+
+type JwtBearerFlowParams struct {
+ AccessToken string
+ IdToken string
+ IdentityProvider *oidc.IdentityProvider
+ TrustedIssuer *oauth.TrustedIssuer
+ Client *oauth.Client
+ Verbose bool
+ KeyPath string
+}
+
+type JwtBearerEndpoints struct {
+ TrustedIssuers string
+ Token string
+ Clients string
+ Register string
+}
+
+func NewJwtBearerFlow(eps JwtBearerEndpoints, params JwtBearerFlowParams) (string, error) {
+ // 1. verify that the JWT from the issuer is valid using all keys
+ var (
+ idp = params.IdentityProvider
+ accessToken = params.AccessToken
+ idToken = params.IdToken
+ client = params.Client
+ trustedIssuer = params.TrustedIssuer
+ verbose = params.Verbose
+ )
+ if accessToken != "" {
+ _, err := jws.Verify([]byte(accessToken), jws.WithKeySet(idp.KeySet), jws.WithValidateKey(true))
+ if err != nil {
+ return "", fmt.Errorf("failed to verify access token: %v", err)
+ }
+ }
+
+ if idToken != "" {
+ _, err := jws.Verify([]byte(idToken), jws.WithKeySet(idp.KeySet), jws.WithValidateKey(true))
+ if err != nil {
+ return "", fmt.Errorf("failed to verify ID token: %v", err)
+ }
+ }
+
+ // 2. Check if we are already registered as a trusted issuer with authorization server...
+
+ // 3.a if not, create a new JWKS (or just JWK) to be verified
+ var (
+ keyPath string = params.KeyPath
+ privateJwk jwk.Key
+ publicJwk jwk.Key
+ )
+ rawPrivateKey, err := os.ReadFile(keyPath)
+ if err != nil {
+ if verbose {
+ fmt.Printf("failed to read private key...generating a new one.\n")
+ }
+ privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ return "", fmt.Errorf("failed to generate new RSA key: %v", err)
+ }
+ privateJwk, publicJwk, err = cryptox.GenerateJwkKeyPairFromPrivateKey(privateKey)
+ if err != nil {
+ return "", fmt.Errorf("failed to generate JWK pair from private key: %v", err)
+ }
+ // save new key to key path to reuse later
+ b := cryptox.MarshalRSAPrivateKey(privateKey)
+ err = os.WriteFile(keyPath, b, os.ModePerm)
+ if err != nil {
+ fmt.Printf("failed to write private key to file: %v", err)
+ }
+ } else {
+ privateKey, err := cryptox.GenerateRSAPrivateKey(rawPrivateKey)
+ if err != nil {
+ return "", fmt.Errorf("failed to generate RSA key from string: %v", err)
+ }
+ privateJwk, publicJwk, err = cryptox.GenerateJwkKeyPairFromPrivateKey(privateKey)
+ if err != nil {
+ return "", fmt.Errorf("failed to generate JWK pair from private key: %v", err)
+ }
+ }
+
+ publicJwk.Set("kid", uuid.New().String())
+ publicJwk.Set("use", "sig")
+
+ if err := publicJwk.Validate(); err != nil {
+ return "", fmt.Errorf("failed to validate public JWK: %v", err)
+ }
+ trustedIssuer.PublicKey = publicJwk
+
+ // 3.b ...and then, add opaal's server host as a trusted issuer with JWK
+ if verbose {
+ fmt.Printf("Attempting to add issuer to authorization server...\n")
+ }
+ res, err := client.AddTrustedIssuer(
+ eps.TrustedIssuers,
+ trustedIssuer,
+ )
+ if err != nil {
+ return "", fmt.Errorf("failed to add trusted issuer: %v", err)
+ }
+ fmt.Printf("trusted issuer: %v\n", string(res))
+ // TODO: add trusted issuer to cache if successful
+
+ // 4. create a new JWT based on the claims from the identity provider and sign
+ parsedIdToken, err := jwt.ParseString(idToken, jwt.WithKeySet(idp.KeySet))
+ if err != nil {
+ return "", fmt.Errorf("failed to parse ID token: %v", err)
+ }
+ payload := parsedIdToken.PrivateClaims()
+ payload["iss"] = trustedIssuer.Issuer
+ payload["aud"] = []string{eps.Token}
+ payload["iat"] = time.Now().Unix()
+ payload["nbf"] = time.Now().Unix()
+ payload["exp"] = time.Now().Add(time.Second * 3600).Unix()
+ payload["sub"] = "opaal"
+ payloadJson, err := json.Marshal(payload)
+ if err != nil {
+ return "", fmt.Errorf("failed to marshal payload: %v", err)
+ }
+ fmt.Printf("payload: %v\n", string(payloadJson))
+ newJwt, err := jws.Sign(payloadJson, jws.WithJSON(), jws.WithKey(jwa.RS256, privateJwk))
+ if err != nil {
+ return "", fmt.Errorf("failed to sign token: %v", err)
+ }
+
+ // 5. dynamically register new OAuth client and authorize it to make jwt_bearer request
+ fmt.Printf("Registering new OAuth2 client with authorization server...\n")
+ res, err = client.RegisterOAuthClient(eps.Register)
+ 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 try to create again
+ fmt.Printf("Attempting to delete client...\n")
+ err := client.DeleteOAuthClient(eps.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(eps.Clients)
+ if err != nil {
+ return "", fmt.Errorf("failed to register client: %v", err)
+ }
+ fmt.Printf("%v\n", string(res))
+ }
+ }
+ // TODO: add OAuth client to cache if successfully
+
+ // 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 eps.Token != "" {
+ fmt.Printf("Fetching access token from authorization server...\n")
+ res, err := client.PerformTokenGrant(eps.Token, string(newJwt))
+ if err != nil {
+ return "", fmt.Errorf("failed to fetch access token: %v", err)
+ }
+ // extract token from response if there are no errors
+ var data map[string]any
+ err = json.Unmarshal(res, &data)
+ if err != nil {
+ return "", fmt.Errorf("failed to unmarshal response: %v", err)
+ }
+ if data["error"] != nil {
+ return "", fmt.Errorf("the authorization server returned an error (%v): %v", data["error"], data["error_description"])
+ }
+ fmt.Printf("%s\n", res)
+
+ err = json.Unmarshal(res, &data)
+ if err != nil {
+ return "", fmt.Errorf("failed to unmarshal access token: %v", err)
+ }
+ return data["access_token"].(string), nil
+ } else {
+ return "", fmt.Errorf("token endpoint not set")
+ }
+
+ return string(res), nil
+}
+
+func ForwardToken(eps JwtBearerEndpoints, params JwtBearerFlowParams) error {
+ var (
+ client = params.Client
+ idToken = params.IdToken
+ idp = params.IdentityProvider
+ verbose = params.Verbose
+ )
+
+ // fetch JWKS and add issuer to authentication server to submit ID token
+ if verbose {
+ 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 {
+ if verbose {
+ fmt.Printf("Successfully retrieved JWK from authentication server.\n\n")
+ fmt.Printf("Attempting to add issuer to authorization server...\n")
+ }
+
+ ti := &oauth.TrustedIssuer{
+ Issuer: idp.Issuer,
+ Subject: "1",
+ ExpiresAt: time.Now().Add(time.Second * 3600),
+ }
+ res, err := client.AddTrustedIssuer(
+ eps.TrustedIssuers,
+ ti,
+ )
+ if err != nil {
+ return fmt.Errorf("failed to add trusted issuer: %v", err)
+ }
+ if verbose {
+ fmt.Printf("%v\n", string(res))
+ }
+ }
+
+ // try and register a new client with authorization server
+ if verbose {
+ fmt.Printf("Registering new OAuth2 client with authorization server...\n")
+ }
+ res, err := client.RegisterOAuthClient(eps.Register)
+ if err != nil {
+ return fmt.Errorf("failed to register client: %v", err)
+ }
+ if verbose {
+ 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(eps.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(eps.Clients)
+ 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 eps.Token != "" {
+ if verbose {
+ fmt.Printf("Fetching access token from authorization server...\n")
+ }
+ res, err := client.PerformTokenGrant(eps.Token, idToken)
+ if err != nil {
+ return fmt.Errorf("failed to fetch access token: %v", err)
+ }
+ if verbose {
+ fmt.Printf("%s\n", res)
+ }
+ } else {
+ return fmt.Errorf("token endpoint is not set")
+ }
+ return nil
+}
diff --git a/internal/authenticate.go b/internal/oauth/authenticate.go
similarity index 99%
rename from internal/authenticate.go
rename to internal/oauth/authenticate.go
index 91f5940..16cff56 100644
--- a/internal/authenticate.go
+++ b/internal/oauth/authenticate.go
@@ -1,4 +1,4 @@
-package opaal
+package oauth
import (
"bytes"
diff --git a/internal/authorize.go b/internal/oauth/client.go
similarity index 74%
rename from internal/authorize.go
rename to internal/oauth/client.go
index a778606..b9cca21 100644
--- a/internal/authorize.go
+++ b/internal/oauth/client.go
@@ -1,65 +1,41 @@
-package opaal
+package oauth
import (
- "bytes"
- "davidallendj/opaal/internal/oidc"
"encoding/json"
"fmt"
- "io"
"net/http"
+ "net/http/cookiejar"
"net/url"
"slices"
"strings"
- "time"
"github.com/davidallendj/go-utils/httpx"
"github.com/davidallendj/go-utils/util"
- "github.com/lestrrat-go/jwx/v2/jwk"
+ "golang.org/x/net/publicsuffix"
)
-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
- 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,"+
- "\"issuer\": \"%s\","+
- "\"subject\": \"%s\","+
- "\"expires_at\": \"%v\","+
- "\"jwk\": %v,"+
- "\"scope\": [ %s ]"+
- "}", 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)
- 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)
+type Client struct {
+ http.Client
+ Id string `db:"id" yaml:"id"`
+ Secret string `db:"secret" yaml:"secret"`
+ Name string `db:"name" yaml:"name"`
+ Description string `db:"description" yaml:"description"`
+ Issuer string `db:"issuer" yaml:"issuer"`
+ RegistrationAccessToken string `db:"registration_access_token" yaml:"registration-access-token"`
+ RedirectUris []string `db:"redirect_uris" yaml:"redirect-uris"`
+ Scope []string `db:"scope" yaml:"scope"`
+ Audience []string `db:"audience" yaml:"audience"`
+ FlowId string
+ CsrfToken string
}
-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 NewClient() *Client {
+ return &Client{}
+}
+
+func (client *Client) ClearCookies() {
+ jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
+ client.Jar = jar
}
func (client *Client) IsOAuthClientRegistered(clientUrl string) (bool, error) {
@@ -107,9 +83,9 @@ func (client *Client) GetOAuthClient(clientUrl string) error {
return nil
}
-func (client *Client) CreateOAuthClient(registerUrl string, audience []string) ([]byte, error) {
+func (client *Client) CreateOAuthClient(registerUrl string) ([]byte, error) {
// hydra endpoint: POST /clients
- audience = util.QuoteArrayStrings(audience)
+ audience := util.QuoteArrayStrings(client.Audience)
body := httpx.Body(fmt.Sprintf(`{
"client_id": "%s",
"client_name": "%s",
@@ -151,9 +127,12 @@ func (client *Client) CreateOAuthClient(registerUrl string, audience []string) (
return b, err
}
-func (client *Client) RegisterOAuthClient(registerUrl string, audience []string) ([]byte, error) {
+func (client *Client) RegisterOAuthClient(registerUrl string) ([]byte, error) {
// hydra endpoint: POST /oauth2/register
- audience = util.QuoteArrayStrings(audience)
+ if registerUrl == "" {
+ return nil, fmt.Errorf("no URL provided")
+ }
+ audience := util.QuoteArrayStrings(client.Audience)
body := httpx.Body(fmt.Sprintf(`{
"client_name": "opaal",
"token_endpoint_auth_method": "client_secret_post",
diff --git a/internal/identities.go b/internal/oauth/identities.go
similarity index 98%
rename from internal/identities.go
rename to internal/oauth/identities.go
index e1ddee5..7ccc11c 100644
--- a/internal/identities.go
+++ b/internal/oauth/identities.go
@@ -1,4 +1,4 @@
-package opaal
+package oauth
import (
"fmt"
diff --git a/internal/oauth/trusted.go b/internal/oauth/trusted.go
new file mode 100644
index 0000000..be8647f
--- /dev/null
+++ b/internal/oauth/trusted.go
@@ -0,0 +1,99 @@
+package oauth
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+
+ "github.com/davidallendj/go-utils/httpx"
+ "github.com/lestrrat-go/jwx/v2/jwk"
+)
+
+type TrustedIssuer struct {
+ Id string `db:"id" yaml:"id"`
+ AllowAnySubject bool `db:"allow_any_subject" yaml:"allow-any-subject"`
+ ExpiresAt time.Time `db:"expires_at" yaml:"expires-at"`
+ Issuer string `db:"issuer" yaml:"issuer"`
+ PublicKey jwk.Key `db:"public_key" yaml:"public-key"`
+ Scope []string `db:"scope" yaml:"scope"`
+ Subject string `db:"subject" yaml:"subject"`
+}
+
+func NewTrustedIssuer() *TrustedIssuer {
+ return &TrustedIssuer{
+ AllowAnySubject: false,
+ ExpiresAt: time.Now().Add(time.Hour),
+ Scope: []string{"openid"},
+ Subject: "1",
+ }
+}
+
+func (ti *TrustedIssuer) IsTrustedIssuerValid() bool {
+ err := ti.PublicKey.Validate()
+ return ti.Issuer != "" && err == nil && ti.Subject != ""
+}
+
+func ParseString(b []byte) (*TrustedIssuer, error) {
+ // take data from JSON to populate fields
+ ti := &TrustedIssuer{}
+ data := map[string]any{}
+ json.Unmarshal(b, &data)
+ return ti, nil
+}
+
+func (client *Client) ListTrustedIssuers(url string) ([]TrustedIssuer, error) {
+ // hydra endpoint: GET /admin/trust/grants/jwt-bearer/issuers
+ _, b, err := httpx.MakeHttpRequest(url, http.MethodGet, nil, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to make request: %v", err)
+ }
+
+ // unmarshal results into TrustedIssuers objects
+ trustedIssuers := []TrustedIssuer{}
+ err = json.Unmarshal(b, &trustedIssuers)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshal JSON: %v", err)
+ }
+ return trustedIssuers, nil
+}
+
+func (client *Client) AddTrustedIssuer(url string, ti *TrustedIssuer) ([]byte, error) {
+ // hydra endpoint: POST /admin/trust/grants/jwt-bearer/issuers
+ 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 of "jwk"
+ body := map[string]any{
+ "allow_any_subject": ti.AllowAnySubject,
+ "issuer": ti.Issuer,
+ "expires_at": ti.ExpiresAt,
+ "jwk": ti.PublicKey,
+ "scope": client.Scope,
+ }
+ if !ti.AllowAnySubject {
+ body["subject"] = ti.Subject
+ }
+ b, err := json.Marshal(body)
+ fmt.Printf("request: %v\n", string(b))
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal request body: %v", err)
+ }
+
+ req, err := http.NewRequest("POST", url, bytes.NewBuffer(b))
+ if err != nil {
+ return nil, fmt.Errorf("failed to make request: %v", err)
+ }
+ req.Header.Add("Content-Type", "application/json")
+ 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)
+}
diff --git a/internal/server.go b/internal/server.go
deleted file mode 100644
index 7c11426..0000000
--- a/internal/server.go
+++ /dev/null
@@ -1,121 +0,0 @@
-package opaal
-
-import (
- "fmt"
- "net/http"
- "os"
- "strings"
-
- "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 {
- *http.Server
- Host string `yaml:"host"`
- Port int `yaml:"port"`
- Callback string `yaml:"callback"`
-}
-
-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),
- },
- Host: host,
- Port: port,
- }
- return server
-}
-
-func (s *Server) SetListenAddr(host string, port int) {
- s.Addr = s.GetListenAddr()
-}
-
-func (s *Server) GetListenAddr() string {
- return fmt.Sprintf("%s:%d", s.Host, s.Port)
-}
-
-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)
- r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- http.Redirect(w, r, "/login", http.StatusSeeOther)
- })
- r.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
- // show login page with notice to redirect
- template, err := gonja.FromFile("pages/index.html")
- if err != nil {
- panic(err)
- }
-
- data := exec.NewContext(map[string]interface{}{
- "loginUrl": loginUrl,
- })
-
- if err = template.Execute(w, data); err != nil { // Prints: Hello Bob!
- panic(err)
- }
- })
- 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")
- fmt.Printf("Authorization code: %v\n", code)
- }
- http.Redirect(w, r, "/redirect", http.StatusSeeOther)
- })
- r.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) {
- err := s.Close()
- if err != nil {
- fmt.Printf("failed to close server: %v\n", err)
- }
- })
- s.Handler = r
-
- return code, s.ListenAndServe()
-}
-
-func (s *Server) Serve(data chan []byte) error {
- output, ok := <-data
- if !ok {
- return fmt.Errorf("failed to receive data")
- }
-
- fmt.Printf("Received data: %v\n", string(output))
- // http.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) {
-
- // })
- r := chi.NewRouter()
- r.HandleFunc("/success", func(w http.ResponseWriter, r *http.Request) {
- fmt.Printf("Serving success page.")
- successPage, err := os.ReadFile("pages/success.html")
- if err != nil {
- fmt.Printf("failed to load success page: %v\n", err)
- }
- successPage = []byte(strings.ReplaceAll(string(successPage), "{{access_token}}", string(output)))
- w.Write(successPage)
- })
- r.HandleFunc("/error", func(w http.ResponseWriter, r *http.Request) {
- fmt.Printf("Serving error page.")
- errorPage, err := os.ReadFile("pages/success.html")
- if err != nil {
- fmt.Printf("failed to load success page: %v\n", err)
- }
- // errorPage = []byte(strings.ReplaceAll(string(errorPage), "{{access_token}}", output))
- w.Write(errorPage)
- })
-
- s.Handler = r
- return s.ListenAndServe()
-}
From 45e8cd7f15801009c17fbf4ab59578c5baf6037f Mon Sep 17 00:00:00 2001
From: "David J. Allen"
Date: Sun, 10 Mar 2024 20:19:48 -0600
Subject: [PATCH 06/11] Modified HTML pages
---
pages/index.html | 5 +++--
pages/success.html | 37 ++++++++++++++++++++++++++++++-------
2 files changed, 33 insertions(+), 9 deletions(-)
diff --git a/pages/index.html b/pages/index.html
index ee880f1..38a2733 100644
--- a/pages/index.html
+++ b/pages/index.html
@@ -1,6 +1,7 @@
- Welcome to Opaal's default login in page! Click the link below to log in with your identity provider:
+ Welcome to Opaal's default login in page! Click the link below to log in for an access token.
+
+ {{loginButtons}}
- Login
\ No newline at end of file
diff --git a/pages/success.html b/pages/success.html
index 82d409d..7c52801 100644
--- a/pages/success.html
+++ b/pages/success.html
@@ -1,13 +1,36 @@
- Success! Here's you access token:
+
+
+ Login successful! Here is your access token:
+
+
+
+
+
+
+
+ You will need this token to access protected services and resources.
+ Make sure to include it in the authorization header if you are making a HTTP request.