More refactoring and code restructure

This commit is contained in:
David Allen 2024-03-10 20:20:53 -06:00
parent 45e8cd7f15
commit f6bf8a8960
No known key found for this signature in database
GPG key ID: 1D2A29322FBB6FCB
4 changed files with 254 additions and 40 deletions

View file

@ -1,10 +1,13 @@
package opaal
import (
"davidallendj/opaal/internal/oauth"
"log"
"os"
"path/filepath"
"davidallendj/opaal/internal/server"
goutil "github.com/davidallendj/go-utils/util"
"gopkg.in/yaml.v2"
@ -15,17 +18,16 @@ type Flows map[string]FlowOptions
type Providers map[string]string
type Options struct {
DecodeIdToken bool `yaml:"decode-id-token"`
DecodeAccessToken bool `yaml:"decode-access-token"`
RunOnce bool `yaml:"run-once"`
OpenBrowser bool `yaml:"open-browser"`
FlowType string `yaml:"flow"`
CachePath string `yaml:"cache"`
LocalOnly bool `yaml:"local-only"`
ForwardToken bool `yaml:"forward-token"`
CacheOnly bool `yaml:"cache-only"`
TokenForwarding bool `yaml:"token-forwarding"`
Verbose bool `yaml:"verbose"`
}
type RequestUrls struct {
type Endpoints struct {
Identities string `yaml:"identities"`
TrustedIssuers string `yaml:"trusted-issuers"`
Login string `yaml:"login"`
@ -36,17 +38,20 @@ type RequestUrls struct {
}
type Authentication struct {
Clients []Client `yaml:"clients"`
Clients []oauth.Client `yaml:"clients"`
Flows Flows `yaml:"flows"`
TestAllClients bool `yaml:"test-all"`
State string `yaml:"state"`
}
type Authorization struct {
RequestUrls RequestUrls `yaml:"urls"`
Endpoints Endpoints `yaml:"endpoints"`
KeyPath string `yaml:"key-path"`
}
type Config struct {
Version string `yaml:"version"`
Server Server `yaml:"server"`
Server server.Server `yaml:"server"`
Providers Providers `yaml:"providers"`
Options Options `yaml:"options"`
Authentication Authentication `yaml:"authentication"`
@ -56,22 +61,25 @@ type Config struct {
func NewConfig() Config {
return Config{
Version: goutil.GetCommit(),
Server: Server{
Server: server.Server{
Host: "127.0.0.1",
Port: 3333,
},
Options: Options{
DecodeIdToken: true,
DecodeAccessToken: true,
RunOnce: true,
OpenBrowser: false,
CachePath: "opaal.db",
FlowType: "authorization_code",
LocalOnly: false,
ForwardToken: false,
CacheOnly: false,
TokenForwarding: false,
Verbose: false,
},
Authentication: Authentication{
TestAllClients: false,
},
Authorization: Authorization{
KeyPath: "./keys",
},
Authentication: Authentication{},
Authorization: Authorization{},
}
}
@ -112,10 +120,10 @@ func HasRequiredConfigParams(config *Config) bool {
// must have athe requirements to perform login
hasClients := len(config.Authentication.Clients) > 0
hasServer := config.Server.Host != "" && config.Server.Port != 0 && config.Server.Callback != ""
hasEndpoints := config.Authorization.RequestUrls.TrustedIssuers != "" &&
config.Authorization.RequestUrls.Login != "" &&
config.Authorization.RequestUrls.Clients != "" &&
config.Authorization.RequestUrls.Authorize != "" &&
config.Authorization.RequestUrls.Token != ""
hasEndpoints := config.Authorization.Endpoints.TrustedIssuers != "" &&
config.Authorization.Endpoints.Login != "" &&
config.Authorization.Endpoints.Clients != "" &&
config.Authorization.Endpoints.Authorize != "" &&
config.Authorization.Endpoints.Token != ""
return hasClients && hasServer && hasEndpoints
}

View file

@ -1,31 +1,70 @@
package opaal
import (
"davidallendj/opaal/internal/db"
cache "davidallendj/opaal/internal/cache/sqlite"
"davidallendj/opaal/internal/flows"
"davidallendj/opaal/internal/oauth"
"davidallendj/opaal/internal/oidc"
"errors"
"fmt"
"net/http"
"time"
)
func Login(config *Config, client *Client, provider *oidc.IdentityProvider) error {
func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider) error {
if config == nil {
return fmt.Errorf("config is not valid")
}
// make cache if it's not where expect
_, err := db.CreateIdentityProvidersIfNotExists(config.Options.CachePath)
_, err := cache.CreateIdentityProvidersIfNotExists(config.Options.CachePath)
if err != nil {
fmt.Printf("failed to create cache: %v\n", err)
}
if config.Options.FlowType == "authorization_code" {
// create a server if doing authorization code flow
server := NewServerWithConfig(config)
err := AuthorizationCodeWithConfig(config, server, client, provider)
if err != nil {
fmt.Printf("failed to complete authorization code flow: %v\n", err)
// build the authorization URL to redirect user for social sign-in
var state = ""
if config.Authentication.Flows["authorization-code"]["state"] != "" {
state = config.Authentication.Flows["authorization-code"]["state"]
}
// print the authorization URL for sharing
var authorizationUrl = client.BuildAuthorizationUrl(provider.Endpoints.Authorization, state)
server := NewServerWithConfig(config)
fmt.Printf("Login with identity provider:\n\n %s/login\n %s\n\n",
server.GetListenAddr(), authorizationUrl,
)
var button = MakeButton(authorizationUrl, "Login with "+client.Name)
// authorize oauth client and listen for callback from provider
fmt.Printf("Waiting for authorization code redirect @%s/oidc/callback...\n", server.GetListenAddr())
eps := flows.JwtBearerEndpoints{
Token: config.Authorization.Endpoints.Token,
TrustedIssuers: config.Authorization.Endpoints.TrustedIssuers,
Register: config.Authorization.Endpoints.Register,
}
params := flows.JwtBearerFlowParams{
Client: oauth.NewClient(),
IdentityProvider: provider,
TrustedIssuer: &oauth.TrustedIssuer{
AllowAnySubject: false,
Issuer: server.Addr,
Subject: "opaal",
ExpiresAt: time.Now().Add(time.Second * 3600),
},
Verbose: config.Options.Verbose,
}
err = server.Login(button, provider, client, eps, params)
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)
}
} else if config.Options.FlowType == "client_credentials" {
err := ClientCredentialsWithConfig(config, client)
err := NewClientCredentialsFlowWithConfig(config, client)
if err != nil {
fmt.Printf("failed to complete client credentials flow: %v", err)
}
@ -35,3 +74,7 @@ func Login(config *Config, client *Client, provider *oidc.IdentityProvider) erro
return nil
}
func MakeButton(url string, text string) string {
return "<a href=\"" + url + "\"> " + text + "</a>"
}

View file

@ -16,7 +16,7 @@ type IdentityProvider struct {
Issuer string `db:"issuer" json:"issuer" yaml:"issuer"`
Endpoints Endpoints `db:"endpoints" json:"endpoints" yaml:"endpoints"`
Supported Supported `db:"supported" json:"supported" yaml:"supported"`
Jwks jwk.Set
KeySet jwk.Set
}
type Endpoints struct {
@ -142,7 +142,7 @@ func (p *IdentityProvider) FetchJwks() error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var err error
p.Jwks, err = jwk.Fetch(ctx, p.Endpoints.JwksUri)
p.KeySet, err = jwk.Fetch(ctx, p.Endpoints.JwksUri)
if err != nil {
return fmt.Errorf("failed to fetch JWKS: %v", err)
}

163
internal/server/server.go Normal file
View file

@ -0,0 +1,163 @@
package server
import (
"davidallendj/opaal/internal/flows"
"davidallendj/opaal/internal/oauth"
"davidallendj/opaal/internal/oidc"
"encoding/json"
"fmt"
"net/http"
"os"
"github.com/davidallendj/go-utils/httpx"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"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"`
State string `yaml:"state"`
}
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) Login(buttons string, provider *oidc.IdentityProvider, client *oauth.Client, eps flows.JwtBearerEndpoints, params flows.JwtBearerFlowParams) error {
var target = ""
// check if callback is set
if s.Callback == "" {
s.Callback = "/oidc/callback"
}
var code string
var accessToken string
r := chi.NewRouter()
r.Use(middleware.RedirectSlashes)
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
target = r.Header.Get("target")
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{}{
"loginButtons": buttons,
})
if err = template.Execute(w, data); err != nil { // Prints: Hello Bob!
panic(err)
}
})
r.HandleFunc(s.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)
// use code from response and exchange for bearer token (with ID token)
bearerToken, err := client.FetchTokenFromAuthenticationServer(
code,
provider.Endpoints.Token,
s.State,
)
if err != nil {
fmt.Printf("failed to fetch token from authentication server: %v\n", err)
http.Redirect(w, r, "/error", http.StatusInternalServerError)
return
}
// extract ID and access tokens from bearer
var data map[string]any
err = json.Unmarshal([]byte(bearerToken), &data)
if err != nil {
fmt.Printf("failed to unmarshal token: %v\n", err)
return
}
if data["error"] != nil {
fmt.Printf("the response from the authentication server returned an error (%v): %v", data["error"], data["error_description"])
http.Redirect(w, r, "/error", http.StatusInternalServerError)
return
}
if data["id_token"] == nil {
fmt.Printf("no ID token found\n")
http.Redirect(w, r, "/error", http.StatusInternalServerError)
return
}
// extract scopes from ID token and add to trusted issuer
// complete JWT bearer flow to receive access token from authorization server
// fmt.Printf("bearer: %v\n", string(bearerToken))
params.IdToken = data["id_token"].(string)
accessToken, err = flows.NewJwtBearerFlow(eps, params)
if err != nil {
fmt.Printf("failed to complete JWT bearer flow: %v\n", err)
w.Header().Add("Content-type", "text/html")
http.Redirect(w, r, "/error", http.StatusInternalServerError)
return
}
}
http.Redirect(w, r, "/success", http.StatusSeeOther)
})
r.HandleFunc("/success", func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Serving success page.\n")
template, err := gonja.FromFile("pages/success.html")
if err != nil {
panic(err)
}
data := exec.NewContext(map[string]interface{}{
"accessToken": accessToken,
})
if err = template.Execute(w, data); err != nil { // Prints: Hello Bob!
panic(err)
}
if target != "" {
httpx.MakeHttpRequest(target, http.MethodPost, []byte(accessToken), httpx.Headers{})
}
})
r.HandleFunc("/error", func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Serving error page.")
errorPage, err := os.ReadFile("pages/error.html")
if err != nil {
fmt.Printf("failed to load error page: %v\n", err)
}
w.Write(errorPage)
})
s.Handler = r
return 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()
s.Handler = r
return s.ListenAndServe()
}