mirror of
https://github.com/davidallendj/opaal.git
synced 2025-12-20 11:37:01 -07:00
Merge pull request #4 from davidallendj/dev
Complete implementation of authorization code flow
This commit is contained in:
commit
0888a64a3c
23 changed files with 1453 additions and 789 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,2 +1,4 @@
|
||||||
*.json
|
*.json
|
||||||
|
*.db
|
||||||
|
*.code-workspace
|
||||||
opaal
|
opaal
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# OIDC Provider Authentication/Authorization Login (OPAAL)
|
# OIDC Provider Authentication/Authorization Login (OPAAL)
|
||||||
|
|
||||||
This is a small, simple, experimental OIDC login helper tool that automates the authorization code lohin flow defined by [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1) for social sign-in with identity providers (IdP) like Google, Facebook, or GitHub. This tool is made to work when your identity provider is separate from your authorization server, and we only need the IdP to receive an ID token. In this document, the identity provider (or authentication server) is strictly the OIDC implementation that identifies the resource owner (ID token) whereas the resource provider (or authorization server) is the OIDC implementation that grants access to a resource (access token). This tool is tested with Ory Kratos and Hydra for user identity and session management and OAuth2/OIDC implementation respectively.
|
This is a small, simple, experimental OIDC login helper tool that automates the authorization flows defined by [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1) for social sign-in with identity providers (IdP) like Google, Facebook, or GitHub. This tool is made to work when your identity provider is separate from your authorization server, and we only need the IdP to receive an ID token. In this document, the identity provider (or authentication server) is strictly the OIDC implementation that identifies the resource owner (ID token) whereas the resource provider (or authorization server) is the OIDC implementation that grants access to a resource (access token). OPAAL assumes that the authentication server is external and the authorization server is owned. This tool is tested with Ory Kratos and Hydra for user identity and session management and OAuth2/OIDC implementation respectively.
|
||||||
|
|
||||||
Note: This tool acts as an OAuth client, contains client secrets, and is not to be exposed to users!
|
Note: This tool acts as an OAuth client, contains client secrets, and should not to be exposed to users! It would probably also be a good idea to use a reverse proxy and firewall to protect admin endpoints.
|
||||||
|
|
||||||
## Build and Usage
|
## Build and Usage
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
opaal "davidallendj/opaal/internal"
|
opaal "davidallendj/opaal/internal"
|
||||||
"davidallendj/opaal/internal/util"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/davidallendj/go-utils/pathx"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -15,7 +16,7 @@ var configCmd = &cobra.Command{
|
||||||
// create a new config at all args (paths)
|
// create a new config at all args (paths)
|
||||||
for _, path := range args {
|
for _, path := range args {
|
||||||
// check and make sure something doesn't exist first
|
// check and make sure something doesn't exist first
|
||||||
if exists, err := util.PathExists(path); exists || err != nil {
|
if exists, err := pathx.PathExists(path); exists || err != nil {
|
||||||
fmt.Printf("file or directory exists\n")
|
fmt.Printf("file or directory exists\n")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
62
cmd/login.go
62
cmd/login.go
|
|
@ -2,22 +2,56 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
opaal "davidallendj/opaal/internal"
|
opaal "davidallendj/opaal/internal"
|
||||||
|
"davidallendj/opaal/internal/db"
|
||||||
|
"davidallendj/opaal/internal/oidc"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
client opaal.Client
|
||||||
|
)
|
||||||
|
|
||||||
var loginCmd = &cobra.Command{
|
var loginCmd = &cobra.Command{
|
||||||
Use: "login",
|
Use: "login",
|
||||||
Short: "Start the login flow",
|
Short: "Start the login flow",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
for {
|
for {
|
||||||
err := opaal.Login(&config)
|
// try and find client with valid identity provider config
|
||||||
|
var provider *oidc.IdentityProvider
|
||||||
|
for _, c := range config.Authentication.Clients {
|
||||||
|
// try to get identity provider info locally first
|
||||||
|
_, err := db.GetIdentityProvider(config.Options.CachePath, c.Issuer)
|
||||||
|
if err != nil && !config.Options.LocalOnly {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if provider == nil {
|
||||||
|
fmt.Printf("failed to retrieve provider config\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := opaal.Login(&config, &client, provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%v\n", err)
|
fmt.Printf("%v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
} else if config.RunOnce {
|
} else if config.Options.RunOnce {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -25,17 +59,17 @@ var loginCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
loginCmd.Flags().StringVar(&config.Client.Id, "client.id", config.Client.Id, "set the client ID")
|
loginCmd.Flags().StringVar(&client.Id, "client.id", client.Id, "set the client ID")
|
||||||
loginCmd.Flags().StringVar(&config.Client.Secret, "client.secret", config.Client.Secret, "set the client secret")
|
loginCmd.Flags().StringVar(&client.Secret, "client.secret", client.Secret, "set the client secret")
|
||||||
loginCmd.Flags().StringSliceVar(&config.Client.RedirectUris, "redirect-uri", config.Client.RedirectUris, "set the redirect URI")
|
loginCmd.Flags().StringSliceVar(&client.RedirectUris, "client.redirect-uris", client.RedirectUris, "set the redirect URI")
|
||||||
loginCmd.Flags().StringVar(&config.ResponseType, "response-type", config.ResponseType, "set the response-type")
|
loginCmd.Flags().StringSliceVar(&client.Scope, "client.scope", client.Scope, "set the scopes")
|
||||||
loginCmd.Flags().StringSliceVar(&config.Scope, "scope", config.Scope, "set the scopes")
|
loginCmd.Flags().StringVar(&config.Server.Host, "server.host", config.Server.Host, "set the listening host")
|
||||||
loginCmd.Flags().StringVar(&config.State, "state", config.State, "set the state")
|
loginCmd.Flags().IntVar(&config.Server.Port, "server.port", config.Server.Port, "set the listening port")
|
||||||
loginCmd.Flags().StringVar(&config.Server.Host, "host", config.Server.Host, "set the listening host")
|
loginCmd.Flags().BoolVar(&config.Options.OpenBrowser, "open-browser", config.Options.OpenBrowser, "automatically open link in browser")
|
||||||
loginCmd.Flags().IntVar(&config.Server.Port, "port", config.Server.Port, "set the listening port")
|
loginCmd.Flags().BoolVar(&config.Options.DecodeIdToken, "decode-id-token", config.Options.DecodeIdToken, "decode and print ID token from identity provider")
|
||||||
loginCmd.Flags().BoolVar(&config.OpenBrowser, "open-browser", config.OpenBrowser, "automatically open link in browser")
|
loginCmd.Flags().BoolVar(&config.Options.DecodeAccessToken, "decore-access-token", config.Options.DecodeAccessToken, "decode and print access token from authorization server")
|
||||||
loginCmd.Flags().BoolVar(&config.DecodeIdToken, "decode-id-token", config.DecodeIdToken, "decode and print ID token from identity provider")
|
loginCmd.Flags().BoolVar(&config.Options.RunOnce, "once", config.Options.RunOnce, "set whether to run login once and exit")
|
||||||
loginCmd.Flags().BoolVar(&config.DecodeAccessToken, "decore-access-token", config.DecodeAccessToken, "decode and print access token from authorization server")
|
loginCmd.Flags().StringVar(&config.Options.FlowType, "flow", config.Options.FlowType, "set the grant-type/authorization flow")
|
||||||
loginCmd.Flags().BoolVar(&config.RunOnce, "once", config.RunOnce, "set whether to run login once and exit")
|
loginCmd.Flags().BoolVar(&config.Options.LocalOnly, "local", config.Options.LocalOnly, "only fetch identity provider configs stored locally")
|
||||||
rootCmd.AddCommand(loginCmd)
|
rootCmd.AddCommand(loginCmd)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
39
cmd/root.go
39
cmd/root.go
|
|
@ -2,16 +2,16 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
opaal "davidallendj/opaal/internal"
|
opaal "davidallendj/opaal/internal"
|
||||||
"davidallendj/opaal/internal/util"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/davidallendj/go-utils/pathx"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
configPath = ""
|
confPath = ""
|
||||||
config opaal.Config
|
config opaal.Config
|
||||||
)
|
)
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "opaal",
|
Use: "opaal",
|
||||||
|
|
@ -21,21 +21,6 @@ var rootCmd = &cobra.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func initConfig() {
|
|
||||||
// load config if found or create a new one
|
|
||||||
if configPath != "" {
|
|
||||||
exists, err := util.PathExists(configPath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("failed to load config")
|
|
||||||
os.Exit(1)
|
|
||||||
} else if exists {
|
|
||||||
config = opaal.LoadConfig(configPath)
|
|
||||||
} else {
|
|
||||||
config = opaal.NewConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "failed to start CLI: %s", err)
|
fmt.Fprintf(os.Stderr, "failed to start CLI: %s", err)
|
||||||
|
|
@ -45,5 +30,21 @@ func Execute() {
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cobra.OnInitialize(initConfig)
|
cobra.OnInitialize(initConfig)
|
||||||
rootCmd.PersistentFlags().StringVar(&configPath, "config", "", "set the config path")
|
rootCmd.PersistentFlags().StringVar(&confPath, "config", "", "set the config path")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&config.Options.CachePath, "cache", "", "set the cache path")
|
||||||
|
}
|
||||||
|
|
||||||
|
func initConfig() {
|
||||||
|
// load config if found or create a new one
|
||||||
|
if confPath != "" {
|
||||||
|
exists, err := pathx.PathExists(confPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to load config")
|
||||||
|
os.Exit(1)
|
||||||
|
} else if exists {
|
||||||
|
config = opaal.LoadConfig(confPath)
|
||||||
|
} else {
|
||||||
|
config = opaal.NewConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
56
config.yaml
Executable file
56
config.yaml
Executable file
|
|
@ -0,0 +1,56 @@
|
||||||
|
version: "0.0.1"
|
||||||
|
server:
|
||||||
|
host: "127.0.0.1"
|
||||||
|
port: 3333
|
||||||
|
callback: "/oidc/callback"
|
||||||
|
|
||||||
|
providers:
|
||||||
|
facebook: "http://facebook.com"
|
||||||
|
forgejo: "http://git.towk.local:3000"
|
||||||
|
gitlab: "https://gitlab.newmexicoconsortium.org"
|
||||||
|
github: "https://github.com"
|
||||||
|
|
||||||
|
authentication:
|
||||||
|
clients:
|
||||||
|
- id: "7527e7b4-c96a-4df0-8fc5-00fde18bb65d"
|
||||||
|
secret: "gto_cc5uvpb5lsdczkwnbarvwmbpv5kcjwg7nhbc75zt65yrfh2ldenq"
|
||||||
|
name: "forgejo"
|
||||||
|
issuer: "http://git.towk.local:3000"
|
||||||
|
scope:
|
||||||
|
- "openid"
|
||||||
|
- "profile"
|
||||||
|
- "read"
|
||||||
|
- "email"
|
||||||
|
redirect-uris:
|
||||||
|
- "http://127.0.0.1:3333/oidc/callback"
|
||||||
|
- id: "7c0fab1153674a258a705976fcb9468350df3addd91de4ec622fc9ed24bfbcdd"
|
||||||
|
secret: "a9a8bc55b0cd99236756093adc00ab17855fa507ce106b8038e7f9390ef2ad99"
|
||||||
|
name: "gitlab"
|
||||||
|
issuer: "http://gitlab.newmexicoconsortium.org"
|
||||||
|
scope:
|
||||||
|
- "openid"
|
||||||
|
- "profile"
|
||||||
|
- "email"
|
||||||
|
redirect-uris:
|
||||||
|
- "http://127.0.0.1:3333/oidc/callback"
|
||||||
|
flows:
|
||||||
|
authorization-code:
|
||||||
|
state: ""
|
||||||
|
client-credentials:
|
||||||
|
|
||||||
|
authorization:
|
||||||
|
urls:
|
||||||
|
#identities: http://127.0.0.1:4434/admin/identities
|
||||||
|
trusted-issuers: http://127.0.0.1:4445/admin/trust/grants/jwt-bearer/issuers
|
||||||
|
login: http://127.0.0.1:4433/self-service/login/api
|
||||||
|
clients: http://127.0.0.1:4445/admin/clients
|
||||||
|
authorize: http://127.0.0.1:4444/oauth2/auth
|
||||||
|
register: http://127.0.0.1:4444/oauth2/register
|
||||||
|
token: http://127.0.0.1:4444/oauth2/token
|
||||||
|
|
||||||
|
|
||||||
|
options:
|
||||||
|
decode-id-token: true
|
||||||
|
decode-access-token: true
|
||||||
|
run-once: true
|
||||||
|
open-browser: false
|
||||||
|
|
@ -18,6 +18,8 @@ secrets:
|
||||||
- youReallyNeedToChangeThis
|
- youReallyNeedToChangeThis
|
||||||
|
|
||||||
oidc:
|
oidc:
|
||||||
|
dynamic_client_registration:
|
||||||
|
enabled: true
|
||||||
subject_identifiers:
|
subject_identifiers:
|
||||||
supported_types:
|
supported_types:
|
||||||
- pairwise
|
- pairwise
|
||||||
|
|
@ -34,3 +36,6 @@ oauth2:
|
||||||
|
|
||||||
log:
|
log:
|
||||||
leak_sensitive_values: true
|
leak_sensitive_values: true
|
||||||
|
|
||||||
|
strategies:
|
||||||
|
access_token: jwt
|
||||||
|
|
|
||||||
26
go.mod
26
go.mod
|
|
@ -3,25 +3,41 @@ module davidallendj/opaal
|
||||||
go 1.22.0
|
go 1.22.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/davidallendj/go-utils v0.0.0-20240302194916-fe292bcf24a4
|
||||||
github.com/go-chi/chi v1.5.5
|
github.com/go-chi/chi v1.5.5
|
||||||
github.com/go-chi/chi/v5 v5.0.12
|
github.com/go-chi/chi/v5 v5.0.12
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
github.com/google/uuid v1.6.0
|
||||||
github.com/lestrrat-go/jwx v1.2.28
|
github.com/jmoiron/sqlx v1.3.5
|
||||||
|
github.com/lestrrat-go/jwx/v2 v2.0.20
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.6
|
||||||
|
github.com/nikolalohinski/gonja/v2 v2.2.0
|
||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
golang.org/x/net v0.10.0
|
golang.org/x/net v0.21.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
k8s.io/apimachinery v0.29.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
|
github.com/lestrrat-go/httprc v1.0.4 // indirect
|
||||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/segmentio/asm v1.2.0 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
golang.org/x/crypto v0.17.0 // indirect
|
golang.org/x/crypto v0.19.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
|
||||||
|
golang.org/x/sys v0.17.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
120
go.sum
120
go.sum
|
|
@ -1,94 +1,116 @@
|
||||||
|
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||||
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
github.com/davidallendj/go-utils v0.0.0-20240302194916-fe292bcf24a4 h1:6LeOczLfpq27cDfu4r6bRU3zGeBER9fy+iecHG5dDSA=
|
||||||
|
github.com/davidallendj/go-utils v0.0.0-20240302194916-fe292bcf24a4/go.mod h1:/hcpHd4um12taX6iLuMmwxosoyN6E2Ws8QxDpnY07oo=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
|
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
|
||||||
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
|
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
|
||||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
|
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||||
|
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
|
||||||
|
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
|
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||||
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
|
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
|
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
|
||||||
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||||
|
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
|
||||||
|
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||||
github.com/lestrrat-go/jwx v1.2.28 h1:uadI6o0WpOVrBSf498tRXZIwPpEtLnR9CvqPFXeI5sA=
|
github.com/lestrrat-go/jwx/v2 v2.0.20 h1:sAgXuWS/t8ykxS9Bi2Qtn5Qhpakw1wrcjxChudjolCc=
|
||||||
github.com/lestrrat-go/jwx v1.2.28/go.mod h1:nF+91HEMh/MYFVwKPl5HHsBGMPscqbQb+8IDQdIazP8=
|
github.com/lestrrat-go/jwx/v2 v2.0.20/go.mod h1:UlCSmKqw+agm5BsOBfEAbTvKsEApaGNqHAEUTv5PJC4=
|
||||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
|
||||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||||
|
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||||
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/nikolalohinski/gonja/v2 v2.2.0 h1:tAs3BDHNjvPj48F2BL5t7iVhN32HhgeldAl3EmdsLh8=
|
||||||
|
github.com/nikolalohinski/gonja/v2 v2.2.0/go.mod h1:l9DuWJvT/BddBr2SsmEimESD6msSqRw7u5HzI2Um+sc=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
|
||||||
|
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
|
||||||
|
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||||
|
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
|
||||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8=
|
||||||
|
k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU=
|
||||||
|
|
|
||||||
118
internal/authenticate.go
Normal file
118
internal/authenticate.go
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
package opaal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/davidallendj/go-utils/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (client *Client) IsFlowInitiated() bool {
|
||||||
|
return client.FlowId != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) BuildAuthorizationUrl(issuer string, state string) string {
|
||||||
|
return issuer + "?" + "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, "+") +
|
||||||
|
"&resource=" + url.QueryEscape("http://127.0.0.1:4444/oauth2/token")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) InitiateLoginFlow(loginUrl string) error {
|
||||||
|
// kratos: GET /self-service/login/api
|
||||||
|
req, err := http.NewRequest("GET", loginUrl, bytes.NewBuffer([]byte{}))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to make request: %v", err)
|
||||||
|
}
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to do request: %v", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
// get the flow ID from response
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var flowData map[string]any
|
||||||
|
err = json.Unmarshal(body, &flowData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal flow data: %v\n%v", err, string(body))
|
||||||
|
} else {
|
||||||
|
client.FlowId = flowData["id"].(string)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) FetchFlowData(url string) (map[string]any, error) {
|
||||||
|
//kratos: GET /self-service/login/flows?id={flowId}
|
||||||
|
|
||||||
|
// replace {id} in string with actual value
|
||||||
|
url = strings.ReplaceAll(url, "{id}", client.FlowId)
|
||||||
|
_, b, err := httpx.MakeHttpRequest(url, http.MethodGet, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to make request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var flowData map[string]any
|
||||||
|
err = json.Unmarshal(b, &flowData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal flow data: %v", err)
|
||||||
|
}
|
||||||
|
return flowData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) FetchCSRFToken(flowUrl string) error {
|
||||||
|
data, err := client.FetchFlowData(flowUrl)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch flow data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate through nodes and extract the CSRF token attribute from the flow data
|
||||||
|
ui := data["ui"].(map[string]any)
|
||||||
|
nodes := ui["nodes"].([]any)
|
||||||
|
for _, node := range nodes {
|
||||||
|
attrs := node.(map[string]any)["attributes"].(map[string]any)
|
||||||
|
name := attrs["name"].(string)
|
||||||
|
if name == "csrf_token" {
|
||||||
|
client.CsrfToken = attrs["value"].(string)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to extract CSRF token: not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl string, state string) ([]byte, error) {
|
||||||
|
body := url.Values{
|
||||||
|
"grant_type": {"authorization_code"},
|
||||||
|
"client_id": {client.Id},
|
||||||
|
"client_secret": {client.Secret},
|
||||||
|
"redirect_uri": {strings.Join(client.RedirectUris, ",")},
|
||||||
|
}
|
||||||
|
// add optional params if valid
|
||||||
|
if code != "" {
|
||||||
|
body["code"] = []string{code}
|
||||||
|
}
|
||||||
|
if state != "" {
|
||||||
|
body["state"] = []string{state}
|
||||||
|
}
|
||||||
|
res, err := http.PostForm(remoteUrl, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get ID token: %s", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
// domain, _ := url.Parse("http://127.0.0.1")
|
||||||
|
// client.Jar.SetCookies(domain, res.Cookies())
|
||||||
|
|
||||||
|
return io.ReadAll(res.Body)
|
||||||
|
}
|
||||||
424
internal/authorization_code.go
Normal file
424
internal/authorization_code.go
Normal file
|
|
@ -0,0 +1,424 @@
|
||||||
|
package opaal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"davidallendj/opaal/internal/oidc"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/davidallendj/go-utils/util"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"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: change authorization code flow to use these instead
|
||||||
|
type AuthorizationCodeFlowEndpoints struct {
|
||||||
|
Login string
|
||||||
|
Token string
|
||||||
|
Identities string
|
||||||
|
TrustedIssuer string
|
||||||
|
Register string
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthorizationCodeWithConfig(config *Config, server *Server, client *Client, idp *oidc.IdentityProvider) error {
|
||||||
|
// check preconditions are met
|
||||||
|
err := verifyParams(config, server, client, idp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the authorization URL to redirect user for social sign-in
|
||||||
|
state := config.Authentication.Flows["authorization_code"]["state"]
|
||||||
|
var authorizationUrl = client.BuildAuthorizationUrl(idp.Endpoints.Authorization, state)
|
||||||
|
|
||||||
|
// print the authorization URL for sharing
|
||||||
|
fmt.Printf("Login with identity provider:\n\n %s/login\n %s\n\n",
|
||||||
|
server.GetListenAddr(), authorizationUrl,
|
||||||
|
)
|
||||||
|
|
||||||
|
// automatically open browser to initiate login flow (only useful for testing and debugging)
|
||||||
|
if config.Options.OpenBrowser {
|
||||||
|
util.OpenUrl(authorizationUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// authorize oauth client and listen for callback from provider
|
||||||
|
fmt.Printf("Waiting for authorization code redirect @%s/oidc/callback...\n", server.GetListenAddr())
|
||||||
|
code, err := server.WaitForAuthorizationCode(authorizationUrl, "")
|
||||||
|
if errors.Is(err, http.ErrServerClosed) {
|
||||||
|
fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n")
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("failed to start server: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// start up another server in background to listen for success or failures
|
||||||
|
d := StartListener(server)
|
||||||
|
|
||||||
|
// use code from response and exchange for bearer token (with ID token)
|
||||||
|
bearerToken, err := client.FetchTokenFromAuthenticationServer(
|
||||||
|
code,
|
||||||
|
idp.Endpoints.Token,
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch token from issuer: %v", err)
|
||||||
|
}
|
||||||
|
// fmt.Printf("%v\n", string(bearerToken))
|
||||||
|
|
||||||
|
// unmarshal data to get id_token and access_token
|
||||||
|
var data map[string]any
|
||||||
|
err = json.Unmarshal([]byte(bearerToken), &data)
|
||||||
|
if err != nil || data == nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we have an ID token
|
||||||
|
if data["id_token"] == nil {
|
||||||
|
return fmt.Errorf("no ID token found...aborting")
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract ID token from bearer as JSON string for easy consumption
|
||||||
|
idToken := data["id_token"].(string)
|
||||||
|
idJwtSegments, err := util.DecodeJwt(idToken)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to parse ID token: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("id_token: %v\n", idToken)
|
||||||
|
if config.Options.DecodeIdToken {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to decode JWT: %v\n", err)
|
||||||
|
} else {
|
||||||
|
for i, segment := range idJwtSegments {
|
||||||
|
// don't print last segment (signatures)
|
||||||
|
if i == len(idJwtSegments)-1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Printf("%s\n", string(segment))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract the access token to get the scopes
|
||||||
|
accessToken := data["access_token"].(string)
|
||||||
|
accessJwtSegments, err := util.DecodeJwt(accessToken)
|
||||||
|
if err != nil || len(accessJwtSegments) <= 0 {
|
||||||
|
fmt.Printf("failed to parse access token: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("access_token (from identity provider): %v\n", accessToken)
|
||||||
|
if config.Options.DecodeIdToken {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to decode JWT: %v\n", err)
|
||||||
|
} else {
|
||||||
|
for i, segment := range accessJwtSegments {
|
||||||
|
// don't print last segment (signatures)
|
||||||
|
if i == len(accessJwtSegments)-1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Printf("%s\n", string(segment))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.Options.ForwardToken {
|
||||||
|
|
||||||
|
// TODO: implement our own JWT to send to Hydra
|
||||||
|
// 1. verify that the JWT from the issuer is valid
|
||||||
|
key, ok := idp.Jwks.Key(0)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("no key found in key set")
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedIdToken, err := jwt.ParseString(idToken, jwt.WithKey(jwa.RS256, key))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse ID token: %v", err)
|
||||||
|
}
|
||||||
|
_, err = jwt.ParseString(accessToken, jwt.WithKeySet(idp.Jwks))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse access token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = jws.Verify([]byte(idToken), jws.WithKeySet(idp.Jwks), jws.WithValidateKey(true))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to verify JWT: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. create a new JWKS (or just JWK) to be verified
|
||||||
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate private RSA k-ey: %v", err)
|
||||||
|
}
|
||||||
|
privateJwk, err := jwk.FromRaw(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create private JWK: %v", err)
|
||||||
|
}
|
||||||
|
publicJwk, err := jwk.PublicKeyOf(privateJwk)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create public JWK: %v", err)
|
||||||
|
}
|
||||||
|
publicJwk.Set("kid", uuid.New().String())
|
||||||
|
|
||||||
|
// 3. add opaal's server host as a trusted issuer with JWK
|
||||||
|
fmt.Printf("Attempting to add issuer to authorization server...\n")
|
||||||
|
res, err := client.AddTrustedIssuer(
|
||||||
|
config.Authorization.RequestUrls.TrustedIssuers,
|
||||||
|
server.Addr,
|
||||||
|
publicJwk,
|
||||||
|
"1",
|
||||||
|
time.Second*3600,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add trusted issuer: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%v\n", string(res))
|
||||||
|
|
||||||
|
// 4. create a new JWT based on the claims from the identity provider and sign
|
||||||
|
payload := parsedIdToken.PrivateClaims()
|
||||||
|
payload["iss"] = server.Addr
|
||||||
|
payload["aud"] = []string{config.Authorization.RequestUrls.Token}
|
||||||
|
payload["iat"] = time.Now().Unix()
|
||||||
|
payload["nbf"] = time.Now().Unix()
|
||||||
|
payload["exp"] = time.Now().Add(time.Second * 3600).Unix()
|
||||||
|
payload["sub"] = "1"
|
||||||
|
payloadJson, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal payload: %v", err)
|
||||||
|
}
|
||||||
|
newToken, err := jws.Sign(payloadJson, jws.WithJSON(), jws.WithKey(jwa.RS256, privateJwk))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to sign token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sig = rsasha256(b64urlencode(header) + "." + b64urlencode(payload))
|
||||||
|
// signature := util.EncodeBase64() + util.EncodeBase64() +
|
||||||
|
|
||||||
|
// 5. dynamically register new OAuth client and authorize it
|
||||||
|
fmt.Printf("Registering new OAuth2 client with authorization server...\n")
|
||||||
|
res, err = client.RegisterOAuthClient(config.Authorization.RequestUrls.Register, []string{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to register client: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%v\n", string(res))
|
||||||
|
|
||||||
|
// extract the client info from response
|
||||||
|
var clientData map[string]any
|
||||||
|
err = json.Unmarshal(res, &clientData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal client data: %v", err)
|
||||||
|
} else {
|
||||||
|
// check for error first
|
||||||
|
errJson := clientData["error"]
|
||||||
|
if errJson == nil {
|
||||||
|
client.Id = clientData["client_id"].(string)
|
||||||
|
client.Secret = clientData["client_secret"].(string)
|
||||||
|
} else {
|
||||||
|
// delete client and create again
|
||||||
|
fmt.Printf("Attempting to delete client...\n")
|
||||||
|
err := client.DeleteOAuthClient(config.Authorization.RequestUrls.Clients)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete OAuth client: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Attempting to re-create client...\n")
|
||||||
|
res, err := client.CreateOAuthClient(config.Authorization.RequestUrls.Clients, []string{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to register client: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%v\n", string(res))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// authorize the client
|
||||||
|
// fmt.Printf("Attempting to authorize client...\n")
|
||||||
|
// res, err = client.AuthorizeOAuthClient(config.Authorization.RequestUrls.Authorize)
|
||||||
|
// if err != nil {
|
||||||
|
// return fmt.Errorf("failed to authorize client: %v", err)
|
||||||
|
// }
|
||||||
|
// fmt.Printf("%v\n", string(res))
|
||||||
|
|
||||||
|
// 6. send JWT to authorization server and receive a access token
|
||||||
|
if config.Authorization.RequestUrls.Token != "" {
|
||||||
|
fmt.Printf("Fetching access token from authorization server...\n")
|
||||||
|
res, err := client.PerformTokenGrant(config.Authorization.RequestUrls.Token, string(newToken))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch access token: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s\n", res)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// extract the scope from access token claims
|
||||||
|
// var scope []string
|
||||||
|
// var accessJsonPayload map[string]any
|
||||||
|
// var accessJwtPayload []byte = accessJwtSegments[1]
|
||||||
|
// if accessJsonPayload != nil {
|
||||||
|
// err := json.Unmarshal(accessJwtPayload, &accessJsonPayload)
|
||||||
|
// if err != nil {
|
||||||
|
// return fmt.Errorf("failed to unmarshal JWT: %v", err)
|
||||||
|
// }
|
||||||
|
// scope = idJsonPayload["scope"].([]string)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// create a new identity with identity and session manager if url is provided
|
||||||
|
// if config.RequestUrls.Identities != "" {
|
||||||
|
// fmt.Printf("Attempting to create a new identity...\n")
|
||||||
|
// err := client.CreateIdentity(config.RequestUrls.Identities, idToken)
|
||||||
|
// if err != nil {
|
||||||
|
// return fmt.Errorf("failed to create new identity: %v", err)
|
||||||
|
// }
|
||||||
|
// _, err = client.FetchIdentities(config.RequestUrls.Identities)
|
||||||
|
// if err != nil {
|
||||||
|
// return fmt.Errorf("failed to fetch identities: %v", err)
|
||||||
|
// }
|
||||||
|
// fmt.Printf("Created new identity successfully.\n\n")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// extract the subject from ID token claims
|
||||||
|
var subject string
|
||||||
|
var audience []string
|
||||||
|
var idJsonPayload map[string]any
|
||||||
|
var idJwtPayload []byte = idJwtSegments[1]
|
||||||
|
if idJwtPayload != nil {
|
||||||
|
err := json.Unmarshal(idJwtPayload, &idJsonPayload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal JWT: %v", err)
|
||||||
|
}
|
||||||
|
subject = idJsonPayload["sub"].(string)
|
||||||
|
audType := reflect.ValueOf(idJsonPayload["aud"])
|
||||||
|
switch audType.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
audience = append(audience, idJsonPayload["aud"].(string))
|
||||||
|
case reflect.Array:
|
||||||
|
audience = idJsonPayload["aud"].([]string)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("failed to extract subject from ID token claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch JWKS and add issuer to authentication server to submit ID token
|
||||||
|
fmt.Printf("Fetching JWKS from authentication server for verification...\n")
|
||||||
|
err = idp.FetchJwks()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch JWK: %v", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Successfully retrieved JWK from authentication server.\n\n")
|
||||||
|
fmt.Printf("Attempting to add issuer to authorization server...\n")
|
||||||
|
res, err := client.AddTrustedIssuerWithIdentityProvider(
|
||||||
|
config.Authorization.RequestUrls.TrustedIssuers,
|
||||||
|
idp,
|
||||||
|
subject,
|
||||||
|
time.Duration(1000),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add trusted issuer: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%v\n", string(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
// add client ID to audience
|
||||||
|
audience = append(audience, client.Id)
|
||||||
|
audience = append(audience, "http://127.0.0.1:4444/oauth2/token")
|
||||||
|
|
||||||
|
// try and register a new client with authorization server
|
||||||
|
fmt.Printf("Registering new OAuth2 client with authorization server...\n")
|
||||||
|
res, err := client.RegisterOAuthClient(config.Authorization.RequestUrls.Register, audience)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to register client: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%v\n", string(res))
|
||||||
|
|
||||||
|
// extract the client info from response
|
||||||
|
var clientData map[string]any
|
||||||
|
err = json.Unmarshal(res, &clientData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal client data: %v", err)
|
||||||
|
} else {
|
||||||
|
// check for error first
|
||||||
|
errJson := clientData["error"]
|
||||||
|
if errJson == nil {
|
||||||
|
client.Id = clientData["client_id"].(string)
|
||||||
|
client.Secret = clientData["client_secret"].(string)
|
||||||
|
} else {
|
||||||
|
// delete client and create again
|
||||||
|
fmt.Printf("Attempting to delete client...\n")
|
||||||
|
err := client.DeleteOAuthClient(config.Authorization.RequestUrls.Clients)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete OAuth client: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Attempting to re-create client...\n")
|
||||||
|
res, err := client.CreateOAuthClient(config.Authorization.RequestUrls.Clients, audience)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to register client: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%v\n", string(res))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// authorize the client
|
||||||
|
// fmt.Printf("Attempting to authorize client...\n")
|
||||||
|
// res, err = client.AuthorizeOAuthClient(config.Authorization.RequestUrls.Authorize)
|
||||||
|
// if err != nil {
|
||||||
|
// return fmt.Errorf("failed to authorize client: %v", err)
|
||||||
|
// }
|
||||||
|
// fmt.Printf("%v\n", string(res))
|
||||||
|
|
||||||
|
// use ID token/user info to fetch access token from authentication server
|
||||||
|
if config.Authorization.RequestUrls.Token != "" {
|
||||||
|
fmt.Printf("Fetching access token from authorization server...\n")
|
||||||
|
res, err := client.PerformTokenGrant(config.Authorization.RequestUrls.Token, idToken)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch access token: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s\n", res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var access_token []byte
|
||||||
|
d <- access_token
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyParams(config *Config, server *Server, client *Client, idp *oidc.IdentityProvider) error {
|
||||||
|
// make sure we have a valid server and client
|
||||||
|
if server == nil {
|
||||||
|
return fmt.Errorf("server not initialized or valid (server == nil)")
|
||||||
|
}
|
||||||
|
if client == nil {
|
||||||
|
return fmt.Errorf("client not initialized or valid (client == nil)")
|
||||||
|
}
|
||||||
|
if idp == nil {
|
||||||
|
return fmt.Errorf("identity provider not initialized or valid (idp == nil)")
|
||||||
|
}
|
||||||
|
// check if all appropriate parameters are set in config
|
||||||
|
if !HasRequiredConfigParams(config) {
|
||||||
|
return fmt.Errorf("required params not set correctly or missing")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartListener(server *Server) chan []byte {
|
||||||
|
d := make(chan []byte)
|
||||||
|
quit := make(chan bool)
|
||||||
|
|
||||||
|
go server.Serve(d)
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-d:
|
||||||
|
fmt.Printf("got access token")
|
||||||
|
quit <- true
|
||||||
|
case <-quit:
|
||||||
|
close(d)
|
||||||
|
close(quit)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return d
|
||||||
|
}
|
||||||
248
internal/authorize.go
Normal file
248
internal/authorize.go
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
package opaal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"davidallendj/opaal/internal/oidc"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/davidallendj/go-utils/httpx"
|
||||||
|
"github.com/davidallendj/go-utils/util"
|
||||||
|
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (client *Client) AddTrustedIssuer(url string, issuer string, key jwk.Key, subject string, expires time.Duration) ([]byte, error) {
|
||||||
|
// hydra endpoint: POST /admin/trust/grants/jwt-bearer/issuers
|
||||||
|
quotedScopes := make([]string, len(client.Scope))
|
||||||
|
for i, s := range client.Scope {
|
||||||
|
quotedScopes[i] = fmt.Sprintf("\"%s\"", s)
|
||||||
|
}
|
||||||
|
jwkstr, err := json.Marshal(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal JWK: %v", err)
|
||||||
|
}
|
||||||
|
// NOTE: Can also include "jwks_uri" instead
|
||||||
|
data := []byte(fmt.Sprintf("{"+
|
||||||
|
"\"allow_any_subject\": false,"+
|
||||||
|
"\"issuer\": \"%s\","+
|
||||||
|
"\"subject\": \"%s\","+
|
||||||
|
"\"expires_at\": \"%v\","+
|
||||||
|
"\"jwk\": %v,"+
|
||||||
|
"\"scope\": [ %s ]"+
|
||||||
|
"}", issuer, subject, time.Now().Add(expires).Format(time.RFC3339), string(jwkstr), strings.Join(quotedScopes, ",")))
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
|
||||||
|
// req.Header.Add("X-CSRF-Token", client.CsrfToken.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to make request: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
// req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken))
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to do request: %v", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
return io.ReadAll(res.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) AddTrustedIssuerWithIdentityProvider(url string, idp *oidc.IdentityProvider, subject string, expires time.Duration) ([]byte, error) {
|
||||||
|
// hydra endpoint: POST /admin/trust/grants/jwt-bearer/issuers
|
||||||
|
key, ok := idp.Jwks.Key(0)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no keys found in key set")
|
||||||
|
}
|
||||||
|
return client.AddTrustedIssuer(url, idp.Issuer, key, subject, expires)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) IsOAuthClientRegistered(clientUrl string) (bool, error) {
|
||||||
|
_, _, err := httpx.MakeHttpRequest(clientUrl, http.MethodGet, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to make request: %v", err)
|
||||||
|
}
|
||||||
|
// TODO: need to check contents of actual response
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) GetOAuthClient(clientUrl string) error {
|
||||||
|
_, b, err := httpx.MakeHttpRequest(clientUrl, http.MethodGet, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to make request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("GetOAuthClient: %v\n", string(b))
|
||||||
|
|
||||||
|
var data []map[string]any
|
||||||
|
err = json.Unmarshal(b, &data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
index := slices.IndexFunc(data, func(c map[string]any) bool {
|
||||||
|
if c["client_id"] == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return c["client_id"].(string) == client.Id
|
||||||
|
})
|
||||||
|
if index < 0 {
|
||||||
|
return fmt.Errorf("client not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// cast the redirect_uris from []any to []string and extract registration token
|
||||||
|
foundClient := data[index]
|
||||||
|
for _, uri := range foundClient["redirect_uris"].([]any) {
|
||||||
|
client.RedirectUris = append(client.RedirectUris, uri.(string))
|
||||||
|
}
|
||||||
|
if foundClient["registration-access-token"] != nil {
|
||||||
|
client.RegistrationAccessToken = foundClient["registration-access-token"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) CreateOAuthClient(registerUrl string, audience []string) ([]byte, error) {
|
||||||
|
// hydra endpoint: POST /clients
|
||||||
|
audience = util.QuoteArrayStrings(audience)
|
||||||
|
body := httpx.Body(fmt.Sprintf(`{
|
||||||
|
"client_id": "%s",
|
||||||
|
"client_name": "%s",
|
||||||
|
"client_secret": "%s",
|
||||||
|
"token_endpoint_auth_method": "client_secret_post",
|
||||||
|
"scope": "%s",
|
||||||
|
"grant_types": ["urn:ietf:params:oauth:grant-type:jwt-bearer"],
|
||||||
|
"response_types": ["token"],
|
||||||
|
"redirect_uris": ["http://127.0.0.1:3333/callback"],
|
||||||
|
"state": 12345678910,
|
||||||
|
"audience": [%s]
|
||||||
|
}`, client.Id, client.Id, client.Secret, strings.Join(client.Scope, " "), strings.Join(audience, ","),
|
||||||
|
))
|
||||||
|
headers := httpx.Headers{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, b, err := httpx.MakeHttpRequest(registerUrl, http.MethodPost, []byte(body), headers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to make request: %v", err)
|
||||||
|
}
|
||||||
|
var rjson map[string]any
|
||||||
|
err = json.Unmarshal(b, &rjson)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for error first
|
||||||
|
errJson := rjson["error"]
|
||||||
|
if errJson == nil {
|
||||||
|
// set the client ID and secret of registered client
|
||||||
|
client.Id = rjson["client_id"].(string)
|
||||||
|
client.Secret = rjson["client_secret"].(string)
|
||||||
|
client.RegistrationAccessToken = rjson["registration_access_token"].(string)
|
||||||
|
} else {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) RegisterOAuthClient(registerUrl string, audience []string) ([]byte, error) {
|
||||||
|
// hydra endpoint: POST /oauth2/register
|
||||||
|
audience = util.QuoteArrayStrings(audience)
|
||||||
|
body := httpx.Body(fmt.Sprintf(`{
|
||||||
|
"client_name": "opaal",
|
||||||
|
"token_endpoint_auth_method": "client_secret_post",
|
||||||
|
"scope": "%s",
|
||||||
|
"grant_types": ["urn:ietf:params:oauth:grant-type:jwt-bearer"],
|
||||||
|
"response_types": ["token"],
|
||||||
|
"redirect_uris": ["http://127.0.0.1:3333/callback"],
|
||||||
|
"state": 12345678910,
|
||||||
|
"audience": [%s]
|
||||||
|
}`, strings.Join(client.Scope, " "), strings.Join(audience, ","),
|
||||||
|
))
|
||||||
|
headers := httpx.Headers{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
_, b, err := httpx.MakeHttpRequest(registerUrl, http.MethodPost, body, headers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to make request: %v", err)
|
||||||
|
}
|
||||||
|
var rjson map[string]any
|
||||||
|
err = json.Unmarshal(b, &rjson)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for error first
|
||||||
|
errJson := rjson["error"]
|
||||||
|
if errJson == nil {
|
||||||
|
// set the client ID and secret of registered client
|
||||||
|
client.Id = rjson["client_id"].(string)
|
||||||
|
client.Secret = rjson["client_secret"].(string)
|
||||||
|
client.RegistrationAccessToken = rjson["registration_access_token"].(string)
|
||||||
|
} else {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) AuthorizeOAuthClient(authorizeUrl string) ([]byte, error) {
|
||||||
|
// set the authorization header
|
||||||
|
body := []byte("grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") +
|
||||||
|
"&scope=" + strings.Join(client.Scope, "+") +
|
||||||
|
"&client_id=" + client.Id +
|
||||||
|
"&client_secret=" + client.Secret +
|
||||||
|
"&redirect_uri=" + url.QueryEscape("http://127.0.0.1:3333/callback") + // FIXME: needs to not be hardcorded
|
||||||
|
"&response_type=token" +
|
||||||
|
"&state=12345678910",
|
||||||
|
)
|
||||||
|
headers := httpx.Headers{
|
||||||
|
"Authorization": "Bearer " + client.RegistrationAccessToken,
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
}
|
||||||
|
_, b, err := httpx.MakeHttpRequest(authorizeUrl, http.MethodPost, body, headers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to make HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) PerformTokenGrant(clientUrl string, encodedJwt string) ([]byte, error) {
|
||||||
|
// hydra endpoint: /oauth/token
|
||||||
|
body := "grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") +
|
||||||
|
"&client_id=" + client.Id +
|
||||||
|
"&client_secret=" + client.Secret +
|
||||||
|
"&redirect_uri=" + url.QueryEscape("http://127.0.0.1:3333/callback")
|
||||||
|
// add optional params if valid
|
||||||
|
if encodedJwt != "" {
|
||||||
|
body += "&assertion=" + encodedJwt
|
||||||
|
}
|
||||||
|
if client.Scope != nil || len(client.Scope) > 0 {
|
||||||
|
body += "&scope=" + strings.Join(client.Scope, "+")
|
||||||
|
}
|
||||||
|
headers := httpx.Headers{
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"Authorization": "Bearer " + client.RegistrationAccessToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, b, err := httpx.MakeHttpRequest(clientUrl, http.MethodPost, []byte(body), headers)
|
||||||
|
|
||||||
|
// set flow ID back to empty string to indicate a completed flow
|
||||||
|
client.FlowId = ""
|
||||||
|
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) DeleteOAuthClient(clientUrl string) error {
|
||||||
|
_, _, err := httpx.MakeHttpRequest(clientUrl+"/"+client.Id, http.MethodDelete, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to make request: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -1,277 +1,90 @@
|
||||||
package opaal
|
package opaal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"davidallendj/opaal/internal/oidc"
|
|
||||||
"davidallendj/opaal/internal/util"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"slices"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
|
"github.com/davidallendj/go-utils/mathx"
|
||||||
"golang.org/x/net/publicsuffix"
|
"golang.org/x/net/publicsuffix"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
http.Client
|
http.Client
|
||||||
Id string `yaml:"id"`
|
Id string `yaml:"id"`
|
||||||
Secret string `yaml:"secret"`
|
Secret string `yaml:"secret"`
|
||||||
RedirectUris []string `yaml:"redirect-uris"`
|
Name string `yaml:"name"`
|
||||||
FlowId string
|
Description string `yaml:"description"`
|
||||||
CsrfToken string
|
Issuer string `yaml:"issuer"`
|
||||||
|
RegistrationAccessToken string `yaml:"registration-access-token"`
|
||||||
|
RedirectUris []string `yaml:"redirect-uris"`
|
||||||
|
Scope []string `yaml:"scope"`
|
||||||
|
FlowId string
|
||||||
|
CsrfToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient() *Client {
|
||||||
|
return &Client{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClientWithConfig(config *Config) *Client {
|
func NewClientWithConfig(config *Config) *Client {
|
||||||
jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
|
// make sure config is valid
|
||||||
|
if config == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we have at least one client
|
||||||
|
clients := config.Authentication.Clients
|
||||||
|
if len(clients) <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the first client found by default
|
||||||
return &Client{
|
return &Client{
|
||||||
Id: config.Client.Id,
|
Id: clients[0].Id,
|
||||||
Secret: config.Client.Secret,
|
Secret: clients[0].Secret,
|
||||||
RedirectUris: config.Client.RedirectUris,
|
Name: clients[0].Name,
|
||||||
Client: http.Client{Jar: jar},
|
Issuer: clients[0].Issuer,
|
||||||
|
Scope: clients[0].Scope,
|
||||||
|
RedirectUris: clients[0].RedirectUris,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) IsFlowInitiated() bool {
|
func NewClientWithConfigByIndex(config *Config, index int) *Client {
|
||||||
return client.FlowId != ""
|
size := len(config.Authentication.Clients)
|
||||||
|
index = mathx.Clamp(index, 0, size)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) BuildAuthorizationUrl(authEndpoint string, state string, responseType string, scope []string) string {
|
func NewClientWithConfigByName(config *Config, name string) *Client {
|
||||||
return authEndpoint + "?" + "client_id=" + client.Id +
|
index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool {
|
||||||
"&redirect_uri=" + util.URLEscape(strings.Join(client.RedirectUris, ",")) +
|
return c.Name == name
|
||||||
"&response_type=" + responseType +
|
})
|
||||||
"&state=" + state +
|
if index >= 0 {
|
||||||
"&scope=" + strings.Join(scope, "+") +
|
return &config.Authentication.Clients[index]
|
||||||
"&audience=http://127.0.0.1:4444/oauth2/token"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) InitiateLoginFlow(loginUrl string) error {
|
|
||||||
// kratos: GET /self-service/login/api
|
|
||||||
req, err := http.NewRequest("GET", loginUrl, bytes.NewBuffer([]byte{}))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to make request: %v", err)
|
|
||||||
}
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to do request: %v", err)
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
// get the flow ID from response
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
|
|
||||||
var flowData map[string]any
|
|
||||||
err = json.Unmarshal(body, &flowData)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to unmarshal flow data: %v\n%v", err, string(body))
|
|
||||||
} else {
|
|
||||||
client.FlowId = flowData["id"].(string)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) FetchFlowData(flowUrl string) (map[string]any, error) {
|
func NewClientWithConfigByProvider(config *Config, issuer string) *Client {
|
||||||
//kratos: GET /self-service/login/flows?id={flowId}
|
index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool {
|
||||||
|
return c.Issuer == issuer
|
||||||
|
})
|
||||||
|
|
||||||
// replace {id} in string with actual value
|
if index >= 0 {
|
||||||
flowUrl = strings.ReplaceAll(flowUrl, "{id}", client.FlowId)
|
return &config.Authentication.Clients[index]
|
||||||
req, err := http.NewRequest("GET", flowUrl, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to make request: %v", err)
|
|
||||||
}
|
}
|
||||||
res, err := client.Do(req)
|
return nil
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to do request: %v", err)
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
// get the flow data from response
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read response body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var flowData map[string]any
|
|
||||||
err = json.Unmarshal(body, &flowData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal flow data: %v", err)
|
|
||||||
}
|
|
||||||
return flowData, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) FetchCSRFToken(flowUrl string) error {
|
func NewClientWithConfigById(config *Config, id string) *Client {
|
||||||
data, err := client.FetchFlowData(flowUrl)
|
index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool {
|
||||||
if err != nil {
|
return c.Id == id
|
||||||
return fmt.Errorf("failed to fetch flow data: %v", err)
|
})
|
||||||
|
if index >= 0 {
|
||||||
|
return &config.Authentication.Clients[index]
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
// iterate through nodes and extract the CSRF token attribute from the flow data
|
|
||||||
ui := data["ui"].(map[string]any)
|
|
||||||
nodes := ui["nodes"].([]any)
|
|
||||||
for _, node := range nodes {
|
|
||||||
attrs := node.(map[string]any)["attributes"].(map[string]any)
|
|
||||||
name := attrs["name"].(string)
|
|
||||||
if name == "csrf_token" {
|
|
||||||
client.CsrfToken = attrs["value"].(string)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("failed to extract CSRF token: not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl string, state string) ([]byte, error) {
|
|
||||||
data := url.Values{
|
|
||||||
"grant_type": {"authorization_code"},
|
|
||||||
"code": {code},
|
|
||||||
"client_id": {client.Id},
|
|
||||||
"client_secret": {client.Secret},
|
|
||||||
"state": {state},
|
|
||||||
"redirect_uri": {strings.Join(client.RedirectUris, ",")},
|
|
||||||
}
|
|
||||||
res, err := http.PostForm(remoteUrl, data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get ID token: %s", err)
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
domain, _ := url.Parse("http://127.0.0.1")
|
|
||||||
client.Jar.SetCookies(domain, res.Cookies())
|
|
||||||
|
|
||||||
return io.ReadAll(res.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) FetchTokenFromAuthorizationServer(remoteUrl string, jwt string, scope []string) ([]byte, error) {
|
|
||||||
// hydra endpoint: /oauth/token
|
|
||||||
data := "grant_type=" + util.URLEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") +
|
|
||||||
"&client_id=" + client.Id +
|
|
||||||
"&client_secret=" + client.Secret +
|
|
||||||
"&scope=" + strings.Join(scope, "+") +
|
|
||||||
"&assertion=" + jwt
|
|
||||||
fmt.Printf("encoded params: %v\n\n", data)
|
|
||||||
req, err := http.NewRequest("POST", remoteUrl, bytes.NewBuffer([]byte(data)))
|
|
||||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to make request: %s", err)
|
|
||||||
}
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to do request: %v", err)
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
// set flow ID back to empty string to indicate a completed flow
|
|
||||||
client.FlowId = ""
|
|
||||||
|
|
||||||
return io.ReadAll(res.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) AddTrustedIssuer(remoteUrl string, idp *oidc.IdentityProvider, subject string, duration time.Duration, scope []string) ([]byte, error) {
|
|
||||||
// hydra endpoint: POST /admin/trust/grants/jwt-bearer/issuers
|
|
||||||
if idp == nil {
|
|
||||||
return nil, fmt.Errorf("identity provided is nil")
|
|
||||||
}
|
|
||||||
jwkstr, err := json.Marshal(idp.Key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal JWK: %v", err)
|
|
||||||
}
|
|
||||||
quotedScopes := make([]string, len(scope))
|
|
||||||
for i, s := range scope {
|
|
||||||
quotedScopes[i] = fmt.Sprintf("\"%s\"", s)
|
|
||||||
}
|
|
||||||
// NOTE: Can also include "jwks_uri" instead
|
|
||||||
data := []byte(fmt.Sprintf("{"+
|
|
||||||
"\"allow_any_subject\": false,"+
|
|
||||||
"\"issuer\": \"%s\","+
|
|
||||||
"\"subject\": \"%s\","+
|
|
||||||
"\"expires_at\": \"%v\","+
|
|
||||||
"\"jwk\": %v,"+
|
|
||||||
"\"scope\": [ %s ]"+
|
|
||||||
"}", idp.Issuer, subject, time.Now().Add(duration).Format(time.RFC3339), string(jwkstr), strings.Join(quotedScopes, ",")))
|
|
||||||
fmt.Printf("%v\n", string(data))
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", remoteUrl, bytes.NewBuffer(data))
|
|
||||||
// req.Header.Add("X-CSRF-Token", client.CsrfToken.Value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to make request: %v", err)
|
|
||||||
}
|
|
||||||
req.Header.Add("Content-Type", "application/json")
|
|
||||||
// req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken))
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to do request: %v", err)
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
return io.ReadAll(res.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) RegisterOAuthClient(registerUrl string, audience []string) ([]byte, error) {
|
|
||||||
// hydra endpoint: POST /clients
|
|
||||||
audience = util.QuoteArrayStrings(audience)
|
|
||||||
data := []byte(fmt.Sprintf(`{
|
|
||||||
"client_name": "%s",
|
|
||||||
"client_secret": "%s",
|
|
||||||
"token_endpoint_auth_method": "client_secret_post",
|
|
||||||
"scope": "openid email profile",
|
|
||||||
"grant_types": ["client_credentials", "urn:ietf:params:oauth:grant-type:jwt-bearer"],
|
|
||||||
"response_types": ["token"],
|
|
||||||
"audience": [%s]
|
|
||||||
}`, client.Id, client.Secret, strings.Join(audience, ",")))
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", registerUrl, bytes.NewBuffer(data))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to make request: %v", err)
|
|
||||||
}
|
|
||||||
req.Header.Add("Content-Type", "application/json")
|
|
||||||
// req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken))
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to do request: %v", err)
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
return io.ReadAll(res.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) CreateIdentity(remoteUrl string, idToken string) ([]byte, error) {
|
|
||||||
// kratos endpoint: /admin/identities
|
|
||||||
data := []byte(`{
|
|
||||||
"schema_id": "preset://email",
|
|
||||||
"traits": {
|
|
||||||
"email": "docs@example.org"
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", remoteUrl, bytes.NewBuffer(data))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create a new request: %v", err)
|
|
||||||
}
|
|
||||||
req.Header.Add("Content-Type", "application/json")
|
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken))
|
|
||||||
// req.Header.Add("X-CSRF-Token", client.CsrfToken.Value)
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to do request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return io.ReadAll(res.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) FetchIdentities(remoteUrl string) ([]byte, error) {
|
|
||||||
req, err := http.NewRequest("GET", remoteUrl, bytes.NewBuffer([]byte{}))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create a new request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to do request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return io.ReadAll(res.Body)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) ClearCookies() {
|
func (client *Client) ClearCookies() {
|
||||||
|
|
|
||||||
48
internal/client_credentials.go
Normal file
48
internal/client_credentials.go
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
package opaal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientCredentialsFlowParams struct {
|
||||||
|
State string `yaml:"state"`
|
||||||
|
ResponseType string `yaml:"response-type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientCredentialsFlowEndpoints struct {
|
||||||
|
Create string
|
||||||
|
Authorize string
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClientCredentials(eps ClientCredentialsFlowEndpoints, client *Client) error {
|
||||||
|
// register a new OAuth 2 client with authorization srever
|
||||||
|
_, err := client.CreateOAuthClient(eps.Create, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to register OAuth client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// authorize the client
|
||||||
|
_, err = client.AuthorizeOAuthClient(eps.Authorize)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to authorize client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// request a token from the authorization server
|
||||||
|
res, err := client.PerformTokenGrant(eps.Token, "")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fetch token from authorization server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("token: %v\n", string(res))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClientCredentialsWithConfig(config *Config, client *Client) error {
|
||||||
|
eps := ClientCredentialsFlowEndpoints{
|
||||||
|
Create: config.Authorization.RequestUrls.Clients,
|
||||||
|
Authorize: config.Authorization.RequestUrls.Authorize,
|
||||||
|
Token: config.Authorization.RequestUrls.Token,
|
||||||
|
}
|
||||||
|
return ClientCredentials(eps, client)
|
||||||
|
}
|
||||||
|
|
@ -1,56 +1,77 @@
|
||||||
package opaal
|
package opaal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"davidallendj/opaal/internal/oidc"
|
|
||||||
"davidallendj/opaal/internal/util"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
goutil "github.com/davidallendj/go-utils/util"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type FlowOptions map[string]string
|
||||||
|
type Flows map[string]FlowOptions
|
||||||
|
type Providers map[string]string
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
DecodeIdToken bool `yaml:"decode-id-token"`
|
||||||
|
DecodeAccessToken bool `yaml:"decode-access-token"`
|
||||||
|
RunOnce bool `yaml:"run-once"`
|
||||||
|
OpenBrowser bool `yaml:"open-browser"`
|
||||||
|
FlowType string `yaml:"flow"`
|
||||||
|
CachePath string `yaml:"cache"`
|
||||||
|
LocalOnly bool `yaml:"local-only"`
|
||||||
|
ForwardToken bool `yaml:"forward-token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestUrls struct {
|
||||||
|
Identities string `yaml:"identities"`
|
||||||
|
TrustedIssuers string `yaml:"trusted-issuers"`
|
||||||
|
Login string `yaml:"login"`
|
||||||
|
Clients string `yaml:"clients"`
|
||||||
|
Token string `yaml:"token"`
|
||||||
|
Authorize string `yaml:"authorize"`
|
||||||
|
Register string `yaml:"register"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Authentication struct {
|
||||||
|
Clients []Client `yaml:"clients"`
|
||||||
|
Flows Flows `yaml:"flows"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Authorization struct {
|
||||||
|
RequestUrls RequestUrls `yaml:"urls"`
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Version string `yaml:"version"`
|
Version string `yaml:"version"`
|
||||||
Server Server `yaml:"server"`
|
Server Server `yaml:"server"`
|
||||||
Client Client `yaml:"client"`
|
Providers Providers `yaml:"providers"`
|
||||||
IdentityProvider oidc.IdentityProvider `yaml:"oidc"`
|
Options Options `yaml:"options"`
|
||||||
State string `yaml:"state"`
|
Authentication Authentication `yaml:"authentication"`
|
||||||
ResponseType string `yaml:"response-type"`
|
Authorization Authorization `yaml:"authorization"`
|
||||||
Scope []string `yaml:"scope"`
|
|
||||||
ActionUrls ActionUrls `yaml:"urls"`
|
|
||||||
OpenBrowser bool `yaml:"open-browser"`
|
|
||||||
DecodeIdToken bool `yaml:"decode-id-token"`
|
|
||||||
DecodeAccessToken bool `yaml:"decode-access-token"`
|
|
||||||
RunOnce bool `yaml:"run-once"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig() Config {
|
func NewConfig() Config {
|
||||||
return Config{
|
return Config{
|
||||||
Version: util.GetCommit(),
|
Version: goutil.GetCommit(),
|
||||||
Server: Server{
|
Server: Server{
|
||||||
Host: "127.0.0.1",
|
Host: "127.0.0.1",
|
||||||
Port: 3333,
|
Port: 3333,
|
||||||
},
|
},
|
||||||
Client: Client{
|
Options: Options{
|
||||||
Id: "",
|
DecodeIdToken: true,
|
||||||
Secret: "",
|
DecodeAccessToken: true,
|
||||||
RedirectUris: []string{""},
|
RunOnce: true,
|
||||||
|
OpenBrowser: false,
|
||||||
|
CachePath: "opaal.db",
|
||||||
|
FlowType: "authorization_code",
|
||||||
|
LocalOnly: false,
|
||||||
|
ForwardToken: false,
|
||||||
},
|
},
|
||||||
IdentityProvider: *oidc.NewIdentityProvider(),
|
Authentication: Authentication{},
|
||||||
State: util.RandomString(20),
|
Authorization: Authorization{},
|
||||||
ResponseType: "code",
|
|
||||||
Scope: []string{"openid", "profile", "email"},
|
|
||||||
ActionUrls: ActionUrls{
|
|
||||||
Identities: "",
|
|
||||||
AccessToken: "",
|
|
||||||
TrustedIssuers: "",
|
|
||||||
ServerConfig: "",
|
|
||||||
},
|
|
||||||
OpenBrowser: false,
|
|
||||||
DecodeIdToken: false,
|
|
||||||
DecodeAccessToken: false,
|
|
||||||
RunOnce: true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,3 +107,15 @@ func SaveDefaultConfig(path string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HasRequiredConfigParams(config *Config) bool {
|
||||||
|
// must have athe requirements to perform login
|
||||||
|
hasClients := len(config.Authentication.Clients) > 0
|
||||||
|
hasServer := config.Server.Host != "" && config.Server.Port != 0 && config.Server.Callback != ""
|
||||||
|
hasEndpoints := config.Authorization.RequestUrls.TrustedIssuers != "" &&
|
||||||
|
config.Authorization.RequestUrls.Login != "" &&
|
||||||
|
config.Authorization.RequestUrls.Clients != "" &&
|
||||||
|
config.Authorization.RequestUrls.Authorize != "" &&
|
||||||
|
config.Authorization.RequestUrls.Token != ""
|
||||||
|
return hasClients && hasServer && hasEndpoints
|
||||||
|
}
|
||||||
|
|
|
||||||
156
internal/db/sqlite.go
Normal file
156
internal/db/sqlite.go
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"davidallendj/opaal/internal/oidc"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateIdentityProvidersIfNotExists(path string) (*sqlx.DB, error) {
|
||||||
|
schema := `
|
||||||
|
CREATE TABLE IF NOT EXISTS identity_providers (
|
||||||
|
issuer TEXT NOT NULL,
|
||||||
|
authorization_endpoint TEXT,
|
||||||
|
token_endpoint TEXT,
|
||||||
|
revocation_endpoint TEXT,
|
||||||
|
introspection_endpoint TEXT,
|
||||||
|
userinfo_endpoint TEXT,
|
||||||
|
jwks_uri TEXT,
|
||||||
|
response_types_supported TEXT,
|
||||||
|
response_modes_supported TEXT,
|
||||||
|
grant_types_supported TEXT,
|
||||||
|
token_endpoint_auth_methods_supported TEXT,
|
||||||
|
subject_types_supported TEXT,
|
||||||
|
id_token_signing_alg_values_supported TEXT,
|
||||||
|
claim_types_supported TEXT,
|
||||||
|
claims_supported TEXT,
|
||||||
|
jwks TEXT,
|
||||||
|
|
||||||
|
PRIMARY KEY (issuer)
|
||||||
|
);
|
||||||
|
`
|
||||||
|
db, err := sqlx.Open("sqlite3", path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not open database: %v", err)
|
||||||
|
}
|
||||||
|
db.MustExec(schema)
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func InsertIdentityProviders(path string, providers *[]oidc.IdentityProvider) error {
|
||||||
|
if providers == nil {
|
||||||
|
return fmt.Errorf("states == nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// create database if it doesn't already exist
|
||||||
|
db, err := CreateIdentityProvidersIfNotExists(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert all probe states into db
|
||||||
|
tx := db.MustBegin()
|
||||||
|
for _, state := range *providers {
|
||||||
|
sql := `INSERT OR REPLACE INTO identity_providers
|
||||||
|
(
|
||||||
|
issuer,
|
||||||
|
authorization_endpoint,
|
||||||
|
token_endpoint,
|
||||||
|
revocation_endpoint,
|
||||||
|
introspection_endpoint,
|
||||||
|
userinfo_endpoint,
|
||||||
|
jwks_uri,
|
||||||
|
response_types_supported,
|
||||||
|
response_modes_supported,
|
||||||
|
grant_types_supported,
|
||||||
|
token_endpoint_auth_methods_supported,
|
||||||
|
subject_types_supported,
|
||||||
|
id_token_signing_alg_values_supported,
|
||||||
|
claim_types_supported,
|
||||||
|
claims_supported,
|
||||||
|
jwks
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
:issuer,
|
||||||
|
:authorization_endpoint,
|
||||||
|
:token_endpoint,
|
||||||
|
:revocation_endpoint,
|
||||||
|
:introspection_endpoint,
|
||||||
|
:userinfo_endpoint,
|
||||||
|
:jwks_uri,
|
||||||
|
:response_types_supported,
|
||||||
|
:response_modes_supported,
|
||||||
|
:grant_types_supported,
|
||||||
|
:token_endpoint_auth_methods_supported,
|
||||||
|
:subject_types_supported,
|
||||||
|
:id_token_signing_alg_values_supported,
|
||||||
|
:claim_types_supported,
|
||||||
|
:claims_supported,
|
||||||
|
:jwks
|
||||||
|
);`
|
||||||
|
_, err := tx.NamedExec(sql, &state)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("could not execute transaction: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not commit transaction: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetIdentityProvider(path string, issuer string) (*oidc.IdentityProvider, error) {
|
||||||
|
db, err := sqlx.Open("sqlite3", path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not open database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results := &oidc.IdentityProvider{}
|
||||||
|
err = db.Select(&results, "SELECT * FROM magellan_scanned_ports ORDER BY host ASC, port ASC LIMIT 1;")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not retrieve probes: %v", err)
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetIdentityProviders(path string) ([]oidc.IdentityProvider, error) {
|
||||||
|
db, err := sqlx.Open("sqlite3", path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not open database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []oidc.IdentityProvider{}
|
||||||
|
err = db.Select(&results, "SELECT * FROM magellan_scanned_ports ORDER BY host ASC, port ASC;")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not retrieve probes: %v", err)
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteIdentityProviders(path string, results *[]oidc.IdentityProvider) error {
|
||||||
|
if results == nil {
|
||||||
|
return fmt.Errorf("no probe results found")
|
||||||
|
}
|
||||||
|
db, err := sqlx.Open("sqlite3", path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not open database: %v", err)
|
||||||
|
}
|
||||||
|
tx := db.MustBegin()
|
||||||
|
for _, state := range *results {
|
||||||
|
sql := `DELETE FROM identity_providers WHERE host = :issuer;`
|
||||||
|
_, err := tx.NamedExec(sql, &state)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("could not execute transaction: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not commit transaction: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
32
internal/identities.go
Normal file
32
internal/identities.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package opaal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/davidallendj/go-utils/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (client *Client) CreateIdentity(url string, idToken string) error {
|
||||||
|
// kratos endpoint: /admin/identities
|
||||||
|
body := []byte(`{
|
||||||
|
"schema_id": "preset://email",
|
||||||
|
"traits": {
|
||||||
|
"email": "docs@example.org"
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
headers := httpx.Headers{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": fmt.Sprintf("Bearer %s", idToken),
|
||||||
|
}
|
||||||
|
_, _, err := httpx.MakeHttpRequest(url, http.MethodPost, body, headers)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read response body: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) FetchIdentities(remoteUrl string) ([]byte, error) {
|
||||||
|
_, b, err := httpx.MakeHttpRequest(remoteUrl, http.MethodGet, []byte{}, httpx.Headers{})
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
@ -1,260 +1,37 @@
|
||||||
package opaal
|
package opaal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"davidallendj/opaal/internal/db"
|
||||||
"davidallendj/opaal/internal/oidc"
|
"davidallendj/opaal/internal/oidc"
|
||||||
"davidallendj/opaal/internal/util"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"reflect"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Login(config *Config) error {
|
func Login(config *Config, client *Client, provider *oidc.IdentityProvider) error {
|
||||||
if config == nil {
|
if config == nil {
|
||||||
return fmt.Errorf("config is not valid")
|
return fmt.Errorf("config is not valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize client that will be used throughout login flow
|
// make cache if it's not where expect
|
||||||
server := NewServerWithConfig(config)
|
_, err := db.CreateIdentityProvidersIfNotExists(config.Options.CachePath)
|
||||||
client := NewClientWithConfig(config)
|
|
||||||
|
|
||||||
// initiate the login flow and get a flow ID and CSRF token
|
|
||||||
{
|
|
||||||
err := client.InitiateLoginFlow(config.ActionUrls.Login)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to initiate login flow: %v", err)
|
|
||||||
}
|
|
||||||
err = client.FetchCSRFToken(config.ActionUrls.LoginFlowId)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to fetch CSRF token: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// try and fetch server configuration if provided URL
|
|
||||||
idp := oidc.NewIdentityProvider()
|
|
||||||
if config.ActionUrls.ServerConfig != "" {
|
|
||||||
fmt.Printf("Fetching server configuration: %s\n", config.ActionUrls.ServerConfig)
|
|
||||||
err := idp.FetchServerConfig(config.ActionUrls.ServerConfig)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to fetch server config: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// otherwise, use what's provided in config file
|
|
||||||
idp.Issuer = config.IdentityProvider.Issuer
|
|
||||||
idp.Endpoints = config.IdentityProvider.Endpoints
|
|
||||||
idp.Supported = config.IdentityProvider.Supported
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if all appropriate parameters are set in config
|
|
||||||
if !HasRequiredParams(config) {
|
|
||||||
return fmt.Errorf("client ID must be set")
|
|
||||||
}
|
|
||||||
|
|
||||||
// build the authorization URL to redirect user for social sign-in
|
|
||||||
var authorizationUrl = client.BuildAuthorizationUrl(
|
|
||||||
idp.Endpoints.Authorization,
|
|
||||||
config.State,
|
|
||||||
config.ResponseType,
|
|
||||||
config.Scope,
|
|
||||||
)
|
|
||||||
|
|
||||||
// print the authorization URL for sharing
|
|
||||||
fmt.Printf("Login with identity provider:\n\n %s/login\n %s\n\n",
|
|
||||||
server.GetListenAddr(), authorizationUrl,
|
|
||||||
)
|
|
||||||
|
|
||||||
// automatically open browser to initiate login flow (only useful for testing)
|
|
||||||
if config.OpenBrowser {
|
|
||||||
util.OpenUrl(authorizationUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
// authorize oauth client and listen for callback from provider
|
|
||||||
fmt.Printf("Waiting for authorization code redirect @%s/oidc/callback...\n", server.GetListenAddr())
|
|
||||||
code, err := server.WaitForAuthorizationCode(authorizationUrl)
|
|
||||||
if errors.Is(err, http.ErrServerClosed) {
|
|
||||||
fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n")
|
|
||||||
} else if err != nil {
|
|
||||||
return fmt.Errorf("failed to start server: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if client == nil {
|
|
||||||
fmt.Printf("client did not initialize\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// start up another serve in background to listen for success or failures
|
|
||||||
d := make(chan []byte)
|
|
||||||
quit := make(chan bool)
|
|
||||||
var access_token []byte
|
|
||||||
go server.Serve(d)
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-d:
|
|
||||||
fmt.Printf("got access token")
|
|
||||||
quit <- true
|
|
||||||
case <-quit:
|
|
||||||
close(d)
|
|
||||||
close(quit)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// use code from response and exchange for bearer token (with ID token)
|
|
||||||
bearerToken, err := client.FetchTokenFromAuthenticationServer(
|
|
||||||
code,
|
|
||||||
idp.Endpoints.Token,
|
|
||||||
config.State,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch token from issuer: %v", err)
|
fmt.Printf("failed to create cache: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// unmarshal data to get id_token and access_token
|
if config.Options.FlowType == "authorization_code" {
|
||||||
var data map[string]any
|
// create a server if doing authorization code flow
|
||||||
err = json.Unmarshal([]byte(bearerToken), &data)
|
server := NewServerWithConfig(config)
|
||||||
if err != nil || data == nil {
|
err := AuthorizationCodeWithConfig(config, server, client, provider)
|
||||||
return fmt.Errorf("failed to unmarshal token: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract ID token from bearer as JSON string for easy consumption
|
|
||||||
idToken := data["id_token"].(string)
|
|
||||||
idJwtSegments, err := util.DecodeJwt(idToken)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("failed to parse ID token: %v\n", err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("id_token: %v\n", idToken)
|
|
||||||
if config.DecodeIdToken {
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("failed to decode JWT: %v\n", err)
|
|
||||||
} else {
|
|
||||||
for i, segment := range idJwtSegments {
|
|
||||||
// don't print last segment (signatures)
|
|
||||||
if i == len(idJwtSegments)-1 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fmt.Printf("%s\n", string(segment))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract the access token to get the scopes
|
|
||||||
accessToken := data["access_token"].(string)
|
|
||||||
accessJwtSegments, err := util.DecodeJwt(accessToken)
|
|
||||||
if err != nil || len(accessJwtSegments) <= 0 {
|
|
||||||
fmt.Printf("failed to parse access token: %v\n", err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("access_token: %v\n", accessToken)
|
|
||||||
if config.DecodeIdToken {
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("failed to decode JWT: %v\n", err)
|
|
||||||
} else {
|
|
||||||
for i, segment := range accessJwtSegments {
|
|
||||||
// don't print last segment (signatures)
|
|
||||||
if i == len(accessJwtSegments)-1 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fmt.Printf("%s\n", string(segment))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract the scope from access token claims
|
|
||||||
// var scope []string
|
|
||||||
// var accessJsonPayload map[string]any
|
|
||||||
// var accessJwtPayload []byte = accessJwtSegments[1]
|
|
||||||
// if accessJsonPayload != nil {
|
|
||||||
// err := json.Unmarshal(accessJwtPayload, &accessJsonPayload)
|
|
||||||
// if err != nil {
|
|
||||||
// return fmt.Errorf("failed to unmarshal JWT: %v", err)
|
|
||||||
// }
|
|
||||||
// scope = idJsonPayload["scope"].([]string)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// create a new identity with identity and session manager if url is provided
|
|
||||||
if config.ActionUrls.Identities != "" {
|
|
||||||
fmt.Printf("Attempting to create a new identity...\n")
|
|
||||||
_, err := client.CreateIdentity(config.ActionUrls.Identities, idToken)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create new identity: %v", err)
|
fmt.Printf("failed to complete authorization code flow: %v\n", err)
|
||||||
}
|
}
|
||||||
_, err = client.FetchIdentities(config.ActionUrls.Identities)
|
} else if config.Options.FlowType == "client_credentials" {
|
||||||
|
err := ClientCredentialsWithConfig(config, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch identities: %v", err)
|
fmt.Printf("failed to complete client credentials flow: %v", err)
|
||||||
}
|
|
||||||
fmt.Printf("Created new identity successfully.\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract the subject from ID token claims
|
|
||||||
var subject string
|
|
||||||
var audience []string
|
|
||||||
var idJsonPayload map[string]any
|
|
||||||
var idJwtPayload []byte = idJwtSegments[1]
|
|
||||||
if idJwtPayload != nil {
|
|
||||||
err := json.Unmarshal(idJwtPayload, &idJsonPayload)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to unmarshal JWT: %v", err)
|
|
||||||
}
|
|
||||||
subject = idJsonPayload["sub"].(string)
|
|
||||||
audType := reflect.ValueOf(idJsonPayload["aud"])
|
|
||||||
switch audType.Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
audience = append(audience, idJsonPayload["aud"].(string))
|
|
||||||
case reflect.Array:
|
|
||||||
audience = idJsonPayload["aud"].([]string)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("failed to extract subject from ID token claims")
|
return fmt.Errorf("invalid grant type (options: authorization_code, client_credentials)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch JWKS and add issuer to authentication server to submit ID token
|
|
||||||
fmt.Printf("Fetching JWKS from authentication server for verification...\n")
|
|
||||||
err = idp.FetchJwk(config.ActionUrls.JwksUri)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to fetch JWK: %v", err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Successfully retrieved JWK from authentication server.\n\n")
|
|
||||||
fmt.Printf("Attempting to add issuer to authorization server...\n")
|
|
||||||
res, err := client.AddTrustedIssuer(config.ActionUrls.TrustedIssuers, idp, subject, time.Duration(1000), config.Scope)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to add trusted issuer: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("%v\n", string(res))
|
|
||||||
}
|
|
||||||
|
|
||||||
// try and register a new client with authorization server
|
|
||||||
fmt.Printf("Registering new OAuth2 client with authorization server...\n")
|
|
||||||
res, err := client.RegisterOAuthClient("http://127.0.0.1:4445/clients", audience)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to register client: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("%v\n", string(res))
|
|
||||||
|
|
||||||
// extract the client info from response
|
|
||||||
var clientData map[string]any
|
|
||||||
err = json.Unmarshal(res, &clientData)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to unmarshal client data: %v", err)
|
|
||||||
} else {
|
|
||||||
client.Id = clientData["client_id"].(string)
|
|
||||||
client.Secret = clientData["client_secret"].(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// use ID token/user info to fetch access token from authentication server
|
|
||||||
if config.ActionUrls.AccessToken != "" {
|
|
||||||
fmt.Printf("Fetching access token from authorization server...\n")
|
|
||||||
res, err := client.FetchTokenFromAuthorizationServer(config.ActionUrls.AccessToken, idToken, config.Scope)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to fetch access token: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("%s\n", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
d <- access_token
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,33 +9,33 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/lestrrat-go/jwx/jwk"
|
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IdentityProvider struct {
|
type IdentityProvider struct {
|
||||||
Issuer string `json:"issuer" yaml:"issuer"`
|
Issuer string `db:"issuer" json:"issuer" yaml:"issuer"`
|
||||||
Endpoints Endpoints `json:"endpoints" yaml:"endpoints"`
|
Endpoints Endpoints `db:"endpoints" json:"endpoints" yaml:"endpoints"`
|
||||||
Supported Supported `json:"supported" yaml:"supported"`
|
Supported Supported `db:"supported" json:"supported" yaml:"supported"`
|
||||||
Key jwk.Key
|
Jwks jwk.Set
|
||||||
}
|
}
|
||||||
|
|
||||||
type Endpoints struct {
|
type Endpoints struct {
|
||||||
Authorization string `json:"authorization_endpoint" yaml:"authorization"`
|
Authorization string `db:"authorization_endpoint" json:"authorization_endpoint" yaml:"authorization"`
|
||||||
Token string `json:"token_endpoint" yaml:"token"`
|
Token string `db:"token_endpoint" json:"token_endpoint" yaml:"token"`
|
||||||
Revocation string `json:"revocation_endpoint" yaml:"revocation"`
|
Revocation string `db:"revocation_endpoint" json:"revocation_endpoint" yaml:"revocation"`
|
||||||
Introspection string `json:"introspection_endpoint" yaml:"introspection"`
|
Introspection string `db:"introspection_endpoint" json:"introspection_endpoint" yaml:"introspection"`
|
||||||
UserInfo string `json:"userinfo_endpoint" yaml:"userinfo"`
|
UserInfo string `db:"userinfo_endpoint" json:"userinfo_endpoint" yaml:"userinfo"`
|
||||||
Jwks string `json:"jwks_uri" yaml:"jwks_uri"`
|
JwksUri string `db:"jwks_uri" json:"jwks_uri" yaml:"jwks_uri"`
|
||||||
}
|
}
|
||||||
type Supported struct {
|
type Supported struct {
|
||||||
ResponseTypes []string `json:"response_types_supported"`
|
ResponseTypes []string `db:"response_types_supported" json:"response_types_supported"`
|
||||||
ResponseModes []string `json:"response_modes_supported"`
|
ResponseModes []string `db:"response_modes_supported" json:"response_modes_supported"`
|
||||||
GrantTypes []string `json:"grant_types_supported"`
|
GrantTypes []string `db:"grant_types_supported" json:"grant_types_supported"`
|
||||||
TokenEndpointAuthMethods []string `json:"token_endpoint_auth_methods_supported"`
|
TokenEndpointAuthMethods []string `db:"token_endpoint_auth_methods_supported" json:"token_endpoint_auth_methods_supported"`
|
||||||
SubjectTypes []string `json:"subject_types_supported"`
|
SubjectTypes []string `db:"subject_types_supported" json:"subject_types_supported"`
|
||||||
IdTokenSigningAlgValues []string `json:"id_token_signing_alg_values_supported"`
|
IdTokenSigningAlgValues []string `db:"id_token_signing_alg_values_supported" json:"id_token_signing_alg_values_supported"`
|
||||||
ClaimTypes []string `json:"claim_types_supported"`
|
ClaimTypes []string `db:"claim_types_supported" json:"claim_types_supported"`
|
||||||
Claims []string `json:"claims_supported"`
|
Claims []string `db:"claims_supported" json:"claims_supported"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIdentityProvider() *IdentityProvider {
|
func NewIdentityProvider() *IdentityProvider {
|
||||||
|
|
@ -46,7 +46,7 @@ func NewIdentityProvider() *IdentityProvider {
|
||||||
Revocation: p.Issuer + "/oauth/revocation",
|
Revocation: p.Issuer + "/oauth/revocation",
|
||||||
Introspection: p.Issuer + "/oauth/introspect",
|
Introspection: p.Issuer + "/oauth/introspect",
|
||||||
UserInfo: p.Issuer + "/oauth/userinfo",
|
UserInfo: p.Issuer + "/oauth/userinfo",
|
||||||
Jwks: p.Issuer + "/oauth/discovery/keys",
|
JwksUri: p.Issuer + "/oauth/discovery/keys",
|
||||||
}
|
}
|
||||||
p.Supported = Supported{
|
p.Supported = Supported{
|
||||||
ResponseTypes: []string{"code"},
|
ResponseTypes: []string{"code"},
|
||||||
|
|
@ -109,55 +109,43 @@ func (p *IdentityProvider) LoadServerConfig(path string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *IdentityProvider) FetchServerConfig(url string) error {
|
func FetchServerConfig(issuer string) (*IdentityProvider, error) {
|
||||||
// make a request to a server's openid-configuration
|
// make a request to a server's openid-configuration
|
||||||
req, err := http.NewRequest("GET", url, bytes.NewBuffer([]byte{}))
|
req, err := http.NewRequest(http.MethodGet, 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 nil, fmt.Errorf("failed to create a new request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{} // temp client to get info and not used in flow
|
client := &http.Client{} // temp client to get info and not used in flow
|
||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to do request: %v", err)
|
return nil, fmt.Errorf("failed to do request: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read response body: %v", err)
|
return nil, fmt.Errorf("failed to read response body: %v", err)
|
||||||
}
|
}
|
||||||
|
var p IdentityProvider
|
||||||
err = p.ParseServerConfig(body)
|
err = p.ParseServerConfig(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse server config: %v", err)
|
return nil, fmt.Errorf("failed to parse server config: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return &p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *IdentityProvider) FetchJwk(url string) error {
|
func (p *IdentityProvider) FetchJwks() error {
|
||||||
if url == "" {
|
if p.Endpoints.JwksUri == "" {
|
||||||
url = p.Endpoints.Jwks
|
return fmt.Errorf("JWKS endpoint not set")
|
||||||
}
|
}
|
||||||
// fetch JWKS from identity provider
|
// fetch JWKS from identity provider
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
set, err := jwk.Fetch(ctx, url)
|
var err error
|
||||||
|
p.Jwks, err = jwk.Fetch(ctx, p.Endpoints.JwksUri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%v", err)
|
return fmt.Errorf("failed to fetch JWKS: %v", err)
|
||||||
}
|
|
||||||
// get the first JWK from set
|
|
||||||
for it := set.Iterate(context.Background()); it.Next(context.Background()); {
|
|
||||||
pair := it.Pair()
|
|
||||||
p.Key = pair.Value.(jwk.Key)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("failed to load public key: %v", err)
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
func (p *IdentityProvider) GetRawJwk() (any, error) {
|
|
||||||
var rawkey any
|
|
||||||
if err := p.Key.Raw(&rawkey); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get raw key: %v", err)
|
|
||||||
}
|
|
||||||
return rawkey, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
package opaal
|
|
||||||
|
|
||||||
type ActionUrls struct {
|
|
||||||
Identities string `yaml:"identities"`
|
|
||||||
TrustedIssuers string `yaml:"trusted-issuers"`
|
|
||||||
AccessToken string `yaml:"access-token"`
|
|
||||||
ServerConfig string `yaml:"server-config"`
|
|
||||||
JwksUri string `yaml:"jwks_uri"`
|
|
||||||
Login string `yaml:"login"`
|
|
||||||
LoginFlowId string `yaml:"login-flow-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func HasRequiredParams(config *Config) bool {
|
|
||||||
return config.Client.Id != "" && config.Client.Secret != ""
|
|
||||||
}
|
|
||||||
|
|
@ -8,17 +8,20 @@ import (
|
||||||
|
|
||||||
"github.com/go-chi/chi/middleware"
|
"github.com/go-chi/chi/middleware"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/nikolalohinski/gonja/v2"
|
||||||
|
"github.com/nikolalohinski/gonja/v2/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
*http.Server
|
*http.Server
|
||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
|
Callback string `yaml:"callback"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServerWithConfig(config *Config) *Server {
|
func NewServerWithConfig(conf *Config) *Server {
|
||||||
host := config.Server.Host
|
host := conf.Server.Host
|
||||||
port := config.Server.Port
|
port := conf.Server.Port
|
||||||
server := &Server{
|
server := &Server{
|
||||||
Server: &http.Server{
|
Server: &http.Server{
|
||||||
Addr: fmt.Sprintf("%s:%d", host, port),
|
Addr: fmt.Sprintf("%s:%d", host, port),
|
||||||
|
|
@ -37,7 +40,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) WaitForAuthorizationCode(loginUrl string) (string, error) {
|
func (s *Server) WaitForAuthorizationCode(loginUrl string, callback string) (string, error) {
|
||||||
|
// check if callback is set
|
||||||
|
if callback == "" {
|
||||||
|
callback = "/oidc/callback"
|
||||||
|
}
|
||||||
|
|
||||||
var code string
|
var code string
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Use(middleware.RedirectSlashes)
|
r.Use(middleware.RedirectSlashes)
|
||||||
|
|
@ -46,14 +54,20 @@ func (s *Server) WaitForAuthorizationCode(loginUrl string) (string, error) {
|
||||||
})
|
})
|
||||||
r.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
|
r.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
|
||||||
// show login page with notice to redirect
|
// show login page with notice to redirect
|
||||||
loginPage, err := os.ReadFile("pages/index.html")
|
template, err := gonja.FromFile("pages/index.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("failed to load login page: %v\n", err)
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := exec.NewContext(map[string]interface{}{
|
||||||
|
"loginUrl": loginUrl,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err = template.Execute(w, data); err != nil { // Prints: Hello Bob!
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
loginPage = []byte(strings.ReplaceAll(string(loginPage), "{{loginUrl}}", loginUrl))
|
|
||||||
w.Write(loginPage)
|
|
||||||
})
|
})
|
||||||
r.HandleFunc("/oidc/callback", func(w http.ResponseWriter, r *http.Request) {
|
r.HandleFunc(callback, func(w http.ResponseWriter, r *http.Request) {
|
||||||
// get the code from the OIDC provider
|
// get the code from the OIDC provider
|
||||||
if r != nil {
|
if r != nil {
|
||||||
code = r.URL.Query().Get("code")
|
code = r.URL.Query().Get("code")
|
||||||
|
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"math/rand"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RandomString(n int) string {
|
|
||||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
||||||
const (
|
|
||||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
|
||||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
|
||||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
|
||||||
)
|
|
||||||
b := make([]byte, n)
|
|
||||||
// A rand.Int63() generates 63 random bits, enough for letterIdxMax letters!
|
|
||||||
for i, cache, remain := n-1, rand.Int63(), letterIdxMax; i >= 0; {
|
|
||||||
if remain == 0 {
|
|
||||||
cache, remain = rand.Int63(), letterIdxMax
|
|
||||||
}
|
|
||||||
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
|
||||||
b[i] = letterBytes[idx]
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
cache >>= letterIdxBits
|
|
||||||
remain--
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func URLEscape(s string) string {
|
|
||||||
return url.QueryEscape(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func EncodeBase64(s string) string {
|
|
||||||
return base64.StdEncoding.EncodeToString([]byte(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecodeJwt(encoded string) ([][]byte, error) {
|
|
||||||
// split the string into 3 segments and decode
|
|
||||||
segments := strings.Split(encoded, ".")
|
|
||||||
decoded := [][]byte{}
|
|
||||||
for _, segment := range segments {
|
|
||||||
bytes, _ := jwt.DecodeSegment(segment)
|
|
||||||
decoded = append(decoded, bytes)
|
|
||||||
}
|
|
||||||
return decoded, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func PathExists(path string) (bool, error) {
|
|
||||||
_, err := os.Stat(path)
|
|
||||||
if err == nil {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/39320371/how-start-web-server-to-open-page-in-browser-in-golang
|
|
||||||
// open opens the specified URL in the default browser of the user.
|
|
||||||
func OpenUrl(url string) error {
|
|
||||||
var cmd string
|
|
||||||
var args []string
|
|
||||||
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "windows":
|
|
||||||
cmd = "cmd"
|
|
||||||
args = []string{"/c", "start"}
|
|
||||||
case "darwin":
|
|
||||||
cmd = "open"
|
|
||||||
default: // "linux", "freebsd", "openbsd", "netbsd"
|
|
||||||
cmd = "xdg-open"
|
|
||||||
}
|
|
||||||
args = append(args, url)
|
|
||||||
return exec.Command(cmd, args...).Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetCommit() string {
|
|
||||||
bytes, err := exec.Command("git", "rev --parse HEAD").Output()
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Tokenize(s string) map[string]any {
|
|
||||||
tokens := make(map[string]any)
|
|
||||||
|
|
||||||
// find token enclosed in curly brackets
|
|
||||||
|
|
||||||
return tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
func QuoteArrayStrings(arr []string) []string {
|
|
||||||
for i, v := range arr {
|
|
||||||
arr[i] = "\"" + v + "\""
|
|
||||||
}
|
|
||||||
return arr
|
|
||||||
}
|
|
||||||
|
|
@ -2,5 +2,5 @@
|
||||||
<html>
|
<html>
|
||||||
Welcome to Opaal's default login in page! Click the link below to log in with your identity provider: </br></br>
|
Welcome to Opaal's default login in page! Click the link below to log in with your identity provider: </br></br>
|
||||||
|
|
||||||
<a href="{{loginUrl}}">Login</a>
|
<a href="{{ loginUrl }}">Login</a>
|
||||||
</html>
|
</html>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue