Merge pull request #5 from davidallendj/refactoring

Major refactor and reoganization
This commit is contained in:
David Allen 2024-03-11 19:02:25 -06:00 committed by GitHub
commit 7d24ee0e10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1549 additions and 806 deletions

View file

@ -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:
@ -106,3 +119,5 @@ options:
- 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
- Allow signing JWTs by supplying key pair
- Separate `jwt_bearer` grant type from the authorization code flow

View file

@ -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)
}

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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
View 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
}

View file

@ -1,4 +1,4 @@
package db
package cache
import (
"davidallendj/opaal/internal/oidc"

107
internal/cache/sqlite/trusted.go vendored Normal file
View 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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View 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
}

View file

@ -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
View 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
// }

View file

@ -1,4 +1,4 @@
package opaal
package oauth
import (
"bytes"

View file

@ -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",

View file

@ -1,4 +1,4 @@
package opaal
package oauth
import (
"fmt"

99
internal/oauth/trusted.go Normal file
View 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)
}

View file

@ -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)
}

View file

@ -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
View 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()
}

View file

@ -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>

View file

@ -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>