From c25e3e2e1e01380921272323fbc4ac508f4e4491 Mon Sep 17 00:00:00 2001 From: "David J. Allen" Date: Mon, 18 Mar 2024 16:03:37 -0600 Subject: [PATCH] Added more to refresh token flow implementation --- internal/config.go | 46 ++++++++++++++++------------ internal/login.go | 38 ++++++++++++++--------- internal/oidc/oidc.go | 25 +++++++++++++++ internal/server/server.go | 64 +++++++++++++++++++++++++++++++-------- 4 files changed, 127 insertions(+), 46 deletions(-) diff --git a/internal/config.go b/internal/config.go index bc61e96..55d457a 100644 --- a/internal/config.go +++ b/internal/config.go @@ -5,12 +5,13 @@ import ( "log" "os" "path/filepath" + "time" "davidallendj/opaal/internal/server" goutil "github.com/davidallendj/go-utils/util" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) type FlowOptions map[string]string @@ -18,17 +19,19 @@ type Flows map[string]FlowOptions type Providers map[string]string type Options struct { - RunOnce bool `yaml:"run-once"` - OpenBrowser bool `yaml:"open-browser"` - FlowType string `yaml:"flow"` - CachePath string `yaml:"cache"` - CacheOnly bool `yaml:"cache-only"` - TokenForwarding bool `yaml:"token-forwarding"` - Refresh bool `yaml:"refresh"` - Verbose bool `yaml:"verbose"` + RunOnce bool `yaml:"run-once"` + OpenBrowser bool `yaml:"open-browser"` + FlowType string `yaml:"flow"` + CachePath string `yaml:"cache"` + CacheOnly bool `yaml:"cache-only"` + Refresh bool `yaml:"refresh"` + Verbose bool `yaml:"verbose"` } type Endpoints struct { + Issuer string `yaml:"issuer"` + Config string `yaml:"config"` + JwksUri string `yaml:"jwks"` Identities string `yaml:"identities"` TrustedIssuers string `yaml:"trusted-issuers"` Login string `yaml:"login"` @@ -46,8 +49,10 @@ type Authentication struct { } type Authorization struct { - Endpoints Endpoints `yaml:"endpoints"` - KeyPath string `yaml:"key-path"` + Endpoints Endpoints `yaml:"endpoints"` + KeyPath string `yaml:"key-path"` + TokenDuration time.Duration `yaml:"token-duration"` + TokenForwarding bool `yaml:"token-forwarding"` } type Config struct { @@ -67,20 +72,21 @@ func NewConfig() Config { Port: 3333, }, Options: Options{ - RunOnce: true, - OpenBrowser: false, - CachePath: "opaal.db", - FlowType: "authorization_code", - CacheOnly: false, - TokenForwarding: false, - Refresh: true, - Verbose: false, + RunOnce: true, + OpenBrowser: false, + CachePath: "opaal.db", + FlowType: "authorization_code", + CacheOnly: false, + Refresh: true, + Verbose: false, }, Authentication: Authentication{ TestAllClients: false, }, Authorization: Authorization{ - KeyPath: "./keys", + KeyPath: "./keys", + TokenForwarding: false, + TokenDuration: 1 * time.Hour, }, } } diff --git a/internal/login.go b/internal/login.go index 9bd41e4..9ef564f 100644 --- a/internal/login.go +++ b/internal/login.go @@ -40,23 +40,33 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider // 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), + params := server.ServerParams{ + AuthProvider: &oidc.IdentityProvider{ + Issuer: config.Authorization.Endpoints.Issuer, + Endpoints: oidc.Endpoints{ + Config: config.Authorization.Endpoints.Config, + JwksUri: config.Authorization.Endpoints.JwksUri, + }, }, Verbose: config.Options.Verbose, + JwtBearerFlowEndpoints: flows.JwtBearerEndpoints{ + Token: config.Authorization.Endpoints.Token, + TrustedIssuers: config.Authorization.Endpoints.TrustedIssuers, + Register: config.Authorization.Endpoints.Register, + }, + JwtBearerFlowParams: flows.JwtBearerFlowParams{ + Client: oauth.NewClient(), + IdentityProvider: provider, + TrustedIssuer: &oauth.TrustedIssuer{ + AllowAnySubject: false, + Issuer: server.Addr, + Subject: "opaal", + ExpiresAt: time.Now().Add(config.Authorization.TokenDuration), + }, + Verbose: config.Options.Verbose, + }, } - err = server.Login(button, provider, client, eps, params) + err = server.Login(button, provider, client, params) if errors.Is(err, http.ErrServerClosed) { fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n") } else if err != nil { diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index 63dbb97..af813ab 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -20,6 +20,7 @@ type IdentityProvider struct { } type Endpoints struct { + Config string `db:"config_endpoint" json:"config_endpoint" yaml:"config"` Authorization string `db:"authorization_endpoint" json:"authorization_endpoint" yaml:"authorization"` Token string `db:"token_endpoint" json:"token_endpoint" yaml:"token"` Revocation string `db:"revocation_endpoint" json:"revocation_endpoint" yaml:"revocation"` @@ -109,6 +110,30 @@ func (p *IdentityProvider) LoadServerConfig(path string) error { return nil } +func (p *IdentityProvider) FetchServerConfig() error { + // make a request to a server's openid-configuration + req, err := http.NewRequest(http.MethodGet, p.Issuer+"/.well-known/openid-configuration", bytes.NewBuffer([]byte{})) + if err != nil { + return fmt.Errorf("failed to create a new request: %v", err) + } + + client := &http.Client{} // temp client to get info and not used in flow + res, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to do request: %v", err) + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %v", err) + } + err = p.ParseServerConfig(body) + if err != nil { + return fmt.Errorf("failed to parse server config: %v", err) + } + return nil +} + func FetchServerConfig(issuer string) (*IdentityProvider, error) { // make a request to a server's openid-configuration req, err := http.NewRequest(http.MethodGet, issuer+"/.well-known/openid-configuration", bytes.NewBuffer([]byte{})) diff --git a/internal/server/server.go b/internal/server/server.go index 6ebfee8..5e80cc6 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -23,6 +23,13 @@ type Server struct { State string `yaml:"state"` } +type ServerParams struct { + AuthProvider *oidc.IdentityProvider + Verbose bool + JwtBearerEndpoints flows.JwtBearerEndpoints + JwtBearerParams flows.JwtBearerFlowParams +} + func (s *Server) SetListenAddr(host string, port int) { s.Addr = s.GetListenAddr() } @@ -31,7 +38,7 @@ 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 { +func (s *Server) Login(buttons string, provider *oidc.IdentityProvider, client *oauth.Client, params ServerParams) error { var target = "" // check if callback is set @@ -66,7 +73,41 @@ func (s *Server) Login(buttons string, provider *oidc.IdentityProvider, client * panic(err) } }) - r.HandleFunc("/key", func(w http.ResponseWriter, r *http.Request) { + r.HandleFunc("/keys", func(w http.ResponseWriter, r *http.Request) { + var ( + p = params.AuthProvider + jwks []byte + ) + // try and get the JWKS from param first + if p.Endpoints.JwksUri != "" { + err := p.FetchJwks() + if err != nil { + fmt.Printf("failed to fetch keys using JWKS url...trying to fetch config and try again...\n") + } + jwks, err = json.Marshal(p.KeySet) + if err != nil { + fmt.Printf("failed to marshal JWKS: %v\n", err) + } + } else if p.Endpoints.Config != "" && jwks == nil { + // otherwise, try and fetch the whole config and try again + err := p.FetchServerConfig() + if err != nil { + fmt.Printf("failed to fetch server config: %v\n", err) + } + err = p.FetchJwks() + if err != nil { + fmt.Printf("failed to fetch JWKS after fetching server config: %v\n", err) + } + + } + + // forward the JWKS from the authorization server + if jwks == nil { + fmt.Printf("no JWKS was fetched from authorization server\n") + http.Redirect(w, r, "/error", http.StatusInternalServerError) + return + } + w.Write(jwks) }) r.HandleFunc("/refresh", func(w http.ResponseWriter, r *http.Request) { @@ -81,12 +122,13 @@ func (s *Server) Login(buttons string, provider *oidc.IdentityProvider, client * // return token to target if set or the sending client returnTarget := r.URL.Query().Get("target") - if returnTarget != "" { - - } else { - host := r.URL.Host - httpx.MakeHttpRequest(host, http.MethodPost, httpx.Body{}, httpx.Headers{}) - + if returnTarget == "" { + returnTarget = r.URL.Host + } + _, _, err = httpx.MakeHttpRequest(returnTarget, http.MethodPost, httpx.Body{}, httpx.Headers{}) + if err != nil { + fmt.Printf("failed to make request") + http.Redirect(w, r, "/error", http.StatusInternalServerError) } }) r.HandleFunc(s.Callback, func(w http.ResponseWriter, r *http.Request) { @@ -127,12 +169,10 @@ func (s *Server) Login(buttons string, provider *oidc.IdentityProvider, client * return } - // TODO: 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) + params.JwtBearerParams.IdToken = data["id_token"].(string) + accessToken, err = flows.NewJwtBearerFlow(params.JwtBearerEndpoints, params.JwtBearerParams) if err != nil { fmt.Printf("failed to complete JWT bearer flow: %v\n", err) w.Header().Add("Content-type", "text/html")