mirror of
https://github.com/davidallendj/opaal.git
synced 2025-12-20 03:27:02 -07:00
commit
5650cf6985
8 changed files with 189 additions and 154 deletions
125
cmd/login.go
125
cmd/login.go
|
|
@ -2,12 +2,9 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
opaal "davidallendj/opaal/internal"
|
opaal "davidallendj/opaal/internal"
|
||||||
cache "davidallendj/opaal/internal/cache/sqlite"
|
|
||||||
"davidallendj/opaal/internal/oauth"
|
"davidallendj/opaal/internal/oauth"
|
||||||
"davidallendj/opaal/internal/oidc"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -25,68 +22,68 @@ var loginCmd = &cobra.Command{
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
for {
|
for {
|
||||||
// try and find client with valid identity provider config
|
// try and find client with valid identity provider config
|
||||||
var provider *oidc.IdentityProvider
|
// var provider *oidc.IdentityProvider
|
||||||
if target != "" {
|
// if target != "" {
|
||||||
// only try to use client with name give
|
// // only try to use client with name give
|
||||||
index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool {
|
// index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool {
|
||||||
return target == c.Name
|
// return target == c.Name
|
||||||
})
|
// })
|
||||||
if index < 0 {
|
// if index < 0 {
|
||||||
fmt.Printf("could not find the target client listed by name")
|
// fmt.Printf("could not find the target client listed by name")
|
||||||
os.Exit(1)
|
// os.Exit(1)
|
||||||
}
|
// }
|
||||||
client := config.Authentication.Clients[index]
|
// client := config.Authentication.Clients[index]
|
||||||
_, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer)
|
// _, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
|
|
||||||
}
|
// }
|
||||||
|
|
||||||
} else if targetIndex >= 0 {
|
// } else if targetIndex >= 0 {
|
||||||
// only try to use client by index
|
// // only try to use client by index
|
||||||
targetCount := len(config.Authentication.Clients) - 1
|
// targetCount := len(config.Authentication.Clients) - 1
|
||||||
if targetIndex > targetCount {
|
// if targetIndex > targetCount {
|
||||||
fmt.Printf("target index out of range (found %d)", targetCount)
|
// fmt.Printf("target index out of range (found %d)", targetCount)
|
||||||
}
|
// }
|
||||||
client := config.Authentication.Clients[targetIndex]
|
// client := config.Authentication.Clients[targetIndex]
|
||||||
_, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer)
|
// _, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
|
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
for _, c := range config.Authentication.Clients {
|
// for _, c := range config.Authentication.Clients {
|
||||||
// try to get identity provider info locally first
|
// // try to get identity provider info locally first
|
||||||
_, err := cache.GetIdentityProvider(config.Options.CachePath, c.Issuer)
|
// _, err := cache.GetIdentityProvider(config.Options.CachePath, c.Issuer)
|
||||||
if err != nil && !config.Options.CacheOnly {
|
// if err != nil && !config.Options.CacheOnly {
|
||||||
fmt.Printf("fetching config from issuer: %v\n", c.Issuer)
|
// fmt.Printf("fetching config from issuer: %v\n", c.Issuer)
|
||||||
// try to get info remotely by fetching
|
// // try to get info remotely by fetching
|
||||||
provider, err = oidc.FetchServerConfig(c.Issuer)
|
// provider, err = oidc.FetchServerConfig(c.Issuer)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
fmt.Printf("failed to fetch server config: %v\n", err)
|
// fmt.Printf("failed to fetch server config: %v\n", err)
|
||||||
continue
|
// continue
|
||||||
}
|
// }
|
||||||
client = c
|
// client = c
|
||||||
// fetch the provider's JWKS
|
// // fetch the provider's JWKS
|
||||||
err := provider.FetchJwks()
|
// err := provider.FetchJwks()
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
fmt.Printf("failed to fetch JWKS: %v\n", err)
|
// fmt.Printf("failed to fetch JWKS: %v\n", err)
|
||||||
}
|
// }
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
// only test the first if --run-all flag is not set
|
// // only test the first if --run-all flag is not set
|
||||||
if !config.Authentication.TestAllClients {
|
// if !config.Authentication.TestAllClients {
|
||||||
fmt.Printf("stopping after first test...\n\n\n")
|
// fmt.Printf("stopping after first test...\n\n\n")
|
||||||
break
|
// break
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if provider == nil {
|
// if provider == nil {
|
||||||
fmt.Printf("failed to retrieve provider config\n")
|
// fmt.Printf("failed to retrieve provider config\n")
|
||||||
os.Exit(1)
|
// os.Exit(1)
|
||||||
}
|
// }
|
||||||
|
|
||||||
// start the listener
|
// start the listener
|
||||||
err := opaal.Login(&config, &client, provider)
|
err := opaal.Login(&config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%v\n", err)
|
fmt.Printf("%v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
@ -115,3 +112,13 @@ func init() {
|
||||||
loginCmd.MarkFlagsMutuallyExclusive("target.name", "target.index")
|
loginCmd.MarkFlagsMutuallyExclusive("target.name", "target.index")
|
||||||
rootCmd.AddCommand(loginCmd)
|
rootCmd.AddCommand(loginCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MakeButton(url string, text string) string {
|
||||||
|
// check if we have http:// a
|
||||||
|
html := "<input type=\"button\" "
|
||||||
|
html += "class=\"button\" "
|
||||||
|
html += fmt.Sprintf("onclick=\"window.location.href='%s';\" ", url)
|
||||||
|
html += fmt.Sprintf("value=\"%s\"", text)
|
||||||
|
return html
|
||||||
|
// return "<a href=\"" + url + "\"> " + text + "</a>"
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"davidallendj/opaal/internal/oauth"
|
"davidallendj/opaal/internal/oauth"
|
||||||
"davidallendj/opaal/internal/oidc"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -19,14 +18,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type JwtBearerFlowParams struct {
|
type JwtBearerFlowParams struct {
|
||||||
AccessToken string
|
AccessToken string
|
||||||
IdToken string
|
IdToken string
|
||||||
IdentityProvider *oidc.IdentityProvider
|
// IdentityProvider *oidc.IdentityProvider
|
||||||
TrustedIssuer *oauth.TrustedIssuer
|
TrustedIssuer *oauth.TrustedIssuer
|
||||||
Client *oauth.Client
|
Client *oauth.Client
|
||||||
Refresh bool
|
Refresh bool
|
||||||
Verbose bool
|
Verbose bool
|
||||||
KeyPath string
|
KeyPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
type JwtBearerFlowEndpoints struct {
|
type JwtBearerFlowEndpoints struct {
|
||||||
|
|
@ -39,22 +38,27 @@ type JwtBearerFlowEndpoints struct {
|
||||||
func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (string, error) {
|
func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (string, error) {
|
||||||
// 1. verify that the JWT from the issuer is valid using all keys
|
// 1. verify that the JWT from the issuer is valid using all keys
|
||||||
var (
|
var (
|
||||||
idp = params.IdentityProvider
|
// idp = params.IdentityProvider
|
||||||
accessToken = params.AccessToken
|
accessToken = params.AccessToken
|
||||||
idToken = params.IdToken
|
idToken = params.IdToken
|
||||||
client = params.Client
|
client = params.Client
|
||||||
trustedIssuer = params.TrustedIssuer
|
trustedIssuer = params.TrustedIssuer
|
||||||
verbose = params.Verbose
|
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 accessToken != "" {
|
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 {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to verify access token: %v", err)
|
return "", fmt.Errorf("failed to verify access token: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if idToken != "" {
|
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 {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to verify ID token: %v", err)
|
return "", fmt.Errorf("failed to verify ID token: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +130,7 @@ func NewJwtBearerFlow(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) (s
|
||||||
// TODO: add trusted issuer to cache if successful
|
// TODO: add trusted issuer to cache if successful
|
||||||
|
|
||||||
// 4. create a new JWT based on the claims from the identity provider and sign
|
// 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 {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to parse ID token: %v", err)
|
return "", fmt.Errorf("failed to parse ID token: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -242,7 +246,7 @@ func ForwardToken(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) error
|
||||||
var (
|
var (
|
||||||
client = params.Client
|
client = params.Client
|
||||||
idToken = params.IdToken
|
idToken = params.IdToken
|
||||||
idp = params.IdentityProvider
|
// idp = params.IdentityProvider
|
||||||
verbose = params.Verbose
|
verbose = params.Verbose
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -250,7 +254,7 @@ func ForwardToken(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) error
|
||||||
if verbose {
|
if verbose {
|
||||||
fmt.Printf("Fetching JWKS from authentication server for verification...\n")
|
fmt.Printf("Fetching JWKS from authentication server for verification...\n")
|
||||||
}
|
}
|
||||||
err := idp.FetchJwks()
|
err := client.Provider.FetchJwks()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch JWK: %v", err)
|
return fmt.Errorf("failed to fetch JWK: %v", err)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -260,7 +264,7 @@ func ForwardToken(eps JwtBearerFlowEndpoints, params JwtBearerFlowParams) error
|
||||||
}
|
}
|
||||||
|
|
||||||
ti := &oauth.TrustedIssuer{
|
ti := &oauth.TrustedIssuer{
|
||||||
Issuer: idp.Issuer,
|
Issuer: client.Provider.Issuer,
|
||||||
Subject: "1",
|
Subject: "1",
|
||||||
ExpiresAt: time.Now().Add(time.Second * 3600),
|
ExpiresAt: time.Now().Add(time.Second * 3600),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,19 +12,11 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider) error {
|
func Login(config *Config) error {
|
||||||
if config == nil {
|
if config == nil {
|
||||||
return fmt.Errorf("invalid config")
|
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
|
// make cache if it's not where expect
|
||||||
_, err := cache.CreateIdentityProvidersIfNotExists(config.Options.CachePath)
|
_, err := cache.CreateIdentityProvidersIfNotExists(config.Options.CachePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -39,18 +31,12 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// print the authorization URL for sharing
|
// print the authorization URL for sharing
|
||||||
var authorizationUrl = client.BuildAuthorizationUrl(provider.Endpoints.Authorization, state)
|
|
||||||
s := NewServerWithConfig(config)
|
s := NewServerWithConfig(config)
|
||||||
fmt.Printf("Login with external identity provider:\n\n %s/login\n %s\n\n",
|
s.State = state
|
||||||
s.GetListenAddr(), authorizationUrl,
|
|
||||||
)
|
|
||||||
|
|
||||||
var button = MakeButton(authorizationUrl, "Login with "+client.Name)
|
|
||||||
var authzClient = oauth.NewClient()
|
var authzClient = oauth.NewClient()
|
||||||
authzClient.Scope = config.Authorization.Token.Scope
|
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{
|
params := server.ServerParams{
|
||||||
Verbose: config.Options.Verbose,
|
Verbose: config.Options.Verbose,
|
||||||
AuthProvider: &oidc.IdentityProvider{
|
AuthProvider: &oidc.IdentityProvider{
|
||||||
|
|
@ -66,8 +52,7 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
|
||||||
Register: config.Authorization.Endpoints.Register,
|
Register: config.Authorization.Endpoints.Register,
|
||||||
},
|
},
|
||||||
JwtBearerParams: flows.JwtBearerFlowParams{
|
JwtBearerParams: flows.JwtBearerFlowParams{
|
||||||
Client: authzClient,
|
Client: authzClient,
|
||||||
IdentityProvider: provider,
|
|
||||||
TrustedIssuer: &oauth.TrustedIssuer{
|
TrustedIssuer: &oauth.TrustedIssuer{
|
||||||
AllowAnySubject: false,
|
AllowAnySubject: false,
|
||||||
Issuer: s.Addr,
|
Issuer: s.Addr,
|
||||||
|
|
@ -87,7 +72,7 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
|
||||||
Client: authzClient,
|
Client: authzClient,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = s.StartLogin(button, provider, client, params)
|
err = s.StartLogin(config.Authentication.Clients, params)
|
||||||
if errors.Is(err, http.ErrServerClosed) {
|
if errors.Is(err, http.ErrServerClosed) {
|
||||||
fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n")
|
fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n")
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
|
@ -96,7 +81,7 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
|
||||||
|
|
||||||
} else if config.Options.FlowType == "client_credentials" {
|
} else if config.Options.FlowType == "client_credentials" {
|
||||||
params := flows.ClientCredentialsFlowParams{
|
params := flows.ClientCredentialsFlowParams{
|
||||||
Client: client,
|
Client: nil, // # FIXME: need to do something about this being nil I think
|
||||||
}
|
}
|
||||||
_, err := NewClientCredentialsFlowWithConfig(config, params)
|
_, err := NewClientCredentialsFlowWithConfig(config, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -108,13 +93,3 @@ func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeButton(url string, text string) string {
|
|
||||||
// check if we have http:// a
|
|
||||||
html := "<input type=\"button\" "
|
|
||||||
html += "class=\"button\" "
|
|
||||||
html += fmt.Sprintf("onclick=\"window.location.href='%s';\" ", url)
|
|
||||||
html += fmt.Sprintf("value=\"%s\"", text)
|
|
||||||
return html
|
|
||||||
// return "<a href=\"" + url + "\"> " + text + "</a>"
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ func NewClientWithConfig(config *Config) *oauth.Client {
|
||||||
Id: clients[0].Id,
|
Id: clients[0].Id,
|
||||||
Secret: clients[0].Secret,
|
Secret: clients[0].Secret,
|
||||||
Name: clients[0].Name,
|
Name: clients[0].Name,
|
||||||
Issuer: clients[0].Issuer,
|
Provider: clients[0].Provider,
|
||||||
Scope: clients[0].Scope,
|
Scope: clients[0].Scope,
|
||||||
RedirectUris: clients[0].RedirectUris,
|
RedirectUris: clients[0].RedirectUris,
|
||||||
}
|
}
|
||||||
|
|
@ -53,7 +53,7 @@ func NewClientWithConfigByName(config *Config, name string) *oauth.Client {
|
||||||
|
|
||||||
func NewClientWithConfigByProvider(config *Config, issuer string) *oauth.Client {
|
func NewClientWithConfigByProvider(config *Config, issuer string) *oauth.Client {
|
||||||
index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool {
|
index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool {
|
||||||
return c.Issuer == issuer
|
return c.Provider.Issuer == issuer
|
||||||
})
|
})
|
||||||
|
|
||||||
if index >= 0 {
|
if index >= 0 {
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,15 @@ func (client *Client) IsFlowInitiated() bool {
|
||||||
return client.FlowId != ""
|
return client.FlowId != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) BuildAuthorizationUrl(issuer string, state string) string {
|
func (client *Client) BuildAuthorizationUrl(state string) string {
|
||||||
return issuer + "?" + "client_id=" + client.Id +
|
url := client.Provider.Endpoints.Authorization + "?client_id=" + client.Id +
|
||||||
"&redirect_uri=" + url.QueryEscape(strings.Join(client.RedirectUris, ",")) +
|
"&redirect_uri=" + url.QueryEscape(strings.Join(client.RedirectUris, ",")) +
|
||||||
"&response_type=code" + // this has to be set to "code"
|
"&response_type=code" + // this has to be set to "code"
|
||||||
"&state=" + state +
|
|
||||||
"&scope=" + strings.Join(client.Scope, "+")
|
"&scope=" + strings.Join(client.Scope, "+")
|
||||||
|
if state != "" {
|
||||||
|
url += "&state=" + state
|
||||||
|
}
|
||||||
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) InitiateLoginFlow(loginUrl string) error {
|
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")
|
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{
|
body := url.Values{
|
||||||
"grant_type": {"authorization_code"},
|
"grant_type": {"authorization_code"},
|
||||||
"client_id": {client.Id},
|
"client_id": {client.Id},
|
||||||
|
|
@ -104,7 +107,7 @@ func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl
|
||||||
if state != "" {
|
if state != "" {
|
||||||
body["state"] = []string{state}
|
body["state"] = []string{state}
|
||||||
}
|
}
|
||||||
res, err := http.PostForm(remoteUrl, body)
|
res, err := http.PostForm(client.Provider.Endpoints.Token, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get ID token: %s", err)
|
return nil, fmt.Errorf("failed to get ID token: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package oauth
|
package oauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"davidallendj/opaal/internal/oidc"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -24,15 +25,15 @@ const (
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
http.Client
|
http.Client
|
||||||
Id string `db:"id" yaml:"id"`
|
Id string `db:"id" yaml:"id"`
|
||||||
Secret string `db:"secret" yaml:"secret"`
|
Secret string `db:"secret" yaml:"secret"`
|
||||||
Name string `db:"name" yaml:"name"`
|
Name string `db:"name" yaml:"name"`
|
||||||
Description string `db:"description" yaml:"description"`
|
Description string `db:"description" yaml:"description"`
|
||||||
Issuer string `db:"issuer" yaml:"issuer"`
|
Provider oidc.IdentityProvider `db:"issuer" yaml:"provider"`
|
||||||
RegistrationAccessToken string `db:"registration_access_token" yaml:"registration-access-token"`
|
RegistrationAccessToken string `db:"registration_access_token" yaml:"registration-access-token"`
|
||||||
RedirectUris []string `db:"redirect_uris" yaml:"redirect-uris"`
|
RedirectUris []string `db:"redirect_uris" yaml:"redirect-uris"`
|
||||||
Scope []string `db:"scope" yaml:"scope"`
|
Scope []string `db:"scope" yaml:"scope"`
|
||||||
Audience []string `db:"audience" yaml:"audience"`
|
Audience []string `db:"audience" yaml:"audience"`
|
||||||
FlowId string
|
FlowId string
|
||||||
CsrfToken string
|
CsrfToken string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,26 +111,11 @@ func (p *IdentityProvider) LoadServerConfig(path string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *IdentityProvider) FetchServerConfig() error {
|
func (p *IdentityProvider) FetchServerConfig() error {
|
||||||
// make a request to a server's openid-configuration
|
tmp, err := FetchServerConfig(p.Issuer)
|
||||||
req, err := http.NewRequest(http.MethodGet, p.Issuer+"/.well-known/openid-configuration", bytes.NewBuffer([]byte{}))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create a new request: %v", err)
|
return 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)
|
|
||||||
}
|
}
|
||||||
|
p = tmp
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,10 +132,15 @@ func FetchServerConfig(issuer string) (*IdentityProvider, error) {
|
||||||
return nil, fmt.Errorf("failed to do request: %v", err)
|
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)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read response body: %v", err)
|
return nil, fmt.Errorf("failed to read response body: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var p IdentityProvider
|
var p IdentityProvider
|
||||||
err = p.ParseServerConfig(body)
|
err = p.ParseServerConfig(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -58,10 +58,12 @@ func (s *Server) GetListenAddr() string {
|
||||||
return fmt.Sprintf("%s:%d", s.Host, s.Port)
|
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 (
|
var (
|
||||||
target = ""
|
target string
|
||||||
callback = ""
|
callback string
|
||||||
|
client *oauth.Client
|
||||||
|
sso string
|
||||||
)
|
)
|
||||||
|
|
||||||
// check if callback is set
|
// check if callback is set
|
||||||
|
|
@ -69,6 +71,29 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli
|
||||||
callback = "/oidc/callback"
|
callback = "/oidc/callback"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make the login page SSO buttons and authorization URLs to write to stdout
|
||||||
|
buttons := ""
|
||||||
|
fmt.Printf("Login with external identity providers: \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)
|
||||||
|
url := client.BuildAuthorizationUrl(s.State)
|
||||||
|
fmt.Printf("\t%s\n", url)
|
||||||
|
}
|
||||||
|
|
||||||
var code string
|
var code string
|
||||||
var accessToken string
|
var accessToken string
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
@ -93,20 +118,32 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli
|
||||||
// add target if query exists
|
// add target if query exists
|
||||||
if r != nil {
|
if r != nil {
|
||||||
target = r.URL.Query().Get("target")
|
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)
|
||||||
|
fmt.Printf("Redirect URL: %s\n", url)
|
||||||
|
http.Redirect(w, r, url, http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// show login page with notice to redirect
|
// show login page with notice to redirect
|
||||||
template, err := gonja.FromFile("pages/index.html")
|
template, err := gonja.FromFile("pages/index.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
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{}{
|
data := exec.NewContext(map[string]interface{}{
|
||||||
// "loginForm": string(form),
|
|
||||||
"loginButtons": buttons,
|
"loginButtons": buttons,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -157,7 +194,7 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli
|
||||||
// use refresh token provided to do a refresh token grant
|
// use refresh token provided to do a refresh token grant
|
||||||
refreshToken := r.URL.Query().Get("refresh-token")
|
refreshToken := r.URL.Query().Get("refresh-token")
|
||||||
if refreshToken != "" {
|
if refreshToken != "" {
|
||||||
_, err := params.JwtBearerParams.Client.PerformRefreshTokenGrant(provider.Endpoints.Token, refreshToken)
|
_, err := params.JwtBearerParams.Client.PerformRefreshTokenGrant(client.Provider.Endpoints.Token, refreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("failed to perform refresh token grant: %v\n", err)
|
fmt.Printf("failed to perform refresh token grant: %v\n", err)
|
||||||
http.Redirect(w, r, "/error", http.StatusInternalServerError)
|
http.Redirect(w, r, "/error", http.StatusInternalServerError)
|
||||||
|
|
@ -195,10 +232,15 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli
|
||||||
fmt.Printf("Authorization code: %v\n", code)
|
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)
|
// use code from response and exchange for bearer token (with ID token)
|
||||||
bearerToken, err := client.FetchTokenFromAuthenticationServer(
|
bearerToken, err := client.FetchTokenFromAuthenticationServer(
|
||||||
code,
|
code,
|
||||||
provider.Endpoints.Token,
|
|
||||||
s.State,
|
s.State,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -228,6 +270,7 @@ func (s *Server) StartLogin(buttons string, provider *oidc.IdentityProvider, cli
|
||||||
// complete JWT bearer flow to receive access token from authorization server
|
// complete JWT bearer flow to receive access token from authorization server
|
||||||
// fmt.Printf("bearer: %v\n", string(bearerToken))
|
// fmt.Printf("bearer: %v\n", string(bearerToken))
|
||||||
params.JwtBearerParams.IdToken = data["id_token"].(string)
|
params.JwtBearerParams.IdToken = data["id_token"].(string)
|
||||||
|
params.JwtBearerParams.Client = client
|
||||||
accessToken, err = flows.NewJwtBearerFlow(params.JwtBearerEndpoints, params.JwtBearerParams)
|
accessToken, err = flows.NewJwtBearerFlow(params.JwtBearerEndpoints, params.JwtBearerParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("failed to complete JWT bearer flow: %v\n", err)
|
fmt.Printf("failed to complete JWT bearer flow: %v\n", err)
|
||||||
|
|
@ -406,10 +449,12 @@ func (s *Server) StartIdentityProvider() error {
|
||||||
// example username and password so do simplified authorization code flow
|
// example username and password so do simplified authorization code flow
|
||||||
if username == "ochami" && password == "ochami" {
|
if username == "ochami" && password == "ochami" {
|
||||||
client := oauth.Client{
|
client := oauth.Client{
|
||||||
Id: "ochami",
|
Id: "ochami",
|
||||||
Secret: "ochami",
|
Secret: "ochami",
|
||||||
Name: "ochami",
|
Name: "ochami",
|
||||||
Issuer: "http://127.0.0.1:3333",
|
Provider: oidc.IdentityProvider{
|
||||||
|
Issuer: "http://127.0.0.1:3333",
|
||||||
|
},
|
||||||
RedirectUris: []string{fmt.Sprintf("http://%s:%d%s", s.Host, s.Port, callback)},
|
RedirectUris: []string{fmt.Sprintf("http://%s:%d%s", s.Host, s.Port, callback)},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -541,3 +586,13 @@ func (s *Server) StartIdentityProvider() error {
|
||||||
s.Handler = r
|
s.Handler = r
|
||||||
return s.ListenAndServe()
|
return s.ListenAndServe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeButton(url string, text string) string {
|
||||||
|
// check if we have http:// a
|
||||||
|
html := "<input type=\"button\" "
|
||||||
|
html += "class=\"button\" "
|
||||||
|
html += fmt.Sprintf("onclick=\"window.location.href='%s';\" ", url)
|
||||||
|
html += fmt.Sprintf("value=\"%s\">", text)
|
||||||
|
return html
|
||||||
|
// return "<a href=\"" + url + "\"> " + text + "</a>"
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue