diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml
index 5b7ff5e..a1336da 100644
--- a/.github/workflows/build_release.yml
+++ b/.github/workflows/build_release.yml
@@ -4,6 +4,7 @@
name: Release with goreleaser
on:
+ workflow_dispatch:
push:
tags:
- v*
@@ -52,7 +53,3 @@ jobs:
echo "fs.writeFileSync('digest.txt', firstNonNullDigest);" >> process.js
node process.js
echo "digest=$(cat digest.txt)" >> $GITHUB_OUTPUT
- - name: Attest opaal binary
- uses: github-early-access/generate-build-provenance@main
- with:
- subject-path: opaal
\ No newline at end of file
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 72956fa..8e9d87d 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -53,4 +53,4 @@ changelog:
# github:
# name_template: "{{.Version}}"
# prerelease: auto
-# mode: append
\ No newline at end of file
+# mode: append
diff --git a/cmd/login.go b/cmd/login.go
index abe3452..855a0e8 100644
--- a/cmd/login.go
+++ b/cmd/login.go
@@ -2,12 +2,9 @@ package cmd
import (
opaal "davidallendj/opaal/internal"
- cache "davidallendj/opaal/internal/cache/sqlite"
"davidallendj/opaal/internal/oauth"
- "davidallendj/opaal/internal/oidc"
"fmt"
"os"
- "slices"
"github.com/spf13/cobra"
)
@@ -25,68 +22,68 @@ var loginCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
for {
// try and find client with valid identity provider config
- var provider *oidc.IdentityProvider
- if target != "" {
- // only try to use client with name give
- index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool {
- return target == c.Name
- })
- if index < 0 {
- fmt.Printf("could not find the target client listed by name")
- os.Exit(1)
- }
- client := config.Authentication.Clients[index]
- _, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer)
- if err != nil {
+ // var provider *oidc.IdentityProvider
+ // if target != "" {
+ // // only try to use client with name give
+ // index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool {
+ // return target == c.Name
+ // })
+ // if index < 0 {
+ // fmt.Printf("could not find the target client listed by name")
+ // os.Exit(1)
+ // }
+ // client := config.Authentication.Clients[index]
+ // _, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer)
+ // if err != nil {
- }
+ // }
- } else if targetIndex >= 0 {
- // only try to use client by index
- targetCount := len(config.Authentication.Clients) - 1
- if targetIndex > targetCount {
- fmt.Printf("target index out of range (found %d)", targetCount)
- }
- client := config.Authentication.Clients[targetIndex]
- _, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer)
- if err != nil {
+ // } else if targetIndex >= 0 {
+ // // only try to use client by index
+ // targetCount := len(config.Authentication.Clients) - 1
+ // if targetIndex > targetCount {
+ // fmt.Printf("target index out of range (found %d)", targetCount)
+ // }
+ // client := config.Authentication.Clients[targetIndex]
+ // _, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer)
+ // if err != nil {
- }
- } else {
- for _, c := range config.Authentication.Clients {
- // try to get identity provider info locally first
- _, err := cache.GetIdentityProvider(config.Options.CachePath, c.Issuer)
- if err != nil && !config.Options.CacheOnly {
- fmt.Printf("fetching config from issuer: %v\n", c.Issuer)
- // try to get info remotely by fetching
- provider, err = oidc.FetchServerConfig(c.Issuer)
- if err != nil {
- fmt.Printf("failed to fetch server config: %v\n", err)
- continue
- }
- client = c
- // fetch the provider's JWKS
- err := provider.FetchJwks()
- if err != nil {
- fmt.Printf("failed to fetch JWKS: %v\n", err)
- }
- break
- }
- // only test the first if --run-all flag is not set
- if !config.Authentication.TestAllClients {
- fmt.Printf("stopping after first test...\n\n\n")
- break
- }
- }
- }
+ // }
+ // } else {
+ // for _, c := range config.Authentication.Clients {
+ // // try to get identity provider info locally first
+ // _, err := cache.GetIdentityProvider(config.Options.CachePath, c.Issuer)
+ // if err != nil && !config.Options.CacheOnly {
+ // fmt.Printf("fetching config from issuer: %v\n", c.Issuer)
+ // // try to get info remotely by fetching
+ // provider, err = oidc.FetchServerConfig(c.Issuer)
+ // if err != nil {
+ // fmt.Printf("failed to fetch server config: %v\n", err)
+ // continue
+ // }
+ // client = c
+ // // fetch the provider's JWKS
+ // err := provider.FetchJwks()
+ // if err != nil {
+ // fmt.Printf("failed to fetch JWKS: %v\n", err)
+ // }
+ // break
+ // }
+ // // only test the first if --run-all flag is not set
+ // if !config.Authentication.TestAllClients {
+ // fmt.Printf("stopping after first test...\n\n\n")
+ // break
+ // }
+ // }
+ // }
- if provider == nil {
- fmt.Printf("failed to retrieve provider config\n")
- os.Exit(1)
- }
+ // if provider == nil {
+ // fmt.Printf("failed to retrieve provider config\n")
+ // os.Exit(1)
+ // }
// start the listener
- err := opaal.Login(&config, &client, provider)
+ err := opaal.Login(&config)
if err != nil {
fmt.Printf("%v\n", err)
os.Exit(1)
@@ -115,3 +112,13 @@ func init() {
loginCmd.MarkFlagsMutuallyExclusive("target.name", "target.index")
rootCmd.AddCommand(loginCmd)
}
+
+func MakeButton(url string, text string) string {
+ // check if we have http:// a
+ html := " " + text + ""
+}
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 1515013..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"
@@ -45,6 +46,7 @@ type TokenOptions struct {
Forwarding bool `yaml:"forwarding"`
Refresh bool `yaml:"refresh"`
Scope []string `yaml:"scope"`
+ //TODO: allow specifying audience in returned token
}
type Authentication struct {
@@ -55,9 +57,10 @@ type Authentication struct {
}
type Authorization struct {
+ Token TokenOptions `yaml:"token"`
Endpoints Endpoints `yaml:"endpoints"`
KeyPath string `yaml:"key-path"`
- Token TokenOptions `yaml:"token"`
+ Audience []string `yaml:"audience"` // NOTE: overrides the "aud" claim in token sent to authorization server
}
type Config struct {
@@ -70,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,
@@ -97,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 652944b..a0287d9 100644
--- a/internal/flows/jwt_bearer.go
+++ b/internal/flows/jwt_bearer.go
@@ -4,7 +4,6 @@ import (
"crypto/rand"
"crypto/rsa"
"davidallendj/opaal/internal/oauth"
- "davidallendj/opaal/internal/oidc"
"encoding/json"
"fmt"
"os"
@@ -19,14 +18,15 @@ import (
)
type JwtBearerFlowParams struct {
- AccessToken string
- IdToken string
- IdentityProvider *oidc.IdentityProvider
- TrustedIssuer *oauth.TrustedIssuer
- Client *oauth.Client
- Refresh bool
- Verbose bool
- KeyPath string
+ AccessToken string
+ IdToken string
+ // IdentityProvider *oidc.IdentityProvider
+ TrustedIssuer *oauth.TrustedIssuer
+ Client *oauth.Client
+ Audience []string
+ Refresh bool
+ Verbose bool
+ KeyPath string
}
type JwtBearerFlowEndpoints struct {
@@ -39,22 +39,30 @@ type JwtBearerFlowEndpoints struct {
func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (string, error) {
// 1. verify that the JWT from the issuer is valid using all keys
var (
- idp = params.IdentityProvider
+ // idp = params.IdentityProvider
accessToken = params.AccessToken
idToken = params.IdToken
client = params.Client
trustedIssuer = params.TrustedIssuer
verbose = params.Verbose
)
+
+ // pre-condition checks to make sure certain variables are set
+ 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(idp.KeySet), jws.WithValidateKey(true))
+ _, err := jws.Verify([]byte(accessToken), jws.WithKeySet(client.Provider.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))
+ _, err := jws.Verify([]byte(idToken), jws.WithKeySet(client.Provider.KeySet), jws.WithValidateKey(true))
if err != nil {
return "", fmt.Errorf("failed to verify ID token: %v", err)
}
@@ -126,7 +134,7 @@ func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (s
// 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))
+ parsedIdToken, err := jwt.ParseString(idToken, jwt.WithKeySet(client.Provider.KeySet))
if err != nil {
return "", fmt.Errorf("failed to parse ID token: %v", err)
}
@@ -139,6 +147,11 @@ func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (s
payload["exp"] = time.Now().Add(time.Second * 3600 * 16).Unix()
payload["sub"] = "opaal"
+ // if an "audience" value is set, then override the token endpoint value
+ if len(params.Audience) > 0 {
+ payload["aud"] = params.Audience
+ }
+
// include the offline_access scope if refresh tokens are enabled
if params.Refresh {
v, ok := payload["scope"]
@@ -242,7 +255,7 @@ func ForwardToken(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) error
var (
client = params.Client
idToken = params.IdToken
- idp = params.IdentityProvider
+ // idp = params.IdentityProvider
verbose = params.Verbose
)
@@ -250,7 +263,7 @@ func ForwardToken(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) error
if verbose {
fmt.Printf("Fetching JWKS from authentication server for verification...\n")
}
- err := idp.FetchJwks()
+ err := client.Provider.FetchJwks()
if err != nil {
return fmt.Errorf("failed to fetch JWK: %v", err)
} else {
@@ -260,7 +273,7 @@ func ForwardToken(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) error
}
ti := &oauth.TrustedIssuer{
- Issuer: idp.Issuer,
+ Issuer: client.Provider.Issuer,
Subject: "1",
ExpiresAt: time.Now().Add(time.Second * 3600),
}
diff --git a/internal/login.go b/internal/login.go
index e9c6374..b6aaf84 100644
--- a/internal/login.go
+++ b/internal/login.go
@@ -12,19 +12,11 @@ import (
"time"
)
-func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider) error {
+func Login(config *Config) error {
if config == nil {
return fmt.Errorf("invalid config")
}
- if client == nil {
- return fmt.Errorf("invalid client")
- }
-
- if provider == nil {
- return fmt.Errorf("invalid identity provider")
- }
-
// make cache if it's not where expect
_, err := cache.CreateIdentityProvidersIfNotExists(config.Options.CachePath)
if err != nil {
@@ -39,25 +31,20 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
}
// print the authorization URL for sharing
- var authorizationUrl = client.BuildAuthorizationUrl(provider.Endpoints.Authorization, state)
s := NewServerWithConfig(config)
- fmt.Printf("Login with external identity provider:\n\n %s/login\n %s\n\n",
- s.GetListenAddr(), authorizationUrl,
- )
+ s.State = state
- var button = MakeButton(authorizationUrl, "Login with "+client.Name)
var authzClient = oauth.NewClient()
authzClient.Scope = config.Authorization.Token.Scope
- // authorize oauth client and listen for callback from provider
- fmt.Printf("Waiting for authorization code redirect @%s/oidc/callback...\n", s.GetListenAddr())
params := server.ServerParams{
Verbose: config.Options.Verbose,
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{
@@ -66,8 +53,7 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
Register: config.Authorization.Endpoints.Register,
},
JwtBearerParams: flows.JwtBearerFlowParams{
- Client: authzClient,
- IdentityProvider: provider,
+ Client: authzClient,
TrustedIssuer: &oauth.TrustedIssuer{
AllowAnySubject: false,
Issuer: s.Addr,
@@ -75,8 +61,9 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
ExpiresAt: time.Now().Add(config.Authorization.Token.Duration),
Scope: []string{},
},
- Verbose: config.Options.Verbose,
- Refresh: config.Authorization.Token.Refresh,
+ Verbose: config.Options.Verbose,
+ Refresh: config.Authorization.Token.Refresh,
+ Audience: config.Authorization.Audience,
},
ClientCredentialsEndpoints: flows.ClientCredentialsFlowEndpoints{
Clients: config.Authorization.Endpoints.Clients,
@@ -87,7 +74,7 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
Client: authzClient,
},
}
- err = s.StartLogin(button, provider, client, params)
+ err = s.StartLogin(config.Authentication.Clients, params)
if errors.Is(err, http.ErrServerClosed) {
fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n")
} else if err != nil {
@@ -96,7 +83,7 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
} else if config.Options.FlowType == "client_credentials" {
params := flows.ClientCredentialsFlowParams{
- Client: client,
+ Client: nil, // # FIXME: need to do something about this being nil I think
}
_, err := NewClientCredentialsFlowWithConfig(config, params)
if err != nil {
@@ -108,13 +95,3 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
return nil
}
-
-func MakeButton(url string, text string) string {
- // check if we have http:// a
- html := " " + text + ""
-}
diff --git a/internal/new.go b/internal/new.go
index 2b2bad8..2799d88 100644
--- a/internal/new.go
+++ b/internal/new.go
@@ -29,7 +29,7 @@ func NewClientWithConfig(config *Config) *oauth.Client {
Id: clients[0].Id,
Secret: clients[0].Secret,
Name: clients[0].Name,
- Issuer: clients[0].Issuer,
+ Provider: clients[0].Provider,
Scope: clients[0].Scope,
RedirectUris: clients[0].RedirectUris,
}
@@ -53,7 +53,7 @@ func NewClientWithConfigByName(config *Config, name string) *oauth.Client {
func NewClientWithConfigByProvider(config *Config, issuer string) *oauth.Client {
index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool {
- return c.Issuer == issuer
+ return c.Provider.Issuer == issuer
})
if index >= 0 {
@@ -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 5724526..4af65cb 100644
--- a/internal/oauth/authenticate.go
+++ b/internal/oauth/authenticate.go
@@ -16,12 +16,15 @@ func (client *Client) IsFlowInitiated() bool {
return client.FlowId != ""
}
-func (client *Client) BuildAuthorizationUrl(issuer string, state string) string {
- return issuer + "?" + "client_id=" + client.Id +
+func (client *Client) BuildAuthorizationUrl(state string) string {
+ url := client.Provider.Endpoints.Authorization + "?client_id=" + client.Id +
"&redirect_uri=" + url.QueryEscape(strings.Join(client.RedirectUris, ",")) +
"&response_type=code" + // this has to be set to "code"
- "&state=" + state +
"&scope=" + strings.Join(client.Scope, "+")
+ if state != "" {
+ url += "&state=" + state
+ }
+ return url
}
func (client *Client) InitiateLoginFlow(loginUrl string) error {
@@ -90,7 +93,7 @@ func (client *Client) FetchCSRFToken(flowUrl string) error {
return fmt.Errorf("failed to extract CSRF token: not found")
}
-func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl string, state string) ([]byte, error) {
+func (client *Client) FetchTokenFromAuthenticationServer(code string, state string) ([]byte, error) {
body := url.Values{
"grant_type": {"authorization_code"},
"client_id": {client.Id},
@@ -104,14 +107,16 @@ func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl
if state != "" {
body["state"] = []string{state}
}
- res, err := http.PostForm(remoteUrl, body)
+ 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/oauth/client.go b/internal/oauth/client.go
index 2040117..78ab9cc 100644
--- a/internal/oauth/client.go
+++ b/internal/oauth/client.go
@@ -1,6 +1,7 @@
package oauth
import (
+ "davidallendj/opaal/internal/oidc"
"encoding/json"
"fmt"
"net/http"
@@ -24,15 +25,15 @@ const (
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"`
+ Id string `db:"id" yaml:"id"`
+ Secret string `db:"secret" yaml:"secret"`
+ Name string `db:"name" yaml:"name"`
+ Description string `db:"description" yaml:"description"`
+ Provider oidc.IdentityProvider `db:"issuer" yaml:"provider"`
+ 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
}
diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go
index af813ab..f52e0c4 100644
--- a/internal/oidc/oidc.go
+++ b/internal/oidc/oidc.go
@@ -111,26 +111,11 @@ func (p *IdentityProvider) LoadServerConfig(path string) error {
}
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{}))
+ tmp, err := FetchServerConfig(p.Issuer)
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 err
}
+ p = tmp
return nil
}
@@ -147,10 +132,15 @@ func FetchServerConfig(issuer string) (*IdentityProvider, error) {
return nil, fmt.Errorf("failed to do request: %v", err)
}
+ if res.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("HTTP status code: %d", res.StatusCode)
+ }
+
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %v", err)
}
+
var p IdentityProvider
err = p.ParseServerConfig(body)
if err != nil {
@@ -174,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
new file mode 100644
index 0000000..bb82a28
--- /dev/null
+++ b/internal/server/idp.go
@@ -0,0 +1,290 @@
+package server
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "davidallendj/opaal/internal/oidc"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/davidallendj/go-utils/cryptox"
+ "github.com/davidallendj/go-utils/util"
+ "github.com/go-chi/chi/v5"
+ "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: 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
+
+ // create an example identity provider
+ var (
+ r = chi.NewRouter()
+ // clients = []oauth.Client{}
+ activeCodes = []string{}
+ )
+
+ // 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)
+ 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)
+ }
+ kid, _ := privateJwk.Get("kid")
+ publicJwk.Set("kid", kid)
+ publicJwk.Set("use", "sig")
+ publicJwk.Set("kty", "RSA")
+ publicJwk.Set("alg", "RS256")
+ if err := publicJwk.Validate(); err != nil {
+ return fmt.Errorf("failed to validate public JWK: %v", err)
+ }
+
+ // TODO: create .well-known JWKS endpoint with json
+ r.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) {
+ // TODO: generate new JWKs from a private key
+
+ jwks := map[string]any{
+ "keys": []jwk.Key{
+ publicJwk,
+ },
+ }
+ b, err := json.Marshal(jwks)
+ if err != nil {
+ return
+ }
+ w.Write(b)
+ })
+
+ 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": s.Issuer.Endpoints.Authorization,
+ "token_endpoint": s.Issuer.Endpoints.Token,
+ "jwks_uri": s.Issuer.Endpoints.JwksUri,
+ "scopes_supported": []string{
+ "openid",
+ "profile",
+ "email",
+ },
+ "response_types_supported": []string{
+ "code",
+ },
+ "grant_types_supported": []string{
+ "authorization_code",
+ },
+ "id_token_signing_alg_values_supported": []string{
+ "RS256",
+ },
+ "claims_supported": []string{
+ "iss",
+ "sub",
+ "aud",
+ "exp",
+ "iat",
+ "name",
+ "email",
+ },
+ }
+
+ b, err := json.Marshal(config)
+ if err != nil {
+ return
+ }
+ w.Write(b)
+ })
+ r.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) {
+ // serve up a simple login page
+ })
+ r.HandleFunc("/consent", func(w http.ResponseWriter, r *http.Request) {
+ // give consent for app to use
+ })
+ r.HandleFunc("/browser/login", func(w http.ResponseWriter, r *http.Request) {
+ // serve up a login page for user creds
+ form, err := os.ReadFile("pages/login.html")
+ if err != nil {
+ fmt.Printf("failed to load login form: %v", err)
+ }
+ w.Write(form)
+ })
+ r.HandleFunc("/api/login", func(w http.ResponseWriter, r *http.Request) {
+ // check for example identity with POST request
+ r.ParseForm()
+ 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 == "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 for client %s (ID: %s)\n", client.Name, client.Id)
+ return
+ }
+ for _, url := range client.RedirectUris {
+ // send an authorization code to each URI
+ code := util.RandomString(64)
+ activeCodes = append(activeCodes, code)
+ redirectUrl := fmt.Sprintf("%s?code=%s", url, code)
+ fmt.Printf("redirect URL: %s\n", redirectUrl)
+ http.Redirect(w, r, redirectUrl, http.StatusFound)
+ // _, _, err := httpx.MakeHttpRequest(fmt.Sprintf("%s?code=%s", url, code), http.MethodGet, nil, nil)
+ // if err != nil {
+ // fmt.Printf("failed to make request: %v\n", err)
+ // continue
+ // }
+ }
+ } else {
+ w.Write([]byte("error logging in"))
+ http.Redirect(w, r, "/browser/login", http.StatusUnauthorized)
+ }
+ })
+ r.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) {
+ r.ParseForm()
+
+ // check for authorization code and make sure it's valid
+ var code = r.Form.Get("code")
+ index := slices.IndexFunc(activeCodes, func(s string) bool { return s == code })
+ if index < 0 {
+ fmt.Printf("invalid authorization code: %s\n", code)
+ return
+ }
+
+ // now create and return a JWT that can be verified with by authorization server
+ iat := time.Now().Unix()
+ exp := time.Now().Add(time.Second * 3600 * 16).Unix()
+ t := jwt.New()
+ t.Set(jwt.IssuerKey, s.Addr)
+ t.Set(jwt.SubjectKey, "ochami")
+ t.Set(jwt.AudienceKey, "ochami")
+ t.Set(jwt.IssuedAtKey, iat)
+ t.Set(jwt.ExpirationKey, exp)
+ t.Set("name", "ochami")
+ t.Set("email", "example@ochami.org")
+ t.Set("email_verified", true)
+ t.Set("scope", []string{
+ "openid",
+ "profile",
+ "email",
+ "example",
+ })
+ // payload := map[string]any{}
+ // payload["iss"] = s.Addr
+ // payload["aud"] = "ochami"
+ // payload["iat"] = iat
+ // payload["nbf"] = iat
+ // payload["exp"] = exp
+ // payload["sub"] = "ochami"
+ // payload["name"] = "ochami"
+ // payload["email"] = "example@ochami.org"
+ // payload["email_verified"] = true
+ // payload["scope"] = []string{
+ // "openid",
+ // "profile",
+ // "email",
+ // "example",
+ // }
+ payloadJson, err := json.MarshalIndent(t, "", "\t")
+ if err != nil {
+ fmt.Printf("failed to marshal payload: %v", err)
+ return
+ }
+ signed, err := jws.Sign(payloadJson, jws.WithKey(jwa.RS256, privateJwk))
+ if err != nil {
+ fmt.Printf("failed to sign token: %v\n", err)
+ return
+ }
+
+ // construct the bearer token with required fields
+ scope, _ := t.Get("scope")
+ bearer := map[string]any{
+ "token_type": "Bearer",
+ "id_token": string(signed),
+ "expires_in": exp,
+ "created_at": iat,
+ "scope": strings.Join(scope.([]string), " "),
+ }
+
+ b, err := json.MarshalIndent(bearer, "", "\t")
+ if err != nil {
+ fmt.Printf("failed to marshal bearer token: %v\n", err)
+ return
+ }
+ fmt.Printf("bearer: %s\n", string(b))
+ w.Write(b)
+ })
+ 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")
+ redirectUris = r.URL.Query().Get("redirect_uri")
+ )
+
+ // check for required authorization code params
+ if responseType != "code" {
+ fmt.Printf("invalid response type\n")
+ return
+ }
+
+ // 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
+ }
+
+ // TODO: check that our redirect URIs all match
+ for _, uri := range redirectUris {
+ _ = uri
+ }
+
+ // redirect to browser login since we don't do session management here
+ http.Redirect(w, r, "/browser/login", http.StatusFound)
+ })
+
+ s.Handler = r
+ return s.ListenAndServe()
+}
diff --git a/internal/server/server.go b/internal/server/server.go
index 66c45d4..3fdae97 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -1,44 +1,28 @@
package server
import (
- "crypto/rand"
- "crypto/rsa"
"davidallendj/opaal/internal/flows"
"davidallendj/opaal/internal/oauth"
"davidallendj/opaal/internal/oidc"
"encoding/json"
"fmt"
"net/http"
- "os"
"slices"
- "strings"
- "time"
- "github.com/davidallendj/go-utils/cryptox"
"github.com/davidallendj/go-utils/httpx"
- "github.com/davidallendj/go-utils/util"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
- "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"
"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"`
- 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 {
@@ -58,10 +42,12 @@ func (s *Server) GetListenAddr() string {
return fmt.Sprintf("%s:%d", s.Host, s.Port)
}
-func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, client *oauth.Client, params ServerParams) error {
+func (s *Server) StartLogin(clients []oauth.Client, params ServerParams) error {
var (
- target = ""
- callback = ""
+ target string
+ callback string
+ client *oauth.Client
+ sso string
)
// check if callback is set
@@ -69,6 +55,28 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli
callback = "/oidc/callback"
}
+ // make the login page SSO buttons and authorization URLs to write to stdout
+ buttons := ""
+ 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)
+ if err != nil {
+ fmt.Printf("failed to fetch server config: %v\n", err)
+ continue
+ }
+
+ // if we're able to get the config, go ahead and try to fetch jwks too
+ if err = p.FetchJwks(); err != nil {
+ fmt.Printf("failed to fetch JWKS: %v\n", err)
+ continue
+ }
+
+ clients[i].Provider = *p
+ buttons += makeButton(fmt.Sprintf("/login?sso=%s", client.Id), client.Name)
+ fmt.Printf("\t%s: /login?sso=%s\n", client.Name, client.Id)
+ }
+
var code string
var accessToken string
r := chi.NewRouter()
@@ -93,20 +101,34 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli
// add target if query exists
if r != nil {
target = r.URL.Query().Get("target")
+ sso = r.URL.Query().Get("sso")
+
+ // TODO: get client from list and build the authorization URL string
+ index := slices.IndexFunc(clients, func(c oauth.Client) bool {
+ return c.Id == sso
+ })
+
+ // TODO: redirect the user to authorization URL and return from func
+ foundClient := index >= 0
+ if foundClient {
+ client = &clients[index]
+
+ url := client.BuildAuthorizationUrl(s.State)
+ if params.Verbose {
+ fmt.Printf("Redirect URL: %s\n", url)
+ }
+ http.Redirect(w, r, url, http.StatusFound)
+ return
+ }
}
+
// show login page with notice to redirect
template, err := gonja.FromFile("pages/index.html")
if err != nil {
panic(err)
}
- // form, err := os.ReadFile("pages/login.html")
- // if err != nil {
- // fmt.Printf("failed to load login form: %v", err)
- // }
-
data := exec.NewContext(map[string]interface{}{
- // "loginForm": string(form),
"loginButtons": buttons,
})
@@ -119,45 +141,54 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli
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) {
// use refresh token provided to do a refresh token grant
refreshToken := r.URL.Query().Get("refresh-token")
if refreshToken != "" {
- _, err := params.JwtBearerParams.Client.PerformRefreshTokenGrant(provider.Endpoints.Token, refreshToken)
+ _, err := params.JwtBearerParams.Client.PerformRefreshTokenGrant(client.Provider.Endpoints.Token, refreshToken)
if err != nil {
fmt.Printf("failed to perform refresh token grant: %v\n", err)
http.Redirect(w, r, "/error", http.StatusInternalServerError)
@@ -176,6 +207,8 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli
return
}
} else {
+ // FIXME: I think this probably needs to reworked or removed
+ // NOTE: this logic fetches a token for services to retrieve like BSS
// perform a client credentials grant and return a token
var err error
accessToken, err = flows.NewClientCredentialsFlow(params.ClientCredentialsEndpoints, params.ClientCredentialsParams)
@@ -195,10 +228,15 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli
fmt.Printf("Authorization code: %v\n", code)
}
+ // make sure we have the correct client to use
+ if client == nil {
+ fmt.Printf("failed to find valid client")
+ return
+ }
+
// 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 {
@@ -228,6 +266,7 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli
// complete JWT bearer flow to receive access token from authorization server
// fmt.Printf("bearer: %v\n", string(bearerToken))
params.JwtBearerParams.IdToken = data["id_token"].(string)
+ params.JwtBearerParams.Client = client
accessToken, err = flows.NewJwtBearerFlow(params.JwtBearerEndpoints, params.JwtBearerParams)
if err != nil {
fmt.Printf("failed to complete JWT bearer flow: %v\n", err)
@@ -294,250 +333,14 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli
return s.ListenAndServe()
}
-func (s *Server) StartIdentityProvider() error {
- // NOTE: this example does NOT implement CSRF tokens nor use them
-
- // create an example identity provider
- var (
- r = chi.NewRouter()
- // clients = []oauth.Client{}
- callback = ""
- activeCodes = []string{}
- )
-
- // check if callback is set
- if s.Callback == "" {
- callback = "/oidc/callback"
- }
-
- // generate key pair used to sign JWKS and create JWTs
- 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)
- }
- kid, _ := privateJwk.Get("kid")
- publicJwk.Set("kid", kid)
- publicJwk.Set("use", "sig")
- publicJwk.Set("kty", "RSA")
- publicJwk.Set("alg", "RS256")
- if err := publicJwk.Validate(); err != nil {
- return fmt.Errorf("failed to validate public JWK: %v", err)
- }
-
- // TODO: create .well-known JWKS endpoint with json
- r.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) {
- // TODO: generate new JWKs from a private key
-
- jwks := map[string]any{
- "keys": []jwk.Key{
- publicJwk,
- },
- }
- b, err := json.Marshal(jwks)
- if err != nil {
- return
- }
- 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",
- "scopes_supported": []string{
- "openid",
- "profile",
- "email",
- },
- "response_types_supported": []string{
- "code",
- },
- "grant_types_supported": []string{
- "authorization_code",
- },
- "id_token_signing_alg_values_supported": []string{
- "RS256",
- },
- "claims_supported": []string{
- "iss",
- "sub",
- "aud",
- "exp",
- "iat",
- "name",
- "email",
- },
- }
-
- b, err := json.Marshal(config)
- if err != nil {
- return
- }
- w.Write(b)
- })
- r.HandleFunc("/register", func(w http.ResponseWriter, r *http.Request) {
- // serve up a simple login page
- })
- r.HandleFunc("/consent", func(w http.ResponseWriter, r *http.Request) {
- // give consent for app to use
- })
- r.HandleFunc("/browser/login", func(w http.ResponseWriter, r *http.Request) {
- // serve up a login page for user creds
- form, err := os.ReadFile("pages/login.html")
- if err != nil {
- fmt.Printf("failed to load login form: %v", err)
- }
- w.Write(form)
- })
- r.HandleFunc("/api/login", func(w http.ResponseWriter, r *http.Request) {
- // check for example identity with POST request
- r.ParseForm()
- username := r.Form.Get("username")
- password := r.Form.Get("password")
-
- // example username and password so do simplified authorization code flow
- if username == "ochami" && password == "ochami" {
- client := oauth.Client{
- Id: "ochami",
- Secret: "ochami",
- Name: "ochami",
- Issuer: "http://127.0.0.1:3333",
- RedirectUris: []string{fmt.Sprintf("http://%s:%d%s", s.Host, s.Port, callback)},
- }
-
- // check if there are any redirect URIs supplied
- if len(client.RedirectUris) <= 0 {
- fmt.Printf("no redirect URIs found")
- return
- }
- for _, url := range client.RedirectUris {
- // send an authorization code to each URI
- code := util.RandomString(64)
- activeCodes = append(activeCodes, code)
- redirectUrl := fmt.Sprintf("%s?code=%s", url, code)
- fmt.Printf("redirect URL: %s\n", redirectUrl)
- http.Redirect(w, r, redirectUrl, http.StatusFound)
- // _, _, err := httpx.MakeHttpRequest(fmt.Sprintf("%s?code=%s", url, code), http.MethodGet, nil, nil)
- // if err != nil {
- // fmt.Printf("failed to make request: %v\n", err)
- // continue
- // }
- }
- } else {
- w.Write([]byte("error logging in"))
- http.Redirect(w, r, "/browser/login", http.StatusUnauthorized)
- }
- })
- r.HandleFunc("/oauth/token", func(w http.ResponseWriter, r *http.Request) {
- r.ParseForm()
-
- // check for authorization code and make sure it's valid
- var code = r.Form.Get("code")
- index := slices.IndexFunc(activeCodes, func(s string) bool { return s == code })
- if index < 0 {
- fmt.Printf("invalid authorization code: %s\n", code)
- return
- }
-
- // now create and return a JWT that can be verified with by authorization server
- iat := time.Now().Unix()
- exp := time.Now().Add(time.Second * 3600 * 16).Unix()
- t := jwt.New()
- t.Set(jwt.IssuerKey, s.Addr)
- t.Set(jwt.SubjectKey, "ochami")
- t.Set(jwt.AudienceKey, "ochami")
- t.Set(jwt.IssuedAtKey, iat)
- t.Set(jwt.ExpirationKey, exp)
- t.Set("name", "ochami")
- t.Set("email", "example@ochami.org")
- t.Set("email_verified", true)
- t.Set("scope", []string{
- "openid",
- "profile",
- "email",
- "example",
- })
- // payload := map[string]any{}
- // payload["iss"] = s.Addr
- // payload["aud"] = "ochami"
- // payload["iat"] = iat
- // payload["nbf"] = iat
- // payload["exp"] = exp
- // payload["sub"] = "ochami"
- // payload["name"] = "ochami"
- // payload["email"] = "example@ochami.org"
- // payload["email_verified"] = true
- // payload["scope"] = []string{
- // "openid",
- // "profile",
- // "email",
- // "example",
- // }
- payloadJson, err := json.MarshalIndent(t, "", "\t")
- if err != nil {
- fmt.Printf("failed to marshal payload: %v", err)
- return
- }
- signed, err := jws.Sign(payloadJson, jws.WithKey(jwa.RS256, privateJwk))
- if err != nil {
- fmt.Printf("failed to sign token: %v\n", err)
- return
- }
-
- // construct the bearer token with required fields
- scope, _ := t.Get("scope")
- bearer := map[string]any{
- "token_type": "Bearer",
- "id_token": string(signed),
- "expires_in": exp,
- "created_at": iat,
- "scope": strings.Join(scope.([]string), " "),
- }
-
- b, err := json.MarshalIndent(bearer, "", "\t")
- if err != nil {
- fmt.Printf("failed to marshal bearer token: %v\n", err)
- return
- }
- fmt.Printf("bearer: %s\n", string(b))
- w.Write(b)
- })
- r.HandleFunc("/oauth/authorize", func(w http.ResponseWriter, r *http.Request) {
- var (
- responseType = r.URL.Query().Get("response_type")
- clientId = r.URL.Query().Get("client_id")
- redirectUris = r.URL.Query().Get("redirect_uri")
- )
-
- // check for required authorization code params
- if responseType != "code" {
- fmt.Printf("invalid response type\n")
- return
- }
-
- // check that we're using the default registered client
- if clientId != "ochami" {
- fmt.Printf("invalid client\n")
- return
- }
-
- // TODO: check that our redirect URIs all match
- for _, uri := range redirectUris {
- _ = uri
- }
-
- // redirect to browser login since we don't do session management here
- http.Redirect(w, r, "/browser/login", http.StatusFound)
- })
-
- s.Handler = r
- return s.ListenAndServe()
+func makeButton(url string, text string) string {
+ // check if we have http:// a
+ // html := "", text)
+ html := "%s", url, text)
+ html += ""
+ return html
}
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?
-
+