mirror of
https://github.com/davidallendj/opaal.git
synced 2025-12-20 03:27:02 -07:00
More refactoring and code restructure
This commit is contained in:
parent
45e8cd7f15
commit
f6bf8a8960
4 changed files with 254 additions and 40 deletions
|
|
@ -1,10 +1,13 @@
|
||||||
package opaal
|
package opaal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"davidallendj/opaal/internal/oauth"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"davidallendj/opaal/internal/server"
|
||||||
|
|
||||||
goutil "github.com/davidallendj/go-utils/util"
|
goutil "github.com/davidallendj/go-utils/util"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
|
|
@ -15,17 +18,16 @@ type Flows map[string]FlowOptions
|
||||||
type Providers map[string]string
|
type Providers map[string]string
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
DecodeIdToken bool `yaml:"decode-id-token"`
|
RunOnce bool `yaml:"run-once"`
|
||||||
DecodeAccessToken bool `yaml:"decode-access-token"`
|
OpenBrowser bool `yaml:"open-browser"`
|
||||||
RunOnce bool `yaml:"run-once"`
|
FlowType string `yaml:"flow"`
|
||||||
OpenBrowser bool `yaml:"open-browser"`
|
CachePath string `yaml:"cache"`
|
||||||
FlowType string `yaml:"flow"`
|
CacheOnly bool `yaml:"cache-only"`
|
||||||
CachePath string `yaml:"cache"`
|
TokenForwarding bool `yaml:"token-forwarding"`
|
||||||
LocalOnly bool `yaml:"local-only"`
|
Verbose bool `yaml:"verbose"`
|
||||||
ForwardToken bool `yaml:"forward-token"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RequestUrls struct {
|
type Endpoints struct {
|
||||||
Identities string `yaml:"identities"`
|
Identities string `yaml:"identities"`
|
||||||
TrustedIssuers string `yaml:"trusted-issuers"`
|
TrustedIssuers string `yaml:"trusted-issuers"`
|
||||||
Login string `yaml:"login"`
|
Login string `yaml:"login"`
|
||||||
|
|
@ -36,17 +38,20 @@ type RequestUrls struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Authentication struct {
|
type Authentication struct {
|
||||||
Clients []Client `yaml:"clients"`
|
Clients []oauth.Client `yaml:"clients"`
|
||||||
Flows Flows `yaml:"flows"`
|
Flows Flows `yaml:"flows"`
|
||||||
|
TestAllClients bool `yaml:"test-all"`
|
||||||
|
State string `yaml:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Authorization struct {
|
type Authorization struct {
|
||||||
RequestUrls RequestUrls `yaml:"urls"`
|
Endpoints Endpoints `yaml:"endpoints"`
|
||||||
|
KeyPath string `yaml:"key-path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Version string `yaml:"version"`
|
Version string `yaml:"version"`
|
||||||
Server Server `yaml:"server"`
|
Server server.Server `yaml:"server"`
|
||||||
Providers Providers `yaml:"providers"`
|
Providers Providers `yaml:"providers"`
|
||||||
Options Options `yaml:"options"`
|
Options Options `yaml:"options"`
|
||||||
Authentication Authentication `yaml:"authentication"`
|
Authentication Authentication `yaml:"authentication"`
|
||||||
|
|
@ -56,22 +61,25 @@ type Config struct {
|
||||||
func NewConfig() Config {
|
func NewConfig() Config {
|
||||||
return Config{
|
return Config{
|
||||||
Version: goutil.GetCommit(),
|
Version: goutil.GetCommit(),
|
||||||
Server: Server{
|
Server: server.Server{
|
||||||
Host: "127.0.0.1",
|
Host: "127.0.0.1",
|
||||||
Port: 3333,
|
Port: 3333,
|
||||||
},
|
},
|
||||||
Options: Options{
|
Options: Options{
|
||||||
DecodeIdToken: true,
|
RunOnce: true,
|
||||||
DecodeAccessToken: true,
|
OpenBrowser: false,
|
||||||
RunOnce: true,
|
CachePath: "opaal.db",
|
||||||
OpenBrowser: false,
|
FlowType: "authorization_code",
|
||||||
CachePath: "opaal.db",
|
CacheOnly: false,
|
||||||
FlowType: "authorization_code",
|
TokenForwarding: false,
|
||||||
LocalOnly: false,
|
Verbose: false,
|
||||||
ForwardToken: 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
|
// must have athe requirements to perform login
|
||||||
hasClients := len(config.Authentication.Clients) > 0
|
hasClients := len(config.Authentication.Clients) > 0
|
||||||
hasServer := config.Server.Host != "" && config.Server.Port != 0 && config.Server.Callback != ""
|
hasServer := config.Server.Host != "" && config.Server.Port != 0 && config.Server.Callback != ""
|
||||||
hasEndpoints := config.Authorization.RequestUrls.TrustedIssuers != "" &&
|
hasEndpoints := config.Authorization.Endpoints.TrustedIssuers != "" &&
|
||||||
config.Authorization.RequestUrls.Login != "" &&
|
config.Authorization.Endpoints.Login != "" &&
|
||||||
config.Authorization.RequestUrls.Clients != "" &&
|
config.Authorization.Endpoints.Clients != "" &&
|
||||||
config.Authorization.RequestUrls.Authorize != "" &&
|
config.Authorization.Endpoints.Authorize != "" &&
|
||||||
config.Authorization.RequestUrls.Token != ""
|
config.Authorization.Endpoints.Token != ""
|
||||||
return hasClients && hasServer && hasEndpoints
|
return hasClients && hasServer && hasEndpoints
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,70 @@
|
||||||
package opaal
|
package opaal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"davidallendj/opaal/internal/db"
|
cache "davidallendj/opaal/internal/cache/sqlite"
|
||||||
|
"davidallendj/opaal/internal/flows"
|
||||||
|
"davidallendj/opaal/internal/oauth"
|
||||||
"davidallendj/opaal/internal/oidc"
|
"davidallendj/opaal/internal/oidc"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"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 {
|
if config == nil {
|
||||||
return fmt.Errorf("config is not valid")
|
return fmt.Errorf("config is not valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// make cache if it's not where expect
|
// make cache if it's not where expect
|
||||||
_, err := db.CreateIdentityProvidersIfNotExists(config.Options.CachePath)
|
_, err := cache.CreateIdentityProvidersIfNotExists(config.Options.CachePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("failed to create cache: %v\n", err)
|
fmt.Printf("failed to create cache: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Options.FlowType == "authorization_code" {
|
if config.Options.FlowType == "authorization_code" {
|
||||||
// create a server if doing authorization code flow
|
// build the authorization URL to redirect user for social sign-in
|
||||||
server := NewServerWithConfig(config)
|
var state = ""
|
||||||
err := AuthorizationCodeWithConfig(config, server, client, provider)
|
if config.Authentication.Flows["authorization-code"]["state"] != "" {
|
||||||
if err != nil {
|
state = config.Authentication.Flows["authorization-code"]["state"]
|
||||||
fmt.Printf("failed to complete authorization code flow: %v\n", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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" {
|
} else if config.Options.FlowType == "client_credentials" {
|
||||||
err := ClientCredentialsWithConfig(config, client)
|
err := NewClientCredentialsFlowWithConfig(config, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("failed to complete client credentials flow: %v", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MakeButton(url string, text string) string {
|
||||||
|
return "<a href=\"" + url + "\"> " + text + "</a>"
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ type IdentityProvider struct {
|
||||||
Issuer string `db:"issuer" json:"issuer" yaml:"issuer"`
|
Issuer string `db:"issuer" json:"issuer" yaml:"issuer"`
|
||||||
Endpoints Endpoints `db:"endpoints" json:"endpoints" yaml:"endpoints"`
|
Endpoints Endpoints `db:"endpoints" json:"endpoints" yaml:"endpoints"`
|
||||||
Supported Supported `db:"supported" json:"supported" yaml:"supported"`
|
Supported Supported `db:"supported" json:"supported" yaml:"supported"`
|
||||||
Jwks jwk.Set
|
KeySet jwk.Set
|
||||||
}
|
}
|
||||||
|
|
||||||
type Endpoints struct {
|
type Endpoints struct {
|
||||||
|
|
@ -142,7 +142,7 @@ func (p *IdentityProvider) FetchJwks() error {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
var err error
|
var err error
|
||||||
p.Jwks, err = jwk.Fetch(ctx, p.Endpoints.JwksUri)
|
p.KeySet, err = jwk.Fetch(ctx, p.Endpoints.JwksUri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch JWKS: %v", err)
|
return fmt.Errorf("failed to fetch JWKS: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
163
internal/server/server.go
Normal file
163
internal/server/server.go
Normal 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()
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue