diff --git a/cmd/serve.go b/cmd/serve.go
index 67cec1a..b68b814 100644
--- a/cmd/serve.go
+++ b/cmd/serve.go
@@ -2,6 +2,7 @@ package cmd
import (
opaal "davidallendj/opaal/internal"
+ "davidallendj/opaal/internal/oidc"
"errors"
"fmt"
"net/http"
@@ -9,9 +10,13 @@ import (
"github.com/spf13/cobra"
)
-var exampleCmd = &cobra.Command{
+var (
+ endpoints oidc.Endpoints
+)
+
+var serveCmd = &cobra.Command{
Use: "serve",
- Short: "Start an simple identity provider server",
+ Short: "Start an simple, bare minimal identity provider server",
Long: "The built-in identity provider is not (nor meant to be) a complete OIDC implementation and behaves like an external IdP",
Run: func(cmd *cobra.Command, args []string) {
s := opaal.NewServerWithConfig(&config)
@@ -21,11 +26,15 @@ var exampleCmd = &cobra.Command{
if errors.Is(err, http.ErrServerClosed) {
fmt.Printf("Identity provider server closed.\n")
} else if err != nil {
- fmt.Errorf("failed to start server: %v", err)
+ fmt.Printf("failed to start server: %v", err)
}
},
}
func init() {
- rootCmd.AddCommand(exampleCmd)
+ serveCmd.Flags().StringVar(&endpoints.Authorization, "endpoints.authorization", "", "set the authorization endpoint for the identity provider")
+ serveCmd.Flags().StringVar(&endpoints.Token, "endpoints.token", "", "set the token endpoint for the identity provider")
+ serveCmd.Flags().StringVar(&endpoints.JwksUri, "endpoints.jwks_uri", "", "set the JWKS endpoints for the identity provider")
+
+ rootCmd.AddCommand(serveCmd)
}
diff --git a/internal/config.go b/internal/config.go
index 8e2139f..9108ac1 100644
--- a/internal/config.go
+++ b/internal/config.go
@@ -2,6 +2,7 @@ package opaal
import (
"davidallendj/opaal/internal/oauth"
+ "davidallendj/opaal/internal/oidc"
"log"
"os"
"path/filepath"
@@ -72,11 +73,18 @@ type Config struct {
}
func NewConfig() Config {
- return Config{
+ config := Config{
Version: goutil.GetCommit(),
Server: server.Server{
Host: "127.0.0.1",
Port: 3333,
+ Issuer: server.IdentityProviderServer{
+ Endpoints: oidc.Endpoints{
+ Authorization: "",
+ Token: "",
+ JwksUri: "",
+ },
+ },
},
Options: Options{
RunOnce: true,
@@ -99,6 +107,7 @@ func NewConfig() Config {
},
},
}
+ return config
}
func LoadConfig(path string) Config {
diff --git a/internal/flows/jwt_bearer.go b/internal/flows/jwt_bearer.go
index 2e93265..a0287d9 100644
--- a/internal/flows/jwt_bearer.go
+++ b/internal/flows/jwt_bearer.go
@@ -51,6 +51,9 @@ func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (s
if client == nil {
return "", fmt.Errorf("invalid client (client is nil)")
}
+ if verbose {
+ fmt.Printf("ID token (IDP): %s\n access token (IDP): %s", accessToken, idToken)
+ }
if accessToken != "" {
_, err := jws.Verify([]byte(accessToken), jws.WithKeySet(client.Provider.KeySet), jws.WithValidateKey(true))
if err != nil {
diff --git a/internal/login.go b/internal/login.go
index ab51747..b6aaf84 100644
--- a/internal/login.go
+++ b/internal/login.go
@@ -42,8 +42,9 @@ func Login(config *Config) error {
AuthProvider: &oidc.IdentityProvider{
Issuer: config.Authorization.Endpoints.Issuer,
Endpoints: oidc.Endpoints{
- Config: config.Authorization.Endpoints.Config,
- JwksUri: config.Authorization.Endpoints.JwksUri,
+ Config: config.Authorization.Endpoints.Config,
+ Authorization: config.Authorization.Endpoints.Authorize,
+ JwksUri: config.Authorization.Endpoints.JwksUri,
},
},
JwtBearerEndpoints: flows.JwtBearerFlowEndpoints{
diff --git a/internal/new.go b/internal/new.go
index 55d9cda..2799d88 100644
--- a/internal/new.go
+++ b/internal/new.go
@@ -90,9 +90,11 @@ func NewServerWithConfig(conf *Config) *server.Server {
},
Host: host,
Port: port,
- Issuer: server.Issuer{
- Host: conf.Server.Issuer.Host,
- Port: conf.Server.Issuer.Port,
+ Issuer: server.IdentityProviderServer{
+ Host: conf.Server.Issuer.Host,
+ Port: conf.Server.Issuer.Port,
+ Endpoints: conf.Server.Issuer.Endpoints,
+ Clients: conf.Server.Issuer.Clients,
},
}
return server
diff --git a/internal/oauth/authenticate.go b/internal/oauth/authenticate.go
index b579e8e..4af65cb 100644
--- a/internal/oauth/authenticate.go
+++ b/internal/oauth/authenticate.go
@@ -109,12 +109,14 @@ func (client *Client) FetchTokenFromAuthenticationServer(code string, state stri
}
res, err := http.PostForm(client.Provider.Endpoints.Token, body)
if err != nil {
- return nil, fmt.Errorf("failed to get ID token: %s", err)
+ return nil, fmt.Errorf("failed to get ID token: %v", err)
}
+ b, err := io.ReadAll(res.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response body: %v", err)
+ }
+ fmt.Printf("%s\n", string(b))
defer res.Body.Close()
- // domain, _ := url.Parse("http://127.0.0.1")
- // client.Jar.SetCookies(domain, res.Cookies())
-
- return io.ReadAll(res.Body)
+ return b, nil
}
diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go
index 2ec1f9c..f52e0c4 100644
--- a/internal/oidc/oidc.go
+++ b/internal/oidc/oidc.go
@@ -164,3 +164,25 @@ func (p *IdentityProvider) FetchJwks() error {
return nil
}
+
+func (p *IdentityProvider) UpdateEndpoints(other *IdentityProvider) {
+ UpdateEndpoints(&p.Endpoints, &other.Endpoints)
+}
+
+func UpdateEndpoints(eps *Endpoints, other *Endpoints) {
+ // only update endpoints that are not empty
+ var UpdateIfEmpty = func(ep *string, s string) {
+ if ep != nil {
+ if *ep == "" {
+ *ep = s
+ }
+ }
+ }
+ UpdateIfEmpty(&eps.Config, other.Config)
+ UpdateIfEmpty(&eps.Authorization, other.Authorization)
+ UpdateIfEmpty(&eps.Token, other.Token)
+ UpdateIfEmpty(&eps.Revocation, other.Revocation)
+ UpdateIfEmpty(&eps.Introspection, other.Introspection)
+ UpdateIfEmpty(&eps.UserInfo, other.UserInfo)
+ UpdateIfEmpty(&eps.JwksUri, other.JwksUri)
+}
diff --git a/internal/server/idp.go b/internal/server/idp.go
index 67c70b1..bb82a28 100644
--- a/internal/server/idp.go
+++ b/internal/server/idp.go
@@ -3,7 +3,6 @@ package server
import (
"crypto/rand"
"crypto/rsa"
- "davidallendj/opaal/internal/oauth"
"davidallendj/opaal/internal/oidc"
"encoding/json"
"fmt"
@@ -22,6 +21,22 @@ import (
"github.com/lestrrat-go/jwx/v2/jwt"
)
+// TODO: make this a completely separate server
+type IdentityProviderServer struct {
+ Host string `yaml:"host"`
+ Port int `yaml:"port"`
+ Endpoints oidc.Endpoints `yaml:"endpoints"`
+ Clients []RegisteredClient `yaml:"clients"`
+}
+
+// NOTE: could we use a oauth.Client here instead??
+type RegisteredClient struct {
+ Id string `yaml:"id"`
+ Secret string `yaml:"secret"`
+ Name string `yaml:"name"`
+ RedirectUris []string `yaml:"redirect-uris"`
+}
+
func (s *Server) StartIdentityProvider() error {
// NOTE: this example does NOT implement CSRF tokens nor use them
@@ -29,14 +44,16 @@ func (s *Server) StartIdentityProvider() error {
var (
r = chi.NewRouter()
// clients = []oauth.Client{}
- callback = ""
activeCodes = []string{}
)
- // check if callback is set
- if s.Callback == "" {
- callback = "/oidc/callback"
+ // update endpoints that have values set
+ defaultEps := oidc.Endpoints{
+ Authorization: "http://" + s.Addr + "/oauth2/authorize",
+ Token: "http://" + s.Addr + "/oauth2/token",
+ JwksUri: "http://" + s.Addr + "/.well-known/jwks.json",
}
+ oidc.UpdateEndpoints(&s.Issuer.Endpoints, &defaultEps)
// generate key pair used to sign JWKS and create JWTs
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
@@ -72,14 +89,13 @@ func (s *Server) StartIdentityProvider() error {
w.Write(b)
})
- // TODO: create .well-known openid configuration
r.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
// create config JSON to serve with GET request
config := map[string]any{
"issuer": "http://" + s.Addr,
- "authorization_endpoint": "http://" + s.Addr + "/oauth/authorize",
- "token_endpoint": "http://" + s.Addr + "/oauth/token",
- "jwks_uri": "http://" + s.Addr + "/.well-known/jwks.json",
+ "authorization_endpoint": s.Issuer.Endpoints.Authorization,
+ "token_endpoint": s.Issuer.Endpoints.Token,
+ "jwks_uri": s.Issuer.Endpoints.JwksUri,
"scopes_supported": []string{
"openid",
"profile",
@@ -131,21 +147,18 @@ func (s *Server) StartIdentityProvider() error {
username := r.Form.Get("username")
password := r.Form.Get("password")
+ if len(s.Issuer.Clients) <= 0 {
+ fmt.Printf("no registered clients found with identity provider (add them in config)\n")
+ return
+ }
+
// example username and password so do simplified authorization code flow
- if username == "ochami" && password == "ochami" {
- client := oauth.Client{
- Id: "ochami",
- Secret: "ochami",
- Name: "ochami",
- Provider: oidc.IdentityProvider{
- Issuer: "http://127.0.0.1:3333",
- },
- RedirectUris: []string{fmt.Sprintf("http://%s:%d%s", s.Host, s.Port, callback)},
- }
+ if username == "openchami" && password == "openchami" {
+ client := s.Issuer.Clients[0]
// check if there are any redirect URIs supplied
if len(client.RedirectUris) <= 0 {
- fmt.Printf("no redirect URIs found")
+ fmt.Printf("no redirect URIs found for client %s (ID: %s)\n", client.Name, client.Id)
return
}
for _, url := range client.RedirectUris {
@@ -166,7 +179,7 @@ func (s *Server) StartIdentityProvider() error {
http.Redirect(w, r, "/browser/login", http.StatusUnauthorized)
}
})
- r.HandleFunc("/oauth/token", func(w http.ResponseWriter, r *http.Request) {
+ r.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
// check for authorization code and make sure it's valid
@@ -240,7 +253,7 @@ func (s *Server) StartIdentityProvider() error {
fmt.Printf("bearer: %s\n", string(b))
w.Write(b)
})
- r.HandleFunc("/oauth/authorize", func(w http.ResponseWriter, r *http.Request) {
+ r.HandleFunc("/oauth2/authorize", func(w http.ResponseWriter, r *http.Request) {
var (
responseType = r.URL.Query().Get("response_type")
clientId = r.URL.Query().Get("client_id")
@@ -253,9 +266,13 @@ func (s *Server) StartIdentityProvider() error {
return
}
- // check that we're using the default registered client
- if clientId != "ochami" {
- fmt.Printf("invalid client\n")
+ // find a valid client
+ index := slices.IndexFunc(s.Issuer.Clients, func(c RegisteredClient) bool {
+ fmt.Printf("%s ? %s\n", c.Id, clientId)
+ return c.Id == clientId
+ })
+ if index < 0 {
+ fmt.Printf("no valid client found")
return
}
diff --git a/internal/server/server.go b/internal/server/server.go
index 3c7507b..3fdae97 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -18,16 +18,11 @@ import (
type Server struct {
*http.Server
- Host string `yaml:"host"`
- Port int `yaml:"port"`
- Callback string `yaml:"callback"`
- State string `yaml:"state"`
- Issuer Issuer `yaml:"issuer"`
-}
-
-type Issuer struct {
- Host string `yaml:"host"`
- Port int `yaml:"port"`
+ Host string `yaml:"host"`
+ Port int `yaml:"port"`
+ Callback string `yaml:"callback"`
+ State string `yaml:"state"`
+ Issuer IdentityProviderServer `yaml:"issuer"`
}
type ServerParams struct {
@@ -62,7 +57,7 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
// make the login page SSO buttons and authorization URLs to write to stdout
buttons := ""
- fmt.Printf("Login with external identity providers: \n")
+ fmt.Printf("Login with an identity provider: \n")
for i, client := range clients {
// fetch provider configuration before adding button
p, err := oidc.FetchServerConfig(client.Provider.Issuer)
@@ -79,8 +74,7 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
clients[i].Provider = *p
buttons += makeButton(fmt.Sprintf("/login?sso=%s", client.Id), client.Name)
- url := client.BuildAuthorizationUrl(s.State)
- fmt.Printf("\t%s\n", url)
+ fmt.Printf("\t%s: /login?sso=%s\n", client.Name, client.Id)
}
var code string
@@ -120,7 +114,9 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
client = &clients[index]
url := client.BuildAuthorizationUrl(s.State)
- fmt.Printf("Redirect URL: %s\n", url)
+ if params.Verbose {
+ fmt.Printf("Redirect URL: %s\n", url)
+ }
http.Redirect(w, r, url, http.StatusFound)
return
}
@@ -145,38 +141,47 @@ func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
p = params.AuthProvider
jwks []byte
)
- // try and get the JWKS from param first
- if p.Endpoints.JwksUri != "" {
- err := p.FetchJwks()
+
+ fetchAndMarshal := func() (err error) {
+ err = p.FetchJwks()
if err != nil {
- fmt.Printf("failed to fetch keys using JWKS url...trying to fetch config and try again...\n")
+ fmt.Printf("failed to fetch keys: %v\n", err)
+ return
}
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)
- http.Redirect(w, r, "/error", http.StatusInternalServerError)
- return
- }
- err = p.FetchJwks()
- if err != nil {
- fmt.Printf("failed to fetch JWKS after fetching server config: %v\n", err)
- http.Redirect(w, r, "/error", http.StatusInternalServerError)
+ return
+ }
+
+ // try and get the JWKS from param first
+ if p.Endpoints.JwksUri != "" {
+ if err := fetchAndMarshal(); err != nil {
+ w.Write(jwks)
return
}
}
- // 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)
+ // otherwise or if fetching the JWKS failed, try and fetch the whole config first and try again
+ if p.Endpoints.Config != "" {
+ if err := p.FetchServerConfig(); err != nil {
+ fmt.Printf("failed to fetch server config: %v\n", err)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
+ }
+ } else {
+ fmt.Printf("getting JWKS from param failed and endpoints config unavailable\n")
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
+
+ if err := fetchAndMarshal(); err != nil {
+ fmt.Printf("failed to fetch and marshal JWKS after config update: %v\n", err)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
+ }
+
w.Write(jwks)
})
r.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
diff --git a/pages/login.html b/pages/login.html
index e55f5cf..c25f8c3 100644
--- a/pages/login.html
+++ b/pages/login.html
@@ -7,7 +7,7 @@
Forgot Username?
-
+