mirror of
https://github.com/davidallendj/opaal.git
synced 2025-12-20 03:27:02 -07:00
Merge pull request #5 from davidallendj/refactoring
Major refactor and reoganization
This commit is contained in:
commit
7d24ee0e10
24 changed files with 1549 additions and 806 deletions
19
README.md
19
README.md
|
|
@ -23,9 +23,17 @@ To start the authentication flow, run the following commands:
|
|||
./opaal login --flow authorization_code --config config.yaml
|
||||
```
|
||||
|
||||
These commands will create a default config, then start the login process. Maybe sure to change the config file to match your setup!
|
||||
These commands will create a default config, then start the login process. Maybe sure to change the config file to match your setup! The tool has been tested and confirmed to work with the following identity providers so far:
|
||||
|
||||
- [Gitlab](https://about.gitlab.com/)
|
||||
- [Forgejo](https://forgejo.org/) (fork of Gitea)
|
||||
|
||||
### Authorization Code Flow
|
||||
|
||||
`opaal` has the ability to completely execute the authorization code and return an access token from an authorization server using social sign-in. The process works as follows:
|
||||
|
||||
1. Click the authorization link or navigate to the hosted endpoint in your browser (127.0.0.1:3333 by default)
|
||||
- Alternatively, you can use a link produced
|
||||
2. Login using identity provider credentials
|
||||
3. Authorize application registered with IdP
|
||||
4. IdP redirects to specified redirect URI
|
||||
|
|
@ -37,6 +45,11 @@ These commands will create a default config, then start the login process. Maybe
|
|||
|
||||
*After receiving the ID token, the rest of the flow requires the appropriate URLs to be set to continue.
|
||||
|
||||
### Client Credentials Flow
|
||||
|
||||
`opaal` also has
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
Here is an example configuration file:
|
||||
|
|
@ -105,4 +118,6 @@ options:
|
|||
- Add details about configuration parameters
|
||||
- Implement client credentials flow to easily fetch tokens
|
||||
- Fix how OAuth clients are managed with the authorization server
|
||||
- Fix how the trusted issuer is added to the authorization server
|
||||
- Fix how the trusted issuer is added to the authorization server
|
||||
- Allow signing JWTs by supplying key pair
|
||||
- Separate `jwt_bearer` grant type from the authorization code flow
|
||||
82
cmd/login.go
82
cmd/login.go
|
|
@ -2,16 +2,20 @@ package cmd
|
|||
|
||||
import (
|
||||
opaal "davidallendj/opaal/internal"
|
||||
"davidallendj/opaal/internal/db"
|
||||
cache "davidallendj/opaal/internal/cache/sqlite"
|
||||
"davidallendj/opaal/internal/oauth"
|
||||
"davidallendj/opaal/internal/oidc"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
client opaal.Client
|
||||
client oauth.Client
|
||||
target string = ""
|
||||
targetIndex int = -1
|
||||
)
|
||||
|
||||
var loginCmd = &cobra.Command{
|
||||
|
|
@ -21,24 +25,57 @@ var loginCmd = &cobra.Command{
|
|||
for {
|
||||
// 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
|
||||
if target != "" {
|
||||
// only try to use client with name give
|
||||
index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool {
|
||||
return target == c.Name
|
||||
})
|
||||
if index < 0 {
|
||||
fmt.Printf("could not find the target client listed by name")
|
||||
os.Exit(1)
|
||||
}
|
||||
client := config.Authentication.Clients[index]
|
||||
_, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer)
|
||||
if err != nil {
|
||||
|
||||
}
|
||||
|
||||
} else if targetIndex >= 0 {
|
||||
// only try to use client by index
|
||||
targetCount := len(config.Authentication.Clients) - 1
|
||||
if targetIndex > targetCount {
|
||||
fmt.Printf("target index out of range (found %d)", targetCount)
|
||||
}
|
||||
client := config.Authentication.Clients[targetIndex]
|
||||
_, err := cache.GetIdentityProvider(config.Options.CachePath, client.Issuer)
|
||||
if err != nil {
|
||||
|
||||
}
|
||||
} else {
|
||||
for _, c := range config.Authentication.Clients {
|
||||
// try to get identity provider info locally first
|
||||
_, err := cache.GetIdentityProvider(config.Options.CachePath, c.Issuer)
|
||||
if err != nil && !config.Options.CacheOnly {
|
||||
fmt.Printf("fetching config from issuer: %v\n", c.Issuer)
|
||||
// try to get info remotely by fetching
|
||||
provider, err = oidc.FetchServerConfig(c.Issuer)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to fetch server config: %v\n", err)
|
||||
continue
|
||||
}
|
||||
client = c
|
||||
// fetch the provider's JWKS
|
||||
err := provider.FetchJwks()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to fetch JWKS: %v\n", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
client = c
|
||||
// fetch the provider's JWKS
|
||||
err := provider.FetchJwks()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to fetch JWKS: %v\n", err)
|
||||
// only test the first if --run-all flag is not set
|
||||
if !config.Authentication.TestAllClients {
|
||||
fmt.Printf("stopping after first test...\n\n\n")
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,10 +103,13 @@ func init() {
|
|||
loginCmd.Flags().StringVar(&config.Server.Host, "server.host", config.Server.Host, "set the listening host")
|
||||
loginCmd.Flags().IntVar(&config.Server.Port, "server.port", config.Server.Port, "set the listening port")
|
||||
loginCmd.Flags().BoolVar(&config.Options.OpenBrowser, "open-browser", config.Options.OpenBrowser, "automatically open link in browser")
|
||||
loginCmd.Flags().BoolVar(&config.Options.DecodeIdToken, "decode-id-token", config.Options.DecodeIdToken, "decode and print ID token from identity provider")
|
||||
loginCmd.Flags().BoolVar(&config.Options.DecodeAccessToken, "decore-access-token", config.Options.DecodeAccessToken, "decode and print access token from authorization server")
|
||||
loginCmd.Flags().BoolVar(&config.Options.RunOnce, "once", config.Options.RunOnce, "set whether to run login once and exit")
|
||||
loginCmd.Flags().StringVar(&config.Options.FlowType, "flow", config.Options.FlowType, "set the grant-type/authorization flow")
|
||||
loginCmd.Flags().BoolVar(&config.Options.LocalOnly, "local", config.Options.LocalOnly, "only fetch identity provider configs stored locally")
|
||||
loginCmd.Flags().BoolVar(&config.Options.CacheOnly, "local", config.Options.CacheOnly, "only fetch identity provider configs stored locally")
|
||||
loginCmd.Flags().BoolVar(&config.Authentication.TestAllClients, "test-all", config.Authentication.TestAllClients, "test all clients in config for a valid provider")
|
||||
loginCmd.Flags().StringVar(&target, "target", "", "set target client to use from config by name")
|
||||
loginCmd.Flags().IntVar(&targetIndex, "index", -1, "set target client to use from config by index")
|
||||
loginCmd.MarkFlagsMutuallyExclusive("target", "index")
|
||||
|
||||
rootCmd.AddCommand(loginCmd)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ func Execute() {
|
|||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
rootCmd.PersistentFlags().StringVar(&confPath, "config", "", "set the config path")
|
||||
rootCmd.PersistentFlags().StringVarP(&confPath, "config", "c", "", "set the config path")
|
||||
rootCmd.PersistentFlags().StringVar(&config.Options.CachePath, "cache", "", "set the cache path")
|
||||
}
|
||||
|
||||
|
|
|
|||
23
go.mod
23
go.mod
|
|
@ -3,41 +3,44 @@ module davidallendj/opaal
|
|||
go 1.22.0
|
||||
|
||||
require (
|
||||
github.com/davidallendj/go-utils v0.0.0-20240302194916-fe292bcf24a4
|
||||
github.com/go-chi/chi v1.5.5
|
||||
github.com/davidallendj/go-utils v0.0.0-20240310194826-5a1300f3bcbf
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
github.com/google/uuid v1.6.0
|
||||
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/lestrrat-go/jwx/v2 v2.0.21
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/nikolalohinski/gonja/v2 v2.2.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
golang.org/x/net v0.21.0
|
||||
golang.org/x/net v0.22.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/apimachinery v0.29.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-logr/logr v1.3.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.4 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.5 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // 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/onsi/ginkgo/v2 v2.13.0 // indirect
|
||||
github.com/onsi/gomega v1.29.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // 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
|
||||
golang.org/x/crypto v0.19.0 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
)
|
||||
|
|
|
|||
44
go.sum
44
go.sum
|
|
@ -1,18 +1,21 @@
|
|||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/davidallendj/go-utils v0.0.0-20240310034007-5fa47be83de0 h1:nWyYmigcFeC2iCDxylGv3FFLSfh1so00iUYVzwZUJP4=
|
||||
github.com/davidallendj/go-utils v0.0.0-20240310034007-5fa47be83de0/go.mod h1:kiv3jEnBbeueMNNJclaMMJULL/tjqJ6wc136d+uxqSs=
|
||||
github.com/davidallendj/go-utils v0.0.0-20240310194826-5a1300f3bcbf h1:gY89rDLnc+70S0JcyHoPGU+XFpMwY1iVYNQzdC/qAHc=
|
||||
github.com/davidallendj/go-utils v0.0.0-20240310194826-5a1300f3bcbf/go.mod h1:kiv3jEnBbeueMNNJclaMMJULL/tjqJ6wc136d+uxqSs=
|
||||
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/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/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/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||
|
|
@ -32,6 +35,7 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJY
|
|||
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/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
|
|
@ -46,18 +50,19 @@ github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N
|
|||
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/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/httprc v1.0.5 h1:bsTfiH8xaKOJPrg1R+E3iE/AWZr/x0Phj9PBTG/OLUk=
|
||||
github.com/lestrrat-go/httprc v1.0.5/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||
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/jwx/v2 v2.0.20 h1:sAgXuWS/t8ykxS9Bi2Qtn5Qhpakw1wrcjxChudjolCc=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.20/go.mod h1:UlCSmKqw+agm5BsOBfEAbTvKsEApaGNqHAEUTv5PJC4=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55FHrR0=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM=
|
||||
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/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/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
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=
|
||||
|
|
@ -69,10 +74,12 @@ 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/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
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=
|
||||
|
|
@ -89,17 +96,18 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||
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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
|
||||
|
|
@ -112,5 +120,3 @@ 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
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=
|
||||
|
|
|
|||
|
|
@ -1,424 +0,0 @@
|
|||
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
|
||||
}
|
||||
154
internal/cache/sqlite/clients.go
vendored
Normal file
154
internal/cache/sqlite/clients.go
vendored
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"davidallendj/opaal/internal/oauth"
|
||||
"fmt"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
func CreateOAuthClientsIfNotExists(path string) (*sqlx.DB, error) {
|
||||
schema := `
|
||||
CREATE TABLE IF NOT EXISTS oauth_clients (
|
||||
id TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
issuer TEXT,
|
||||
registration_access_token TEXT,
|
||||
redirect_uris TEXT,
|
||||
scope TEXT
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
`
|
||||
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 InsertOAuthClients(path string, clients *[]oauth.Client) error {
|
||||
if clients == nil {
|
||||
return fmt.Errorf("states == nil")
|
||||
}
|
||||
|
||||
// create database if it doesn't already exist
|
||||
db, err := CreateOAuthClientsIfNotExists(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// insert all probe states into db
|
||||
tx := db.MustBegin()
|
||||
for _, state := range *clients {
|
||||
sql := `INSERT OR REPLACE INTO oauth_clients
|
||||
(
|
||||
id,
|
||||
secret,
|
||||
name,
|
||||
description,
|
||||
issuer,
|
||||
registration_access_token,
|
||||
redirect_uris,
|
||||
scope
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
:id,
|
||||
:secret,
|
||||
:name,
|
||||
:description,
|
||||
:issuer,
|
||||
:registration_access_token,
|
||||
:redirect_uris,
|
||||
:scope
|
||||
);`
|
||||
_, 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 GetOAuthClient(path string, id string) (*oauth.Client, error) {
|
||||
db, err := sqlx.Open("sqlite3", path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open database: %v", err)
|
||||
}
|
||||
|
||||
results := &oauth.Client{}
|
||||
err = db.Select(&results, "SELECT * FROM oauth_clients 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 GetOAuthClients(path string) ([]oauth.Client, error) {
|
||||
db, err := sqlx.Open("sqlite3", path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open database: %v", err)
|
||||
}
|
||||
|
||||
results := []oauth.Client{}
|
||||
err = db.Select(&results, "SELECT * FROM oauth_clients;")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not retrieve probes: %v", err)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func UpdateOAuthClient(path string, clients *[]oauth.Client) error {
|
||||
if clients == nil {
|
||||
return fmt.Errorf("clients is nil")
|
||||
}
|
||||
db, err := sqlx.Open("sqlite3", path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open database: %v", err)
|
||||
}
|
||||
tx := db.MustBegin()
|
||||
for _, state := range *clients {
|
||||
sql := `UPDATE FROM identity_providers WHERE client_id = :client_id;`
|
||||
_, 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 DeleteOAuthClients(path string, clientIds []string) error {
|
||||
if clientIds == 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 clientIds {
|
||||
sql := `DELETE FROM identity_providers WHERE client_id = :client_id;`
|
||||
_, 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
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package db
|
||||
package cache
|
||||
|
||||
import (
|
||||
"davidallendj/opaal/internal/oidc"
|
||||
107
internal/cache/sqlite/trusted.go
vendored
Normal file
107
internal/cache/sqlite/trusted.go
vendored
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"davidallendj/opaal/internal/oauth"
|
||||
"fmt"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
func CreateTrustedIfNotExists(path string) (*sqlx.DB, error) {
|
||||
schema := `
|
||||
CREATE TABLE IF NOT EXISTS trusted_issuers (
|
||||
id TEXT NOT NULL,
|
||||
allow_any_subject NUMBER,
|
||||
expires_at NUMBER,
|
||||
issuer TEXT,
|
||||
public_key NUMBER,
|
||||
scope TEXT,
|
||||
subject TEXT,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
`
|
||||
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 InsertTrustedIssuer(path string, issuer *oauth.TrustedIssuer) error {
|
||||
// create database if it doesn't already exist
|
||||
db, err := CreateOAuthClientsIfNotExists(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// insert all probe states into db
|
||||
tx := db.MustBegin()
|
||||
sql := `INSERT OR REPLACE INTO trusted_issuers
|
||||
(
|
||||
id,
|
||||
allow_any_subject
|
||||
expires_at,
|
||||
issuer,
|
||||
public_key,
|
||||
scope,
|
||||
subject
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
:id,
|
||||
:allow_any_subject,
|
||||
:expires_at,
|
||||
:issuer,
|
||||
:public_key,
|
||||
:scope,
|
||||
:subject
|
||||
);`
|
||||
_, err = tx.NamedExec(sql, &issuer)
|
||||
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 GetTrustedIssuer(path string, issuer string) (*oauth.TrustedIssuer, error) {
|
||||
db, err := sqlx.Open("sqlite3", path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open database: %v", err)
|
||||
}
|
||||
|
||||
results := &oauth.TrustedIssuer{}
|
||||
err = db.Select(&results, "SELECT * FROM trusted_issuers 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 DeleteTrustedIssuer(path string, ids []string) error {
|
||||
if ids == 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 ids {
|
||||
sql := `DELETE FROM identity_providers WHERE id = :id;`
|
||||
_, 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
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
package opaal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"slices"
|
||||
|
||||
"github.com/davidallendj/go-utils/mathx"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
http.Client
|
||||
Id string `yaml:"id"`
|
||||
Secret string `yaml:"secret"`
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description"`
|
||||
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 {
|
||||
// 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{
|
||||
Id: clients[0].Id,
|
||||
Secret: clients[0].Secret,
|
||||
Name: clients[0].Name,
|
||||
Issuer: clients[0].Issuer,
|
||||
Scope: clients[0].Scope,
|
||||
RedirectUris: clients[0].RedirectUris,
|
||||
}
|
||||
}
|
||||
|
||||
func NewClientWithConfigByIndex(config *Config, index int) *Client {
|
||||
size := len(config.Authentication.Clients)
|
||||
index = mathx.Clamp(index, 0, size)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewClientWithConfigByName(config *Config, name string) *Client {
|
||||
index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool {
|
||||
return c.Name == name
|
||||
})
|
||||
if index >= 0 {
|
||||
return &config.Authentication.Clients[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewClientWithConfigByProvider(config *Config, issuer string) *Client {
|
||||
index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool {
|
||||
return c.Issuer == issuer
|
||||
})
|
||||
|
||||
if index >= 0 {
|
||||
return &config.Authentication.Clients[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewClientWithConfigById(config *Config, id string) *Client {
|
||||
index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool {
|
||||
return c.Id == id
|
||||
})
|
||||
if index >= 0 {
|
||||
return &config.Authentication.Clients[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) ClearCookies() {
|
||||
jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
|
||||
client.Jar = jar
|
||||
}
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
package opaal
|
||||
|
||||
import (
|
||||
"davidallendj/opaal/internal/oauth"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"davidallendj/opaal/internal/server"
|
||||
|
||||
goutil "github.com/davidallendj/go-utils/util"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
|
@ -15,17 +18,16 @@ 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"`
|
||||
RunOnce bool `yaml:"run-once"`
|
||||
OpenBrowser bool `yaml:"open-browser"`
|
||||
FlowType string `yaml:"flow"`
|
||||
CachePath string `yaml:"cache"`
|
||||
CacheOnly bool `yaml:"cache-only"`
|
||||
TokenForwarding bool `yaml:"token-forwarding"`
|
||||
Verbose bool `yaml:"verbose"`
|
||||
}
|
||||
|
||||
type RequestUrls struct {
|
||||
type Endpoints struct {
|
||||
Identities string `yaml:"identities"`
|
||||
TrustedIssuers string `yaml:"trusted-issuers"`
|
||||
Login string `yaml:"login"`
|
||||
|
|
@ -36,17 +38,20 @@ type RequestUrls struct {
|
|||
}
|
||||
|
||||
type Authentication struct {
|
||||
Clients []Client `yaml:"clients"`
|
||||
Flows Flows `yaml:"flows"`
|
||||
Clients []oauth.Client `yaml:"clients"`
|
||||
Flows Flows `yaml:"flows"`
|
||||
TestAllClients bool `yaml:"test-all"`
|
||||
State string `yaml:"state"`
|
||||
}
|
||||
|
||||
type Authorization struct {
|
||||
RequestUrls RequestUrls `yaml:"urls"`
|
||||
Endpoints Endpoints `yaml:"endpoints"`
|
||||
KeyPath string `yaml:"key-path"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Version string `yaml:"version"`
|
||||
Server Server `yaml:"server"`
|
||||
Server server.Server `yaml:"server"`
|
||||
Providers Providers `yaml:"providers"`
|
||||
Options Options `yaml:"options"`
|
||||
Authentication Authentication `yaml:"authentication"`
|
||||
|
|
@ -56,22 +61,25 @@ type Config struct {
|
|||
func NewConfig() Config {
|
||||
return Config{
|
||||
Version: goutil.GetCommit(),
|
||||
Server: Server{
|
||||
Server: server.Server{
|
||||
Host: "127.0.0.1",
|
||||
Port: 3333,
|
||||
},
|
||||
Options: Options{
|
||||
DecodeIdToken: true,
|
||||
DecodeAccessToken: true,
|
||||
RunOnce: true,
|
||||
OpenBrowser: false,
|
||||
CachePath: "opaal.db",
|
||||
FlowType: "authorization_code",
|
||||
LocalOnly: false,
|
||||
ForwardToken: false,
|
||||
RunOnce: true,
|
||||
OpenBrowser: false,
|
||||
CachePath: "opaal.db",
|
||||
FlowType: "authorization_code",
|
||||
CacheOnly: false,
|
||||
TokenForwarding: false,
|
||||
Verbose: false,
|
||||
},
|
||||
Authentication: Authentication{
|
||||
TestAllClients: false,
|
||||
},
|
||||
Authorization: Authorization{
|
||||
KeyPath: "./keys",
|
||||
},
|
||||
Authentication: Authentication{},
|
||||
Authorization: Authorization{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,10 +120,10 @@ 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 != ""
|
||||
hasEndpoints := config.Authorization.Endpoints.TrustedIssuers != "" &&
|
||||
config.Authorization.Endpoints.Login != "" &&
|
||||
config.Authorization.Endpoints.Clients != "" &&
|
||||
config.Authorization.Endpoints.Authorize != "" &&
|
||||
config.Authorization.Endpoints.Token != ""
|
||||
return hasClients && hasServer && hasEndpoints
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package opaal
|
||||
package flows
|
||||
|
||||
import (
|
||||
"davidallendj/opaal/internal/oauth"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
|
|
@ -15,9 +16,9 @@ type ClientCredentialsFlowEndpoints struct {
|
|||
Token string
|
||||
}
|
||||
|
||||
func ClientCredentials(eps ClientCredentialsFlowEndpoints, client *Client) error {
|
||||
func NewClientCredentialsFlow(eps ClientCredentialsFlowEndpoints, client *oauth.Client) error {
|
||||
// register a new OAuth 2 client with authorization srever
|
||||
_, err := client.CreateOAuthClient(eps.Create, nil)
|
||||
_, err := client.CreateOAuthClient(eps.Create)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register OAuth client: %v", err)
|
||||
}
|
||||
|
|
@ -37,12 +38,3 @@ func ClientCredentials(eps ClientCredentialsFlowEndpoints, client *Client) error
|
|||
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)
|
||||
}
|
||||
319
internal/flows/jwt_bearer.go
Normal file
319
internal/flows/jwt_bearer.go
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
package flows
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"davidallendj/opaal/internal/oauth"
|
||||
"davidallendj/opaal/internal/oidc"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/davidallendj/go-utils/cryptox"
|
||||
"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"
|
||||
)
|
||||
|
||||
type JwtBearerFlowParams struct {
|
||||
AccessToken string
|
||||
IdToken string
|
||||
IdentityProvider *oidc.IdentityProvider
|
||||
TrustedIssuer *oauth.TrustedIssuer
|
||||
Client *oauth.Client
|
||||
Verbose bool
|
||||
KeyPath string
|
||||
}
|
||||
|
||||
type JwtBearerEndpoints struct {
|
||||
TrustedIssuers string
|
||||
Token string
|
||||
Clients string
|
||||
Register string
|
||||
}
|
||||
|
||||
func NewJwtBearerFlow(eps JwtBearerEndpoints, params JwtBearerFlowParams) (string, error) {
|
||||
// 1. verify that the JWT from the issuer is valid using all keys
|
||||
var (
|
||||
idp = params.IdentityProvider
|
||||
accessToken = params.AccessToken
|
||||
idToken = params.IdToken
|
||||
client = params.Client
|
||||
trustedIssuer = params.TrustedIssuer
|
||||
verbose = params.Verbose
|
||||
)
|
||||
if accessToken != "" {
|
||||
_, err := jws.Verify([]byte(accessToken), jws.WithKeySet(idp.KeySet), jws.WithValidateKey(true))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to verify access token: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if idToken != "" {
|
||||
_, err := jws.Verify([]byte(idToken), jws.WithKeySet(idp.KeySet), jws.WithValidateKey(true))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to verify ID token: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check if we are already registered as a trusted issuer with authorization server...
|
||||
|
||||
// 3.a if not, create a new JWKS (or just JWK) to be verified
|
||||
var (
|
||||
keyPath string = params.KeyPath
|
||||
privateJwk jwk.Key
|
||||
publicJwk jwk.Key
|
||||
)
|
||||
rawPrivateKey, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
if verbose {
|
||||
fmt.Printf("failed to read private key...generating a new one.\n")
|
||||
}
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate new RSA key: %v", err)
|
||||
}
|
||||
privateJwk, publicJwk, err = cryptox.GenerateJwkKeyPairFromPrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate JWK pair from private key: %v", err)
|
||||
}
|
||||
// save new key to key path to reuse later
|
||||
b := cryptox.MarshalRSAPrivateKey(privateKey)
|
||||
err = os.WriteFile(keyPath, b, os.ModePerm)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to write private key to file: %v", err)
|
||||
}
|
||||
} else {
|
||||
privateKey, err := cryptox.GenerateRSAPrivateKey(rawPrivateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate RSA key from string: %v", err)
|
||||
}
|
||||
privateJwk, publicJwk, err = cryptox.GenerateJwkKeyPairFromPrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate JWK pair from private key: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
publicJwk.Set("kid", uuid.New().String())
|
||||
publicJwk.Set("use", "sig")
|
||||
|
||||
if err := publicJwk.Validate(); err != nil {
|
||||
return "", fmt.Errorf("failed to validate public JWK: %v", err)
|
||||
}
|
||||
trustedIssuer.PublicKey = publicJwk
|
||||
|
||||
// 3.b ...and then, add opaal's server host as a trusted issuer with JWK
|
||||
if verbose {
|
||||
fmt.Printf("Attempting to add issuer to authorization server...\n")
|
||||
}
|
||||
res, err := client.AddTrustedIssuer(
|
||||
eps.TrustedIssuers,
|
||||
trustedIssuer,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to add trusted issuer: %v", err)
|
||||
}
|
||||
fmt.Printf("trusted issuer: %v\n", string(res))
|
||||
// TODO: add trusted issuer to cache if successful
|
||||
|
||||
// 4. create a new JWT based on the claims from the identity provider and sign
|
||||
parsedIdToken, err := jwt.ParseString(idToken, jwt.WithKeySet(idp.KeySet))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse ID token: %v", err)
|
||||
}
|
||||
payload := parsedIdToken.PrivateClaims()
|
||||
payload["iss"] = trustedIssuer.Issuer
|
||||
payload["aud"] = []string{eps.Token}
|
||||
payload["iat"] = time.Now().Unix()
|
||||
payload["nbf"] = time.Now().Unix()
|
||||
payload["exp"] = time.Now().Add(time.Second * 3600).Unix()
|
||||
payload["sub"] = "opaal"
|
||||
payloadJson, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal payload: %v", err)
|
||||
}
|
||||
fmt.Printf("payload: %v\n", string(payloadJson))
|
||||
newJwt, err := jws.Sign(payloadJson, jws.WithJSON(), jws.WithKey(jwa.RS256, privateJwk))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign token: %v", err)
|
||||
}
|
||||
|
||||
// 5. dynamically register new OAuth client and authorize it to make jwt_bearer request
|
||||
fmt.Printf("Registering new OAuth2 client with authorization server...\n")
|
||||
res, err = client.RegisterOAuthClient(eps.Register)
|
||||
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 try to create again
|
||||
fmt.Printf("Attempting to delete client...\n")
|
||||
err := client.DeleteOAuthClient(eps.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(eps.Clients)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to register client: %v", err)
|
||||
}
|
||||
fmt.Printf("%v\n", string(res))
|
||||
}
|
||||
}
|
||||
// TODO: add OAuth client to cache if successfully
|
||||
|
||||
// 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 eps.Token != "" {
|
||||
fmt.Printf("Fetching access token from authorization server...\n")
|
||||
res, err := client.PerformTokenGrant(eps.Token, string(newJwt))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to fetch access token: %v", err)
|
||||
}
|
||||
// extract token from response if there are no errors
|
||||
var data map[string]any
|
||||
err = json.Unmarshal(res, &data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
if data["error"] != nil {
|
||||
return "", fmt.Errorf("the authorization server returned an error (%v): %v", data["error"], data["error_description"])
|
||||
}
|
||||
fmt.Printf("%s\n", res)
|
||||
|
||||
err = json.Unmarshal(res, &data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal access token: %v", err)
|
||||
}
|
||||
return data["access_token"].(string), nil
|
||||
} else {
|
||||
return "", fmt.Errorf("token endpoint not set")
|
||||
}
|
||||
|
||||
return string(res), nil
|
||||
}
|
||||
|
||||
func ForwardToken(eps JwtBearerEndpoints, params JwtBearerFlowParams) error {
|
||||
var (
|
||||
client = params.Client
|
||||
idToken = params.IdToken
|
||||
idp = params.IdentityProvider
|
||||
verbose = params.Verbose
|
||||
)
|
||||
|
||||
// fetch JWKS and add issuer to authentication server to submit ID token
|
||||
if verbose {
|
||||
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 {
|
||||
if verbose {
|
||||
fmt.Printf("Successfully retrieved JWK from authentication server.\n\n")
|
||||
fmt.Printf("Attempting to add issuer to authorization server...\n")
|
||||
}
|
||||
|
||||
ti := &oauth.TrustedIssuer{
|
||||
Issuer: idp.Issuer,
|
||||
Subject: "1",
|
||||
ExpiresAt: time.Now().Add(time.Second * 3600),
|
||||
}
|
||||
res, err := client.AddTrustedIssuer(
|
||||
eps.TrustedIssuers,
|
||||
ti,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add trusted issuer: %v", err)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("%v\n", string(res))
|
||||
}
|
||||
}
|
||||
|
||||
// try and register a new client with authorization server
|
||||
if verbose {
|
||||
fmt.Printf("Registering new OAuth2 client with authorization server...\n")
|
||||
}
|
||||
res, err := client.RegisterOAuthClient(eps.Register)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register client: %v", err)
|
||||
}
|
||||
if verbose {
|
||||
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(eps.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(eps.Clients)
|
||||
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 eps.Token != "" {
|
||||
if verbose {
|
||||
fmt.Printf("Fetching access token from authorization server...\n")
|
||||
}
|
||||
res, err := client.PerformTokenGrant(eps.Token, idToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch access token: %v", err)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("%s\n", res)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("token endpoint is not set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,31 +1,70 @@
|
|||
package opaal
|
||||
|
||||
import (
|
||||
"davidallendj/opaal/internal/db"
|
||||
cache "davidallendj/opaal/internal/cache/sqlite"
|
||||
"davidallendj/opaal/internal/flows"
|
||||
"davidallendj/opaal/internal/oauth"
|
||||
"davidallendj/opaal/internal/oidc"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Login(config *Config, client *Client, provider *oidc.IdentityProvider) error {
|
||||
func Login(config *Config, client *oauth.Client, provider *oidc.IdentityProvider) error {
|
||||
if config == nil {
|
||||
return fmt.Errorf("config is not valid")
|
||||
}
|
||||
|
||||
// make cache if it's not where expect
|
||||
_, err := db.CreateIdentityProvidersIfNotExists(config.Options.CachePath)
|
||||
_, err := cache.CreateIdentityProvidersIfNotExists(config.Options.CachePath)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create cache: %v\n", err)
|
||||
}
|
||||
|
||||
if config.Options.FlowType == "authorization_code" {
|
||||
// create a server if doing authorization code flow
|
||||
server := NewServerWithConfig(config)
|
||||
err := AuthorizationCodeWithConfig(config, server, client, provider)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to complete authorization code flow: %v\n", err)
|
||||
// build the authorization URL to redirect user for social sign-in
|
||||
var state = ""
|
||||
if config.Authentication.Flows["authorization-code"]["state"] != "" {
|
||||
state = config.Authentication.Flows["authorization-code"]["state"]
|
||||
}
|
||||
|
||||
// print the authorization URL for sharing
|
||||
var authorizationUrl = client.BuildAuthorizationUrl(provider.Endpoints.Authorization, state)
|
||||
server := NewServerWithConfig(config)
|
||||
fmt.Printf("Login with identity provider:\n\n %s/login\n %s\n\n",
|
||||
server.GetListenAddr(), authorizationUrl,
|
||||
)
|
||||
|
||||
var button = MakeButton(authorizationUrl, "Login with "+client.Name)
|
||||
|
||||
// authorize oauth client and listen for callback from provider
|
||||
fmt.Printf("Waiting for authorization code redirect @%s/oidc/callback...\n", server.GetListenAddr())
|
||||
eps := flows.JwtBearerEndpoints{
|
||||
Token: config.Authorization.Endpoints.Token,
|
||||
TrustedIssuers: config.Authorization.Endpoints.TrustedIssuers,
|
||||
Register: config.Authorization.Endpoints.Register,
|
||||
}
|
||||
params := flows.JwtBearerFlowParams{
|
||||
Client: oauth.NewClient(),
|
||||
IdentityProvider: provider,
|
||||
TrustedIssuer: &oauth.TrustedIssuer{
|
||||
AllowAnySubject: false,
|
||||
Issuer: server.Addr,
|
||||
Subject: "opaal",
|
||||
ExpiresAt: time.Now().Add(time.Second * 3600),
|
||||
},
|
||||
Verbose: config.Options.Verbose,
|
||||
}
|
||||
err = server.Login(button, provider, client, eps, params)
|
||||
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)
|
||||
}
|
||||
|
||||
} else if config.Options.FlowType == "client_credentials" {
|
||||
err := ClientCredentialsWithConfig(config, client)
|
||||
err := NewClientCredentialsFlowWithConfig(config, client)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to complete client credentials flow: %v", err)
|
||||
}
|
||||
|
|
@ -35,3 +74,7 @@ func Login(config *Config, client *Client, provider *oidc.IdentityProvider) erro
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func MakeButton(url string, text string) string {
|
||||
return "<a href=\"" + url + "\"> " + text + "</a>"
|
||||
}
|
||||
|
|
|
|||
420
internal/new.go
Normal file
420
internal/new.go
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
package opaal
|
||||
|
||||
import (
|
||||
"davidallendj/opaal/internal/oauth"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"davidallendj/opaal/internal/flows"
|
||||
"davidallendj/opaal/internal/server"
|
||||
|
||||
"github.com/davidallendj/go-utils/mathx"
|
||||
)
|
||||
|
||||
func NewClientWithConfig(config *Config) *oauth.Client {
|
||||
// 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 &oauth.Client{
|
||||
Id: clients[0].Id,
|
||||
Secret: clients[0].Secret,
|
||||
Name: clients[0].Name,
|
||||
Issuer: clients[0].Issuer,
|
||||
Scope: clients[0].Scope,
|
||||
RedirectUris: clients[0].RedirectUris,
|
||||
}
|
||||
}
|
||||
|
||||
func NewClientWithConfigByIndex(config *Config, index int) *oauth.Client {
|
||||
size := len(config.Authentication.Clients)
|
||||
index = mathx.Clamp(index, 0, size)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewClientWithConfigByName(config *Config, name string) *oauth.Client {
|
||||
index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool {
|
||||
return c.Name == name
|
||||
})
|
||||
if index >= 0 {
|
||||
return &config.Authentication.Clients[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewClientWithConfigByProvider(config *Config, issuer string) *oauth.Client {
|
||||
index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool {
|
||||
return c.Issuer == issuer
|
||||
})
|
||||
|
||||
if index >= 0 {
|
||||
return &config.Authentication.Clients[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewClientWithConfigById(config *Config, id string) *oauth.Client {
|
||||
index := slices.IndexFunc(config.Authentication.Clients, func(c oauth.Client) bool {
|
||||
return c.Id == id
|
||||
})
|
||||
if index >= 0 {
|
||||
return &config.Authentication.Clients[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewClientCredentialsFlowWithConfig(config *Config, client *oauth.Client) error {
|
||||
eps := flows.ClientCredentialsFlowEndpoints{
|
||||
Create: config.Authorization.Endpoints.Clients,
|
||||
Authorize: config.Authorization.Endpoints.Authorize,
|
||||
Token: config.Authorization.Endpoints.Token,
|
||||
}
|
||||
return flows.NewClientCredentialsFlow(eps, client)
|
||||
}
|
||||
|
||||
func NewServerWithConfig(conf *Config) *server.Server {
|
||||
host := conf.Server.Host
|
||||
port := conf.Server.Port
|
||||
server := &server.Server{
|
||||
Server: &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", host, port),
|
||||
},
|
||||
Host: host,
|
||||
Port: port,
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
// func NewAuthorizationCodeFlowWithConfig(config *Config, client *oauth.Client, idp *oidc.IdentityProvider) error {
|
||||
// // create new server and client to use for flow
|
||||
// server := NewServerWithConfig(config)
|
||||
|
||||
// // 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())
|
||||
// err := server.Login(authorizationUrl, c)
|
||||
// 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)
|
||||
// }
|
||||
|
||||
// // 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)
|
||||
// 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)
|
||||
// fmt.Println()
|
||||
// }
|
||||
|
||||
// if !config.Options.TokenForwarding {
|
||||
// // 1. verify that the JWT from the issuer is valid using all keys
|
||||
// _, err = jws.Verify([]byte(idToken), jws.WithKeySet(idp.KeySet), jws.WithValidateKey(true))
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("failed to verify JWT: %v", err)
|
||||
// }
|
||||
|
||||
// // 2. Check if we are already registered as a trusted issuer with authorization server...
|
||||
|
||||
// // 3.a if not, create a new JWKS (or just JWK) to be verified
|
||||
// var (
|
||||
// keyPath string
|
||||
// privateJwk jwk.Key
|
||||
// publicJwk jwk.Key
|
||||
// )
|
||||
// if config.Authorization.KeyPath != "" {
|
||||
// keyPath = config.Authorization.Endpoints.Authorize
|
||||
// }
|
||||
// privateKey, err := os.ReadFile(keyPath)
|
||||
// if err != nil {
|
||||
// privateJwk, publicJwk, err = cryptox.GenerateJwkKeyPair()
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("failed to generate JWK pair: %v", err)
|
||||
// }
|
||||
// } else {
|
||||
// privateJwk, publicJwk, err = cryptox.GenerateJwkKeyPairFromPrivateKey(privateKey)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("failed to generate JWK pair from private key: %v", err)
|
||||
// }
|
||||
// }
|
||||
// privateJwk.Set("kid", uuid.New().String())
|
||||
// publicJwk.Set("kid", uuid.New().String())
|
||||
|
||||
// // 3.b ...and then, add opaal's server host as a trusted issuer with JWK
|
||||
// fmt.Printf("Attempting to add issuer to authorization server...\n")
|
||||
// ti := oauth.NewTrustedIssuer()
|
||||
// ti.Issuer = server.Addr
|
||||
// ti.PublicKey = publicJwk
|
||||
// ti.Subject = "1"
|
||||
// ti.ExpiresAt = time.Now().Add(time.Second * 3600)
|
||||
// res, err := client.AddTrustedIssuer(
|
||||
// config.Authorization.Endpoints.TrustedIssuers,
|
||||
// ti,
|
||||
// )
|
||||
// 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
|
||||
// parsedIdToken, err := jwt.ParseString(idToken, jwt.WithKeySet(idp.KeySet))
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("failed to parse ID token: %v", err)
|
||||
// }
|
||||
// payload := parsedIdToken.PrivateClaims()
|
||||
// payload["iss"] = server.Addr
|
||||
// payload["aud"] = []string{config.Authorization.Endpoints.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)
|
||||
// }
|
||||
|
||||
// // 5. dynamically register new OAuth client and authorize it to make jwt_bearer request
|
||||
// fmt.Printf("Registering new OAuth2 client with authorization server...\n")
|
||||
// res, err = client.RegisterOAuthClient(config.Authorization.Endpoints.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 try to create again
|
||||
// fmt.Printf("Attempting to delete client...\n")
|
||||
// err := client.DeleteOAuthClient(config.Authorization.Endpoints.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.Endpoints.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.Endpoints.Token != "" {
|
||||
// fmt.Printf("Fetching access token from authorization server...\n")
|
||||
// res, err := client.PerformTokenGrant(config.Authorization.Endpoints.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.Endpoints.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.Endpoints.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.Endpoints.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.Endpoints.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.Endpoints.Token != "" {
|
||||
// fmt.Printf("Fetching access token from authorization server...\n")
|
||||
// res, err := client.PerformTokenGrant(config.Authorization.Endpoints.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
|
||||
// }
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package opaal
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
@ -1,65 +1,41 @@
|
|||
package opaal
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"davidallendj/opaal/internal/oidc"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/davidallendj/go-utils/httpx"
|
||||
"github.com/davidallendj/go-utils/util"
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
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)
|
||||
type Client struct {
|
||||
http.Client
|
||||
Id string `db:"id" yaml:"id"`
|
||||
Secret string `db:"secret" yaml:"secret"`
|
||||
Name string `db:"name" yaml:"name"`
|
||||
Description string `db:"description" yaml:"description"`
|
||||
Issuer string `db:"issuer" yaml:"issuer"`
|
||||
RegistrationAccessToken string `db:"registration_access_token" yaml:"registration-access-token"`
|
||||
RedirectUris []string `db:"redirect_uris" yaml:"redirect-uris"`
|
||||
Scope []string `db:"scope" yaml:"scope"`
|
||||
Audience []string `db:"audience" yaml:"audience"`
|
||||
FlowId string
|
||||
CsrfToken string
|
||||
}
|
||||
|
||||
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 NewClient() *Client {
|
||||
return &Client{}
|
||||
}
|
||||
|
||||
func (client *Client) ClearCookies() {
|
||||
jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
|
||||
client.Jar = jar
|
||||
}
|
||||
|
||||
func (client *Client) IsOAuthClientRegistered(clientUrl string) (bool, error) {
|
||||
|
|
@ -107,9 +83,9 @@ func (client *Client) GetOAuthClient(clientUrl string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) CreateOAuthClient(registerUrl string, audience []string) ([]byte, error) {
|
||||
func (client *Client) CreateOAuthClient(registerUrl string) ([]byte, error) {
|
||||
// hydra endpoint: POST /clients
|
||||
audience = util.QuoteArrayStrings(audience)
|
||||
audience := util.QuoteArrayStrings(client.Audience)
|
||||
body := httpx.Body(fmt.Sprintf(`{
|
||||
"client_id": "%s",
|
||||
"client_name": "%s",
|
||||
|
|
@ -151,9 +127,12 @@ func (client *Client) CreateOAuthClient(registerUrl string, audience []string) (
|
|||
return b, err
|
||||
}
|
||||
|
||||
func (client *Client) RegisterOAuthClient(registerUrl string, audience []string) ([]byte, error) {
|
||||
func (client *Client) RegisterOAuthClient(registerUrl string) ([]byte, error) {
|
||||
// hydra endpoint: POST /oauth2/register
|
||||
audience = util.QuoteArrayStrings(audience)
|
||||
if registerUrl == "" {
|
||||
return nil, fmt.Errorf("no URL provided")
|
||||
}
|
||||
audience := util.QuoteArrayStrings(client.Audience)
|
||||
body := httpx.Body(fmt.Sprintf(`{
|
||||
"client_name": "opaal",
|
||||
"token_endpoint_auth_method": "client_secret_post",
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package opaal
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
99
internal/oauth/trusted.go
Normal file
99
internal/oauth/trusted.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
package oauth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/davidallendj/go-utils/httpx"
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
)
|
||||
|
||||
type TrustedIssuer struct {
|
||||
Id string `db:"id" yaml:"id"`
|
||||
AllowAnySubject bool `db:"allow_any_subject" yaml:"allow-any-subject"`
|
||||
ExpiresAt time.Time `db:"expires_at" yaml:"expires-at"`
|
||||
Issuer string `db:"issuer" yaml:"issuer"`
|
||||
PublicKey jwk.Key `db:"public_key" yaml:"public-key"`
|
||||
Scope []string `db:"scope" yaml:"scope"`
|
||||
Subject string `db:"subject" yaml:"subject"`
|
||||
}
|
||||
|
||||
func NewTrustedIssuer() *TrustedIssuer {
|
||||
return &TrustedIssuer{
|
||||
AllowAnySubject: false,
|
||||
ExpiresAt: time.Now().Add(time.Hour),
|
||||
Scope: []string{"openid"},
|
||||
Subject: "1",
|
||||
}
|
||||
}
|
||||
|
||||
func (ti *TrustedIssuer) IsTrustedIssuerValid() bool {
|
||||
err := ti.PublicKey.Validate()
|
||||
return ti.Issuer != "" && err == nil && ti.Subject != ""
|
||||
}
|
||||
|
||||
func ParseString(b []byte) (*TrustedIssuer, error) {
|
||||
// take data from JSON to populate fields
|
||||
ti := &TrustedIssuer{}
|
||||
data := map[string]any{}
|
||||
json.Unmarshal(b, &data)
|
||||
return ti, nil
|
||||
}
|
||||
|
||||
func (client *Client) ListTrustedIssuers(url string) ([]TrustedIssuer, error) {
|
||||
// hydra endpoint: GET /admin/trust/grants/jwt-bearer/issuers
|
||||
_, b, err := httpx.MakeHttpRequest(url, http.MethodGet, nil, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make request: %v", err)
|
||||
}
|
||||
|
||||
// unmarshal results into TrustedIssuers objects
|
||||
trustedIssuers := []TrustedIssuer{}
|
||||
err = json.Unmarshal(b, &trustedIssuers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal JSON: %v", err)
|
||||
}
|
||||
return trustedIssuers, nil
|
||||
}
|
||||
|
||||
func (client *Client) AddTrustedIssuer(url string, ti *TrustedIssuer) ([]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)
|
||||
}
|
||||
|
||||
// NOTE: Can also include "jwks_uri" instead of "jwk"
|
||||
body := map[string]any{
|
||||
"allow_any_subject": ti.AllowAnySubject,
|
||||
"issuer": ti.Issuer,
|
||||
"expires_at": ti.ExpiresAt,
|
||||
"jwk": ti.PublicKey,
|
||||
"scope": client.Scope,
|
||||
}
|
||||
if !ti.AllowAnySubject {
|
||||
body["subject"] = ti.Subject
|
||||
}
|
||||
b, err := json.Marshal(body)
|
||||
fmt.Printf("request: %v\n", string(b))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request body: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make request: %v", err)
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
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)
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ type IdentityProvider struct {
|
|||
Issuer string `db:"issuer" json:"issuer" yaml:"issuer"`
|
||||
Endpoints Endpoints `db:"endpoints" json:"endpoints" yaml:"endpoints"`
|
||||
Supported Supported `db:"supported" json:"supported" yaml:"supported"`
|
||||
Jwks jwk.Set
|
||||
KeySet jwk.Set
|
||||
}
|
||||
|
||||
type Endpoints struct {
|
||||
|
|
@ -142,7 +142,7 @@ func (p *IdentityProvider) FetchJwks() error {
|
|||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
var err error
|
||||
p.Jwks, err = jwk.Fetch(ctx, p.Endpoints.JwksUri)
|
||||
p.KeySet, err = jwk.Fetch(ctx, p.Endpoints.JwksUri)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch JWKS: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
package opaal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/nikolalohinski/gonja/v2"
|
||||
"github.com/nikolalohinski/gonja/v2/exec"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
*http.Server
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
Callback string `yaml:"callback"`
|
||||
}
|
||||
|
||||
func NewServerWithConfig(conf *Config) *Server {
|
||||
host := conf.Server.Host
|
||||
port := conf.Server.Port
|
||||
server := &Server{
|
||||
Server: &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", host, port),
|
||||
},
|
||||
Host: host,
|
||||
Port: port,
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *Server) SetListenAddr(host string, port int) {
|
||||
s.Addr = s.GetListenAddr()
|
||||
}
|
||||
|
||||
func (s *Server) GetListenAddr() string {
|
||||
return fmt.Sprintf("%s:%d", s.Host, s.Port)
|
||||
}
|
||||
|
||||
func (s *Server) WaitForAuthorizationCode(loginUrl string, callback string) (string, error) {
|
||||
// check if callback is set
|
||||
if callback == "" {
|
||||
callback = "/oidc/callback"
|
||||
}
|
||||
|
||||
var code string
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.RedirectSlashes)
|
||||
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
})
|
||||
r.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
// show login page with notice to redirect
|
||||
template, err := gonja.FromFile("pages/index.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
data := exec.NewContext(map[string]interface{}{
|
||||
"loginUrl": loginUrl,
|
||||
})
|
||||
|
||||
if err = template.Execute(w, data); err != nil { // Prints: Hello Bob!
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
r.HandleFunc(callback, func(w http.ResponseWriter, r *http.Request) {
|
||||
// get the code from the OIDC provider
|
||||
if r != nil {
|
||||
code = r.URL.Query().Get("code")
|
||||
fmt.Printf("Authorization code: %v\n", code)
|
||||
}
|
||||
http.Redirect(w, r, "/redirect", http.StatusSeeOther)
|
||||
})
|
||||
r.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) {
|
||||
err := s.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to close server: %v\n", err)
|
||||
}
|
||||
})
|
||||
s.Handler = r
|
||||
|
||||
return code, s.ListenAndServe()
|
||||
}
|
||||
|
||||
func (s *Server) Serve(data chan []byte) error {
|
||||
output, ok := <-data
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to receive data")
|
||||
}
|
||||
|
||||
fmt.Printf("Received data: %v\n", string(output))
|
||||
// http.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// })
|
||||
r := chi.NewRouter()
|
||||
r.HandleFunc("/success", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Printf("Serving success page.")
|
||||
successPage, err := os.ReadFile("pages/success.html")
|
||||
if err != nil {
|
||||
fmt.Printf("failed to load success page: %v\n", err)
|
||||
}
|
||||
successPage = []byte(strings.ReplaceAll(string(successPage), "{{access_token}}", string(output)))
|
||||
w.Write(successPage)
|
||||
})
|
||||
r.HandleFunc("/error", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Printf("Serving error page.")
|
||||
errorPage, err := os.ReadFile("pages/success.html")
|
||||
if err != nil {
|
||||
fmt.Printf("failed to load success page: %v\n", err)
|
||||
}
|
||||
// errorPage = []byte(strings.ReplaceAll(string(errorPage), "{{access_token}}", output))
|
||||
w.Write(errorPage)
|
||||
})
|
||||
|
||||
s.Handler = r
|
||||
return s.ListenAndServe()
|
||||
}
|
||||
172
internal/server/server.go
Normal file
172
internal/server/server.go
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"davidallendj/opaal/internal/flows"
|
||||
"davidallendj/opaal/internal/oauth"
|
||||
"davidallendj/opaal/internal/oidc"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/davidallendj/go-utils/httpx"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/nikolalohinski/gonja/v2"
|
||||
"github.com/nikolalohinski/gonja/v2/exec"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
*http.Server
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
Callback string `yaml:"callback"`
|
||||
State string `yaml:"state"`
|
||||
}
|
||||
|
||||
func (s *Server) SetListenAddr(host string, port int) {
|
||||
s.Addr = s.GetListenAddr()
|
||||
}
|
||||
|
||||
func (s *Server) GetListenAddr() string {
|
||||
return fmt.Sprintf("%s:%d", s.Host, s.Port)
|
||||
}
|
||||
|
||||
func (s *Server) Login(buttons string, provider *oidc.IdentityProvider, client *oauth.Client, eps flows.JwtBearerEndpoints, params flows.JwtBearerFlowParams) error {
|
||||
var target = ""
|
||||
|
||||
// check if callback is set
|
||||
if s.Callback == "" {
|
||||
s.Callback = "/oidc/callback"
|
||||
}
|
||||
|
||||
var code string
|
||||
var accessToken string
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.RedirectSlashes)
|
||||
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
target = r.Header.Get("target")
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
})
|
||||
r.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
// add target if query exists
|
||||
if r != nil {
|
||||
target = r.URL.Query().Get("target")
|
||||
}
|
||||
// show login page with notice to redirect
|
||||
template, err := gonja.FromFile("pages/index.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
data := exec.NewContext(map[string]interface{}{
|
||||
"loginButtons": buttons,
|
||||
})
|
||||
|
||||
if err = template.Execute(w, data); err != nil { // Prints: Hello Bob!
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
r.HandleFunc(s.Callback, func(w http.ResponseWriter, r *http.Request) {
|
||||
// get the code from the OIDC provider
|
||||
if r != nil {
|
||||
code = r.URL.Query().Get("code")
|
||||
fmt.Printf("Authorization code: %v\n", code)
|
||||
|
||||
// use code from response and exchange for bearer token (with ID token)
|
||||
bearerToken, err := client.FetchTokenFromAuthenticationServer(
|
||||
code,
|
||||
provider.Endpoints.Token,
|
||||
s.State,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to fetch token from authentication server: %v\n", err)
|
||||
http.Redirect(w, r, "/error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// extract ID and access tokens from bearer
|
||||
var data map[string]any
|
||||
err = json.Unmarshal([]byte(bearerToken), &data)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to unmarshal token: %v\n", err)
|
||||
return
|
||||
}
|
||||
if data["error"] != nil {
|
||||
fmt.Printf("the response from the authentication server returned an error (%v): %v", data["error"], data["error_description"])
|
||||
http.Redirect(w, r, "/error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if data["id_token"] == nil {
|
||||
fmt.Printf("no ID token found\n")
|
||||
http.Redirect(w, r, "/error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// extract scopes from ID token and add to trusted issuer
|
||||
|
||||
// complete JWT bearer flow to receive access token from authorization server
|
||||
// fmt.Printf("bearer: %v\n", string(bearerToken))
|
||||
params.IdToken = data["id_token"].(string)
|
||||
accessToken, err = flows.NewJwtBearerFlow(eps, params)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to complete JWT bearer flow: %v\n", err)
|
||||
w.Header().Add("Content-type", "text/html")
|
||||
http.Redirect(w, r, "/error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/success", http.StatusSeeOther)
|
||||
})
|
||||
r.HandleFunc("/success", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Printf("Serving success page.\n")
|
||||
template, err := gonja.FromFile("pages/success.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
data := exec.NewContext(map[string]interface{}{
|
||||
"accessToken": accessToken,
|
||||
})
|
||||
|
||||
if err = template.Execute(w, data); err != nil { // Prints: Hello Bob!
|
||||
panic(err)
|
||||
}
|
||||
// try and send access code to target if set
|
||||
if target != "" {
|
||||
fmt.Printf("Send access token to target: %s\n", target)
|
||||
_, _, err := httpx.MakeHttpRequest(target, http.MethodPost, nil, httpx.Headers{"access_token": accessToken})
|
||||
if err != nil {
|
||||
fmt.Printf("failed to make request: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
r.HandleFunc("/error", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Printf("Serving error page.")
|
||||
errorPage, err := os.ReadFile("pages/error.html")
|
||||
if err != nil {
|
||||
fmt.Printf("failed to load error page: %v\n", err)
|
||||
}
|
||||
w.Write(errorPage)
|
||||
})
|
||||
s.Handler = r
|
||||
|
||||
return s.ListenAndServe()
|
||||
}
|
||||
|
||||
func (s *Server) Serve(data chan []byte) error {
|
||||
output, ok := <-data
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to receive data")
|
||||
}
|
||||
|
||||
fmt.Printf("Received data: %v\n", string(output))
|
||||
// http.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// })
|
||||
r := chi.NewRouter()
|
||||
|
||||
s.Handler = r
|
||||
return s.ListenAndServe()
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<!DOCTYPE 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 for an access token. </br></br>
|
||||
|
||||
{{loginButtons}}
|
||||
|
||||
<a href="{{ loginUrl }}">Login</a>
|
||||
</html>
|
||||
|
|
@ -1,13 +1,36 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
Success! Here's you access token:
|
||||
<script>
|
||||
function copyText() {
|
||||
// Get the text field
|
||||
var copyText = document.getElementById("token");
|
||||
|
||||
{{access_token}}
|
||||
...
|
||||
You will need this token to access protected services and resources.
|
||||
Make sure to include it in the authorization header when making a HTTP request:
|
||||
// Select the text field
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
curl -k http://smd:27779/hsm/v2/Inventory/RedfishEndpoints -H "authorization: Bearer [access_token]"
|
||||
// Copy the text inside the text field
|
||||
navigator.clipboard.writeText(copyText.value);
|
||||
|
||||
When you done, you can close this page.
|
||||
// Alert the copied text
|
||||
alert("Copied the text: " + copyText.value);
|
||||
}
|
||||
</script>
|
||||
<p>
|
||||
Login successful! Here is your access token: </br>
|
||||
<br>
|
||||
<!-- The text field -->
|
||||
<input type="text" value="{{accessToken}}" style="width: 350px" id="token" readonly>
|
||||
|
||||
<!-- The button used to copy the text -->
|
||||
<button onclick="copyText()">Copy</button>
|
||||
<br><br>
|
||||
You will need this token to access protected services and resources.<br>
|
||||
Make sure to include it in the authorization header if you are making a HTTP request.<br><br>
|
||||
<pre>
|
||||
curl -k http://smd:27779/hsm/v2/Inventory/RedfishEndpoints -H "Authorization: Bearer [access_token]"
|
||||
</pre>
|
||||
|
||||
When you done, you can close this page.<br>
|
||||
</p>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue